stitchkit 0.54.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/browser/socket-io.d.ts +11 -1
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.js +14 -9
- package/dist/contract/file-ref.d.ts +13 -0
- package/dist/contract/file-ref.d.ts.map +1 -0
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +21 -17
- package/dist/files/boundary.d.ts +52 -0
- package/dist/files/boundary.d.ts.map +1 -0
- package/dist/files.d.ts +3 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +16 -0
- package/dist/{index-7vb44hy8.js → index-0gv7k0ra.js} +127 -92
- package/dist/index-1bx83sw4.js +4 -0
- package/dist/index-1k16zv57.js +269 -0
- package/dist/index-6k1937bx.js +20 -0
- package/dist/{index-bmnhqtya.js → index-7eq9va6e.js} +1 -4
- package/dist/{index-yxpe3phd.js → index-cby4ar3v.js} +1 -18
- package/dist/index-h60df7rj.js +110 -0
- package/dist/{index-yez419px.js → index-js7yexng.js} +19 -6
- package/dist/index-k4dftwf7.js +66 -0
- package/dist/{index-z7761jk2.js → index-npye5kt7.js} +16 -22
- package/dist/{index-b15rtha1.js → index-pzc32v75.js} +61 -3
- package/dist/{index-e1es808r.js → index-qh4xevf5.js} +1 -1
- package/dist/{index-gvha32jh.js → index-qx84bqzk.js} +2 -2
- package/dist/{index-pfqjb5xy.js → index-rgd18hx3.js} +23 -5
- package/dist/index-sbdmyz75.js +18 -0
- package/dist/{index-173tvqcp.js → index-v4bbq3p4.js} +9 -114
- package/dist/index.js +162 -35
- package/dist/node.js +30 -27
- package/dist/observability/index.js +28 -25
- package/dist/react.js +2 -2
- package/dist/realtime/contract.d.ts +9 -0
- package/dist/realtime/contract.d.ts.map +1 -1
- package/dist/realtime/index.d.ts +2 -1
- package/dist/realtime/index.d.ts.map +1 -1
- package/dist/realtime/request.d.ts +21 -0
- package/dist/realtime/request.d.ts.map +1 -0
- package/dist/realtime/socket.d.ts +3 -0
- package/dist/realtime/socket.d.ts.map +1 -1
- package/dist/remote.d.ts +9 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +79 -0
- package/dist/server/context-contribution.d.ts +10 -0
- package/dist/server/context-contribution.d.ts.map +1 -0
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +133 -88
- package/dist/server/lifecycle.d.ts +4 -0
- package/dist/server/lifecycle.d.ts.map +1 -0
- package/dist/server/middleware/auth.d.ts +35 -7
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/testing/surface-conformance.d.ts +47 -0
- package/dist/testing/surface-conformance.d.ts.map +1 -0
- package/dist/testing/surface-manifest.d.ts +156 -0
- package/dist/testing/surface-manifest.d.ts.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +366 -3
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/async-operation.d.ts +168 -0
- package/dist/tools/async-operation.d.ts.map +1 -0
- package/dist/tools/cli-args.d.ts +1 -1
- package/dist/tools/cli-args.d.ts.map +1 -1
- package/dist/tools/cli-format.d.ts +2 -2
- package/dist/tools/cli-format.d.ts.map +1 -1
- package/dist/tools/cli-wait.d.ts +11 -2
- package/dist/tools/cli-wait.d.ts.map +1 -1
- package/dist/tools/cli.d.ts +2 -0
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/define-download-tool.d.ts +6 -5
- package/dist/tools/define-download-tool.d.ts.map +1 -1
- package/dist/tools/define-upload-tool.d.ts +3 -1
- package/dist/tools/define-upload-tool.d.ts.map +1 -1
- package/dist/tools/define-wait-tool.d.ts +1 -1
- package/dist/tools/define-wait-tool.d.ts.map +1 -1
- package/dist/tools/download-core.d.ts +5 -10
- package/dist/tools/download-core.d.ts.map +1 -1
- package/dist/tools/lifecycle.d.ts +4 -0
- package/dist/tools/lifecycle.d.ts.map +1 -0
- package/dist/tools/mount-download.d.ts +5 -4
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/mount-upload.d.ts +4 -2
- package/dist/tools/mount-upload.d.ts.map +1 -1
- package/dist/tools/upload-core.d.ts +3 -2
- package/dist/tools/upload-core.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts +4 -3
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools/wait-core.d.ts +6 -1
- package/dist/tools/wait-core.d.ts.map +1 -1
- package/dist/tools.d.ts +3 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +385 -244
- package/llms-full.txt +417 -27
- package/package.json +10 -2
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isWithinDir
|
|
3
|
+
} from "./index-sbdmyz75.js";
|
|
4
|
+
import {
|
|
5
|
+
ManagedFilePathSchema,
|
|
6
|
+
ManagedFileRefSchema
|
|
7
|
+
} from "./index-6k1937bx.js";
|
|
8
|
+
|
|
9
|
+
// src/files/boundary.ts
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { constants } from "node:fs";
|
|
12
|
+
import { link, open, realpath, rename, stat, unlink } from "node:fs/promises";
|
|
13
|
+
import { basename, dirname, resolve } from "node:path";
|
|
14
|
+
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
15
|
+
var DEFAULT_INSPECTION_BYTES = 64 * 1024;
|
|
16
|
+
var NOFOLLOW_FLAG = constants.O_NOFOLLOW ?? 0;
|
|
17
|
+
|
|
18
|
+
class ManagedFileError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code, message, options) {
|
|
21
|
+
super(message, options);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.name = "ManagedFileError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function positiveLimit(value, fallback, name) {
|
|
27
|
+
const resolved = value ?? fallback;
|
|
28
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
29
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
30
|
+
}
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
function filePath(path) {
|
|
34
|
+
const parsed = ManagedFilePathSchema.safeParse(path);
|
|
35
|
+
if (!parsed.success) {
|
|
36
|
+
throw new ManagedFileError("FILE_INVALID_PATH", "invalid managed-file path");
|
|
37
|
+
}
|
|
38
|
+
return parsed.data;
|
|
39
|
+
}
|
|
40
|
+
function errorCode(error) {
|
|
41
|
+
if (typeof error !== "object" || error === null || !("code" in error))
|
|
42
|
+
return;
|
|
43
|
+
return typeof error.code === "string" ? error.code : undefined;
|
|
44
|
+
}
|
|
45
|
+
function ioError(message, error) {
|
|
46
|
+
if (error instanceof ManagedFileError)
|
|
47
|
+
return error;
|
|
48
|
+
return new ManagedFileError("FILE_IO_ERROR", message, { cause: error });
|
|
49
|
+
}
|
|
50
|
+
async function existingParent(root, target) {
|
|
51
|
+
const parent = await realpath(dirname(target)).catch(() => null);
|
|
52
|
+
if (parent === null) {
|
|
53
|
+
throw new ManagedFileError("FILE_NOT_FOUND", "managed-file parent directory not found");
|
|
54
|
+
}
|
|
55
|
+
if (!isWithinDir(root, parent)) {
|
|
56
|
+
throw new ManagedFileError("FILE_OUTSIDE_ROOT", "managed-file path escapes its boundary");
|
|
57
|
+
}
|
|
58
|
+
const info = await stat(parent).catch(() => null);
|
|
59
|
+
if (!info?.isDirectory()) {
|
|
60
|
+
throw new ManagedFileError("FILE_NOT_FOUND", "managed-file parent directory not found");
|
|
61
|
+
}
|
|
62
|
+
return parent;
|
|
63
|
+
}
|
|
64
|
+
async function readHandle(handle, maxBytes, signal) {
|
|
65
|
+
const chunks = [];
|
|
66
|
+
let total = 0;
|
|
67
|
+
while (true) {
|
|
68
|
+
signal?.throwIfAborted();
|
|
69
|
+
const capacity = Math.min(64 * 1024, maxBytes + 1 - total);
|
|
70
|
+
if (capacity <= 0) {
|
|
71
|
+
throw new ManagedFileError("FILE_TOO_LARGE", `file exceeds the ${maxBytes}-byte cap`);
|
|
72
|
+
}
|
|
73
|
+
const chunk = new Uint8Array(capacity);
|
|
74
|
+
const { bytesRead } = await handle.read(chunk, 0, capacity);
|
|
75
|
+
if (bytesRead === 0)
|
|
76
|
+
break;
|
|
77
|
+
total += bytesRead;
|
|
78
|
+
if (total > maxBytes) {
|
|
79
|
+
throw new ManagedFileError("FILE_TOO_LARGE", `file exceeds the ${maxBytes}-byte cap`);
|
|
80
|
+
}
|
|
81
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
82
|
+
}
|
|
83
|
+
const bytes = new Uint8Array(total);
|
|
84
|
+
let offset = 0;
|
|
85
|
+
for (const chunk of chunks) {
|
|
86
|
+
bytes.set(chunk, offset);
|
|
87
|
+
offset += chunk.byteLength;
|
|
88
|
+
}
|
|
89
|
+
return bytes;
|
|
90
|
+
}
|
|
91
|
+
async function writeChunk(handle, chunk) {
|
|
92
|
+
let offset = 0;
|
|
93
|
+
while (offset < chunk.byteLength) {
|
|
94
|
+
const { bytesWritten } = await handle.write(chunk, offset, chunk.byteLength - offset);
|
|
95
|
+
if (bytesWritten === 0)
|
|
96
|
+
throw new Error("zero-byte managed-file write");
|
|
97
|
+
offset += bytesWritten;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function writeSource(handle, source, maxBytes, inspectionBytes, signal) {
|
|
101
|
+
let size = 0;
|
|
102
|
+
const prefixChunks = [];
|
|
103
|
+
let prefixSize = 0;
|
|
104
|
+
const consume = async (chunk) => {
|
|
105
|
+
signal?.throwIfAborted();
|
|
106
|
+
size += chunk.byteLength;
|
|
107
|
+
if (size > maxBytes) {
|
|
108
|
+
throw new ManagedFileError("FILE_TOO_LARGE", `file exceeds the ${maxBytes}-byte cap`);
|
|
109
|
+
}
|
|
110
|
+
if (prefixSize < inspectionBytes) {
|
|
111
|
+
const kept = chunk.subarray(0, inspectionBytes - prefixSize);
|
|
112
|
+
prefixChunks.push(kept);
|
|
113
|
+
prefixSize += kept.byteLength;
|
|
114
|
+
}
|
|
115
|
+
await writeChunk(handle, chunk);
|
|
116
|
+
};
|
|
117
|
+
if (source instanceof Uint8Array) {
|
|
118
|
+
await consume(source);
|
|
119
|
+
} else {
|
|
120
|
+
const reader = source.getReader();
|
|
121
|
+
try {
|
|
122
|
+
while (true) {
|
|
123
|
+
const result = await reader.read();
|
|
124
|
+
if (result.done)
|
|
125
|
+
break;
|
|
126
|
+
await consume(result.value);
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
await reader.cancel(error).catch(() => {
|
|
130
|
+
return;
|
|
131
|
+
});
|
|
132
|
+
throw error;
|
|
133
|
+
} finally {
|
|
134
|
+
reader.releaseLock();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const prefix = new Uint8Array(prefixSize);
|
|
138
|
+
let offset = 0;
|
|
139
|
+
for (const chunk of prefixChunks) {
|
|
140
|
+
prefix.set(chunk, offset);
|
|
141
|
+
offset += chunk.byteLength;
|
|
142
|
+
}
|
|
143
|
+
return { size, prefix };
|
|
144
|
+
}
|
|
145
|
+
async function createManagedFileBoundary(config) {
|
|
146
|
+
const root = await realpath(resolve(config.root)).catch(() => null);
|
|
147
|
+
if (root === null || !(await stat(root).catch(() => null))?.isDirectory()) {
|
|
148
|
+
throw new ManagedFileError("FILE_NOT_FOUND", "managed-file root must exist");
|
|
149
|
+
}
|
|
150
|
+
const maxReadBytes = positiveLimit(config.maxReadBytes, DEFAULT_MAX_BYTES, "maxReadBytes");
|
|
151
|
+
const maxWriteBytes = positiveLimit(config.maxWriteBytes, DEFAULT_MAX_BYTES, "maxWriteBytes");
|
|
152
|
+
const inspectionBytes = positiveLimit(config.inspectionBytes, DEFAULT_INSPECTION_BYTES, "inspectionBytes");
|
|
153
|
+
const targetFor = (path) => {
|
|
154
|
+
const relative = filePath(path);
|
|
155
|
+
const target = resolve(root, ...relative.split("/"));
|
|
156
|
+
if (!isWithinDir(root, target)) {
|
|
157
|
+
throw new ManagedFileError("FILE_OUTSIDE_ROOT", "managed-file path escapes its boundary");
|
|
158
|
+
}
|
|
159
|
+
return { path: relative, target };
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
async read(path, options = {}) {
|
|
163
|
+
const resolved = targetFor(path);
|
|
164
|
+
const realTarget = await realpath(resolved.target).catch(() => null);
|
|
165
|
+
if (realTarget === null) {
|
|
166
|
+
throw new ManagedFileError("FILE_NOT_FOUND", "managed file not found");
|
|
167
|
+
}
|
|
168
|
+
if (!isWithinDir(root, realTarget)) {
|
|
169
|
+
throw new ManagedFileError("FILE_OUTSIDE_ROOT", "managed-file path escapes its boundary");
|
|
170
|
+
}
|
|
171
|
+
const maxBytes = positiveLimit(options.maxBytes, maxReadBytes, "maxBytes");
|
|
172
|
+
let handle;
|
|
173
|
+
try {
|
|
174
|
+
handle = await open(realTarget, constants.O_RDONLY | NOFOLLOW_FLAG);
|
|
175
|
+
const info = await handle.stat();
|
|
176
|
+
if (!info.isFile()) {
|
|
177
|
+
throw new ManagedFileError("FILE_NOT_REGULAR", "managed path is not a regular file");
|
|
178
|
+
}
|
|
179
|
+
const bytes = await readHandle(handle, maxBytes, options.signal);
|
|
180
|
+
return {
|
|
181
|
+
ref: ManagedFileRefSchema.parse({ path: resolved.path, size: bytes.byteLength }),
|
|
182
|
+
bytes
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (options.signal?.aborted && error === options.signal.reason)
|
|
186
|
+
throw error;
|
|
187
|
+
if (errorCode(error) === "ENOENT") {
|
|
188
|
+
throw new ManagedFileError("FILE_NOT_FOUND", "managed file not found");
|
|
189
|
+
}
|
|
190
|
+
throw ioError("failed to read managed file", error);
|
|
191
|
+
} finally {
|
|
192
|
+
await handle?.close().catch(() => {
|
|
193
|
+
return;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
async write(path, source, options = {}) {
|
|
198
|
+
options.signal?.throwIfAborted();
|
|
199
|
+
const resolved = targetFor(path);
|
|
200
|
+
const parent = await existingParent(root, resolved.target);
|
|
201
|
+
const maxBytes = positiveLimit(options.maxBytes, maxWriteBytes, "maxBytes");
|
|
202
|
+
const temporary = resolve(parent, `.stitchkit-${randomUUID()}.tmp`);
|
|
203
|
+
let handle;
|
|
204
|
+
let committed = false;
|
|
205
|
+
let size = 0;
|
|
206
|
+
try {
|
|
207
|
+
handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | NOFOLLOW_FLAG, 384);
|
|
208
|
+
const written = await writeSource(handle, source, maxBytes, inspectionBytes, options.signal);
|
|
209
|
+
size = written.size;
|
|
210
|
+
if (options.durable)
|
|
211
|
+
await handle.sync();
|
|
212
|
+
await handle.close();
|
|
213
|
+
handle = undefined;
|
|
214
|
+
let inspection = {};
|
|
215
|
+
if (config.inspect) {
|
|
216
|
+
try {
|
|
217
|
+
inspection = await config.inspect({
|
|
218
|
+
prefix: written.prefix,
|
|
219
|
+
declaredMediaType: options.mediaType,
|
|
220
|
+
name: options.name ?? basename(resolved.path)
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new ManagedFileError("FILE_INSPECTION_REJECTED", "managed file rejected by inspection", { cause: error });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
options.signal?.throwIfAborted();
|
|
227
|
+
if (options.replace) {
|
|
228
|
+
await rename(temporary, resolved.target);
|
|
229
|
+
committed = true;
|
|
230
|
+
} else {
|
|
231
|
+
try {
|
|
232
|
+
await link(temporary, resolved.target);
|
|
233
|
+
committed = true;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (errorCode(error) === "EEXIST") {
|
|
236
|
+
throw new ManagedFileError("FILE_EXISTS", "managed file already exists");
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const mediaType = inspection.mediaType ?? options.mediaType;
|
|
242
|
+
const name = inspection.name ?? options.name;
|
|
243
|
+
const ref = ManagedFileRefSchema.parse({
|
|
244
|
+
path: resolved.path,
|
|
245
|
+
size,
|
|
246
|
+
...mediaType ? { mediaType } : {},
|
|
247
|
+
...name ? { name } : {}
|
|
248
|
+
});
|
|
249
|
+
return ref;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (options.signal?.aborted && error === options.signal.reason)
|
|
252
|
+
throw error;
|
|
253
|
+
throw ioError("failed to write managed file", error);
|
|
254
|
+
} finally {
|
|
255
|
+
await handle?.close().catch(() => {
|
|
256
|
+
return;
|
|
257
|
+
});
|
|
258
|
+
if (!options.replace || !committed) {
|
|
259
|
+
await unlink(temporary).catch((error) => {
|
|
260
|
+
if (errorCode(error) !== "ENOENT")
|
|
261
|
+
config.onCleanupError?.(error);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export { ManagedFileError, createManagedFileBoundary };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/contract/file-ref.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
function isManagedFilePath(value) {
|
|
4
|
+
if (value.length === 0 || value.includes("\x00") || value.includes("\\"))
|
|
5
|
+
return false;
|
|
6
|
+
if (value.startsWith("/") || value.startsWith("//") || /^[A-Za-z]:/.test(value)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const parts = value.split("/");
|
|
10
|
+
return parts.every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
11
|
+
}
|
|
12
|
+
var ManagedFilePathSchema = z.string().refine(isManagedFilePath, "expected a canonical relative managed-file path");
|
|
13
|
+
var ManagedFileRefSchema = z.object({
|
|
14
|
+
path: ManagedFilePathSchema,
|
|
15
|
+
size: z.number().int().nonnegative(),
|
|
16
|
+
mediaType: z.string().min(1).optional(),
|
|
17
|
+
name: z.string().min(1).optional()
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export { ManagedFilePathSchema, ManagedFileRefSchema };
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
-
|
|
4
1
|
// src/observability/trace.ts
|
|
5
2
|
var TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(.*)$/i;
|
|
6
3
|
function randomHex(bytes) {
|
|
@@ -149,4 +146,4 @@ function mapObject(source, mapper) {
|
|
|
149
146
|
return result;
|
|
150
147
|
}
|
|
151
148
|
|
|
152
|
-
export {
|
|
149
|
+
export { parseTrailingWildcard, typedEntries, isRecord, callRuntimeHandler, transportResult, mapObject, createTraceContext, parseTraceparent, formatTraceparent, resolveTraceContext, childSpan, resolvePropagationContext };
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
// src/internal/within-dir.ts
|
|
2
|
-
import { realpath } from "node:fs/promises";
|
|
3
|
-
import { sep } from "node:path";
|
|
4
|
-
function isWithinDir(root, target) {
|
|
5
|
-
const base = root.endsWith(sep) ? root.slice(0, -sep.length) : root;
|
|
6
|
-
return target === root || target === base || target.startsWith(base + sep);
|
|
7
|
-
}
|
|
8
|
-
async function realPathWithinDir(root, target) {
|
|
9
|
-
const [realRoot, realTarget] = await Promise.all([
|
|
10
|
-
realpath(root).catch(() => null),
|
|
11
|
-
realpath(target).catch(() => null)
|
|
12
|
-
]);
|
|
13
|
-
if (realRoot === null || realTarget === null)
|
|
14
|
-
return null;
|
|
15
|
-
return isWithinDir(realRoot, realTarget) ? realTarget : null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
1
|
// src/tools/json-schema.ts
|
|
19
2
|
import { z } from "zod";
|
|
20
3
|
function toJsonSchema(schema, io, unrepresentable = "throw", target = "draft-2020-12") {
|
|
@@ -60,4 +43,4 @@ function jsonSchemaFields(jsonSchema) {
|
|
|
60
43
|
return fields;
|
|
61
44
|
}
|
|
62
45
|
|
|
63
|
-
export {
|
|
46
|
+
export { toJsonSchema, jsonSchemaFields };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/contract/errors.ts
|
|
2
|
+
var APP_ERROR_BRAND = Symbol.for("stitchkit.AppError");
|
|
3
|
+
|
|
4
|
+
class AppError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
details;
|
|
8
|
+
hint;
|
|
9
|
+
traceId;
|
|
10
|
+
constructor(code, message, status = 500, details, hint, traceId) {
|
|
11
|
+
super(message ?? code);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.details = details;
|
|
15
|
+
this.hint = hint;
|
|
16
|
+
this.traceId = traceId;
|
|
17
|
+
this.name = "AppError";
|
|
18
|
+
Object.defineProperty(this, APP_ERROR_BRAND, { value: true });
|
|
19
|
+
}
|
|
20
|
+
static is(err) {
|
|
21
|
+
return typeof err === "object" && err !== null && APP_ERROR_BRAND in err;
|
|
22
|
+
}
|
|
23
|
+
toJSON() {
|
|
24
|
+
return {
|
|
25
|
+
error: {
|
|
26
|
+
code: this.code,
|
|
27
|
+
message: this.message,
|
|
28
|
+
...this.details && { details: this.details },
|
|
29
|
+
...this.hint && { hint: this.hint }
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function notFound(message = "Not found") {
|
|
35
|
+
throw new AppError("NOT_FOUND", message, 404);
|
|
36
|
+
}
|
|
37
|
+
function badRequest(message, details) {
|
|
38
|
+
throw new AppError("BAD_REQUEST", message, 400, details);
|
|
39
|
+
}
|
|
40
|
+
function unauthorized(message = "Unauthorized") {
|
|
41
|
+
throw new AppError("UNAUTHORIZED", message, 401);
|
|
42
|
+
}
|
|
43
|
+
function forbidden(message = "Forbidden") {
|
|
44
|
+
throw new AppError("FORBIDDEN", message, 403);
|
|
45
|
+
}
|
|
46
|
+
function conflict(message = "Conflict", details) {
|
|
47
|
+
throw new AppError("CONFLICT", message, 409, details);
|
|
48
|
+
}
|
|
49
|
+
function rateLimited(message = "Too many requests") {
|
|
50
|
+
throw new AppError("RATE_LIMITED", message, 429);
|
|
51
|
+
}
|
|
52
|
+
var STITCH_ERROR_STATUS = {
|
|
53
|
+
BAD_REQUEST: 400,
|
|
54
|
+
UNAUTHORIZED: 401,
|
|
55
|
+
FORBIDDEN: 403,
|
|
56
|
+
NOT_FOUND: 404,
|
|
57
|
+
METHOD_NOT_ALLOWED: 405,
|
|
58
|
+
CONFLICT: 409,
|
|
59
|
+
RATE_LIMITED: 429,
|
|
60
|
+
VALIDATION_ERROR: 400,
|
|
61
|
+
REALTIME_CONTRACT_VIOLATION: 500,
|
|
62
|
+
INTERNAL_SERVER_ERROR: 500
|
|
63
|
+
};
|
|
64
|
+
function isStitchErrorCode(code) {
|
|
65
|
+
return Object.hasOwn(STITCH_ERROR_STATUS, code);
|
|
66
|
+
}
|
|
67
|
+
function appError(code, message, details) {
|
|
68
|
+
throw new AppError(code, message, isStitchErrorCode(code) ? STITCH_ERROR_STATUS[code] : 500, details);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/contract/define.ts
|
|
72
|
+
import { z } from "zod";
|
|
73
|
+
|
|
74
|
+
// src/internal/safe-json.ts
|
|
75
|
+
function isUnsafeKey(key) {
|
|
76
|
+
return key === "__proto__";
|
|
77
|
+
}
|
|
78
|
+
function safeJsonParse(text) {
|
|
79
|
+
return JSON.parse(text, (key, value) => isUnsafeKey(key) ? undefined : value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/contract/define.ts
|
|
83
|
+
function mergeMeta(contractMeta, endpointMeta) {
|
|
84
|
+
if (!contractMeta)
|
|
85
|
+
return endpointMeta;
|
|
86
|
+
if (!endpointMeta)
|
|
87
|
+
return { ...contractMeta };
|
|
88
|
+
return { ...contractMeta, ...endpointMeta };
|
|
89
|
+
}
|
|
90
|
+
// src/contract/errors-factory.ts
|
|
91
|
+
import { z as z2 } from "zod";
|
|
92
|
+
// src/contract/pagination.ts
|
|
93
|
+
import { z as z3 } from "zod";
|
|
94
|
+
|
|
95
|
+
// src/internal/base64url.ts
|
|
96
|
+
function bytesToBase64Url(bytes) {
|
|
97
|
+
let binary = "";
|
|
98
|
+
for (const byte of bytes)
|
|
99
|
+
binary += String.fromCharCode(byte);
|
|
100
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
101
|
+
}
|
|
102
|
+
function base64UrlToBytes(segment) {
|
|
103
|
+
if (!/^[A-Za-z0-9_-]*$/.test(segment) || segment.length % 4 === 1) {
|
|
104
|
+
throw new Error("invalid base64url segment");
|
|
105
|
+
}
|
|
106
|
+
const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
107
|
+
const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, "=");
|
|
108
|
+
return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
109
|
+
}
|
|
110
|
+
export { isUnsafeKey, safeJsonParse, mergeMeta, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, bytesToBase64Url, base64UrlToBytes };
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mergeContextContribution
|
|
3
|
+
} from "./index-pzc32v75.js";
|
|
1
4
|
import {
|
|
2
5
|
base64UrlToBytes,
|
|
3
6
|
bytesToBase64Url,
|
|
@@ -5,10 +8,10 @@ import {
|
|
|
5
8
|
isUnsafeKey,
|
|
6
9
|
safeJsonParse,
|
|
7
10
|
unauthorized
|
|
8
|
-
} from "./index-
|
|
11
|
+
} from "./index-h60df7rj.js";
|
|
9
12
|
import {
|
|
10
13
|
isRecord
|
|
11
|
-
} from "./index-
|
|
14
|
+
} from "./index-7eq9va6e.js";
|
|
12
15
|
|
|
13
16
|
// src/server/middleware/cookies.ts
|
|
14
17
|
function parseCookies(header) {
|
|
@@ -151,7 +154,11 @@ function extractToken(req, cookieName) {
|
|
|
151
154
|
return null;
|
|
152
155
|
}
|
|
153
156
|
function isThenable(value) {
|
|
154
|
-
|
|
157
|
+
try {
|
|
158
|
+
return "then" in value && typeof value.then === "function";
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
155
162
|
}
|
|
156
163
|
function createAuthHook(config) {
|
|
157
164
|
const onAnonymous = config.onAnonymous ?? (() => unauthorized());
|
|
@@ -169,10 +176,10 @@ function createAuthHook(config) {
|
|
|
169
176
|
const rule = typeof entry === "object" ? entry.rule : entry;
|
|
170
177
|
if (typeof entry === "object" && entry.inject && identity !== null) {
|
|
171
178
|
const fields = entry.inject(identity, ctx);
|
|
172
|
-
if (isThenable(fields)) {
|
|
179
|
+
if (typeof fields === "object" && fields !== null && isThenable(fields)) {
|
|
173
180
|
throw new Error(`[stitchkit] auth: the inject of scope "${scope}" must be synchronous — an async inject merges a Promise, not fields`);
|
|
174
181
|
}
|
|
175
|
-
|
|
182
|
+
mergeContextContribution(ctx, fields, scope);
|
|
176
183
|
}
|
|
177
184
|
if (rule === "public")
|
|
178
185
|
return;
|
|
@@ -182,8 +189,14 @@ function createAuthHook(config) {
|
|
|
182
189
|
}
|
|
183
190
|
if (rule === "authenticated")
|
|
184
191
|
return;
|
|
185
|
-
|
|
192
|
+
const result = await rule(identity, ctx);
|
|
193
|
+
if (result === false) {
|
|
186
194
|
onForbidden();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (result === true)
|
|
198
|
+
return;
|
|
199
|
+
mergeContextContribution(ctx, result, scope);
|
|
187
200
|
}
|
|
188
201
|
return auth;
|
|
189
202
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/tools/names.ts
|
|
2
|
+
var SINGULAR_EXCEPTIONS = new Set([
|
|
3
|
+
"analytics",
|
|
4
|
+
"status",
|
|
5
|
+
"stats",
|
|
6
|
+
"settings",
|
|
7
|
+
"media",
|
|
8
|
+
"progress",
|
|
9
|
+
"news"
|
|
10
|
+
]);
|
|
11
|
+
var TOOL_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
12
|
+
function normalizeService(value) {
|
|
13
|
+
return value.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
14
|
+
}
|
|
15
|
+
function normalizeMethod(value) {
|
|
16
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
17
|
+
}
|
|
18
|
+
function singularize(name) {
|
|
19
|
+
if (SINGULAR_EXCEPTIONS.has(name))
|
|
20
|
+
return name;
|
|
21
|
+
if (name.endsWith("ies"))
|
|
22
|
+
return `${name.slice(0, -3)}y`;
|
|
23
|
+
if (name.endsWith("s") && !name.endsWith("ss"))
|
|
24
|
+
return name.slice(0, -1);
|
|
25
|
+
return name;
|
|
26
|
+
}
|
|
27
|
+
function singularizeTail(name) {
|
|
28
|
+
const cut = name.lastIndexOf("_");
|
|
29
|
+
if (cut === -1)
|
|
30
|
+
return singularize(name);
|
|
31
|
+
return `${name.slice(0, cut + 1)}${singularize(name.slice(cut + 1))}`;
|
|
32
|
+
}
|
|
33
|
+
function hasUsableChars(value) {
|
|
34
|
+
return /[a-zA-Z0-9]/.test(value);
|
|
35
|
+
}
|
|
36
|
+
function assertToolName(name, serviceName, key) {
|
|
37
|
+
const where = `service "${serviceName}", method "${key}"`;
|
|
38
|
+
if (!TOOL_NAME_RE.test(name)) {
|
|
39
|
+
const why = name.length > 64 ? `is ${name.length} characters (max 64) — set an explicit \`toolName\`` : "must match [a-zA-Z0-9_-]";
|
|
40
|
+
throw new Error(`Tool name "${name}" (${where}) ${why}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function toToolName(serviceName, methodName) {
|
|
44
|
+
const normalized = normalizeService(serviceName);
|
|
45
|
+
const singular = singularizeTail(normalized);
|
|
46
|
+
const method = normalizeMethod(methodName);
|
|
47
|
+
if (method === "list")
|
|
48
|
+
return `list_${normalized}`;
|
|
49
|
+
if (method === "get")
|
|
50
|
+
return `get_${singular}`;
|
|
51
|
+
if (method === "create")
|
|
52
|
+
return `create_${singular}`;
|
|
53
|
+
if (method === "update")
|
|
54
|
+
return `update_${singular}`;
|
|
55
|
+
if (method === "delete")
|
|
56
|
+
return `delete_${singular}`;
|
|
57
|
+
const snake = method.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
58
|
+
return `${snake}_${singular}`.replace(/^_/, "");
|
|
59
|
+
}
|
|
60
|
+
function assertUniqueToolName(name, taken, surface) {
|
|
61
|
+
if (taken) {
|
|
62
|
+
throw new Error(`Duplicate ${surface} "${name}" across mounted operations`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { hasUsableChars, assertToolName, toToolName, assertUniqueToolName };
|
|
@@ -1,39 +1,44 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_PROCESS_SIGNALS,
|
|
3
|
+
RUNTIME_CONTEXT_RESERVED_KEYS,
|
|
3
4
|
assertCorsConfig,
|
|
4
5
|
corsHeaders,
|
|
5
6
|
corsPreflightResponse,
|
|
6
7
|
defaultSignalSource,
|
|
7
8
|
guardSignalCallback,
|
|
8
9
|
reportSignalError
|
|
9
|
-
} from "./index-
|
|
10
|
+
} from "./index-pzc32v75.js";
|
|
10
11
|
import {
|
|
11
|
-
AppError,
|
|
12
|
-
badRequest,
|
|
13
12
|
errorCode,
|
|
14
13
|
extractIp,
|
|
15
14
|
getClientInfo,
|
|
16
15
|
getRequestContext,
|
|
17
|
-
isUnsafeKey,
|
|
18
16
|
normalizeError,
|
|
19
17
|
parseQueryParams,
|
|
20
18
|
recordedErrorMessage,
|
|
21
19
|
resolveSocketIp,
|
|
22
20
|
resolveTraceId,
|
|
23
21
|
runWithRequestContext,
|
|
24
|
-
safeJsonParse,
|
|
25
22
|
setRequestEndpoint,
|
|
26
23
|
setRequestError,
|
|
27
24
|
validateDeclaredOutput,
|
|
28
25
|
zodIssues
|
|
29
|
-
} from "./index-
|
|
26
|
+
} from "./index-v4bbq3p4.js";
|
|
27
|
+
import {
|
|
28
|
+
AppError,
|
|
29
|
+
badRequest,
|
|
30
|
+
isUnsafeKey,
|
|
31
|
+
safeJsonParse
|
|
32
|
+
} from "./index-h60df7rj.js";
|
|
30
33
|
import {
|
|
31
|
-
__require,
|
|
32
34
|
isRecord,
|
|
33
35
|
parseTrailingWildcard,
|
|
34
36
|
resolveTraceContext,
|
|
35
37
|
transportResult
|
|
36
|
-
} from "./index-
|
|
38
|
+
} from "./index-7eq9va6e.js";
|
|
39
|
+
import {
|
|
40
|
+
__require
|
|
41
|
+
} from "./index-1bx83sw4.js";
|
|
37
42
|
|
|
38
43
|
// src/server/multipart.ts
|
|
39
44
|
var DEFAULT_MAX_REQUEST_BYTES = 25 * 1024 * 1024;
|
|
@@ -500,19 +505,6 @@ async function readRequestText(req, maxBytes) {
|
|
|
500
505
|
}
|
|
501
506
|
|
|
502
507
|
// src/server/context.ts
|
|
503
|
-
var RESERVED_KEYS = new Set([
|
|
504
|
-
"params",
|
|
505
|
-
"input",
|
|
506
|
-
"source",
|
|
507
|
-
"req",
|
|
508
|
-
"url",
|
|
509
|
-
"headers",
|
|
510
|
-
"rawBody",
|
|
511
|
-
"traceId",
|
|
512
|
-
"spanId",
|
|
513
|
-
"ipAddress",
|
|
514
|
-
"userAgent"
|
|
515
|
-
]);
|
|
516
508
|
function parseJsonBody(req, text) {
|
|
517
509
|
if (text.trim() === "")
|
|
518
510
|
return {};
|
|
@@ -528,7 +520,7 @@ function parseJsonBody(req, text) {
|
|
|
528
520
|
function buildBaseContext(req, url, pathParams, traceId, clientIp) {
|
|
529
521
|
const safePathParams = {};
|
|
530
522
|
for (const [k, v] of Object.entries(pathParams)) {
|
|
531
|
-
if (!
|
|
523
|
+
if (!RUNTIME_CONTEXT_RESERVED_KEYS.has(k) && !isUnsafeKey(k))
|
|
532
524
|
safePathParams[k] = v;
|
|
533
525
|
}
|
|
534
526
|
return {
|
|
@@ -538,6 +530,7 @@ function buildBaseContext(req, url, pathParams, traceId, clientIp) {
|
|
|
538
530
|
req,
|
|
539
531
|
url,
|
|
540
532
|
headers: req.headers,
|
|
533
|
+
signal: req.signal,
|
|
541
534
|
...safePathParams,
|
|
542
535
|
traceId,
|
|
543
536
|
...getClientInfo(req, clientIp)
|
|
@@ -585,6 +578,7 @@ function buildErrorContext(req, url, traceId, clientIp) {
|
|
|
585
578
|
req,
|
|
586
579
|
url,
|
|
587
580
|
headers: req.headers,
|
|
581
|
+
signal: req.signal,
|
|
588
582
|
traceId,
|
|
589
583
|
...getClientInfo(req, clientIp)
|
|
590
584
|
};
|