toolcraft-openapi 0.0.49 → 0.0.51
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/dist/mock/fetch.js +187 -19
- package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +17 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.js +33 -5
- package/node_modules/@poe-code/frontmatter/dist/parse.js +86 -14
- package/node_modules/@poe-code/frontmatter/dist/stringify.js +13 -0
- package/node_modules/auth-store/dist/create-secret-store.js +4 -3
- package/node_modules/auth-store/dist/encrypted-file-store.js +49 -2
- package/node_modules/auth-store/dist/keychain-store.js +11 -4
- package/package.json +2 -2
package/dist/mock/fetch.js
CHANGED
|
@@ -34,6 +34,10 @@ export async function mockFetch(options) {
|
|
|
34
34
|
body: parsedBody,
|
|
35
35
|
at: new Date()
|
|
36
36
|
});
|
|
37
|
+
const parameterErrors = validateRequestParameters(operation, requestUrl, headers, document);
|
|
38
|
+
if (parameterErrors.length > 0) {
|
|
39
|
+
return jsonResponse(422, { errors: parameterErrors });
|
|
40
|
+
}
|
|
37
41
|
if (operation.requestBodySchema !== undefined && parsedBody !== undefined) {
|
|
38
42
|
const errors = validateAgainstSchema(parsedBody, operation.requestBodySchema, document, "$");
|
|
39
43
|
if (errors.length > 0) {
|
|
@@ -46,7 +50,7 @@ export async function mockFetch(options) {
|
|
|
46
50
|
const fixture = await fixtureLoader(operation.operationId);
|
|
47
51
|
if (fixture !== undefined) {
|
|
48
52
|
const status = fixture.status ?? operation.defaultStatus;
|
|
49
|
-
const responseSchema = operation.responseSchemas
|
|
53
|
+
const responseSchema = findResponseSchema(operation.responseSchemas, status);
|
|
50
54
|
if (responseSchema !== undefined && fixture.body !== undefined) {
|
|
51
55
|
const errors = validateAgainstSchema(fixture.body, responseSchema, document, "$response");
|
|
52
56
|
if (errors.length > 0) {
|
|
@@ -110,13 +114,16 @@ function compileOperations(document) {
|
|
|
110
114
|
const operationId = resolvedOperation.operationId ?? `${method.toUpperCase()} ${pathTemplate}`;
|
|
111
115
|
const { defaultStatus, defaultExample, responseSchemas } = pickResponseMetadata(resolvedOperation, document);
|
|
112
116
|
const { schema: requestBodySchema, required: requestBodyRequired } = pickRequestBody(resolvedOperation, document);
|
|
117
|
+
const pathParameterNames = collectPathParameterNames(pathTemplate);
|
|
113
118
|
compiled.push({
|
|
114
119
|
method: method.toUpperCase(),
|
|
115
120
|
pathTemplate,
|
|
116
121
|
pathRegex: pathTemplateToRegex(pathTemplate),
|
|
122
|
+
pathParameterNames,
|
|
117
123
|
pathSpecificity: countPathPlaceholders(pathTemplate),
|
|
118
124
|
operationId,
|
|
119
125
|
operation: resolvedOperation,
|
|
126
|
+
parameters: collectCompiledParameters(pathItem.parameters, resolvedOperation.parameters, document),
|
|
120
127
|
defaultStatus,
|
|
121
128
|
defaultExample,
|
|
122
129
|
requestBodySchema,
|
|
@@ -131,14 +138,31 @@ function compileOperations(document) {
|
|
|
131
138
|
return compiled;
|
|
132
139
|
}
|
|
133
140
|
function countPathPlaceholders(template) {
|
|
134
|
-
return (template
|
|
141
|
+
return collectPathParameterNames(template).length;
|
|
135
142
|
}
|
|
136
143
|
function pathTemplateToRegex(template) {
|
|
137
144
|
// Escape regex metacharacters except for "{...}" placeholders, which become non-slash captures.
|
|
138
145
|
const pattern = template.replace(/[.*+?^${}()|[\]\\]/g, (match) => match === "{" || match === "}" ? match : `\\${match}`);
|
|
139
|
-
const withParams = pattern.replace(/\{[^}]+\}/g, "[^/]+");
|
|
146
|
+
const withParams = pattern.replace(/\{[^}]+\}/g, "([^/]+)");
|
|
140
147
|
return new RegExp(`^${withParams}$`);
|
|
141
148
|
}
|
|
149
|
+
function collectPathParameterNames(template) {
|
|
150
|
+
const names = [];
|
|
151
|
+
let index = 0;
|
|
152
|
+
while (index < template.length) {
|
|
153
|
+
const start = template.indexOf("{", index);
|
|
154
|
+
if (start === -1) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
const end = template.indexOf("}", start + 1);
|
|
158
|
+
if (end === -1) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
names.push(template.slice(start + 1, end));
|
|
162
|
+
index = end + 1;
|
|
163
|
+
}
|
|
164
|
+
return names;
|
|
165
|
+
}
|
|
142
166
|
function resolveOperation(operation, document) {
|
|
143
167
|
if (isReference(operation)) {
|
|
144
168
|
return resolveReference(operation, document);
|
|
@@ -147,10 +171,11 @@ function resolveOperation(operation, document) {
|
|
|
147
171
|
}
|
|
148
172
|
function pickResponseMetadata(operation, document) {
|
|
149
173
|
const responses = operation.responses ?? {};
|
|
150
|
-
const responseSchemas =
|
|
174
|
+
const responseSchemas = [];
|
|
175
|
+
const successResponses = [];
|
|
151
176
|
for (const [code, response] of Object.entries(responses)) {
|
|
152
|
-
const status =
|
|
153
|
-
if (
|
|
177
|
+
const status = parseResponseStatus(code);
|
|
178
|
+
if (status === undefined || response === undefined) {
|
|
154
179
|
continue;
|
|
155
180
|
}
|
|
156
181
|
const resolved = isReference(response)
|
|
@@ -158,27 +183,46 @@ function pickResponseMetadata(operation, document) {
|
|
|
158
183
|
: response;
|
|
159
184
|
const schema = extractResponseSchema(resolved, document);
|
|
160
185
|
if (schema !== undefined) {
|
|
161
|
-
responseSchemas.
|
|
186
|
+
responseSchemas.push({ ...status, schema });
|
|
187
|
+
}
|
|
188
|
+
if (isSuccessStatus(status.status)) {
|
|
189
|
+
successResponses.push({ status: status.status, response });
|
|
162
190
|
}
|
|
163
191
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.sort((a, b) => a - b);
|
|
168
|
-
if (successCodes.length === 0) {
|
|
192
|
+
successResponses.sort((a, b) => a.status - b.status);
|
|
193
|
+
const firstSuccess = successResponses[0];
|
|
194
|
+
if (firstSuccess === undefined) {
|
|
169
195
|
return { defaultStatus: 200, defaultExample: undefined, responseSchemas };
|
|
170
196
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
? resolveReference(response, document)
|
|
175
|
-
: response;
|
|
197
|
+
const resolvedResponse = isReference(firstSuccess.response)
|
|
198
|
+
? resolveReference(firstSuccess.response, document)
|
|
199
|
+
: firstSuccess.response;
|
|
176
200
|
return {
|
|
177
|
-
defaultStatus: status,
|
|
201
|
+
defaultStatus: firstSuccess.status,
|
|
178
202
|
defaultExample: extractExample(resolvedResponse, document),
|
|
179
203
|
responseSchemas
|
|
180
204
|
};
|
|
181
205
|
}
|
|
206
|
+
function parseResponseStatus(code) {
|
|
207
|
+
if (code.length === 3 && code[1]?.toUpperCase() === "X" && code[2]?.toUpperCase() === "X") {
|
|
208
|
+
const family = Number(code[0]);
|
|
209
|
+
return Number.isInteger(family) && family >= 1 && family <= 5
|
|
210
|
+
? { status: family * 100, range: true }
|
|
211
|
+
: undefined;
|
|
212
|
+
}
|
|
213
|
+
if (code.length !== 3 || [...code].some((character) => character < "0" || character > "9")) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
return { status: Number(code), range: false };
|
|
217
|
+
}
|
|
218
|
+
function isSuccessStatus(status) {
|
|
219
|
+
return status >= 200 && status < 300;
|
|
220
|
+
}
|
|
221
|
+
function findResponseSchema(schemas, status) {
|
|
222
|
+
return (schemas.find((candidate) => !candidate.range && candidate.status === status)?.schema ??
|
|
223
|
+
schemas.find((candidate) => candidate.range && Math.floor(status / 100) === candidate.status / 100)
|
|
224
|
+
?.schema);
|
|
225
|
+
}
|
|
182
226
|
function extractResponseSchema(response, document) {
|
|
183
227
|
if (response === undefined) {
|
|
184
228
|
return undefined;
|
|
@@ -211,6 +255,95 @@ function pickRequestBody(operation, document) {
|
|
|
211
255
|
: media.schema;
|
|
212
256
|
return { schema, required: resolved.required === true };
|
|
213
257
|
}
|
|
258
|
+
function collectCompiledParameters(pathParameters, operationParameters, document) {
|
|
259
|
+
const parameters = new Map();
|
|
260
|
+
for (const raw of [...(pathParameters ?? []), ...(operationParameters ?? [])]) {
|
|
261
|
+
const parameter = isReference(raw)
|
|
262
|
+
? resolveReference(raw, document)
|
|
263
|
+
: raw;
|
|
264
|
+
if (parameter.in !== "path" && parameter.in !== "query" && parameter.in !== "header") {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
parameters.set(`${parameter.in}:${parameter.name}`, {
|
|
268
|
+
name: parameter.name,
|
|
269
|
+
location: parameter.in,
|
|
270
|
+
required: parameter.in === "path" || parameter.required === true,
|
|
271
|
+
schema: parameter.schema
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return [...parameters.values()];
|
|
275
|
+
}
|
|
276
|
+
function validateRequestParameters(operation, requestUrl, headers, document) {
|
|
277
|
+
const pathValues = collectPathParameterValues(operation, requestUrl.pathname);
|
|
278
|
+
const errors = [];
|
|
279
|
+
for (const parameter of operation.parameters) {
|
|
280
|
+
const pointer = `$${parameter.location}/${parameter.name}`;
|
|
281
|
+
const value = readParameterValue(parameter, requestUrl, headers, pathValues);
|
|
282
|
+
if (value === undefined) {
|
|
283
|
+
if (parameter.required) {
|
|
284
|
+
errors.push(`${pointer}: required`);
|
|
285
|
+
}
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (parameter.schema !== undefined) {
|
|
289
|
+
errors.push(...validateAgainstSchema(value, parameter.schema, document, pointer));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return errors;
|
|
293
|
+
}
|
|
294
|
+
function collectPathParameterValues(operation, pathname) {
|
|
295
|
+
const match = operation.pathRegex.exec(pathname);
|
|
296
|
+
const values = new Map();
|
|
297
|
+
if (match === null) {
|
|
298
|
+
return values;
|
|
299
|
+
}
|
|
300
|
+
for (let index = 0; index < operation.pathParameterNames.length; index++) {
|
|
301
|
+
const value = match[index + 1];
|
|
302
|
+
if (value !== undefined) {
|
|
303
|
+
values.set(operation.pathParameterNames[index], decodeUrlComponent(value));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return values;
|
|
307
|
+
}
|
|
308
|
+
function readParameterValue(parameter, requestUrl, headers, pathValues) {
|
|
309
|
+
switch (parameter.location) {
|
|
310
|
+
case "path":
|
|
311
|
+
return coerceParameterValue(parameter.schema, pathValues.get(parameter.name));
|
|
312
|
+
case "query": {
|
|
313
|
+
const values = requestUrl.searchParams.getAll(parameter.name);
|
|
314
|
+
if (values.length === 0) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
return schemaAcceptsArray(parameter.schema) ? coerceParameterValues(values) : values[0];
|
|
318
|
+
}
|
|
319
|
+
case "header":
|
|
320
|
+
return coerceParameterValue(parameter.schema, headers[parameter.name.toLowerCase()]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function coerceParameterValue(schema, value) {
|
|
324
|
+
if (value === undefined || !schemaAcceptsArray(schema)) {
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
return coerceParameterValues([value]);
|
|
328
|
+
}
|
|
329
|
+
function coerceParameterValues(values) {
|
|
330
|
+
return values.flatMap((value) => value.split(","));
|
|
331
|
+
}
|
|
332
|
+
function schemaAcceptsArray(schema) {
|
|
333
|
+
if (schema === undefined || isReference(schema)) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const type = schema.type;
|
|
337
|
+
return Array.isArray(type) ? type.includes("array") : type === "array";
|
|
338
|
+
}
|
|
339
|
+
function decodeUrlComponent(value) {
|
|
340
|
+
try {
|
|
341
|
+
return decodeURIComponent(value);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
214
347
|
function extractExample(response, document) {
|
|
215
348
|
if (response === undefined) {
|
|
216
349
|
return undefined;
|
|
@@ -265,7 +398,7 @@ function resolveReference(reference, document) {
|
|
|
265
398
|
const segments = ref
|
|
266
399
|
.slice(2)
|
|
267
400
|
.split("/")
|
|
268
|
-
.map(
|
|
401
|
+
.map(decodeReferenceSegment);
|
|
269
402
|
let current = document;
|
|
270
403
|
for (const segment of segments) {
|
|
271
404
|
if (current === null || typeof current !== "object") {
|
|
@@ -281,6 +414,16 @@ function resolveReference(reference, document) {
|
|
|
281
414
|
}
|
|
282
415
|
return current;
|
|
283
416
|
}
|
|
417
|
+
function decodeReferenceSegment(segment) {
|
|
418
|
+
let decoded = segment;
|
|
419
|
+
try {
|
|
420
|
+
decoded = decodeURIComponent(segment);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
decoded = segment;
|
|
424
|
+
}
|
|
425
|
+
return decoded.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
426
|
+
}
|
|
284
427
|
function validateAgainstSchema(value, schema, document, pointer) {
|
|
285
428
|
const resolved = isReference(schema)
|
|
286
429
|
? resolveReference(schema, document)
|
|
@@ -334,11 +477,36 @@ function validateAgainstSchema(value, schema, document, pointer) {
|
|
|
334
477
|
errors.push(...validateAgainstSchema(value[i], resolved.items, document, `${pointer}/${i}`));
|
|
335
478
|
}
|
|
336
479
|
}
|
|
480
|
+
if (typeof value === "number") {
|
|
481
|
+
validateNumberConstraints(value, resolved, pointer, errors);
|
|
482
|
+
}
|
|
483
|
+
if (typeof value === "string") {
|
|
484
|
+
validateStringConstraints(value, resolved, pointer, errors);
|
|
485
|
+
}
|
|
337
486
|
if (resolved.enum !== undefined && !resolved.enum.includes(value)) {
|
|
338
487
|
errors.push(`${pointer}: not in enum`);
|
|
339
488
|
}
|
|
340
489
|
return errors;
|
|
341
490
|
}
|
|
491
|
+
function validateNumberConstraints(value, schema, pointer, errors) {
|
|
492
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
493
|
+
errors.push(`${pointer}: expected value to be >= ${schema.minimum}`);
|
|
494
|
+
}
|
|
495
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
496
|
+
errors.push(`${pointer}: expected value to be <= ${schema.maximum}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function validateStringConstraints(value, schema, pointer, errors) {
|
|
500
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
501
|
+
errors.push(`${pointer}: expected length to be >= ${schema.minLength}`);
|
|
502
|
+
}
|
|
503
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
504
|
+
errors.push(`${pointer}: expected length to be <= ${schema.maxLength}`);
|
|
505
|
+
}
|
|
506
|
+
if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) {
|
|
507
|
+
errors.push(`${pointer}: expected value to match pattern ${schema.pattern}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
342
510
|
function normalizeTypes(schema) {
|
|
343
511
|
const type = schema.type;
|
|
344
512
|
if (type === undefined) {
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
export type FrontmatterBlock = {
|
|
2
2
|
raw: string;
|
|
3
3
|
body: string;
|
|
4
|
+
rawStart: number;
|
|
5
|
+
rawEnd: number;
|
|
4
6
|
};
|
|
5
7
|
export type SplitFrontmatterResult = FrontmatterBlock | {
|
|
6
8
|
body: string;
|
|
7
9
|
};
|
|
10
|
+
export type InspectFrontmatterResult = (FrontmatterBlock & {
|
|
11
|
+
kind: "frontmatter";
|
|
12
|
+
}) | {
|
|
13
|
+
kind: "missing-closing-fence";
|
|
14
|
+
raw: string;
|
|
15
|
+
rawStart: number;
|
|
16
|
+
rawEnd: number;
|
|
17
|
+
body: "";
|
|
18
|
+
message: string;
|
|
19
|
+
position: number;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "body";
|
|
22
|
+
body: string;
|
|
23
|
+
};
|
|
8
24
|
export declare function splitFrontmatterBlock(source: string): SplitFrontmatterResult;
|
|
25
|
+
export declare function inspectFrontmatterBlock(source: string): InspectFrontmatterResult;
|
|
@@ -1,15 +1,43 @@
|
|
|
1
|
+
const MISSING_END_DELIMITER_MESSAGE = "Missing YAML frontmatter end delimiter (---).";
|
|
1
2
|
export function splitFrontmatterBlock(source) {
|
|
3
|
+
const inspected = inspectFrontmatterBlock(source);
|
|
4
|
+
if (inspected.kind === "body") {
|
|
5
|
+
return { body: inspected.body };
|
|
6
|
+
}
|
|
7
|
+
if (inspected.kind === "missing-closing-fence") {
|
|
8
|
+
throw new Error(inspected.message);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
raw: inspected.raw,
|
|
12
|
+
rawStart: inspected.rawStart,
|
|
13
|
+
rawEnd: inspected.rawEnd,
|
|
14
|
+
body: inspected.body
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function inspectFrontmatterBlock(source) {
|
|
2
18
|
const content = source.startsWith("\uFEFF") ? source.slice(1) : source;
|
|
19
|
+
const sourceOffset = source.length - content.length;
|
|
3
20
|
const opening = readOpeningFence(content);
|
|
4
21
|
if (opening === undefined) {
|
|
5
|
-
return { body: source };
|
|
22
|
+
return { kind: "body", body: source };
|
|
6
23
|
}
|
|
7
24
|
const closing = findClosingFence(content, opening.next);
|
|
8
25
|
if (closing === undefined) {
|
|
9
|
-
|
|
26
|
+
return {
|
|
27
|
+
kind: "missing-closing-fence",
|
|
28
|
+
raw: content.slice(opening.next),
|
|
29
|
+
rawStart: sourceOffset + opening.next,
|
|
30
|
+
rawEnd: source.length,
|
|
31
|
+
body: "",
|
|
32
|
+
message: MISSING_END_DELIMITER_MESSAGE,
|
|
33
|
+
position: source.length
|
|
34
|
+
};
|
|
10
35
|
}
|
|
11
36
|
return {
|
|
37
|
+
kind: "frontmatter",
|
|
12
38
|
raw: content.slice(opening.next, closing.index),
|
|
39
|
+
rawStart: sourceOffset + opening.next,
|
|
40
|
+
rawEnd: sourceOffset + closing.index,
|
|
13
41
|
body: content.slice(closing.end + closing.lineBreakLength)
|
|
14
42
|
};
|
|
15
43
|
}
|
|
@@ -18,7 +46,7 @@ function readOpeningFence(source) {
|
|
|
18
46
|
return undefined;
|
|
19
47
|
}
|
|
20
48
|
const lineEnd = findLineEnd(source, 0);
|
|
21
|
-
if (lineEnd.lineBreakLength === 0 || source.slice(0, lineEnd.index)
|
|
49
|
+
if (lineEnd.lineBreakLength === 0 || !isFenceLine(source.slice(0, lineEnd.index))) {
|
|
22
50
|
return undefined;
|
|
23
51
|
}
|
|
24
52
|
return { next: lineEnd.index + lineEnd.lineBreakLength };
|
|
@@ -27,7 +55,7 @@ function findClosingFence(source, start) {
|
|
|
27
55
|
let lineStart = start;
|
|
28
56
|
while (lineStart <= source.length) {
|
|
29
57
|
const lineEnd = findLineEnd(source, lineStart);
|
|
30
|
-
if (
|
|
58
|
+
if (isFenceLine(source.slice(lineStart, lineEnd.index))) {
|
|
31
59
|
return {
|
|
32
60
|
index: lineStart,
|
|
33
61
|
end: lineEnd.index,
|
|
@@ -41,7 +69,7 @@ function findClosingFence(source, start) {
|
|
|
41
69
|
}
|
|
42
70
|
return undefined;
|
|
43
71
|
}
|
|
44
|
-
function
|
|
72
|
+
function isFenceLine(line) {
|
|
45
73
|
if (!line.startsWith("---")) {
|
|
46
74
|
return false;
|
|
47
75
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LineCounter, parse, parseDocument } from "yaml";
|
|
2
|
-
import { splitFrontmatterBlock } from "./fences.js";
|
|
2
|
+
import { inspectFrontmatterBlock, splitFrontmatterBlock } from "./fences.js";
|
|
3
3
|
export class FrontmatterParseError extends Error {
|
|
4
4
|
constructor(message) {
|
|
5
5
|
super(message);
|
|
@@ -20,9 +20,9 @@ export function parseFrontmatter(source, options = {}) {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
export function parseFrontmatterDocument(source, options = {}) {
|
|
23
|
-
const split =
|
|
24
|
-
const lineCounter =
|
|
25
|
-
if (split.
|
|
23
|
+
const split = inspectFrontmatterBlock(source);
|
|
24
|
+
const lineCounter = createSourceLineCounter(source);
|
|
25
|
+
if (split.kind === "body") {
|
|
26
26
|
return {
|
|
27
27
|
frontmatter: {},
|
|
28
28
|
body: split.body,
|
|
@@ -30,14 +30,26 @@ export function parseFrontmatterDocument(source, options = {}) {
|
|
|
30
30
|
lineCounter
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
if (split.kind === "missing-closing-fence") {
|
|
34
|
+
return {
|
|
35
|
+
frontmatter: {},
|
|
36
|
+
body: split.body,
|
|
37
|
+
errors: [{ message: split.message, pos: [split.position, split.position] }],
|
|
38
|
+
lineCounter
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const yamlLineCounter = new LineCounter();
|
|
42
|
+
const normalizedYaml = normalizeYamlLineEndings(split.raw);
|
|
43
|
+
const document = parseDocument(normalizedYaml, {
|
|
44
|
+
lineCounter: yamlLineCounter,
|
|
35
45
|
prettyErrors: false,
|
|
36
46
|
uniqueKeys: options.uniqueKeys ?? false
|
|
37
47
|
});
|
|
38
48
|
const errors = document.errors.map((error) => ({
|
|
39
49
|
message: error.message,
|
|
40
|
-
...(error.pos === undefined
|
|
50
|
+
...(error.pos === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { pos: translateYamlErrorPosition(error.pos, split) })
|
|
41
53
|
}));
|
|
42
54
|
if (errors.length > 0) {
|
|
43
55
|
return {
|
|
@@ -47,12 +59,72 @@ export function parseFrontmatterDocument(source, options = {}) {
|
|
|
47
59
|
lineCounter
|
|
48
60
|
};
|
|
49
61
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
try {
|
|
63
|
+
return {
|
|
64
|
+
frontmatter: normalizeYamlFrontmatter(document.toJSON()),
|
|
65
|
+
body: split.body,
|
|
66
|
+
errors,
|
|
67
|
+
lineCounter
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof FrontmatterParseError) {
|
|
72
|
+
return {
|
|
73
|
+
frontmatter: {},
|
|
74
|
+
body: split.body,
|
|
75
|
+
errors: [{ message: error.message }],
|
|
76
|
+
lineCounter
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function createSourceLineCounter(source) {
|
|
83
|
+
const lineCounter = new LineCounter();
|
|
84
|
+
lineCounter.addNewLine(0);
|
|
85
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
86
|
+
const character = source[index];
|
|
87
|
+
if (character === "\n") {
|
|
88
|
+
lineCounter.addNewLine(index + 1);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (character === "\r") {
|
|
92
|
+
lineCounter.addNewLine(index + (source[index + 1] === "\n" ? 2 : 1));
|
|
93
|
+
if (source[index + 1] === "\n") {
|
|
94
|
+
index += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lineCounter;
|
|
99
|
+
}
|
|
100
|
+
function translateYamlErrorPosition(pos, split) {
|
|
101
|
+
return pos.map((offset) => split.rawStart + normalizeYamlErrorOffset(split.raw, offset));
|
|
102
|
+
}
|
|
103
|
+
function normalizeYamlErrorOffset(raw, offset) {
|
|
104
|
+
if (offset >= raw.length && endsWithLineBreak(raw)) {
|
|
105
|
+
return findPreviousLineStart(raw, raw.length);
|
|
106
|
+
}
|
|
107
|
+
return offset;
|
|
108
|
+
}
|
|
109
|
+
function endsWithLineBreak(value) {
|
|
110
|
+
return value.endsWith("\n") || value.endsWith("\r");
|
|
111
|
+
}
|
|
112
|
+
function findPreviousLineStart(value, end) {
|
|
113
|
+
let index = end - 1;
|
|
114
|
+
if (value[index] === "\n") {
|
|
115
|
+
index -= 1;
|
|
116
|
+
}
|
|
117
|
+
if (value[index] === "\r") {
|
|
118
|
+
index -= 1;
|
|
119
|
+
}
|
|
120
|
+
while (index >= 0) {
|
|
121
|
+
const character = value[index];
|
|
122
|
+
if (character === "\n" || character === "\r") {
|
|
123
|
+
return index + 1;
|
|
124
|
+
}
|
|
125
|
+
index -= 1;
|
|
126
|
+
}
|
|
127
|
+
return 0;
|
|
56
128
|
}
|
|
57
129
|
function splitFrontmatter(source) {
|
|
58
130
|
try {
|
|
@@ -102,7 +174,7 @@ function normalizeYamlFrontmatter(value) {
|
|
|
102
174
|
if (value === null || value === undefined) {
|
|
103
175
|
return {};
|
|
104
176
|
}
|
|
105
|
-
if (!
|
|
177
|
+
if (!isPlainRecord(value)) {
|
|
106
178
|
throw new FrontmatterParseError("YAML frontmatter must parse to an object.");
|
|
107
179
|
}
|
|
108
180
|
return normalizeYamlValue(value);
|
|
@@ -2,6 +2,7 @@ import { stringify } from "yaml";
|
|
|
2
2
|
import { FrontmatterParseError } from "./parse.js";
|
|
3
3
|
export function stringifyFrontmatter(frontmatter, body) {
|
|
4
4
|
try {
|
|
5
|
+
assertFrontmatterRoot(frontmatter);
|
|
5
6
|
assertAcyclic(frontmatter);
|
|
6
7
|
return `---\n${stringify(frontmatter, { aliasDuplicateObjects: false }).trimEnd()}\n---\n${body}`;
|
|
7
8
|
}
|
|
@@ -13,6 +14,11 @@ export function stringifyFrontmatter(frontmatter, body) {
|
|
|
13
14
|
throw new FrontmatterParseError(`Invalid YAML frontmatter: ${message}`);
|
|
14
15
|
}
|
|
15
16
|
}
|
|
17
|
+
function assertFrontmatterRoot(value) {
|
|
18
|
+
if (!isPlainRecord(value)) {
|
|
19
|
+
throw new FrontmatterParseError("YAML frontmatter must parse to an object.");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
16
22
|
function assertAcyclic(value, seen = new WeakSet()) {
|
|
17
23
|
if (typeof value !== "object" || value === null) {
|
|
18
24
|
return;
|
|
@@ -33,3 +39,10 @@ function assertAcyclic(value, seen = new WeakSet()) {
|
|
|
33
39
|
}
|
|
34
40
|
seen.delete(value);
|
|
35
41
|
}
|
|
42
|
+
function isPlainRecord(value) {
|
|
43
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const prototype = Object.getPrototypeOf(value);
|
|
47
|
+
return prototype === Object.prototype || prototype === null;
|
|
48
|
+
}
|
|
@@ -28,13 +28,14 @@ export function createSecretStore(input) {
|
|
|
28
28
|
function resolveBackend(input) {
|
|
29
29
|
const envVar = input.backendEnvVar ?? DEFAULT_BACKEND_ENV_VAR;
|
|
30
30
|
const configuredBackend = input.backend ?? getOwnEnvValue(input.env, envVar) ?? getOwnEnvValue(process.env, envVar);
|
|
31
|
-
|
|
31
|
+
const backend = configuredBackend?.trim();
|
|
32
|
+
if (backend === "keychain") {
|
|
32
33
|
return "keychain";
|
|
33
34
|
}
|
|
34
|
-
if (
|
|
35
|
+
if (backend === undefined || backend === "file") {
|
|
35
36
|
return "file";
|
|
36
37
|
}
|
|
37
|
-
throw new Error(`Unsupported auth store backend: ${
|
|
38
|
+
throw new Error(`Unsupported auth store backend: ${backend}`);
|
|
38
39
|
}
|
|
39
40
|
function getOwnEnvValue(env, key) {
|
|
40
41
|
return env !== undefined && Object.prototype.hasOwnProperty.call(env, key)
|
|
@@ -24,7 +24,10 @@ export class EncryptedFileStore {
|
|
|
24
24
|
if (input.filePath === undefined) {
|
|
25
25
|
const homeDirectory = (input.getHomeDirectory ?? homedir)();
|
|
26
26
|
const defaultDirectory = input.defaultDirectory ?? ".auth-store";
|
|
27
|
-
|
|
27
|
+
const defaultFileName = input.defaultFileName ?? "credentials.enc";
|
|
28
|
+
assertSafeDefaultDirectory(defaultDirectory);
|
|
29
|
+
assertSafeDefaultFileName(defaultFileName);
|
|
30
|
+
this.filePath = path.join(homeDirectory, defaultDirectory, defaultFileName);
|
|
28
31
|
this.symbolicLinkCheckStartPath = resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory);
|
|
29
32
|
}
|
|
30
33
|
else {
|
|
@@ -154,7 +157,7 @@ function resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory) {
|
|
|
154
157
|
}
|
|
155
158
|
function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
|
|
156
159
|
if (symbolicLinkCheckStartPath === null) {
|
|
157
|
-
return
|
|
160
|
+
return getExplicitProtectedCredentialPaths(resolvedPath);
|
|
158
161
|
}
|
|
159
162
|
const resolvedStartPath = path.resolve(symbolicLinkCheckStartPath);
|
|
160
163
|
if (!isPathInsideOrEqual(resolvedPath, resolvedStartPath)) {
|
|
@@ -168,6 +171,50 @@ function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
|
|
|
168
171
|
}
|
|
169
172
|
return protectedPaths;
|
|
170
173
|
}
|
|
174
|
+
function assertSafeDefaultDirectory(defaultDirectory) {
|
|
175
|
+
if (path.isAbsolute(defaultDirectory) || path.win32.isAbsolute(defaultDirectory)) {
|
|
176
|
+
throw new Error("defaultDirectory must be a relative path inside the home directory");
|
|
177
|
+
}
|
|
178
|
+
for (const segment of splitPathSegments(defaultDirectory)) {
|
|
179
|
+
if (segment === "..") {
|
|
180
|
+
throw new Error("defaultDirectory must be a relative path inside the home directory");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function assertSafeDefaultFileName(defaultFileName) {
|
|
185
|
+
if (defaultFileName.trim().length === 0 ||
|
|
186
|
+
defaultFileName === "." ||
|
|
187
|
+
defaultFileName === ".." ||
|
|
188
|
+
splitPathSegments(defaultFileName).length !== 1) {
|
|
189
|
+
throw new Error("defaultFileName must be a file name without path separators");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function splitPathSegments(value) {
|
|
193
|
+
return value
|
|
194
|
+
.split("/")
|
|
195
|
+
.flatMap((segment) => segment.split("\\"))
|
|
196
|
+
.filter((segment) => segment.length > 0);
|
|
197
|
+
}
|
|
198
|
+
function getExplicitProtectedCredentialPaths(resolvedPath) {
|
|
199
|
+
const parsed = path.parse(resolvedPath);
|
|
200
|
+
const segments = resolvedPath
|
|
201
|
+
.slice(parsed.root.length)
|
|
202
|
+
.split(path.sep)
|
|
203
|
+
.filter((segment) => segment.length > 0);
|
|
204
|
+
if (segments.length <= 1) {
|
|
205
|
+
return [resolvedPath];
|
|
206
|
+
}
|
|
207
|
+
const protectedPaths = [];
|
|
208
|
+
let currentPath = parsed.root;
|
|
209
|
+
for (const [index, segment] of segments.entries()) {
|
|
210
|
+
currentPath = path.join(currentPath, segment);
|
|
211
|
+
if (index === 0) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
protectedPaths.push(currentPath);
|
|
215
|
+
}
|
|
216
|
+
return protectedPaths;
|
|
217
|
+
}
|
|
171
218
|
function isPathInsideOrEqual(childPath, parentPath) {
|
|
172
219
|
const relativePath = path.relative(parentPath, childPath);
|
|
173
220
|
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
@@ -7,8 +7,14 @@ export class KeychainStore {
|
|
|
7
7
|
account;
|
|
8
8
|
constructor(input) {
|
|
9
9
|
this.runCommand = input.runCommand ?? runSecurityCommand;
|
|
10
|
-
this.service = input.service;
|
|
11
|
-
this.account = input.account;
|
|
10
|
+
this.service = input.service.trim();
|
|
11
|
+
this.account = input.account.trim();
|
|
12
|
+
if (this.service.length === 0) {
|
|
13
|
+
throw new Error("Keychain service must not be empty");
|
|
14
|
+
}
|
|
15
|
+
if (this.account.length === 0) {
|
|
16
|
+
throw new Error("Keychain account must not be empty");
|
|
17
|
+
}
|
|
12
18
|
}
|
|
13
19
|
async get() {
|
|
14
20
|
const result = await this.executeSecurityCommand(["find-generic-password", "-s", this.service, "-a", this.account, "-w"], "read secret from macOS Keychain");
|
|
@@ -31,8 +37,9 @@ export class KeychainStore {
|
|
|
31
37
|
"-a",
|
|
32
38
|
this.account,
|
|
33
39
|
"-U",
|
|
34
|
-
"-w"
|
|
35
|
-
|
|
40
|
+
"-w",
|
|
41
|
+
value
|
|
42
|
+
], "store secret in macOS Keychain");
|
|
36
43
|
if (getCommandExitCode(result) !== 0) {
|
|
37
44
|
throw createSecurityCliFailure("store secret in macOS Keychain", result);
|
|
38
45
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft-openapi",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.51",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"toolcraft-openapi-generate": "dist/bin/generate.js"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"toolcraft": "0.0.
|
|
31
|
+
"toolcraft": "0.0.51",
|
|
32
32
|
"auth-store": "^0.0.1",
|
|
33
33
|
"yaml": "^2.8.2"
|
|
34
34
|
},
|