toolcraft-openapi 0.0.50 → 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.
@@ -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
- throw new Error("Missing YAML frontmatter end delimiter (---).");
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 (isClosingFenceLine(source.slice(lineStart, lineEnd.index))) {
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 isClosingFenceLine(line) {
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 = splitFrontmatter(source);
24
- const lineCounter = new LineCounter();
25
- if (split.raw === undefined) {
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
- const document = parseDocument(normalizeYamlLineEndings(split.raw), {
34
- lineCounter,
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 ? {} : { pos: error.pos })
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
- return {
51
- frontmatter: normalizeYamlFrontmatter(document.toJSON()),
52
- body: split.body,
53
- errors,
54
- lineCounter
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 (!isRecord(value)) {
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
- if (configuredBackend === "keychain") {
31
+ const backend = configuredBackend?.trim();
32
+ if (backend === "keychain") {
32
33
  return "keychain";
33
34
  }
34
- if (configuredBackend === undefined || configuredBackend === "file") {
35
+ if (backend === undefined || backend === "file") {
35
36
  return "file";
36
37
  }
37
- throw new Error(`Unsupported auth store backend: ${configuredBackend}`);
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
- this.filePath = path.join(homeDirectory, defaultDirectory, input.defaultFileName ?? "credentials.enc");
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 [path.dirname(resolvedPath), resolvedPath];
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
- ], "store secret in macOS Keychain", { stdin: value });
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.50",
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.50",
31
+ "toolcraft": "0.0.51",
32
32
  "auth-store": "^0.0.1",
33
33
  "yaml": "^2.8.2"
34
34
  },