@powerduck/conf-patch 0.3.2 → 0.3.4

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 CHANGED
@@ -1,348 +1,315 @@
1
1
  # @powerduck/conf-patch
2
2
 
3
- A production-grade configuration editor with a clean **two-layer architecture**:
3
+ [![npm version](https://img.shields.io/npm/v/@powerduck/conf-patch)](https://www.npmjs.com/package/@powerduck/conf-patch)
4
+ [![license](https://img.shields.io/npm/l/@powerduck/conf-patch)](https://github.com/PowerDuckie/conf-patch/blob/main/LICENSE)
5
+ [![downloads](https://img.shields.io/npm/dm/@powerduck/conf-patch)](https://www.npmjs.com/package/@powerduck/conf-patch)
4
6
 
5
- 1. **Core layer** (browser-safe): Pure functions that patch JSON, JSONC, and YAML strings. No filesystem access. Works in Node.js, Electron, and browsers.
6
- 2. **File layer** (Node.js / Electron only): Functions that read/write files, combining the core layer with atomic writes and cross-process file locking.
7
+ Production-grade configuration file editor with a clean two-layer architecture: a browser-safe core layer for patching JSON/JSONC/YAML strings, and a Node.js/Electron file layer with atomic writes and cross-process file locking.
7
8
 
8
- RFC 6902 JSON Patch semantics, comment and formatting preservation, OpenAPI validation, and full TypeScript support.
9
+ ---
9
10
 
10
- ## Features
11
+ Powerduck is an open-source developer tooling platform for teams building modern API workflows.
12
+
13
+ - **Core Layer** — Browser-safe patching for JSON, JSONC, and YAML strings with no filesystem dependency
14
+ - **File Layer** — Atomic writes with temp-file + rename, cross-process file locking for Node.js/Electron
15
+ - **RFC 6902 JSON Patch** — `add`, `replace`, and `remove` operations with strict/non-strict modes
16
+ - **OpenAPI Validation** — Built on `@powerduck/openapi-parser` with secure input handling and DoS guards
17
+ - **Comment-Preserving Edits** — JSONC edits are range-based, preserving comments and trailing commas
18
+ - **Dual ESM/CJS** — Works with `import` and `require`, with bundled TypeScript declarations
19
+
20
+ ---
21
+
22
+ ## Quick Start
11
23
 
12
- ### Core Layer (Browser-Safe)
13
- - **`patchContent`** — Apply RFC 6902 patch operations to a configuration string
14
- - **`setContentValue`** — Set or create a nested value
15
- - **`deleteContentValue`** — Remove a key or array element
16
- - **No filesystem access** — Works in browsers, IndexedDB, localStorage, or any storage layer
17
-
18
- ### File Layer (Node.js / Electron)
19
- - **`readConfigFile`** — Read a configuration file
20
- - **`writeConfigFile`** — Write a file with atomic writes and optional locking
21
- - **`patchConfigFile`** — Read-patch-write transaction inside a file lock
22
- - **`setConfigValue`** — Set a value in a file
23
- - **`deleteConfigValue`** — Delete a value from a file
24
- - **Atomic writes** — Temp file + rename pattern, no partial writes on crash
25
- - **Cross-process file locking** — Exclusive locks with ownership tokens, exponential backoff, stale-lock recovery
26
-
27
- ### Format Support
28
- - **JSON** — Standard JSON
29
- - **JSONC** — JSON with comments and trailing commas (preserved)
30
- - **YAML** — YAML 1.2 with comments and indentation preservation
31
-
32
- ### OpenAPI Validation
33
- - **`validateOpenAPISpec`** — Validate raw OpenAPI/Swagger content (JSON or YAML)
34
- - **`validateOpenAPIFile`** — Validate from a file path
35
- - Delegates to `@powerduck/openapi-parser` with secure input handling (size limits, path sandboxing, deadlines, structural complexity guards)
36
-
37
- ## Installation
24
+ ### Install
38
25
 
39
26
  ```bash
40
27
  npm install @powerduck/conf-patch
41
- pnpm add @powerduck/conf-patch
42
- yarn add @powerduck/conf-patch
43
28
  ```
44
29
 
45
- ## Module System Support
30
+ ### Patch a JSON string (browser-safe)
46
31
 
47
- This package supports both **CommonJS (`require`)** and **ES Modules (`import`)** for all entry points.
48
-
49
- ### CommonJS (Node.js / Electron main process)
32
+ ```typescript
33
+ import { patchContent } from "@powerduck/conf-patch/core";
50
34
 
51
- ```javascript
52
- // Full library (includes file IO)
53
- const { setConfigValue, readConfigFile } = require("@powerduck/conf-patch");
35
+ const updated = patchContent(
36
+ '{"name": "app", "version": "1.0.0"}',
37
+ [
38
+ { op: "replace", path: ["version"], value: "2.0.0" },
39
+ { op: "add", path: ["description"], value: "My application" },
40
+ ],
41
+ "json",
42
+ );
54
43
 
55
- // Core layer only (browser-safe, no fs dependency)
56
- const { patchContent, setContentValue } = require("@powerduck/conf-patch/core");
44
+ console.log(updated);
57
45
  ```
58
46
 
59
- ### ES Modules (modern Node.js / browsers / bundlers)
47
+ ### Set a value in a file (Node.js / Electron)
60
48
 
61
49
  ```typescript
62
- // Full library
63
- import { setConfigValue, readConfigFile } from "@powerduck/conf-patch";
50
+ import { setConfigValue } from "@powerduck/conf-patch";
64
51
 
65
- // Core layer only (browser-safe)
66
- import { patchContent, setContentValue } from "@powerduck/conf-patch/core";
52
+ // Atomic write with file locking
53
+ await setConfigValue("config.yaml", ["database", "port"], 5432);
67
54
  ```
68
55
 
69
- ### Build Outputs
56
+ ---
70
57
 
71
- | Entry | CJS (`require`) | ESM (`import`) | Type Declarations |
72
- |---|---|---|---|
73
- | Main | `dist/index.js` (26KB) | `dist/index.mjs` (16KB) | `dist/index.d.ts` |
74
- | Core | `dist/core.js` (9.5KB) | `dist/core.mjs` (1KB) | `dist/core.d.ts` |
58
+ ## Links
75
59
 
76
- The package does not set `"type": "module"`, so `.js` files are treated as CommonJS by default. The `exports` field in `package.json` explicitly maps `import` and `require` conditions to the correct builds.
60
+ - [Official Website](https://www.powerduck.com/opensource/conf-patch.html)
61
+ - [Documentation](https://www.powerduck.com/docs/conf-patch/introduction)
62
+ - [Live Demo](https://www.powerduck.com/demo/conf-patch)
63
+ - [GitHub](https://github.com/PowerDuckie/conf-patch)
64
+ - [npm](https://www.npmjs.com/package/@powerduck/conf-patch)
77
65
 
78
- ## Quick Start
66
+ ---
79
67
 
80
- ### Browser Usage (Core Layer Only)
68
+ ## Features
81
69
 
82
- > **Important:** For browser usage, import from `@powerduck/conf-patch/core` (subpath export).
83
- > This ensures the file layer (which depends on `node:fs`) is not bundled into your browser code.
84
- > The core entry is only ~9KB gzipped and has zero Node.js dependencies.
70
+ - **Two-layer architecture** core layer runs in browsers, Edge Functions, and IndexedDB; file layer adds atomic writes and locking for Node.js/Electron
71
+ - **JSON, JSONC, and YAML support** patch all three formats with one API
72
+ - **Comment-preserving edits** JSONC edits are range-based, so comments and trailing commas around touched lines are preserved
73
+ - **RFC 6902 JSON Patch** — `add`, `replace`, and `remove` operations with strict/non-strict modes
74
+ - **Atomic writes** — temp file + rename, so a crash never leaves a half-written file
75
+ - **Cross-process file locking** — ownership tokens, exponential backoff, and stale-lock recovery
76
+ - **OpenAPI validation** — built on `@powerduck/openapi-parser` with secure input handling, size limits, and DoS guards
77
+ - **Format auto-detection** — file extension detection for `.json`, `.jsonc`, `.yaml`, `.yml`
78
+ - **Dual ESM/CJS builds** — works with `import` and `require`, with bundled TypeScript declarations
85
79
 
86
- ```typescript
87
- import { patchContent, setContentValue, deleteContentValue } from "@powerduck/conf-patch/core";
80
+ ---
88
81
 
89
- // Patch a JSON string (no filesystem access)
90
- const updated = patchContent(
91
- '{"name": "app"}',
92
- [{ op: "add", path: ["version"], value: "1.0.0" }],
93
- "json",
94
- );
82
+ ## Architecture
95
83
 
96
- // Set a nested value in YAML
97
- const yaml = setContentValue("name: app\n", ["server", "port"], 8080, "yaml");
84
+ ```
85
+ ┌──────────────────────────────────────────────────────────────┐
86
+ │ Application code │
87
+ ├──────────────────────────────────────────────────────────────┤
88
+ │ File layer (Node.js / Electron only) │
89
+ │ ┌───────────────┐ ┌────────────────┐ ┌──────────────────┐ │
90
+ │ │ readConfigFile │ │ writeConfigFile│ │ patchConfigFile │ │
91
+ │ │ setConfigValue │ │ deleteConfig… │ │ withFileLock │ │
92
+ │ └───────┬───────┘ └───────┬────────┘ └────────┬─────────┘ │
93
+ ├──────────┼───────────────────┼─────────────────────┼──────────┤
94
+ │ Core layer (browser-safe, no filesystem) │
95
+ │ ┌───────────────┐ ┌────────────────┐ ┌──────────────────┐ │
96
+ │ │ patchContent │ │ setContentValue│ │ deleteContentValue│ │
97
+ │ └───────────────┘ └────────────────┘ └──────────────────┘ │
98
+ └──────────────────────────────────────────────────────────────┘
99
+ ```
98
100
 
99
- // Delete a value
100
- const cleaned = deleteContentValue(updated, ["legacy"], "json");
101
+ ---
101
102
 
102
- // Store in IndexedDB, localStorage, or any storage layer
103
- localStorage.setItem("config", updated);
104
- ```
103
+ ## Core Layer (Browser-Safe)
104
+
105
+ Import from `@powerduck/conf-patch/core` for pure string operations with no filesystem access.
106
+
107
+ ### `patchContent(content, operations, format)`
105
108
 
106
- ### Node.js / Electron Usage (File Layer)
109
+ Patches a JSON/JSONC/YAML string with RFC 6902 operations.
107
110
 
108
111
  ```typescript
109
- import { setConfigValue, readConfigFile, patchConfigFile } from "@powerduck/conf-patch";
112
+ import { patchContent } from "@powerduck/conf-patch/core";
110
113
 
111
- // Set a nested value in a file (atomic, lock-guarded)
112
- await setConfigValue("config.yaml", ["database", "port"], 5432);
114
+ const result = patchContent(
115
+ '{"a": 1, "b": 2}',
116
+ [{ op: "replace", path: ["a"], value: 10 }],
117
+ "json",
118
+ );
119
+ ```
113
120
 
114
- // Read a file
115
- const content = await readConfigFile("config.json");
121
+ ### `setContentValue(content, path, value, format)`
116
122
 
117
- // Apply multiple patch operations atomically
118
- await patchConfigFile("config.jsonc", [
119
- { op: "replace", path: ["server", "host"], value: "0.0.0.0" },
120
- { op: "add", path: ["server", "ssl"], value: true },
121
- { op: "remove", path: ["legacySection"] },
122
- ]);
123
+ Convenience function to set a single value at a path.
124
+
125
+ ```typescript
126
+ import { setContentValue } from "@powerduck/conf-patch/core";
127
+
128
+ const result = setContentValue(
129
+ '{"server": {"port": 3000}}',
130
+ ["server", "port"],
131
+ 8080,
132
+ "json",
133
+ );
123
134
  ```
124
135
 
125
- ### Electron IPC Example
136
+ ### `deleteContentValue(content, path, format)`
126
137
 
127
- ```javascript
128
- // In the Electron main process (CommonJS)
129
- const { setConfigValue, readConfigFile } = require("@powerduck/conf-patch");
138
+ Delete a value at a path.
130
139
 
131
- ipcMain.handle("config:set", async (_event, { filePath, path, value }) => {
132
- try {
133
- await setConfigValue(filePath, path, value);
134
- return { success: true };
135
- } catch (error) {
136
- return { success: false, error: error.message };
137
- }
138
- });
140
+ ```typescript
141
+ import { deleteContentValue } from "@powerduck/conf-patch/core";
139
142
 
140
- ipcMain.handle("config:read", async (_event, { filePath }) => {
141
- try {
142
- const data = await readConfigFile(filePath);
143
- return { success: true, data };
144
- } catch (error) {
145
- return { success: false, error: error.message };
146
- }
147
- });
143
+ const result = deleteContentValue('{"a": 1, "b": 2}', ["b"], "json");
148
144
  ```
149
145
 
150
- ## API Reference
146
+ ### `detectFormat(filePath)`
151
147
 
152
- ### Core Layer (Browser-Safe)
148
+ Detect format from file extension.
153
149
 
154
- #### `patchContent(content, ops, format, options?)`
150
+ ```typescript
151
+ import { detectFormat } from "@powerduck/conf-patch";
155
152
 
156
- Applies RFC 6902 patch operations to a configuration string.
153
+ const format = detectFormat("./config.yaml"); // "yaml"
154
+ ```
157
155
 
158
- | Parameter | Type | Description |
159
- |---|---|---|
160
- | `content` | `string` | The raw configuration content |
161
- | `ops` | `JsonPatchOp[]` | Array of patch operations |
162
- | `format` | `"json" \| "jsonc" \| "yaml"` | The configuration format |
163
- | `options.strict` | `boolean` | When true, failed operations throw. Default: `true` |
156
+ ---
164
157
 
165
- **Returns:** The patched configuration content (`string`).
158
+ ## File Layer (Node.js / Electron)
166
159
 
167
- #### `setContentValue(content, path, value, format)`
160
+ Import from `@powerduck/conf-patch` for filesystem operations with atomic writes and file locking.
168
161
 
169
- Sets or creates a single value. Uses the `add` operation (replaces existing object properties, inserts at array indices).
162
+ ### `readConfigFile(filePath, options?)`
170
163
 
171
- #### `deleteContentValue(content, path, format)`
164
+ Read and parse a config file.
172
165
 
173
- Removes a key or array element.
166
+ ```typescript
167
+ import { readConfigFile } from "@powerduck/conf-patch";
174
168
 
175
- ### File Layer (Node.js / Electron Only)
169
+ const config = await readConfigFile("./config.json");
170
+ console.log(config.value);
171
+ ```
172
+
173
+ ### `writeConfigFile(filePath, value, options?)`
176
174
 
177
- #### `readConfigFile(filePath)`
175
+ Write a config value to a file with atomic write.
178
176
 
179
- Reads UTF-8 text from a file. Throws on failure.
177
+ ```typescript
178
+ import { writeConfigFile } from "@powerduck/conf-patch";
180
179
 
181
- #### `writeConfigFile(filePath, content, options?)`
180
+ await writeConfigFile("./config.json", { name: "app", version: "1.0.0" });
181
+ ```
182
182
 
183
- Writes content to a file using atomic writes and optional file locking.
183
+ ### `setConfigValue(filePath, path, value, options?)`
184
184
 
185
- | Option | Type | Description |
186
- |---|---|---|
187
- | `lock` | `boolean` | Enable file locking. Default: `true` |
188
- | `lockTimeoutMs` | `number` | Maximum time to wait for a lock |
189
- | `lockRetryDelayMs` | `number` | Initial retry delay (exponential backoff) |
190
- | `lockStaleThresholdMs` | `number` | Lock age after which recovery is allowed |
191
- | `allowStaleRecovery` | `boolean` | Allow automatic stale-lock recovery |
185
+ Set a single value in a config file with atomic write and file locking.
192
186
 
193
- #### `patchConfigFile(filePath, ops, options?)`
187
+ ```typescript
188
+ import { setConfigValue } from "@powerduck/conf-patch";
194
189
 
195
- Reads, patches, and writes a file inside a single atomic, lock-guarded transaction.
190
+ await setConfigValue("./config.yaml", ["database", "host"], "localhost");
191
+ ```
196
192
 
197
- | Option | Type | Description |
198
- |---|---|---|
199
- | `format` | `"json" \| "jsonc" \| "yaml"` | Explicit format (auto-detected from extension if omitted) |
200
- | `strict` | `boolean` | When true, failed operations throw. Default: `true` |
201
- | `lock` | `boolean` | Enable file locking. Default: `true` |
202
- | `lockTimeoutMs` | `number` | Maximum time to wait for a lock |
203
- | `lockRetryDelayMs` | `number` | Initial retry delay |
204
- | `lockStaleThresholdMs` | `number` | Lock age after which recovery is allowed |
205
- | `allowStaleRecovery` | `boolean` | Allow automatic stale-lock recovery |
193
+ ### `patchConfigFile(filePath, operations, options?)`
206
194
 
207
- #### `setConfigValue(filePath, path, value, options?)`
195
+ Apply RFC 6902 patch operations to a config file.
208
196
 
209
- Sets or creates a nested value in a file.
197
+ ```typescript
198
+ import { patchConfigFile } from "@powerduck/conf-patch";
210
199
 
211
- #### `deleteConfigValue(filePath, path, options?)`
200
+ await patchConfigFile("./config.json", [
201
+ { op: "replace", path: ["version"], value: "2.0.0" },
202
+ { op: "add", path: ["author"], value: "Powerduck" },
203
+ ]);
204
+ ```
212
205
 
213
- Removes a nested value from a file.
206
+ ### `withFileLock(filePath, callback, options?)`
214
207
 
215
- ### Types
208
+ Acquire a file lock and execute a callback.
216
209
 
217
210
  ```typescript
218
- type ConfigFormat = "json" | "jsonc" | "yaml";
219
- type JsonPathSegment = string | number;
211
+ import { withFileLock } from "@powerduck/conf-patch";
220
212
 
221
- interface JsonPatchOp {
222
- op: "add" | "replace" | "remove";
223
- path: JsonPathSegment[];
224
- value?: unknown; // required for add and replace
225
- }
213
+ await withFileLock("./config.json", async () => {
214
+ // Critical section - no other process can modify the file
215
+ await setConfigValue("./config.json", ["counter"], 42);
216
+ });
226
217
  ```
227
218
 
228
- ### OpenAPI Validation
219
+ ---
220
+
221
+ ## OpenAPI Validation
222
+
223
+ Built-in OpenAPI spec validation with secure input handling.
224
+
225
+ ### `validateOpenAPISpec(input, options?)`
226
+
227
+ Validate a raw YAML or JSON OpenAPI document.
229
228
 
230
229
  ```typescript
231
- import { validateOpenAPISpec, validateOpenAPIFile, OpenApiValidationError } from "@powerduck/conf-patch";
230
+ import {
231
+ validateOpenAPISpec,
232
+ OpenApiValidationError,
233
+ } from "@powerduck/conf-patch";
232
234
 
233
- // Validate raw content (JSON or YAML)
234
235
  try {
235
- const doc = await validateOpenAPISpec(specContent);
236
- console.log("Valid:", doc.openapi);
236
+ const doc = await validateOpenAPISpec(`
237
+ openapi: 3.1.0
238
+ info:
239
+ title: Demo API
240
+ version: 1.0.0
241
+ paths: {}
242
+ `);
243
+ console.log("Valid. openapi =", doc.openapi);
237
244
  } catch (error) {
238
245
  if (error instanceof OpenApiValidationError) {
239
- console.error(`Validation failed [${error.code}]:`, error.message);
246
+ console.error(`[${error.code}]`, error.message);
240
247
  }
241
248
  }
242
-
243
- // Validate from a file path (requires allowedRootDirectory for security)
244
- const doc = await validateOpenAPIFile("openapi.yaml", {
245
- allowedRootDirectory: "./configs",
246
- });
247
249
  ```
248
250
 
249
- Returns the validated document on success. Throws `OpenApiValidationError` with a machine-readable `code` on failure.
250
-
251
- #### Validation Options
252
-
253
- | Option | Type | Default | Description |
254
- |---|---|---|---|
255
- | `inputKind` | `"content" \| "file"` | `"content"` | Whether input is raw content or a file path |
256
- | `baseFilePath` | `string` | - | Base path for reference resolution context |
257
- | `allowedRootDirectory` | `string` | - | Root directory for file input (required for file mode) |
258
- | `timeoutMs` | `number` | `15000` | Operation deadline in milliseconds |
259
- | `maxInputBytes` | `number` | `5242880` (5MB) | Maximum raw input size in bytes |
260
- | `maxDocumentNodes` | `number` | `100000` | Maximum nodes in parsed document (DoS protection) |
261
- | `maxDocumentDepth` | `number` | `100` | Maximum nesting depth (DoS protection) |
262
- | `maxValidationErrors` | `number` | `50` | Maximum validation errors included in error message |
263
- | `maxErrorMessageLength` | `number` | `1000` | Maximum characters per validation error |
264
- | `signal` | `AbortSignal` | - | Optional cancellation signal |
265
-
266
- #### Error Codes
267
-
268
- | Code | Description |
269
- |---|---|
270
- | `INVALID_OPTION` | Invalid option value provided |
271
- | `INPUT_TOO_LARGE` | Input exceeds maximum allowed size |
272
- | `INPUT_FILE_TOO_LARGE` | File exceeds maximum allowed size |
273
- | `FILE_INPUT_FORBIDDEN` | File input used without `allowedRootDirectory` |
274
- | `PATH_OUTSIDE_ROOT` | File path is outside the allowed root directory |
275
- | `PARSE_ERROR` | Failed to parse JSON/YAML content |
276
- | `INVALID_DOCUMENT_SHAPE` | Document is not a JSON object |
277
- | `UNSUPPORTED_VERSION` | Document does not declare a supported OpenAPI/Swagger version |
278
- | `DOCUMENT_TOO_DEEP` | Document exceeds maximum nesting depth |
279
- | `DOCUMENT_TOO_LARGE` | Document exceeds maximum node count |
280
- | `SPEC_VALIDATION_FAILED` | OpenAPI specification validation failed |
281
- | `OPERATION_TIMEOUT` | Operation timed out |
282
- | `OPERATION_ABORTED` | Operation was aborted via signal |
283
- | `UNKNOWN_ERROR` | Unexpected error occurred |
284
-
285
- ## Behavior Notes
286
-
287
- ### Array Operations
288
- - `add` at an array index **inserts** the element (RFC 6902 behavior)
289
- - `replace` at an array index **replaces** the element
290
- - To replace an existing array element, use `patchConfigFile` with `op: "replace"`
291
-
292
- ### Nested Paths
293
- - `add` requires the parent path to exist. To create a nested object, first create the parent, then the child.
294
- - `replace` and `remove` require the target path to exist.
295
-
296
- ### Format Detection
297
- - `.json` → JSON
298
- - `.jsonc` → JSON with comments
299
- - `.yaml` / `.yml` → YAML
300
- - Other extensions → throw unless `format` is explicitly set
301
-
302
- ## Architecture
303
-
304
- ```
305
- ┌─────────────────────────────────────────────────────────┐
306
- │ Application Code │
307
- ├─────────────────────────────────────────────────────────┤
308
- │ File Layer (Node.js / Electron only) │
309
- │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
310
- │ │ readConfigFile│ │writeConfigFile│ │patchConfigFile│ │
311
- │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
312
- │ │ │ │ │
313
- │ ┌──────▼───────────────────▼───────────────────▼───────┐│
314
- │ │ Core Layer (Browser-Safe) ││
315
- │ │ patchContent setContentValue deleteContentValue ││
316
- │ └────────────────────────────────────────────────────────┘│
317
- └───────────────────────────────────────────────────────────┘
318
- ```
251
+ ### `validateOpenAPIFile(filePath, options?)`
319
252
 
320
- The core layer has zero dependencies on `node:fs`, `node:path`, or any Node.js-specific APIs. It can be used in browsers, Edge Functions, or any JavaScript environment.
253
+ Validate an OpenAPI document from a local file.
321
254
 
322
- The file layer wraps the core layer with filesystem access, atomic writes, and file locking. It only works in Node.js and Electron.
323
-
324
- ## Performance
255
+ ```typescript
256
+ import { validateOpenAPIFile } from "@powerduck/conf-patch";
325
257
 
326
- - **Core patching**: Incremental text edits via `jsonc-parser` (JSON/JSONC) and AST-based mutation via `yaml` (YAML). No full re-serialization.
327
- - **Atomic writes**: Temp file + rename, no in-place modification.
328
- - **File locking**: Lock files with ownership tokens, exponential backoff, no busy-wait polling.
329
- - **Process-local queue**: Same-file operations are serialized in-process to avoid lock contention.
330
- - **Bundle size**: Core layer ~15KB gzipped. Full library ~30KB gzipped.
258
+ const doc = await validateOpenAPIFile("./openapi.json", {
259
+ allowedRootDirectory: "./specs",
260
+ maxInputBytes: 5 * 1024 * 1024,
261
+ });
262
+ ```
331
263
 
332
- ## Testing
264
+ ### Validation Options
265
+
266
+ | Option | Type | Default | Description |
267
+ | ---------------------- | --------------------- | ----------- | ----------------------------------------------------------- |
268
+ | `inputKind` | `"content" \| "file"` | `"content"` | Whether input is raw content or a file path |
269
+ | `baseFilePath` | `string` | - | Base file path for reference resolution |
270
+ | `allowedRootDirectory` | `string` | - | Root directory for file operations (required for file mode) |
271
+ | `timeoutMs` | `number` | `15000` | Operation timeout in milliseconds |
272
+ | `maxInputBytes` | `number` | `5242880` | Maximum input size in bytes |
273
+ | `maxDocumentNodes` | `number` | `100000` | Maximum nodes in parsed document |
274
+ | `maxDocumentDepth` | `number` | `100` | Maximum nesting depth |
275
+
276
+ ---
277
+
278
+ ## Error Codes
279
+
280
+ | Code | Description |
281
+ | ------------------------ | ----------------------------------------------- |
282
+ | `INVALID_OPTION` | Invalid option value provided |
283
+ | `FILE_INPUT_FORBIDDEN` | File input requires `allowedRootDirectory` |
284
+ | `PATH_OUTSIDE_ROOT` | File path is outside the allowed root directory |
285
+ | `INPUT_TOO_LARGE` | Input exceeds maximum allowed size |
286
+ | `INPUT_FILE_TOO_LARGE` | File exceeds maximum allowed size |
287
+ | `PARSE_ERROR` | Failed to parse the document |
288
+ | `INVALID_DOCUMENT_SHAPE` | Document is not a valid JSON object |
289
+ | `UNSUPPORTED_VERSION` | Unsupported OpenAPI/Swagger version |
290
+ | `DOCUMENT_TOO_LARGE` | Document exceeds maximum node count |
291
+ | `DOCUMENT_TOO_DEEP` | Document exceeds maximum nesting depth |
292
+ | `SPEC_VALIDATION_FAILED` | OpenAPI validation failed |
293
+ | `OPERATION_TIMEOUT` | Operation timed out |
294
+ | `OPERATION_ABORTED` | Operation was aborted |
295
+ | `UNKNOWN_ERROR` | Unknown error occurred |
296
+
297
+ ---
298
+
299
+ ## TypeScript Types
333
300
 
334
- ```bash
335
- npm test # Run all tests (280 tests)
336
- npm run typecheck # TypeScript type checking
337
- npm run build # Build CJS + ESM + type declarations
301
+ ```typescript
302
+ import type {
303
+ PatchOperation,
304
+ ConfigFormat,
305
+ ValidateOpenApiOptions,
306
+ OpenApiValidationError,
307
+ AnyOpenAPIDocument,
308
+ } from "@powerduck/conf-patch";
338
309
  ```
339
310
 
340
- Test coverage: 280 tests across 12 test files, covering all public APIs, edge cases, concurrency, error handling, browser compatibility, dual-module (CJS/ESM) support, and OpenAPI validation security controls.
311
+ ---
341
312
 
342
313
  ## License
343
314
 
344
- MIT see [LICENSE](./LICENSE) for details.
345
-
346
- ## Repository
347
-
348
- [https://github.com/PowerDuckie/confedit](https://github.com/PowerDuckie/confedit)
315
+ MIT © [POWERDUCK LIMITED](https://www.powerduck.com)
package/dist/core.js CHANGED
@@ -1 +1 @@
1
- "use strict";var t,e=Object.defineProperty,r=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,n=Object.prototype.hasOwnProperty,a={};((t,r)=>{for(var o in r)e(t,o,{get:r[o],enumerable:!0})})(a,{assertConfigFormat:()=>assertConfigFormat,assertNonEmptyString:()=>assertNonEmptyString,assertPatchOperations:()=>assertPatchOperations,assertPatchPath:()=>assertPatchPath,createError:()=>createError2,deleteContentValue:()=>deleteContentValue,getErrorMessage:()=>getErrorMessage2,patchContent:()=>patchContent,setContentValue:()=>setContentValue}),module.exports=(t=a,((t,a,i,s)=>{if(a&&"object"==typeof a||"function"==typeof a)for(let p of o(a))n.call(t,p)||p===i||e(t,p,{get:()=>a[p],enumerable:!(s=r(a,p))||s.enumerable});return t})(e({},"__esModule",{value:!0}),t));var i=require("jsonc-parser"),s={insertSpaces:!0,tabSize:2,eol:"\n"};function validateOperation(t){if(null===t||"object"!=typeof t)throw new TypeError("Patch operation must be an object.");if(!Array.isArray(t.path)||0===t.path.length)throw new Error("Patch operation path must be a non-empty array.");if("add"!==t.op&&"replace"!==t.op&&"remove"!==t.op)throw new Error(`Unsupported JSON patch operation: ${String(t.op)}`);if(("add"===t.op||"replace"===t.op)&&!Object.prototype.hasOwnProperty.call(t,"value"))throw new Error(`Patch operation "${t.op}" requires a value.`)}function normalizePath(t){return t.map(t=>{if("number"==typeof t){if(!Number.isSafeInteger(t)||t<0)throw new Error(`Invalid array index: ${t}`);return t}if("string"!=typeof t)throw new TypeError(`Patch path segments must be strings or numbers; received ${typeof t}.`);return t})}function applyAdd(t,e,r){const o=function(t,e){if(1===e.length){if("object"!==t.type&&"array"!==t.type)throw new Error("Cannot add a root child to a scalar JSON value.");return t}const r=e.slice(0,-1),o=(0,i.findNodeAtLocation)(t,r);if(void 0===o)throw new Error(`Cannot add value because its parent does not exist at path: ${formatPath(r)}`);if("object"!==o.type&&"array"!==o.type)throw new Error(`Cannot add value because its parent is not an object or array at path: ${formatPath(r)}`);return o}(getTree(t),e),n=e[e.length-1];if("array"===o.type){const a=function(t,e){if("number"!=typeof t)throw new Error(`Array index must be a number at path: ${formatPath(e)}`);if(!Number.isSafeInteger(t)||t<0)throw new Error(`Invalid array index ${t} at path: ${formatPath(e)}`);return t}(n,e),i=o.children?.length??0;if(a>i)throw new Error(`Cannot add at array index ${a}; array length is ${i} at path: ${formatPath(e.slice(0,-1))}`);return applyModify(t,e,r,!0)}if("object"===o.type){if("string"!=typeof n)throw new Error(`Object property path segment must be a string at path: ${formatPath(e)}`);return applyModify(t,e,r,!1)}throw new Error(`Cannot add a child to non-container value at path: ${formatPath(e.slice(0,-1))}`)}function applyReplace(t,e,r){const o=getTree(t);if(void 0===(0,i.findNodeAtLocation)(o,e))throw new Error(`Cannot replace a value that does not exist at path: ${formatPath(e)}`);return applyModify(t,e,r,!1)}function applyRemove(t,e){const r=getTree(t);if(void 0===(0,i.findNodeAtLocation)(r,e))throw new Error(`Cannot remove a value that does not exist at path: ${formatPath(e)}`);return applyModify(t,e,void 0,!1)}function applyModify(t,e,r,o){const n=(0,i.modify)(t,e,r,{formattingOptions:s,isArrayInsertion:o});if(0===n.length)throw new Error(`No JSONC edit was generated for path: ${formatPath(e)}`);return(0,i.applyEdits)(t,n)}function getTree(t){const e=[],r=(0,i.parseTree)(t,e,{allowTrailingComma:!0,disallowComments:!1});if(void 0===r||e.length>0)throw new Error(`The source text is not valid JSON or JSONC${function(t){if(0===t.length)return"";return` (parse errors: ${t.map(t=>`code=${t.error}, offset=${t.offset}`).join("; ")})`}(e)}.`);return r}function formatPath(t){return`[${t.map(t=>JSON.stringify(t)).join(", ")}]`}function safeStringify(t){try{return JSON.stringify(t)}catch{return"[unserializable patch operation]"}}function getErrorMessage(t){return t instanceof Error?t.message:String(t)}function createError(t,e){const r=new Error(t);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:e,writable:!0})}catch{}return r}var p=require("yaml");function applyYamlOperation(t,e){!function(t){if(!Array.isArray(t.path)||0===t.path.length)throw new Error("[confedit] Patch path must be a non-empty array.");if("add"!==t.op&&"replace"!==t.op&&"remove"!==t.op)throw new Error(`[confedit] Unsupported patch operation: ${String(t.op)}.`)}(e);const r=e.path,o=function(t,e){if(0===e.length)throw new Error("[confedit] Replacing the YAML document root is not supported.");if(1===e.length){if(null===t.contents)throw new Error("[confedit] Cannot patch an empty YAML document without a root container.");return t.contents}const r=e.slice(0,-1),o=t.getIn(r,!0);if(null==o)throw new Error(`[confedit] Missing parent path: ${formatPath2(r)}.`);if(!(0,p.isMap)(o)&&!(0,p.isSeq)(o))throw new Error(`[confedit] Parent at ${formatPath2(r)} is not a YAML map or sequence.`);return o}(t,r),n=r[r.length-1];if((0,p.isMap)(o))!function(t,e,r,o){if("string"!=typeof r)throw new Error(`[confedit] YAML map keys must be strings at ${formatPath2(o.path)}.`);const n=e.has(r);switch(o.op){case"add":return void e.set(r,t.createNode(o.value));case"replace":if(!n)throw new Error(`[confedit] Cannot replace missing value at ${formatPath2(o.path)}.`);return void e.set(r,t.createNode(o.value));case"remove":if(!n)throw new Error(`[confedit] Cannot remove missing value at ${formatPath2(o.path)}.`);return void e.delete(r);default:throw new Error(`[confedit] Unsupported patch operation: ${String(o.op)}.`)}}(t,o,n,e);else{if(!(0,p.isSeq)(o))throw new Error(`[confedit] Cannot apply patch at ${formatPath2(r)} because its parent is not a YAML map or sequence.`);!function(t,e,r,o){if("number"!=typeof r||!Number.isSafeInteger(r)||r<0)throw new Error(`[confedit] YAML sequence indexes must be non-negative integers at ${formatPath2(o.path)}.`);const n=e.items.length;switch(o.op){case"add":if(r>n)throw new Error(`[confedit] Cannot insert at index ${r}; sequence length is ${n}.`);return void e.items.splice(r,0,t.createNode(o.value));case"replace":if(r>=n)throw new Error(`[confedit] Cannot replace index ${r}; sequence length is ${n}.`);return void(e.items[r]=t.createNode(o.value));case"remove":if(r>=n)throw new Error(`[confedit] Cannot remove index ${r}; sequence length is ${n}.`);return void e.items.splice(r,1);default:throw new Error(`[confedit] Unsupported patch operation: ${String(o.op)}.`)}}(t,o,n,e)}}function formatPath2(t){return`[${t.map(t=>JSON.stringify(t)).join(", ")}]`}function assertNonEmptyString(t,e){if("string"!=typeof t||0===t.length)throw new TypeError(`[confedit] ${e} must be a non-empty string.`)}function assertPatchPath(t,e="path"){if(!Array.isArray(t)||0===t.length)throw new TypeError(`[confedit] ${e} must be a non-empty array.`);for(const[r,o]of t.entries())if("string"!=typeof o){if("number"!=typeof o||!Number.isSafeInteger(o)||o<0)throw new TypeError(`[confedit] ${e}[${r}] must be a non-empty string or a non-negative integer.`)}else if(0===o.length)throw new TypeError(`[confedit] ${e}[${r}] must not be an empty string.`)}function assertPatchOperations(t){if(!Array.isArray(t))throw new TypeError("[confedit] ops must be an array.");for(const[e,r]of t.entries()){if(null===r||"object"!=typeof r)throw new TypeError(`[confedit] Patch operation at index ${e} must be an object.`);if("add"!==r.op&&"replace"!==r.op&&"remove"!==r.op)throw new Error(`[confedit] Unsupported patch operation at index ${e}: ${String(r.op)}.`);if(assertPatchPath(r.path,`ops[${e}].path`),("add"===r.op||"replace"===r.op)&&!Object.prototype.hasOwnProperty.call(r,"value"))throw new TypeError(`[confedit] Patch operation at index ${e} requires a value.`)}}function assertConfigFormat(t){if("json"!==t&&"jsonc"!==t&&"yaml"!==t)throw new Error(`[confedit] Unsupported configuration format: ${String(t)}.`)}function getErrorMessage2(t){return t instanceof Error?t.message:String(t)}function createError2(t,e){const r=new Error(t);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:e,writable:!0})}catch{}return r}function patchContent(t,e,r,o={}){if("string"!=typeof t)throw new TypeError("[confedit] content must be a string.");if(assertPatchOperations(e),assertConfigFormat(r),0===e.length)return t;const n=o.strict??!0;switch(r){case"json":case"jsonc":return function(t,e,r=!0){if("string"!=typeof t)throw new TypeError("[confedit] sourceText must be a string.");if(!Array.isArray(e))throw new TypeError("[confedit] ops must be an array.");let o=t;for(const t of e)try{validateOperation(t);const e=normalizePath(t.path);switch(t.op){case"add":o=applyAdd(o,e,t.value);break;case"replace":o=applyReplace(o,e,t.value);break;case"remove":o=applyRemove(o,e);break;default:throw new Error(`[confedit] Unsupported JSON patch operation: ${String(t.op)}`)}}catch(e){const o=`[confedit] Failed to apply JSON patch ${safeStringify(t)}: ${getErrorMessage(e)}`;if(r)throw createError(o,e);console.warn(o)}return o}(t,e,n);case"yaml":return function(t,e,r=!0){const o=(0,p.parseDocument)(t,{prettyErrors:!0,strict:!0});if(o.errors.length>0)throw new Error(`[confedit] Invalid YAML source: ${o.errors.map(t=>t.message).join("; ")}`);for(const t of e)try{applyYamlOperation(o,t)}catch(e){if(r)throw e;console.warn(`[confedit yaml patch warn] Skip operation ${JSON.stringify(t)}`,e)}return o.toString()}(t,e,n)}}function setContentValue(t,e,r,o){return assertPatchPath(e),patchContent(t,[{op:"add",path:[...e],value:r}],o)}function deleteContentValue(t,e,r){return assertPatchPath(e),patchContent(t,[{op:"remove",path:[...e]}],r)}
1
+ var t,e=Object.defineProperty,r=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,n=Object.prototype.hasOwnProperty,a={};((t,r)=>{for(var o in r)e(t,o,{get:r[o],enumerable:!0})})(a,{assertConfigFormat:()=>assertConfigFormat,assertNonEmptyString:()=>assertNonEmptyString,assertPatchOperations:()=>assertPatchOperations,assertPatchPath:()=>assertPatchPath,createError:()=>createError2,deleteContentValue:()=>deleteContentValue,getErrorMessage:()=>getErrorMessage2,patchContent:()=>patchContent,setContentValue:()=>setContentValue}),module.exports=(t=a,((t,a,i,s)=>{if(a&&"object"==typeof a||"function"==typeof a)for(let p of o(a))n.call(t,p)||p===i||e(t,p,{get:()=>a[p],enumerable:!(s=r(a,p))||s.enumerable});return t})(e({},"__esModule",{value:!0}),t));var i=require("jsonc-parser"),s={insertSpaces:!0,tabSize:2,eol:"\n"};function validateOperation(t){if(null===t||"object"!=typeof t)throw new TypeError("Patch operation must be an object.");if(!Array.isArray(t.path)||0===t.path.length)throw new Error("Patch operation path must be a non-empty array.");if("add"!==t.op&&"replace"!==t.op&&"remove"!==t.op)throw new Error(`Unsupported JSON patch operation: ${String(t.op)}`);if(("add"===t.op||"replace"===t.op)&&!Object.prototype.hasOwnProperty.call(t,"value"))throw new Error(`Patch operation "${t.op}" requires a value.`)}function normalizePath(t){return t.map(t=>{if("number"==typeof t){if(!Number.isSafeInteger(t)||t<0)throw new Error(`Invalid array index: ${t}`);return t}if("string"!=typeof t)throw new TypeError(`Patch path segments must be strings or numbers; received ${typeof t}.`);return t})}function applyAdd(t,e,r){const o=function(t,e){if(1===e.length){if("object"!==t.type&&"array"!==t.type)throw new Error("Cannot add a root child to a scalar JSON value.");return t}const r=e.slice(0,-1),o=(0,i.findNodeAtLocation)(t,r);if(void 0===o)throw new Error(`Cannot add value because its parent does not exist at path: ${formatPath(r)}`);if("object"!==o.type&&"array"!==o.type)throw new Error(`Cannot add value because its parent is not an object or array at path: ${formatPath(r)}`);return o}(getTree(t),e),n=e[e.length-1];if("array"===o.type){const a=function(t,e){if("number"!=typeof t)throw new Error(`Array index must be a number at path: ${formatPath(e)}`);if(!Number.isSafeInteger(t)||t<0)throw new Error(`Invalid array index ${t} at path: ${formatPath(e)}`);return t}(n,e),i=o.children?.length??0;if(a>i)throw new Error(`Cannot add at array index ${a}; array length is ${i} at path: ${formatPath(e.slice(0,-1))}`);return applyModify(t,e,r,!0)}if("object"===o.type){if("string"!=typeof n)throw new Error(`Object property path segment must be a string at path: ${formatPath(e)}`);return applyModify(t,e,r,!1)}throw new Error(`Cannot add a child to non-container value at path: ${formatPath(e.slice(0,-1))}`)}function applyReplace(t,e,r){const o=getTree(t);if(void 0===(0,i.findNodeAtLocation)(o,e))throw new Error(`Cannot replace a value that does not exist at path: ${formatPath(e)}`);return applyModify(t,e,r,!1)}function applyRemove(t,e){const r=getTree(t);if(void 0===(0,i.findNodeAtLocation)(r,e))throw new Error(`Cannot remove a value that does not exist at path: ${formatPath(e)}`);return applyModify(t,e,void 0,!1)}function applyModify(t,e,r,o){const n=(0,i.modify)(t,e,r,{formattingOptions:s,isArrayInsertion:o});if(0===n.length)throw new Error(`No JSONC edit was generated for path: ${formatPath(e)}`);return(0,i.applyEdits)(t,n)}function getTree(t){const e=[],r=(0,i.parseTree)(t,e,{allowTrailingComma:!0,disallowComments:!1});if(void 0===r||e.length>0)throw new Error(`The source text is not valid JSON or JSONC${function(t){if(0===t.length)return"";return` (parse errors: ${t.map(t=>`code=${t.error}, offset=${t.offset}`).join("; ")})`}(e)}.`);return r}function formatPath(t){return`[${t.map(t=>JSON.stringify(t)).join(", ")}]`}function safeStringify(t){try{return JSON.stringify(t)}catch{return"[unserializable patch operation]"}}function getErrorMessage(t){return t instanceof Error?t.message:String(t)}function createError(t,e){const r=new Error(t);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:e,writable:!0})}catch{}return r}var p=require("yaml");function applyYamlOperation(t,e){!function(t){if(!Array.isArray(t.path)||0===t.path.length)throw new Error("[confedit] Patch path must be a non-empty array.");if("add"!==t.op&&"replace"!==t.op&&"remove"!==t.op)throw new Error(`[confedit] Unsupported patch operation: ${String(t.op)}.`)}(e);const r=e.path,o=function(t,e){if(0===e.length)throw new Error("[confedit] Replacing the YAML document root is not supported.");if(1===e.length){if(null===t.contents)throw new Error("[confedit] Cannot patch an empty YAML document without a root container.");return t.contents}const r=e.slice(0,-1),o=t.getIn(r,!0);if(null==o)throw new Error(`[confedit] Missing parent path: ${formatPath2(r)}.`);if(!(0,p.isMap)(o)&&!(0,p.isSeq)(o))throw new Error(`[confedit] Parent at ${formatPath2(r)} is not a YAML map or sequence.`);return o}(t,r),n=r[r.length-1];if((0,p.isMap)(o))!function(t,e,r,o){if("string"!=typeof r)throw new Error(`[confedit] YAML map keys must be strings at ${formatPath2(o.path)}.`);const n=e.has(r);switch(o.op){case"add":return void e.set(r,t.createNode(o.value));case"replace":if(!n)throw new Error(`[confedit] Cannot replace missing value at ${formatPath2(o.path)}.`);return void e.set(r,t.createNode(o.value));case"remove":if(!n)throw new Error(`[confedit] Cannot remove missing value at ${formatPath2(o.path)}.`);return void e.delete(r);default:throw new Error(`[confedit] Unsupported patch operation: ${String(o.op)}.`)}}(t,o,n,e);else{if(!(0,p.isSeq)(o))throw new Error(`[confedit] Cannot apply patch at ${formatPath2(r)} because its parent is not a YAML map or sequence.`);!function(t,e,r,o){if("number"!=typeof r||!Number.isSafeInteger(r)||r<0)throw new Error(`[confedit] YAML sequence indexes must be non-negative integers at ${formatPath2(o.path)}.`);const n=e.items.length;switch(o.op){case"add":if(r>n)throw new Error(`[confedit] Cannot insert at index ${r}; sequence length is ${n}.`);return void e.items.splice(r,0,t.createNode(o.value));case"replace":if(r>=n)throw new Error(`[confedit] Cannot replace index ${r}; sequence length is ${n}.`);return void(e.items[r]=t.createNode(o.value));case"remove":if(r>=n)throw new Error(`[confedit] Cannot remove index ${r}; sequence length is ${n}.`);return void e.items.splice(r,1);default:throw new Error(`[confedit] Unsupported patch operation: ${String(o.op)}.`)}}(t,o,n,e)}}function formatPath2(t){return`[${t.map(t=>JSON.stringify(t)).join(", ")}]`}function assertNonEmptyString(t,e){if("string"!=typeof t||0===t.length)throw new TypeError(`[confedit] ${e} must be a non-empty string.`)}function assertPatchPath(t,e="path"){if(!Array.isArray(t)||0===t.length)throw new TypeError(`[confedit] ${e} must be a non-empty array.`);for(const[r,o]of t.entries())if("string"!=typeof o){if("number"!=typeof o||!Number.isSafeInteger(o)||o<0)throw new TypeError(`[confedit] ${e}[${r}] must be a non-empty string or a non-negative integer.`)}else if(0===o.length)throw new TypeError(`[confedit] ${e}[${r}] must not be an empty string.`)}function assertPatchOperations(t){if(!Array.isArray(t))throw new TypeError("[confedit] ops must be an array.");for(const[e,r]of t.entries()){if(null===r||"object"!=typeof r)throw new TypeError(`[confedit] Patch operation at index ${e} must be an object.`);if("add"!==r.op&&"replace"!==r.op&&"remove"!==r.op)throw new Error(`[confedit] Unsupported patch operation at index ${e}: ${String(r.op)}.`);if(assertPatchPath(r.path,`ops[${e}].path`),("add"===r.op||"replace"===r.op)&&!Object.prototype.hasOwnProperty.call(r,"value"))throw new TypeError(`[confedit] Patch operation at index ${e} requires a value.`)}}function assertConfigFormat(t){if("json"!==t&&"jsonc"!==t&&"yaml"!==t)throw new Error(`[confedit] Unsupported configuration format: ${String(t)}.`)}function getErrorMessage2(t){return t instanceof Error?t.message:String(t)}function createError2(t,e){const r=new Error(t);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:e,writable:!0})}catch{}return r}function patchContent(t,e,r,o={}){if("string"!=typeof t)throw new TypeError("[confedit] content must be a string.");if(assertPatchOperations(e),assertConfigFormat(r),0===e.length)return t;const n=o.strict??!0;switch(r){case"json":case"jsonc":return function(t,e,r=!0){if("string"!=typeof t)throw new TypeError("[confedit] sourceText must be a string.");if(!Array.isArray(e))throw new TypeError("[confedit] ops must be an array.");let o=t;for(const t of e)try{validateOperation(t);const e=normalizePath(t.path);switch(t.op){case"add":o=applyAdd(o,e,t.value);break;case"replace":o=applyReplace(o,e,t.value);break;case"remove":o=applyRemove(o,e);break;default:throw new Error(`[confedit] Unsupported JSON patch operation: ${String(t.op)}`)}}catch(e){const o=`[confedit] Failed to apply JSON patch ${safeStringify(t)}: ${getErrorMessage(e)}`;if(r)throw createError(o,e);console.warn(o)}return o}(t,e,n);case"yaml":return function(t,e,r=!0){const o=(0,p.parseDocument)(t,{prettyErrors:!0,strict:!0});if(o.errors.length>0)throw new Error(`[confedit] Invalid YAML source: ${o.errors.map(t=>t.message).join("; ")}`);for(const t of e)try{applyYamlOperation(o,t)}catch(e){if(r)throw e;console.warn(`[confedit yaml patch warn] Skip operation ${JSON.stringify(t)}`,e)}return o.toString()}(t,e,n)}}function setContentValue(t,e,r,o){return assertPatchPath(e),patchContent(t,[{op:"add",path:[...e],value:r}],o)}function deleteContentValue(t,e,r){return assertPatchPath(e),patchContent(t,[{op:"remove",path:[...e]}],r)}
package/dist/index.d.mts CHANGED
@@ -183,8 +183,7 @@ declare class OpenApiValidationError extends Error {
183
183
  * Input is treated as content by default. File-path interpretation is only
184
184
  * enabled by explicitly setting inputKind to "file".
185
185
  *
186
- * Validation is delegated to @powerduck/openapi-parser (which wraps
187
- * @scalar/openapi-parser). This module adds secure input handling: size
186
+ * Validation is delegated to @powerduck/openapi-parser. This module adds secure input handling: size
188
187
  * limits, path sandboxing, structural complexity guards, and deadlines.
189
188
  */
190
189
  declare function validateOpenAPISpec(input: string, options?: ValidateOpenApiOptions): Promise<AnyOpenAPIDocument>;
package/dist/index.d.ts CHANGED
@@ -183,8 +183,7 @@ declare class OpenApiValidationError extends Error {
183
183
  * Input is treated as content by default. File-path interpretation is only
184
184
  * enabled by explicitly setting inputKind to "file".
185
185
  *
186
- * Validation is delegated to @powerduck/openapi-parser (which wraps
187
- * @scalar/openapi-parser). This module adds secure input handling: size
186
+ * Validation is delegated to @powerduck/openapi-parser. This module adds secure input handling: size
188
187
  * limits, path sandboxing, structural complexity guards, and deadlines.
189
188
  */
190
189
  declare function validateOpenAPISpec(input: string, options?: ValidateOpenApiOptions): Promise<AnyOpenAPIDocument>;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.create;var e,t=Object.defineProperty,r=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,n=(Object.getPrototypeOf,Object.prototype.hasOwnProperty),__copyProps=(e,a,i,s)=>{if(a&&"object"==typeof a||"function"==typeof a)for(let c of o(a))n.call(e,c)||c===i||t(e,c,{get:()=>a[c],enumerable:!(s=r(a,c))||s.enumerable});return e},a={};((e,r)=>{for(var o in r)t(e,o,{get:r[o],enumerable:!0})})(a,{OpenApiValidationError:()=>I,assertConfigFormat:()=>assertConfigFormat,assertNonEmptyString:()=>assertNonEmptyString,assertPatchOperations:()=>assertPatchOperations,assertPatchPath:()=>assertPatchPath,createError:()=>createError2,deleteConfigValue:()=>deleteConfigValue,deleteContentValue:()=>deleteContentValue,detectFormat:()=>detectFormat,getErrorMessage:()=>getErrorMessage2,normalizeFilePath:()=>normalizeFilePath,patchConfigFile:()=>patchConfigFile,patchContent:()=>patchContent,readConfigFile:()=>readConfigFile,releaseAllLocalLocks:()=>releaseAllLocalLocks,setConfigValue:()=>setConfigValue,setContentValue:()=>setContentValue,validateOpenAPIFile:()=>validateOpenAPIFile,validateOpenAPISpec:()=>validateOpenAPISpec,withFileLock:()=>withFileLock,writeConfigFile:()=>writeConfigFile}),module.exports=(e=a,__copyProps(t({},"__esModule",{value:!0}),e));var i=require("jsonc-parser"),s={insertSpaces:!0,tabSize:2,eol:"\n"};function validateOperation(e){if(null===e||"object"!=typeof e)throw new TypeError("Patch operation must be an object.");if(!Array.isArray(e.path)||0===e.path.length)throw new Error("Patch operation path must be a non-empty array.");if("add"!==e.op&&"replace"!==e.op&&"remove"!==e.op)throw new Error(`Unsupported JSON patch operation: ${String(e.op)}`);if(("add"===e.op||"replace"===e.op)&&!Object.prototype.hasOwnProperty.call(e,"value"))throw new Error(`Patch operation "${e.op}" requires a value.`)}function normalizePath(e){return e.map(e=>{if("number"==typeof e){if(!Number.isSafeInteger(e)||e<0)throw new Error(`Invalid array index: ${e}`);return e}if("string"!=typeof e)throw new TypeError(`Patch path segments must be strings or numbers; received ${typeof e}.`);return e})}function applyAdd(e,t,r){const o=function(e,t){if(1===t.length){if("object"!==e.type&&"array"!==e.type)throw new Error("Cannot add a root child to a scalar JSON value.");return e}const r=t.slice(0,-1),o=(0,i.findNodeAtLocation)(e,r);if(void 0===o)throw new Error(`Cannot add value because its parent does not exist at path: ${formatPath(r)}`);if("object"!==o.type&&"array"!==o.type)throw new Error(`Cannot add value because its parent is not an object or array at path: ${formatPath(r)}`);return o}(getTree(e),t),n=t[t.length-1];if("array"===o.type){const a=function(e,t){if("number"!=typeof e)throw new Error(`Array index must be a number at path: ${formatPath(t)}`);if(!Number.isSafeInteger(e)||e<0)throw new Error(`Invalid array index ${e} at path: ${formatPath(t)}`);return e}(n,t),i=o.children?.length??0;if(a>i)throw new Error(`Cannot add at array index ${a}; array length is ${i} at path: ${formatPath(t.slice(0,-1))}`);return applyModify(e,t,r,!0)}if("object"===o.type){if("string"!=typeof n)throw new Error(`Object property path segment must be a string at path: ${formatPath(t)}`);return applyModify(e,t,r,!1)}throw new Error(`Cannot add a child to non-container value at path: ${formatPath(t.slice(0,-1))}`)}function applyReplace(e,t,r){const o=getTree(e);if(void 0===(0,i.findNodeAtLocation)(o,t))throw new Error(`Cannot replace a value that does not exist at path: ${formatPath(t)}`);return applyModify(e,t,r,!1)}function applyRemove(e,t){const r=getTree(e);if(void 0===(0,i.findNodeAtLocation)(r,t))throw new Error(`Cannot remove a value that does not exist at path: ${formatPath(t)}`);return applyModify(e,t,void 0,!1)}function applyModify(e,t,r,o){const n=(0,i.modify)(e,t,r,{formattingOptions:s,isArrayInsertion:o});if(0===n.length)throw new Error(`No JSONC edit was generated for path: ${formatPath(t)}`);return(0,i.applyEdits)(e,n)}function getTree(e){const t=[],r=(0,i.parseTree)(e,t,{allowTrailingComma:!0,disallowComments:!1});if(void 0===r||t.length>0)throw new Error(`The source text is not valid JSON or JSONC${function(e){if(0===e.length)return"";return` (parse errors: ${e.map(e=>`code=${e.error}, offset=${e.offset}`).join("; ")})`}(t)}.`);return r}function formatPath(e){return`[${e.map(e=>JSON.stringify(e)).join(", ")}]`}function safeStringify(e){try{return JSON.stringify(e)}catch{return"[unserializable patch operation]"}}function getErrorMessage(e){return e instanceof Error?e.message:String(e)}function createError(e,t){const r=new Error(e);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:t,writable:!0})}catch{}return r}var c=require("yaml");function applyYamlOperation(e,t){!function(e){if(!Array.isArray(e.path)||0===e.path.length)throw new Error("[confedit] Patch path must be a non-empty array.");if("add"!==e.op&&"replace"!==e.op&&"remove"!==e.op)throw new Error(`[confedit] Unsupported patch operation: ${String(e.op)}.`)}(t);const r=t.path,o=function(e,t){if(0===t.length)throw new Error("[confedit] Replacing the YAML document root is not supported.");if(1===t.length){if(null===e.contents)throw new Error("[confedit] Cannot patch an empty YAML document without a root container.");return e.contents}const r=t.slice(0,-1),o=e.getIn(r,!0);if(null==o)throw new Error(`[confedit] Missing parent path: ${formatPath2(r)}.`);if(!(0,c.isMap)(o)&&!(0,c.isSeq)(o))throw new Error(`[confedit] Parent at ${formatPath2(r)} is not a YAML map or sequence.`);return o}(e,r),n=r[r.length-1];if((0,c.isMap)(o))!function(e,t,r,o){if("string"!=typeof r)throw new Error(`[confedit] YAML map keys must be strings at ${formatPath2(o.path)}.`);const n=t.has(r);switch(o.op){case"add":return void t.set(r,e.createNode(o.value));case"replace":if(!n)throw new Error(`[confedit] Cannot replace missing value at ${formatPath2(o.path)}.`);return void t.set(r,e.createNode(o.value));case"remove":if(!n)throw new Error(`[confedit] Cannot remove missing value at ${formatPath2(o.path)}.`);return void t.delete(r);default:throw new Error(`[confedit] Unsupported patch operation: ${String(o.op)}.`)}}(e,o,n,t);else{if(!(0,c.isSeq)(o))throw new Error(`[confedit] Cannot apply patch at ${formatPath2(r)} because its parent is not a YAML map or sequence.`);!function(e,t,r,o){if("number"!=typeof r||!Number.isSafeInteger(r)||r<0)throw new Error(`[confedit] YAML sequence indexes must be non-negative integers at ${formatPath2(o.path)}.`);const n=t.items.length;switch(o.op){case"add":if(r>n)throw new Error(`[confedit] Cannot insert at index ${r}; sequence length is ${n}.`);return void t.items.splice(r,0,e.createNode(o.value));case"replace":if(r>=n)throw new Error(`[confedit] Cannot replace index ${r}; sequence length is ${n}.`);return void(t.items[r]=e.createNode(o.value));case"remove":if(r>=n)throw new Error(`[confedit] Cannot remove index ${r}; sequence length is ${n}.`);return void t.items.splice(r,1);default:throw new Error(`[confedit] Unsupported patch operation: ${String(o.op)}.`)}}(e,o,n,t)}}function formatPath2(e){return`[${e.map(e=>JSON.stringify(e)).join(", ")}]`}function assertNonEmptyString(e,t){if("string"!=typeof e||0===e.length)throw new TypeError(`[confedit] ${t} must be a non-empty string.`)}function assertPatchPath(e,t="path"){if(!Array.isArray(e)||0===e.length)throw new TypeError(`[confedit] ${t} must be a non-empty array.`);for(const[r,o]of e.entries())if("string"!=typeof o){if("number"!=typeof o||!Number.isSafeInteger(o)||o<0)throw new TypeError(`[confedit] ${t}[${r}] must be a non-empty string or a non-negative integer.`)}else if(0===o.length)throw new TypeError(`[confedit] ${t}[${r}] must not be an empty string.`)}function assertPatchOperations(e){if(!Array.isArray(e))throw new TypeError("[confedit] ops must be an array.");for(const[t,r]of e.entries()){if(null===r||"object"!=typeof r)throw new TypeError(`[confedit] Patch operation at index ${t} must be an object.`);if("add"!==r.op&&"replace"!==r.op&&"remove"!==r.op)throw new Error(`[confedit] Unsupported patch operation at index ${t}: ${String(r.op)}.`);if(assertPatchPath(r.path,`ops[${t}].path`),("add"===r.op||"replace"===r.op)&&!Object.prototype.hasOwnProperty.call(r,"value"))throw new TypeError(`[confedit] Patch operation at index ${t} requires a value.`)}}function assertConfigFormat(e){if("json"!==e&&"jsonc"!==e&&"yaml"!==e)throw new Error(`[confedit] Unsupported configuration format: ${String(e)}.`)}function getErrorMessage2(e){return e instanceof Error?e.message:String(e)}function createError2(e,t){const r=new Error(e);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:t,writable:!0})}catch{}return r}function patchContent(e,t,r,o={}){if("string"!=typeof e)throw new TypeError("[confedit] content must be a string.");if(assertPatchOperations(t),assertConfigFormat(r),0===t.length)return e;const n=o.strict??!0;switch(r){case"json":case"jsonc":return function(e,t,r=!0){if("string"!=typeof e)throw new TypeError("[confedit] sourceText must be a string.");if(!Array.isArray(t))throw new TypeError("[confedit] ops must be an array.");let o=e;for(const e of t)try{validateOperation(e);const t=normalizePath(e.path);switch(e.op){case"add":o=applyAdd(o,t,e.value);break;case"replace":o=applyReplace(o,t,e.value);break;case"remove":o=applyRemove(o,t);break;default:throw new Error(`[confedit] Unsupported JSON patch operation: ${String(e.op)}`)}}catch(t){const o=`[confedit] Failed to apply JSON patch ${safeStringify(e)}: ${getErrorMessage(t)}`;if(r)throw createError(o,t);console.warn(o)}return o}(e,t,n);case"yaml":return function(e,t,r=!0){const o=(0,c.parseDocument)(e,{prettyErrors:!0,strict:!0});if(o.errors.length>0)throw new Error(`[confedit] Invalid YAML source: ${o.errors.map(e=>e.message).join("; ")}`);for(const e of t)try{applyYamlOperation(o,e)}catch(t){if(r)throw t;console.warn(`[confedit yaml patch warn] Skip operation ${JSON.stringify(e)}`,t)}return o.toString()}(e,t,n)}}function setContentValue(e,t,r,o){return assertPatchPath(t),patchContent(e,[{op:"add",path:[...t],value:r}],o)}function deleteContentValue(e,t,r){return assertPatchPath(t),patchContent(e,[{op:"remove",path:[...t]}],r)}var l=require("fs/promises"),u=require("path"),f=require("url");function normalizeFilePath(e){if("string"!=typeof e||0===e.length)throw new TypeError("[confedit] filePath must be a non-empty string.");if(e.startsWith("file:"))try{return(0,f.fileURLToPath)(e)}catch(t){throw function(e,t){const r=new Error(e);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:t,writable:!0})}catch{}return r}(`[confedit] Invalid file URL "${e}": ${function(e){return e instanceof Error?e.message:String(e)}(t)}`,t)}return(0,u.isAbsolute)(e)?e:(0,u.resolve)(e)}async function readConfigFile(e){assertNonEmptyString(e,"filePath");const t=normalizeFilePath(e);try{return await(0,l.readFile)(t,"utf8")}catch(t){throw createError2(`[confedit] Failed to read "${e}": ${getErrorMessage2(t)}`,t)}}var p=require("fs/promises"),h=require("path"),d=require("crypto"),m=require("fs/promises"),w=require("path");async function atomicWrite(e,t){if(function(e,t){if("string"!=typeof e||0===e.length)throw new TypeError(`[confedit] ${t} must be a non-empty string.`)}(e,"filePath"),"string"!=typeof t)throw new TypeError("[confedit] content must be a string.");const r=normalizeFilePath(e),o=(0,w.dirname)(r),n=(0,w.basename)(r),a=`${process.pid}.${Date.now()}.${(0,d.randomBytes)(12).toString("hex")}`,i=(0,w.join)(o,`.${n}.${a}.tmp`),s=(0,w.join)(o,`.${n}.${a}.bak`);let c,l=!1,u=!1;try{await(0,m.mkdir)(o,{recursive:!0});const e=await async function(e){try{return 511&(await(0,m.stat)(e)).mode}catch(e){if("ENOENT"===getErrorCode(e))return;throw e}}(r);c=await(0,m.open)(i,"wx",e??384);try{await c.writeFile(t,"utf8"),await c.sync()}finally{await c.close(),c=void 0}void 0!==e&&await(0,m.chmod)(i,e);try{await(0,m.rename)(i,r),u=!0}catch(e){if(!function(e){if("win32"!==process.platform)return!1;const t=getErrorCode(e);return"EEXIST"===t||"EPERM"===t||"EACCES"===t}(e))throw e;await(0,m.rename)(r,s),l=!0;try{await(0,m.rename)(i,r),u=!0}catch(e){const t=await async function(e,t){try{return await(0,m.unlink)(e).catch(e=>{if("ENOENT"!==getErrorCode(e))throw e}),await(0,m.rename)(t,e),!0}catch{return!1}}(r,s);throw t&&(l=!1),createError4(t?`[confedit] Failed to replace "${r}", but the original file was restored.`:`[confedit] Failed to replace "${r}". The backup was retained at "${s}".`,e)}}await syncDirectory(o),l&&(await(0,m.unlink)(s),l=!1,await syncDirectory(o))}catch(t){throw void 0!==c&&await c.close().catch(()=>{}),createError4(`[confedit] Failed to atomically write "${e}": ${function(e){return e instanceof Error?e.message:String(e)}(t)}`,t)}finally{await(0,m.unlink)(i).catch(()=>{}),u&&l&&await(0,m.unlink)(s).catch(()=>{})}}async function syncDirectory(e){let t;try{t=await(0,m.open)(e,"r"),await t.sync()}catch(e){const t=getErrorCode(e);if("EINVAL"!==t&&"EPERM"!==t&&"EISDIR"!==t&&"ENOSYS"!==t&&"ENOTSUP"!==t)throw e}finally{await(t?.close().catch(()=>{}))}}function getErrorCode(e){if(null!==e&&"object"==typeof e&&"code"in e&&"string"==typeof e.code)return e.code}function createError4(e,t){const r=new Error(e);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:t,writable:!0})}catch{}return r}var y=require("crypto"),g=require("fs/promises"),E=require("os"),b=1e4,v=25,P=1e3,O=6e4,$=new Map,M=new Set;async function withFileLock(e,t,r={}){const o=normalizeFilePath(e);return function(e){if(void 0!==e.timeoutMs&&(!Number.isFinite(e.timeoutMs)||e.timeoutMs<0))throw new TypeError("[confedit] lock timeoutMs must be a non-negative finite number.");if(void 0!==e.retryDelayMs&&(!Number.isFinite(e.retryDelayMs)||e.retryDelayMs<=0))throw new TypeError("[confedit] lock retryDelayMs must be a positive finite number.");if(void 0!==e.staleThresholdMs&&(!Number.isFinite(e.staleThresholdMs)||e.staleThresholdMs<=0))throw new TypeError("[confedit] lock staleThresholdMs must be a positive finite number.")}(r),function(e,t){const r=$.get(e)??Promise.resolve();let o;const n=new Promise(e=>{o=e}),a=r.catch(()=>{}).then(()=>n);return $.set(e,a),r.catch(()=>{}).then(t).finally(()=>{o(),$.get(e)===a&&$.delete(e)})}(o,async()=>{const e=await async function(e,t){const r=t.timeoutMs??b,o=t.retryDelayMs??v,n=t.staleThresholdMs??Math.max(2*r,O),a=t.allowStaleRecovery??!1,i=`${e}.confedit.lock`,s=Date.now();let c=o;const l=createToken(),u={version:1,pid:process.pid,hostname:(0,E.hostname)(),createdAt:(new Date).toISOString(),token:l};for(;;)try{const e=await(0,g.open)(i,"wx",384);try{await(0,g.writeFile)(e,`${JSON.stringify(u)}\n`,"utf8"),await e.sync()}finally{await e.close()}return M.add(i),createReleaseHandler(i,l)}catch(t){if("EEXIST"!==getErrorCode2(t))throw createError5(`[confedit] Failed to acquire lock "${i}": ${getErrorMessage5(t)}`,t);if(a&&await tryRecoverStaleLock(i,n),Date.now()-s>=r)throw new Error(`[confedit] Timed out waiting for lock on "${e}" after ${r}ms.`);await sleep(c),c=Math.min(Math.ceil(1.5*c),P)}}(o,r);try{return await t()}finally{await e()}})}async function releaseAllLocalLocks(){const e=Array.from(M).map(e=>(0,g.unlink)(e).catch(()=>{}));await Promise.allSettled(e),M.clear()}function createReleaseHandler(e,t){return async()=>{try{const r=await readLockData(e);r?.token===t&&(await(0,g.unlink)(e).catch(()=>{}),M.delete(e))}catch{}}}async function tryRecoverStaleLock(e,t){const r=await readLockData(e);if(void 0===r||!isExpiredLock(r,t))return;const o=`${e}.stale.${createToken()}`;try{await(0,g.rename)(e,o)}catch(e){return void getErrorCode2(e)}try{const n=await readLockData(o);if(n?.token===r.token&&isExpiredLock(n,t))return void await(0,g.unlink)(o).catch(()=>{});await(0,g.rename)(o,e).catch(()=>{})}catch{}}function isExpiredLock(e,t){const r=Date.parse(e.createdAt);return Number.isFinite(r)&&Date.now()-r>t}async function readLockData(e){try{return function(e){try{const t=JSON.parse(e);if(1!==t.version||"number"!=typeof t.pid||!Number.isSafeInteger(t.pid)||t.pid<=0||"string"!=typeof t.hostname||"string"!=typeof t.createdAt||!Number.isFinite(Date.parse(t.createdAt))||"string"!=typeof t.token||t.token.length<16)return;return{version:1,pid:t.pid,hostname:t.hostname,createdAt:t.createdAt,token:t.token}}catch{return}}(await(0,g.readFile)(e,"utf8"))}catch{return}}function createToken(){return`${process.pid}-${Date.now()}-${(0,y.randomBytes)(16).toString("hex")}`}function sleep(e){return new Promise(t=>setTimeout(t,e))}function getErrorCode2(e){if(null!==e&&"object"==typeof e&&"code"in e&&"string"==typeof e.code)return e.code}function getErrorMessage5(e){return e instanceof Error?e.message:String(e)}function createError5(e,t){const r=new Error(e);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:t,writable:!0})}catch{}return r}async function writeConfigFile(e,t,r={}){if(assertNonEmptyString(e,"filePath"),"string"!=typeof t)throw new TypeError("[confedit] content must be a string.");const o=normalizeFilePath(e),writeTransaction=async()=>{try{await atomicWrite(o,t)}catch(t){throw createError2(`[confedit] Failed to write "${e}": ${getErrorMessage2(t)}`,t)}};r.lock??!0?(await(0,p.mkdir)((0,h.dirname)(o),{recursive:!0}),await withFileLock(o,writeTransaction,{timeoutMs:r.lockTimeoutMs,retryDelayMs:r.lockRetryDelayMs,staleThresholdMs:r.lockStaleThresholdMs,allowStaleRecovery:r.allowStaleRecovery})):await writeTransaction()}var T=require("path");function detectFormat(e){const t=normalizeFilePath(e);switch((0,T.extname)(t).toLowerCase()){case".json":return"json";case".jsonc":return"jsonc";case".yaml":case".yml":return"yaml";default:throw new Error(`[confedit] Cannot detect a supported configuration format for "${e}". Expected .json, .jsonc, .yaml, or .yml.`)}}async function patchConfigFile(e,t,r={}){if("string"!=typeof e||0===e.length)throw new TypeError("[confedit] filePath must be a non-empty string.");if(assertPatchOperations(t),0===t.length)return;const o=normalizeFilePath(e),n=r.format??detectFormat(o);assertConfigFormat(n),function(e){if(void 0!==e.lockTimeoutMs&&(!Number.isFinite(e.lockTimeoutMs)||e.lockTimeoutMs<0))throw new TypeError("[confedit] lockTimeoutMs must be a non-negative finite number.");if(void 0!==e.lockRetryDelayMs&&(!Number.isFinite(e.lockRetryDelayMs)||e.lockRetryDelayMs<=0))throw new TypeError("[confedit] lockRetryDelayMs must be a positive finite number.");if(void 0!==e.lockStaleThresholdMs&&(!Number.isFinite(e.lockStaleThresholdMs)||e.lockStaleThresholdMs<=0))throw new TypeError("[confedit] lockStaleThresholdMs must be a positive finite number.")}(r);const a=r.strict??!0,patchTransaction=async()=>{const r=await readConfigFile(o),i=patchContent(r,t,n,{strict:a});if(i!==r)try{await atomicWrite(o,i)}catch(t){throw createError2(`[confedit] Failed to write "${e}": ${getErrorMessage2(t)}`,t)}};r.lock??!0?await withFileLock(o,patchTransaction,{timeoutMs:r.lockTimeoutMs,retryDelayMs:r.lockRetryDelayMs,staleThresholdMs:r.lockStaleThresholdMs,allowStaleRecovery:r.allowStaleRecovery}):await patchTransaction()}async function setConfigValue(e,t,r,o={}){await patchConfigFile(e,[{op:"add",path:[...t],value:r}],o)}async function deleteConfigValue(e,t,r={}){await patchConfigFile(e,[{op:"remove",path:[...t]}],r)}var D,N=require("fs/promises"),S=require("path"),A=require("yaml"),I=class extends Error{constructor(e,t,r){super(t),this.name="OpenApiValidationError",this.code=e,this.cause=r}},F={inputKind:"content",timeoutMs:15e3,maxInputBytes:5242880,maxDocumentNodes:1e5,maxDocumentDepth:100,maxValidationErrors:50,maxErrorMessageLength:1e3};async function validateOpenAPISpec(e,t={}){!function(e,t){if("string"!=typeof e||0===e.length)throw new TypeError(`[confedit] ${t} must be a non-empty string.`)}(e,"input");const r=function(e){const t={inputKind:e.inputKind??F.inputKind,baseFilePath:e.baseFilePath,allowedRootDirectory:e.allowedRootDirectory,timeoutMs:e.timeoutMs??F.timeoutMs,maxInputBytes:e.maxInputBytes??F.maxInputBytes,maxDocumentNodes:e.maxDocumentNodes??F.maxDocumentNodes,maxDocumentDepth:e.maxDocumentDepth??F.maxDocumentDepth,maxValidationErrors:e.maxValidationErrors??F.maxValidationErrors,maxErrorMessageLength:e.maxErrorMessageLength??F.maxErrorMessageLength,signal:e.signal};if("content"!==t.inputKind&&"file"!==t.inputKind)throw new I("INVALID_OPTION",`inputKind must be "content" or "file", got: ${String(t.inputKind)}.`);return assertPositiveFiniteNumber(t.timeoutMs,"timeoutMs"),assertPositiveFiniteNumber(t.maxInputBytes,"maxInputBytes"),assertPositiveFiniteNumber(t.maxDocumentNodes,"maxDocumentNodes"),assertPositiveFiniteNumber(t.maxDocumentDepth,"maxDocumentDepth"),assertPositiveFiniteNumber(t.maxValidationErrors,"maxValidationErrors"),assertPositiveFiniteNumber(t.maxErrorMessageLength,"maxErrorMessageLength"),t}(t),o=function(e){const t=new AbortController;if(e){if(!e.aborted){const onAbort=()=>t.abort();return e.addEventListener("abort",onAbort,{once:!0}),{controller:t,dispose:()=>e.removeEventListener("abort",onAbort)}}t.abort()}return{controller:t,dispose:()=>{}}}(r.signal),n={options:r,controller:o.controller,deadline:Date.now()+r.timeoutMs};try{return await runWithDeadline(n,async()=>{if("file"===r.inputKind){const t=await async function(e,t){const r=normalizeFilePath(e);if(!t.options.allowedRootDirectory)throw new I("FILE_INPUT_FORBIDDEN","File input requires allowedRootDirectory.");return async function(e,t){const r=(0,S.isAbsolute)(e)?e:(0,S.resolve)(t,e),o=await(0,N.realpath)(r),n=(0,S.relative)(t,o);if(n.startsWith("..")||""===n||"\\"===S.sep&&/^[a-zA-Z]:/.test(n))throw new I("PATH_OUTSIDE_ROOT",`File path is outside the allowed root directory: ${e}.`);return o}(r,await async function(e){if(e.rootDirectory)return e.rootDirectory;const t=e.options.allowedRootDirectory;if(!t)throw new I("INVALID_OPTION","allowedRootDirectory is required for file operations.");const r=(0,S.resolve)(t),o=await(0,N.realpath)(r);if(!(await(0,N.stat)(o)).isDirectory())throw new I("INVALID_OPTION",`allowedRootDirectory is not a directory: ${t}.`);return e.rootDirectory=o,o}(t))}(e,n),o=await async function(e,t,r,o){throwIfCancelled(r);const n=await(0,N.open)(e,"r");try{const r=await n.stat();if(r.size>t)throw new I(o,`File exceeds maximum allowed size of ${t} bytes: ${e}.`);const a=Buffer.alloc(r.size);return await n.read(a,0,r.size,0),a.toString("utf8")}finally{await n.close()}}(t,r.maxInputBytes,n,"INPUT_FILE_TOO_LARGE");return validateRawContent(o,{...n,options:{...r,baseFilePath:t}})}return assertByteLength(e,r.maxInputBytes,"INPUT_TOO_LARGE"),validateRawContent(e,n)})}catch(e){throw function(e){if(e instanceof I)return e;return new I("UNKNOWN_ERROR",`OpenAPI validation failed: ${getErrorMessage6(e)}.`,e)}(e)}finally{o.dispose(),n.controller.abort()}}async function validateOpenAPIFile(e,t={}){return validateOpenAPISpec(e,{...t,inputKind:"file",baseFilePath:e})}async function validateRawContent(e,t){throwIfCancelled(t),assertByteLength(e,t.options.maxInputBytes,"INPUT_TOO_LARGE");const r=function(e){try{const t=(0,A.parse)(e);if(null===t||"object"!=typeof t||Array.isArray(t))throw new I("INVALID_DOCUMENT_SHAPE","OpenAPI document must be a JSON object.");return t}catch(e){if(e instanceof I)throw e;throw new I("PARSE_ERROR",`Failed to parse OpenAPI document: ${getErrorMessage6(e)}.`,e)}}(e);!function(e,t){let r=0;const o=[{value:e,depth:0}];for(;o.length>0;){const{value:e,depth:n}=o.pop();if(n>t.maxDocumentDepth)throw new I("DOCUMENT_TOO_DEEP",`Document exceeds maximum nesting depth of ${t.maxDocumentDepth}.`);if(r++,r>t.maxDocumentNodes)throw new I("DOCUMENT_TOO_LARGE",`Document exceeds maximum node count of ${t.maxDocumentNodes}.`);if(Array.isArray(e))for(const t of e)o.push({value:t,depth:n+1});else if(null!==e&&"object"==typeof e)for(const t of Object.values(e))o.push({value:t,depth:n+1})}}(r,t.options),function(e){const t=e.openapi??e.swagger;if("string"!=typeof t||0===t.length)throw new I("UNSUPPORTED_VERSION","Document must declare an openapi or swagger version.");const r=Number.parseInt(t.split(".")[0]??"",10);if(!Number.isFinite(r)||2!==r&&3!==r)throw new I("UNSUPPORTED_VERSION",`Unsupported OpenAPI/Swagger version: ${t}. Supported: 2.x, 3.x.`)}(r);const o=await runWithDeadline(t,()=>async function(){return D??=import("@powerduck/openapi-parser").then(({validate:e})=>e),D}()),n=await runWithDeadline(t,()=>o(r,{throwOnError:!1}));if(!n.valid)throw new I("SPEC_VALIDATION_FAILED",`OpenAPI validation failed: ${function(e,t,r){const o=e.slice(0,t).map(e=>{const t=e.instancePath??"",o=e.message??"Unknown error",n=t?`${t}: ${o}`:o;return n.length>r?`${n.slice(0,r)}...`:n});e.length>t&&o.push(`... and ${e.length-t} more errors`);return o.join("; ")}(n.errors??[],t.options.maxValidationErrors,t.options.maxErrorMessageLength)}`);return n.specification}function assertPositiveFiniteNumber(e,t){if(!Number.isFinite(e)||e<=0)throw new I("INVALID_OPTION",`${t} must be a positive finite number, got: ${String(e)}.`)}async function runWithDeadline(e,t){throwIfCancelled(e);const r=await Promise.race([t(),createDeadlinePromise(e)]);return throwIfCancelled(e),r}function createDeadlinePromise(e){return new Promise((t,r)=>{const o=e.deadline-Date.now(),n=Math.max(0,Math.min(o,2147483647)),a=setTimeout(()=>{r(new I("OPERATION_TIMEOUT",`OpenAPI validation operation timed out after ${e.options.timeoutMs}ms.`))},n);e.controller.signal.addEventListener("abort",()=>{clearTimeout(a),r(new I("OPERATION_ABORTED","OpenAPI validation operation was aborted."))},{once:!0})})}function throwIfCancelled(e){if(e.controller.signal.aborted)throw new I("OPERATION_ABORTED","OpenAPI validation operation was aborted.");if(Date.now()>e.deadline)throw new I("OPERATION_TIMEOUT",`OpenAPI validation operation timed out after ${e.options.timeoutMs}ms.`)}function assertByteLength(e,t,r){const o=Buffer.byteLength(e,"utf8");if(o>t)throw new I(r,`Input exceeds maximum allowed size of ${t} bytes (actual: ${o} bytes).`)}function getErrorMessage6(e){return e instanceof Error?e.message:String(e)}
1
+ Object.create;var e,t=Object.defineProperty,r=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,n=(Object.getPrototypeOf,Object.prototype.hasOwnProperty),__copyProps=(e,a,i,s)=>{if(a&&"object"==typeof a||"function"==typeof a)for(let c of o(a))n.call(e,c)||c===i||t(e,c,{get:()=>a[c],enumerable:!(s=r(a,c))||s.enumerable});return e},a={};((e,r)=>{for(var o in r)t(e,o,{get:r[o],enumerable:!0})})(a,{OpenApiValidationError:()=>I,assertConfigFormat:()=>assertConfigFormat,assertNonEmptyString:()=>assertNonEmptyString,assertPatchOperations:()=>assertPatchOperations,assertPatchPath:()=>assertPatchPath,createError:()=>createError2,deleteConfigValue:()=>deleteConfigValue,deleteContentValue:()=>deleteContentValue,detectFormat:()=>detectFormat,getErrorMessage:()=>getErrorMessage2,normalizeFilePath:()=>normalizeFilePath,patchConfigFile:()=>patchConfigFile,patchContent:()=>patchContent,readConfigFile:()=>readConfigFile,releaseAllLocalLocks:()=>releaseAllLocalLocks,setConfigValue:()=>setConfigValue,setContentValue:()=>setContentValue,validateOpenAPIFile:()=>validateOpenAPIFile,validateOpenAPISpec:()=>validateOpenAPISpec,withFileLock:()=>withFileLock,writeConfigFile:()=>writeConfigFile}),module.exports=(e=a,__copyProps(t({},"__esModule",{value:!0}),e));var i=require("jsonc-parser"),s={insertSpaces:!0,tabSize:2,eol:"\n"};function validateOperation(e){if(null===e||"object"!=typeof e)throw new TypeError("Patch operation must be an object.");if(!Array.isArray(e.path)||0===e.path.length)throw new Error("Patch operation path must be a non-empty array.");if("add"!==e.op&&"replace"!==e.op&&"remove"!==e.op)throw new Error(`Unsupported JSON patch operation: ${String(e.op)}`);if(("add"===e.op||"replace"===e.op)&&!Object.prototype.hasOwnProperty.call(e,"value"))throw new Error(`Patch operation "${e.op}" requires a value.`)}function normalizePath(e){return e.map(e=>{if("number"==typeof e){if(!Number.isSafeInteger(e)||e<0)throw new Error(`Invalid array index: ${e}`);return e}if("string"!=typeof e)throw new TypeError(`Patch path segments must be strings or numbers; received ${typeof e}.`);return e})}function applyAdd(e,t,r){const o=function(e,t){if(1===t.length){if("object"!==e.type&&"array"!==e.type)throw new Error("Cannot add a root child to a scalar JSON value.");return e}const r=t.slice(0,-1),o=(0,i.findNodeAtLocation)(e,r);if(void 0===o)throw new Error(`Cannot add value because its parent does not exist at path: ${formatPath(r)}`);if("object"!==o.type&&"array"!==o.type)throw new Error(`Cannot add value because its parent is not an object or array at path: ${formatPath(r)}`);return o}(getTree(e),t),n=t[t.length-1];if("array"===o.type){const a=function(e,t){if("number"!=typeof e)throw new Error(`Array index must be a number at path: ${formatPath(t)}`);if(!Number.isSafeInteger(e)||e<0)throw new Error(`Invalid array index ${e} at path: ${formatPath(t)}`);return e}(n,t),i=o.children?.length??0;if(a>i)throw new Error(`Cannot add at array index ${a}; array length is ${i} at path: ${formatPath(t.slice(0,-1))}`);return applyModify(e,t,r,!0)}if("object"===o.type){if("string"!=typeof n)throw new Error(`Object property path segment must be a string at path: ${formatPath(t)}`);return applyModify(e,t,r,!1)}throw new Error(`Cannot add a child to non-container value at path: ${formatPath(t.slice(0,-1))}`)}function applyReplace(e,t,r){const o=getTree(e);if(void 0===(0,i.findNodeAtLocation)(o,t))throw new Error(`Cannot replace a value that does not exist at path: ${formatPath(t)}`);return applyModify(e,t,r,!1)}function applyRemove(e,t){const r=getTree(e);if(void 0===(0,i.findNodeAtLocation)(r,t))throw new Error(`Cannot remove a value that does not exist at path: ${formatPath(t)}`);return applyModify(e,t,void 0,!1)}function applyModify(e,t,r,o){const n=(0,i.modify)(e,t,r,{formattingOptions:s,isArrayInsertion:o});if(0===n.length)throw new Error(`No JSONC edit was generated for path: ${formatPath(t)}`);return(0,i.applyEdits)(e,n)}function getTree(e){const t=[],r=(0,i.parseTree)(e,t,{allowTrailingComma:!0,disallowComments:!1});if(void 0===r||t.length>0)throw new Error(`The source text is not valid JSON or JSONC${function(e){if(0===e.length)return"";return` (parse errors: ${e.map(e=>`code=${e.error}, offset=${e.offset}`).join("; ")})`}(t)}.`);return r}function formatPath(e){return`[${e.map(e=>JSON.stringify(e)).join(", ")}]`}function safeStringify(e){try{return JSON.stringify(e)}catch{return"[unserializable patch operation]"}}function getErrorMessage(e){return e instanceof Error?e.message:String(e)}function createError(e,t){const r=new Error(e);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:t,writable:!0})}catch{}return r}var c=require("yaml");function applyYamlOperation(e,t){!function(e){if(!Array.isArray(e.path)||0===e.path.length)throw new Error("[confedit] Patch path must be a non-empty array.");if("add"!==e.op&&"replace"!==e.op&&"remove"!==e.op)throw new Error(`[confedit] Unsupported patch operation: ${String(e.op)}.`)}(t);const r=t.path,o=function(e,t){if(0===t.length)throw new Error("[confedit] Replacing the YAML document root is not supported.");if(1===t.length){if(null===e.contents)throw new Error("[confedit] Cannot patch an empty YAML document without a root container.");return e.contents}const r=t.slice(0,-1),o=e.getIn(r,!0);if(null==o)throw new Error(`[confedit] Missing parent path: ${formatPath2(r)}.`);if(!(0,c.isMap)(o)&&!(0,c.isSeq)(o))throw new Error(`[confedit] Parent at ${formatPath2(r)} is not a YAML map or sequence.`);return o}(e,r),n=r[r.length-1];if((0,c.isMap)(o))!function(e,t,r,o){if("string"!=typeof r)throw new Error(`[confedit] YAML map keys must be strings at ${formatPath2(o.path)}.`);const n=t.has(r);switch(o.op){case"add":return void t.set(r,e.createNode(o.value));case"replace":if(!n)throw new Error(`[confedit] Cannot replace missing value at ${formatPath2(o.path)}.`);return void t.set(r,e.createNode(o.value));case"remove":if(!n)throw new Error(`[confedit] Cannot remove missing value at ${formatPath2(o.path)}.`);return void t.delete(r);default:throw new Error(`[confedit] Unsupported patch operation: ${String(o.op)}.`)}}(e,o,n,t);else{if(!(0,c.isSeq)(o))throw new Error(`[confedit] Cannot apply patch at ${formatPath2(r)} because its parent is not a YAML map or sequence.`);!function(e,t,r,o){if("number"!=typeof r||!Number.isSafeInteger(r)||r<0)throw new Error(`[confedit] YAML sequence indexes must be non-negative integers at ${formatPath2(o.path)}.`);const n=t.items.length;switch(o.op){case"add":if(r>n)throw new Error(`[confedit] Cannot insert at index ${r}; sequence length is ${n}.`);return void t.items.splice(r,0,e.createNode(o.value));case"replace":if(r>=n)throw new Error(`[confedit] Cannot replace index ${r}; sequence length is ${n}.`);return void(t.items[r]=e.createNode(o.value));case"remove":if(r>=n)throw new Error(`[confedit] Cannot remove index ${r}; sequence length is ${n}.`);return void t.items.splice(r,1);default:throw new Error(`[confedit] Unsupported patch operation: ${String(o.op)}.`)}}(e,o,n,t)}}function formatPath2(e){return`[${e.map(e=>JSON.stringify(e)).join(", ")}]`}function assertNonEmptyString(e,t){if("string"!=typeof e||0===e.length)throw new TypeError(`[confedit] ${t} must be a non-empty string.`)}function assertPatchPath(e,t="path"){if(!Array.isArray(e)||0===e.length)throw new TypeError(`[confedit] ${t} must be a non-empty array.`);for(const[r,o]of e.entries())if("string"!=typeof o){if("number"!=typeof o||!Number.isSafeInteger(o)||o<0)throw new TypeError(`[confedit] ${t}[${r}] must be a non-empty string or a non-negative integer.`)}else if(0===o.length)throw new TypeError(`[confedit] ${t}[${r}] must not be an empty string.`)}function assertPatchOperations(e){if(!Array.isArray(e))throw new TypeError("[confedit] ops must be an array.");for(const[t,r]of e.entries()){if(null===r||"object"!=typeof r)throw new TypeError(`[confedit] Patch operation at index ${t} must be an object.`);if("add"!==r.op&&"replace"!==r.op&&"remove"!==r.op)throw new Error(`[confedit] Unsupported patch operation at index ${t}: ${String(r.op)}.`);if(assertPatchPath(r.path,`ops[${t}].path`),("add"===r.op||"replace"===r.op)&&!Object.prototype.hasOwnProperty.call(r,"value"))throw new TypeError(`[confedit] Patch operation at index ${t} requires a value.`)}}function assertConfigFormat(e){if("json"!==e&&"jsonc"!==e&&"yaml"!==e)throw new Error(`[confedit] Unsupported configuration format: ${String(e)}.`)}function getErrorMessage2(e){return e instanceof Error?e.message:String(e)}function createError2(e,t){const r=new Error(e);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:t,writable:!0})}catch{}return r}function patchContent(e,t,r,o={}){if("string"!=typeof e)throw new TypeError("[confedit] content must be a string.");if(assertPatchOperations(t),assertConfigFormat(r),0===t.length)return e;const n=o.strict??!0;switch(r){case"json":case"jsonc":return function(e,t,r=!0){if("string"!=typeof e)throw new TypeError("[confedit] sourceText must be a string.");if(!Array.isArray(t))throw new TypeError("[confedit] ops must be an array.");let o=e;for(const e of t)try{validateOperation(e);const t=normalizePath(e.path);switch(e.op){case"add":o=applyAdd(o,t,e.value);break;case"replace":o=applyReplace(o,t,e.value);break;case"remove":o=applyRemove(o,t);break;default:throw new Error(`[confedit] Unsupported JSON patch operation: ${String(e.op)}`)}}catch(t){const o=`[confedit] Failed to apply JSON patch ${safeStringify(e)}: ${getErrorMessage(t)}`;if(r)throw createError(o,t);console.warn(o)}return o}(e,t,n);case"yaml":return function(e,t,r=!0){const o=(0,c.parseDocument)(e,{prettyErrors:!0,strict:!0});if(o.errors.length>0)throw new Error(`[confedit] Invalid YAML source: ${o.errors.map(e=>e.message).join("; ")}`);for(const e of t)try{applyYamlOperation(o,e)}catch(t){if(r)throw t;console.warn(`[confedit yaml patch warn] Skip operation ${JSON.stringify(e)}`,t)}return o.toString()}(e,t,n)}}function setContentValue(e,t,r,o){return assertPatchPath(t),patchContent(e,[{op:"add",path:[...t],value:r}],o)}function deleteContentValue(e,t,r){return assertPatchPath(t),patchContent(e,[{op:"remove",path:[...t]}],r)}var l=require("fs/promises"),u=require("path"),f=require("url");function normalizeFilePath(e){if("string"!=typeof e||0===e.length)throw new TypeError("[confedit] filePath must be a non-empty string.");if(e.startsWith("file:"))try{return(0,f.fileURLToPath)(e)}catch(t){throw function(e,t){const r=new Error(e);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:t,writable:!0})}catch{}return r}(`[confedit] Invalid file URL "${e}": ${function(e){return e instanceof Error?e.message:String(e)}(t)}`,t)}return(0,u.isAbsolute)(e)?e:(0,u.resolve)(e)}async function readConfigFile(e){assertNonEmptyString(e,"filePath");const t=normalizeFilePath(e);try{return await(0,l.readFile)(t,"utf8")}catch(t){throw createError2(`[confedit] Failed to read "${e}": ${getErrorMessage2(t)}`,t)}}var p=require("fs/promises"),h=require("path"),d=require("crypto"),m=require("fs/promises"),w=require("path");async function atomicWrite(e,t){if(function(e,t){if("string"!=typeof e||0===e.length)throw new TypeError(`[confedit] ${t} must be a non-empty string.`)}(e,"filePath"),"string"!=typeof t)throw new TypeError("[confedit] content must be a string.");const r=normalizeFilePath(e),o=(0,w.dirname)(r),n=(0,w.basename)(r),a=`${process.pid}.${Date.now()}.${(0,d.randomBytes)(12).toString("hex")}`,i=(0,w.join)(o,`.${n}.${a}.tmp`),s=(0,w.join)(o,`.${n}.${a}.bak`);let c,l=!1,u=!1;try{await(0,m.mkdir)(o,{recursive:!0});const e=await async function(e){try{return 511&(await(0,m.stat)(e)).mode}catch(e){if("ENOENT"===getErrorCode(e))return;throw e}}(r);c=await(0,m.open)(i,"wx",e??384);try{await c.writeFile(t,"utf8"),await c.sync()}finally{await c.close(),c=void 0}void 0!==e&&await(0,m.chmod)(i,e);try{await(0,m.rename)(i,r),u=!0}catch(e){if(!function(e){if("win32"!==process.platform)return!1;const t=getErrorCode(e);return"EEXIST"===t||"EPERM"===t||"EACCES"===t}(e))throw e;await(0,m.rename)(r,s),l=!0;try{await(0,m.rename)(i,r),u=!0}catch(e){const t=await async function(e,t){try{return await(0,m.unlink)(e).catch(e=>{if("ENOENT"!==getErrorCode(e))throw e}),await(0,m.rename)(t,e),!0}catch{return!1}}(r,s);throw t&&(l=!1),createError4(t?`[confedit] Failed to replace "${r}", but the original file was restored.`:`[confedit] Failed to replace "${r}". The backup was retained at "${s}".`,e)}}await syncDirectory(o),l&&(await(0,m.unlink)(s),l=!1,await syncDirectory(o))}catch(t){throw void 0!==c&&await c.close().catch(()=>{}),createError4(`[confedit] Failed to atomically write "${e}": ${function(e){return e instanceof Error?e.message:String(e)}(t)}`,t)}finally{await(0,m.unlink)(i).catch(()=>{}),u&&l&&await(0,m.unlink)(s).catch(()=>{})}}async function syncDirectory(e){let t;try{t=await(0,m.open)(e,"r"),await t.sync()}catch(e){const t=getErrorCode(e);if("EINVAL"!==t&&"EPERM"!==t&&"EISDIR"!==t&&"ENOSYS"!==t&&"ENOTSUP"!==t)throw e}finally{await(t?.close().catch(()=>{}))}}function getErrorCode(e){if(null!==e&&"object"==typeof e&&"code"in e&&"string"==typeof e.code)return e.code}function createError4(e,t){const r=new Error(e);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:t,writable:!0})}catch{}return r}var y=require("crypto"),g=require("fs/promises"),E=require("os"),b=1e4,v=25,P=1e3,O=6e4,$=new Map,M=new Set;async function withFileLock(e,t,r={}){const o=normalizeFilePath(e);return function(e){if(void 0!==e.timeoutMs&&(!Number.isFinite(e.timeoutMs)||e.timeoutMs<0))throw new TypeError("[confedit] lock timeoutMs must be a non-negative finite number.");if(void 0!==e.retryDelayMs&&(!Number.isFinite(e.retryDelayMs)||e.retryDelayMs<=0))throw new TypeError("[confedit] lock retryDelayMs must be a positive finite number.");if(void 0!==e.staleThresholdMs&&(!Number.isFinite(e.staleThresholdMs)||e.staleThresholdMs<=0))throw new TypeError("[confedit] lock staleThresholdMs must be a positive finite number.")}(r),function(e,t){const r=$.get(e)??Promise.resolve();let o;const n=new Promise(e=>{o=e}),a=r.catch(()=>{}).then(()=>n);return $.set(e,a),r.catch(()=>{}).then(t).finally(()=>{o(),$.get(e)===a&&$.delete(e)})}(o,async()=>{const e=await async function(e,t){const r=t.timeoutMs??b,o=t.retryDelayMs??v,n=t.staleThresholdMs??Math.max(2*r,O),a=t.allowStaleRecovery??!1,i=`${e}.confedit.lock`,s=Date.now();let c=o;const l=createToken(),u={version:1,pid:process.pid,hostname:(0,E.hostname)(),createdAt:(new Date).toISOString(),token:l};for(;;)try{const e=await(0,g.open)(i,"wx",384);try{await(0,g.writeFile)(e,`${JSON.stringify(u)}\n`,"utf8"),await e.sync()}finally{await e.close()}return M.add(i),createReleaseHandler(i,l)}catch(t){if("EEXIST"!==getErrorCode2(t))throw createError5(`[confedit] Failed to acquire lock "${i}": ${getErrorMessage5(t)}`,t);if(a&&await tryRecoverStaleLock(i,n),Date.now()-s>=r)throw new Error(`[confedit] Timed out waiting for lock on "${e}" after ${r}ms.`);await sleep(c),c=Math.min(Math.ceil(1.5*c),P)}}(o,r);try{return await t()}finally{await e()}})}async function releaseAllLocalLocks(){const e=Array.from(M).map(e=>(0,g.unlink)(e).catch(()=>{}));await Promise.allSettled(e),M.clear()}function createReleaseHandler(e,t){return async()=>{try{const r=await readLockData(e);r?.token===t&&(await(0,g.unlink)(e).catch(()=>{}),M.delete(e))}catch{}}}async function tryRecoverStaleLock(e,t){const r=await readLockData(e);if(void 0===r||!isExpiredLock(r,t))return;const o=`${e}.stale.${createToken()}`;try{await(0,g.rename)(e,o)}catch(e){return void getErrorCode2(e)}try{const n=await readLockData(o);if(n?.token===r.token&&isExpiredLock(n,t))return void await(0,g.unlink)(o).catch(()=>{});await(0,g.rename)(o,e).catch(()=>{})}catch{}}function isExpiredLock(e,t){const r=Date.parse(e.createdAt);return Number.isFinite(r)&&Date.now()-r>t}async function readLockData(e){try{return function(e){try{const t=JSON.parse(e);if(1!==t.version||"number"!=typeof t.pid||!Number.isSafeInteger(t.pid)||t.pid<=0||"string"!=typeof t.hostname||"string"!=typeof t.createdAt||!Number.isFinite(Date.parse(t.createdAt))||"string"!=typeof t.token||t.token.length<16)return;return{version:1,pid:t.pid,hostname:t.hostname,createdAt:t.createdAt,token:t.token}}catch{return}}(await(0,g.readFile)(e,"utf8"))}catch{return}}function createToken(){return`${process.pid}-${Date.now()}-${(0,y.randomBytes)(16).toString("hex")}`}function sleep(e){return new Promise(t=>setTimeout(t,e))}function getErrorCode2(e){if(null!==e&&"object"==typeof e&&"code"in e&&"string"==typeof e.code)return e.code}function getErrorMessage5(e){return e instanceof Error?e.message:String(e)}function createError5(e,t){const r=new Error(e);try{Object.defineProperty(r,"cause",{configurable:!0,enumerable:!1,value:t,writable:!0})}catch{}return r}async function writeConfigFile(e,t,r={}){if(assertNonEmptyString(e,"filePath"),"string"!=typeof t)throw new TypeError("[confedit] content must be a string.");const o=normalizeFilePath(e),writeTransaction=async()=>{try{await atomicWrite(o,t)}catch(t){throw createError2(`[confedit] Failed to write "${e}": ${getErrorMessage2(t)}`,t)}};r.lock??!0?(await(0,p.mkdir)((0,h.dirname)(o),{recursive:!0}),await withFileLock(o,writeTransaction,{timeoutMs:r.lockTimeoutMs,retryDelayMs:r.lockRetryDelayMs,staleThresholdMs:r.lockStaleThresholdMs,allowStaleRecovery:r.allowStaleRecovery})):await writeTransaction()}var T=require("path");function detectFormat(e){const t=normalizeFilePath(e);switch((0,T.extname)(t).toLowerCase()){case".json":return"json";case".jsonc":return"jsonc";case".yaml":case".yml":return"yaml";default:throw new Error(`[confedit] Cannot detect a supported configuration format for "${e}". Expected .json, .jsonc, .yaml, or .yml.`)}}async function patchConfigFile(e,t,r={}){if("string"!=typeof e||0===e.length)throw new TypeError("[confedit] filePath must be a non-empty string.");if(assertPatchOperations(t),0===t.length)return;const o=normalizeFilePath(e),n=r.format??detectFormat(o);assertConfigFormat(n),function(e){if(void 0!==e.lockTimeoutMs&&(!Number.isFinite(e.lockTimeoutMs)||e.lockTimeoutMs<0))throw new TypeError("[confedit] lockTimeoutMs must be a non-negative finite number.");if(void 0!==e.lockRetryDelayMs&&(!Number.isFinite(e.lockRetryDelayMs)||e.lockRetryDelayMs<=0))throw new TypeError("[confedit] lockRetryDelayMs must be a positive finite number.");if(void 0!==e.lockStaleThresholdMs&&(!Number.isFinite(e.lockStaleThresholdMs)||e.lockStaleThresholdMs<=0))throw new TypeError("[confedit] lockStaleThresholdMs must be a positive finite number.")}(r);const a=r.strict??!0,patchTransaction=async()=>{const r=await readConfigFile(o),i=patchContent(r,t,n,{strict:a});if(i!==r)try{await atomicWrite(o,i)}catch(t){throw createError2(`[confedit] Failed to write "${e}": ${getErrorMessage2(t)}`,t)}};r.lock??!0?await withFileLock(o,patchTransaction,{timeoutMs:r.lockTimeoutMs,retryDelayMs:r.lockRetryDelayMs,staleThresholdMs:r.lockStaleThresholdMs,allowStaleRecovery:r.allowStaleRecovery}):await patchTransaction()}async function setConfigValue(e,t,r,o={}){await patchConfigFile(e,[{op:"add",path:[...t],value:r}],o)}async function deleteConfigValue(e,t,r={}){await patchConfigFile(e,[{op:"remove",path:[...t]}],r)}var D,N=require("fs/promises"),S=require("path"),A=require("yaml"),I=class extends Error{constructor(e,t,r){super(t),this.name="OpenApiValidationError",this.code=e,this.cause=r}},F={inputKind:"content",timeoutMs:15e3,maxInputBytes:5242880,maxDocumentNodes:1e5,maxDocumentDepth:100,maxValidationErrors:50,maxErrorMessageLength:1e3};async function validateOpenAPISpec(e,t={}){!function(e,t){if("string"!=typeof e||0===e.length)throw new TypeError(`[confedit] ${t} must be a non-empty string.`)}(e,"input");const r=function(e){const t={inputKind:e.inputKind??F.inputKind,baseFilePath:e.baseFilePath,allowedRootDirectory:e.allowedRootDirectory,timeoutMs:e.timeoutMs??F.timeoutMs,maxInputBytes:e.maxInputBytes??F.maxInputBytes,maxDocumentNodes:e.maxDocumentNodes??F.maxDocumentNodes,maxDocumentDepth:e.maxDocumentDepth??F.maxDocumentDepth,maxValidationErrors:e.maxValidationErrors??F.maxValidationErrors,maxErrorMessageLength:e.maxErrorMessageLength??F.maxErrorMessageLength,signal:e.signal};if("content"!==t.inputKind&&"file"!==t.inputKind)throw new I("INVALID_OPTION",`inputKind must be "content" or "file", got: ${String(t.inputKind)}.`);return assertPositiveFiniteNumber(t.timeoutMs,"timeoutMs"),assertPositiveFiniteNumber(t.maxInputBytes,"maxInputBytes"),assertPositiveFiniteNumber(t.maxDocumentNodes,"maxDocumentNodes"),assertPositiveFiniteNumber(t.maxDocumentDepth,"maxDocumentDepth"),assertPositiveFiniteNumber(t.maxValidationErrors,"maxValidationErrors"),assertPositiveFiniteNumber(t.maxErrorMessageLength,"maxErrorMessageLength"),t}(t),o=function(e){const t=new AbortController;if(e){if(!e.aborted){const onAbort=()=>t.abort();return e.addEventListener("abort",onAbort,{once:!0}),{controller:t,dispose:()=>e.removeEventListener("abort",onAbort)}}t.abort()}return{controller:t,dispose:()=>{}}}(r.signal),n={options:r,controller:o.controller,deadline:Date.now()+r.timeoutMs};try{return await runWithDeadline(n,async()=>{if("file"===r.inputKind){const t=await async function(e,t){const r=normalizeFilePath(e);if(!t.options.allowedRootDirectory)throw new I("FILE_INPUT_FORBIDDEN","File input requires allowedRootDirectory.");return async function(e,t){const r=(0,S.isAbsolute)(e)?e:(0,S.resolve)(t,e),o=await(0,N.realpath)(r),n=(0,S.relative)(t,o);if(n.startsWith("..")||""===n||"\\"===S.sep&&/^[a-zA-Z]:/.test(n))throw new I("PATH_OUTSIDE_ROOT",`File path is outside the allowed root directory: ${e}.`);return o}(r,await async function(e){if(e.rootDirectory)return e.rootDirectory;const t=e.options.allowedRootDirectory;if(!t)throw new I("INVALID_OPTION","allowedRootDirectory is required for file operations.");const r=(0,S.resolve)(t),o=await(0,N.realpath)(r);if(!(await(0,N.stat)(o)).isDirectory())throw new I("INVALID_OPTION",`allowedRootDirectory is not a directory: ${t}.`);return e.rootDirectory=o,o}(t))}(e,n),o=await async function(e,t,r,o){throwIfCancelled(r);const n=await(0,N.open)(e,"r");try{const r=await n.stat();if(r.size>t)throw new I(o,`File exceeds maximum allowed size of ${t} bytes: ${e}.`);const a=Buffer.alloc(r.size);return await n.read(a,0,r.size,0),a.toString("utf8")}finally{await n.close()}}(t,r.maxInputBytes,n,"INPUT_FILE_TOO_LARGE");return validateRawContent(o,{...n,options:{...r,baseFilePath:t}})}return assertByteLength(e,r.maxInputBytes,"INPUT_TOO_LARGE"),validateRawContent(e,n)})}catch(e){throw function(e){if(e instanceof I)return e;return new I("UNKNOWN_ERROR",`OpenAPI validation failed: ${getErrorMessage6(e)}.`,e)}(e)}finally{o.dispose(),n.controller.abort()}}async function validateOpenAPIFile(e,t={}){return validateOpenAPISpec(e,{...t,inputKind:"file",baseFilePath:e})}async function validateRawContent(e,t){throwIfCancelled(t),assertByteLength(e,t.options.maxInputBytes,"INPUT_TOO_LARGE");const r=function(e){try{const t=(0,A.parse)(e);if(null===t||"object"!=typeof t||Array.isArray(t))throw new I("INVALID_DOCUMENT_SHAPE","OpenAPI document must be a JSON object.");return t}catch(e){if(e instanceof I)throw e;throw new I("PARSE_ERROR",`Failed to parse OpenAPI document: ${getErrorMessage6(e)}.`,e)}}(e);!function(e,t){let r=0;const o=[{value:e,depth:0}];for(;o.length>0;){const{value:e,depth:n}=o.pop();if(n>t.maxDocumentDepth)throw new I("DOCUMENT_TOO_DEEP",`Document exceeds maximum nesting depth of ${t.maxDocumentDepth}.`);if(r++,r>t.maxDocumentNodes)throw new I("DOCUMENT_TOO_LARGE",`Document exceeds maximum node count of ${t.maxDocumentNodes}.`);if(Array.isArray(e))for(const t of e)o.push({value:t,depth:n+1});else if(null!==e&&"object"==typeof e)for(const t of Object.values(e))o.push({value:t,depth:n+1})}}(r,t.options),function(e){const t=e.openapi??e.swagger;if("string"!=typeof t||0===t.length)throw new I("UNSUPPORTED_VERSION","Document must declare an openapi or swagger version.");const r=Number.parseInt(t.split(".")[0]??"",10);if(!Number.isFinite(r)||2!==r&&3!==r)throw new I("UNSUPPORTED_VERSION",`Unsupported OpenAPI/Swagger version: ${t}. Supported: 2.x, 3.x.`)}(r);const o=await runWithDeadline(t,()=>async function(){return D??=import("@powerduck/openapi-parser").then(e=>e.validate),D}()),n=await runWithDeadline(t,()=>o(r,{throwOnError:!1}));if(!n.valid)throw new I("SPEC_VALIDATION_FAILED",`OpenAPI validation failed: ${function(e,t,r){const o=e.slice(0,t).map(e=>{const t=e.instancePath??"",o=e.message??"Unknown error",n=t?`${t}: ${o}`:o;return n.length>r?`${n.slice(0,r)}...`:n});e.length>t&&o.push(`... and ${e.length-t} more errors`);return o.join("; ")}(n.errors??[],t.options.maxValidationErrors,t.options.maxErrorMessageLength)}`);return n.specification}function assertPositiveFiniteNumber(e,t){if(!Number.isFinite(e)||e<=0)throw new I("INVALID_OPTION",`${t} must be a positive finite number, got: ${String(e)}.`)}async function runWithDeadline(e,t){throwIfCancelled(e);const r=await Promise.race([t(),createDeadlinePromise(e)]);return throwIfCancelled(e),r}function createDeadlinePromise(e){return new Promise((t,r)=>{const o=e.deadline-Date.now(),n=Math.max(0,Math.min(o,2147483647)),a=setTimeout(()=>{r(new I("OPERATION_TIMEOUT",`OpenAPI validation operation timed out after ${e.options.timeoutMs}ms.`))},n);e.controller.signal.addEventListener("abort",()=>{clearTimeout(a),r(new I("OPERATION_ABORTED","OpenAPI validation operation was aborted."))},{once:!0})})}function throwIfCancelled(e){if(e.controller.signal.aborted)throw new I("OPERATION_ABORTED","OpenAPI validation operation was aborted.");if(Date.now()>e.deadline)throw new I("OPERATION_TIMEOUT",`OpenAPI validation operation timed out after ${e.options.timeoutMs}ms.`)}function assertByteLength(e,t,r){const o=Buffer.byteLength(e,"utf8");if(o>t)throw new I(r,`Input exceeds maximum allowed size of ${t} bytes (actual: ${o} bytes).`)}function getErrorMessage6(e){return e instanceof Error?e.message:String(e)}
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{assertConfigFormat as t,assertNonEmptyString as e,assertPatchOperations as o,assertPatchPath as r,createError as n,deleteContentValue as i,getErrorMessage as a,patchContent as s,setContentValue as c}from"./chunk-YWMVVRYM.mjs";import{readFile as l}from"fs/promises";import{isAbsolute as u,resolve as f}from"path";import{fileURLToPath as m}from"url";function normalizeFilePath(t){if("string"!=typeof t||0===t.length)throw new TypeError("[confedit] filePath must be a non-empty string.");if(t.startsWith("file:"))try{return m(t)}catch(e){throw function(t,e){const o=new Error(t);try{Object.defineProperty(o,"cause",{configurable:!0,enumerable:!1,value:e,writable:!0})}catch{}return o}(`[confedit] Invalid file URL "${t}": ${function(t){return t instanceof Error?t.message:String(t)}(e)}`,e)}return u(t)?t:f(t)}async function readConfigFile(t){e(t,"filePath");const o=normalizeFilePath(t);try{return await l(o,"utf8")}catch(e){throw n(`[confedit] Failed to read "${t}": ${a(e)}`,e)}}import{mkdir as d}from"fs/promises";import{dirname as w}from"path";import{randomBytes as h}from"crypto";import{chmod as p,mkdir as y,open as g,rename as E,stat as D,unlink as b}from"fs/promises";import{basename as T,dirname as v,join as M}from"path";async function atomicWrite(t,e){if(function(t,e){if("string"!=typeof t||0===t.length)throw new TypeError(`[confedit] ${e} must be a non-empty string.`)}(t,"filePath"),"string"!=typeof e)throw new TypeError("[confedit] content must be a string.");const o=normalizeFilePath(t),r=v(o),n=T(o),i=`${process.pid}.${Date.now()}.${h(12).toString("hex")}`,a=M(r,`.${n}.${i}.tmp`),s=M(r,`.${n}.${i}.bak`);let c,l=!1,u=!1;try{await y(r,{recursive:!0});const t=await async function(t){try{return 511&(await D(t)).mode}catch(t){if("ENOENT"===getErrorCode(t))return;throw t}}(o);c=await g(a,"wx",t??384);try{await c.writeFile(e,"utf8"),await c.sync()}finally{await c.close(),c=void 0}void 0!==t&&await p(a,t);try{await E(a,o),u=!0}catch(t){if(!function(t){if("win32"!==process.platform)return!1;const e=getErrorCode(t);return"EEXIST"===e||"EPERM"===e||"EACCES"===e}(t))throw t;await E(o,s),l=!0;try{await E(a,o),u=!0}catch(t){const e=await async function(t,e){try{return await b(t).catch(t=>{if("ENOENT"!==getErrorCode(t))throw t}),await E(e,t),!0}catch{return!1}}(o,s);throw e&&(l=!1),createError3(e?`[confedit] Failed to replace "${o}", but the original file was restored.`:`[confedit] Failed to replace "${o}". The backup was retained at "${s}".`,t)}}await syncDirectory(r),l&&(await b(s),l=!1,await syncDirectory(r))}catch(e){throw void 0!==c&&await c.close().catch(()=>{}),createError3(`[confedit] Failed to atomically write "${t}": ${function(t){return t instanceof Error?t.message:String(t)}(e)}`,e)}finally{await b(a).catch(()=>{}),u&&l&&await b(s).catch(()=>{})}}async function syncDirectory(t){let e;try{e=await g(t,"r"),await e.sync()}catch(t){const e=getErrorCode(t);if("EINVAL"!==e&&"EPERM"!==e&&"EISDIR"!==e&&"ENOSYS"!==e&&"ENOTSUP"!==e)throw t}finally{await(e?.close().catch(()=>{}))}}function getErrorCode(t){if(null!==t&&"object"==typeof t&&"code"in t&&"string"==typeof t.code)return t.code}function createError3(t,e){const o=new Error(t);try{Object.defineProperty(o,"cause",{configurable:!0,enumerable:!1,value:e,writable:!0})}catch{}return o}import{randomBytes as P}from"crypto";import{open as O,readFile as I,rename as N,unlink as F,writeFile as x}from"fs/promises";import{hostname as k}from"os";var $=new Map,A=new Set;async function withFileLock(t,e,o={}){const r=normalizeFilePath(t);return function(t){if(void 0!==t.timeoutMs&&(!Number.isFinite(t.timeoutMs)||t.timeoutMs<0))throw new TypeError("[confedit] lock timeoutMs must be a non-negative finite number.");if(void 0!==t.retryDelayMs&&(!Number.isFinite(t.retryDelayMs)||t.retryDelayMs<=0))throw new TypeError("[confedit] lock retryDelayMs must be a positive finite number.");if(void 0!==t.staleThresholdMs&&(!Number.isFinite(t.staleThresholdMs)||t.staleThresholdMs<=0))throw new TypeError("[confedit] lock staleThresholdMs must be a positive finite number.")}(o),function(t,e){const o=$.get(t)??Promise.resolve();let r;const n=new Promise(t=>{r=t}),i=o.catch(()=>{}).then(()=>n);return $.set(t,i),o.catch(()=>{}).then(e).finally(()=>{r(),$.get(t)===i&&$.delete(t)})}(r,async()=>{const t=await async function(t,e){const o=e.timeoutMs??1e4,r=e.retryDelayMs??25,n=e.staleThresholdMs??Math.max(2*o,6e4),i=e.allowStaleRecovery??!1,a=`${t}.confedit.lock`,s=Date.now();let c=r;const l=createToken(),u={version:1,pid:process.pid,hostname:k(),createdAt:(new Date).toISOString(),token:l};for(;;)try{const t=await O(a,"wx",384);try{await x(t,`${JSON.stringify(u)}\n`,"utf8"),await t.sync()}finally{await t.close()}return A.add(a),createReleaseHandler(a,l)}catch(e){if("EEXIST"!==getErrorCode2(e))throw createError4(`[confedit] Failed to acquire lock "${a}": ${getErrorMessage4(e)}`,e);if(i&&await tryRecoverStaleLock(a,n),Date.now()-s>=o)throw new Error(`[confedit] Timed out waiting for lock on "${t}" after ${o}ms.`);await sleep(c),c=Math.min(Math.ceil(1.5*c),1e3)}}(r,o);try{return await e()}finally{await t()}})}async function releaseAllLocalLocks(){const t=Array.from(A).map(t=>F(t).catch(()=>{}));await Promise.allSettled(t),A.clear()}function createReleaseHandler(t,e){return async()=>{try{const o=await readLockData(t);o?.token===e&&(await F(t).catch(()=>{}),A.delete(t))}catch{}}}async function tryRecoverStaleLock(t,e){const o=await readLockData(t);if(void 0===o||!isExpiredLock(o,e))return;const r=`${t}.stale.${createToken()}`;try{await N(t,r)}catch(t){return void getErrorCode2(t)}try{const n=await readLockData(r);if(n?.token===o.token&&isExpiredLock(n,e))return void await F(r).catch(()=>{});await N(r,t).catch(()=>{})}catch{}}function isExpiredLock(t,e){const o=Date.parse(t.createdAt);return Number.isFinite(o)&&Date.now()-o>e}async function readLockData(t){try{return function(t){try{const e=JSON.parse(t);if(1!==e.version||"number"!=typeof e.pid||!Number.isSafeInteger(e.pid)||e.pid<=0||"string"!=typeof e.hostname||"string"!=typeof e.createdAt||!Number.isFinite(Date.parse(e.createdAt))||"string"!=typeof e.token||e.token.length<16)return;return{version:1,pid:e.pid,hostname:e.hostname,createdAt:e.createdAt,token:e.token}}catch{return}}(await I(t,"utf8"))}catch{return}}function createToken(){return`${process.pid}-${Date.now()}-${P(16).toString("hex")}`}function sleep(t){return new Promise(e=>setTimeout(e,t))}function getErrorCode2(t){if(null!==t&&"object"==typeof t&&"code"in t&&"string"==typeof t.code)return t.code}function getErrorMessage4(t){return t instanceof Error?t.message:String(t)}function createError4(t,e){const o=new Error(t);try{Object.defineProperty(o,"cause",{configurable:!0,enumerable:!1,value:e,writable:!0})}catch{}return o}async function writeConfigFile(t,o,r={}){if(e(t,"filePath"),"string"!=typeof o)throw new TypeError("[confedit] content must be a string.");const i=normalizeFilePath(t),writeTransaction=async()=>{try{await atomicWrite(i,o)}catch(e){throw n(`[confedit] Failed to write "${t}": ${a(e)}`,e)}};r.lock??!0?(await d(w(i),{recursive:!0}),await withFileLock(i,writeTransaction,{timeoutMs:r.lockTimeoutMs,retryDelayMs:r.lockRetryDelayMs,staleThresholdMs:r.lockStaleThresholdMs,allowStaleRecovery:r.allowStaleRecovery})):await writeTransaction()}import{extname as R}from"path";function detectFormat(t){const e=normalizeFilePath(t);switch(R(e).toLowerCase()){case".json":return"json";case".jsonc":return"jsonc";case".yaml":case".yml":return"yaml";default:throw new Error(`[confedit] Cannot detect a supported configuration format for "${t}". Expected .json, .jsonc, .yaml, or .yml.`)}}async function patchConfigFile(e,r,i={}){if("string"!=typeof e||0===e.length)throw new TypeError("[confedit] filePath must be a non-empty string.");if(o(r),0===r.length)return;const c=normalizeFilePath(e),l=i.format??detectFormat(c);t(l),function(t){if(void 0!==t.lockTimeoutMs&&(!Number.isFinite(t.lockTimeoutMs)||t.lockTimeoutMs<0))throw new TypeError("[confedit] lockTimeoutMs must be a non-negative finite number.");if(void 0!==t.lockRetryDelayMs&&(!Number.isFinite(t.lockRetryDelayMs)||t.lockRetryDelayMs<=0))throw new TypeError("[confedit] lockRetryDelayMs must be a positive finite number.");if(void 0!==t.lockStaleThresholdMs&&(!Number.isFinite(t.lockStaleThresholdMs)||t.lockStaleThresholdMs<=0))throw new TypeError("[confedit] lockStaleThresholdMs must be a positive finite number.")}(i);const u=i.strict??!0,patchTransaction=async()=>{const t=await readConfigFile(c),o=s(t,r,l,{strict:u});if(o!==t)try{await atomicWrite(c,o)}catch(t){throw n(`[confedit] Failed to write "${e}": ${a(t)}`,t)}};i.lock??!0?await withFileLock(c,patchTransaction,{timeoutMs:i.lockTimeoutMs,retryDelayMs:i.lockRetryDelayMs,staleThresholdMs:i.lockStaleThresholdMs,allowStaleRecovery:i.allowStaleRecovery}):await patchTransaction()}async function setConfigValue(t,e,o,r={}){await patchConfigFile(t,[{op:"add",path:[...e],value:o}],r)}async function deleteConfigValue(t,e,o={}){await patchConfigFile(t,[{op:"remove",path:[...e]}],o)}import{open as S,realpath as L,stat as C}from"fs/promises";import{isAbsolute as _,relative as V,resolve as U,sep as B}from"path";import{parse as j}from"yaml";var z,K=class extends Error{constructor(t,e,o){super(e),this.name="OpenApiValidationError",this.code=t,this.cause=o}},W={inputKind:"content",timeoutMs:15e3,maxInputBytes:5242880,maxDocumentNodes:1e5,maxDocumentDepth:100,maxValidationErrors:50,maxErrorMessageLength:1e3};async function validateOpenAPISpec(t,e={}){!function(t,e){if("string"!=typeof t||0===t.length)throw new TypeError(`[confedit] ${e} must be a non-empty string.`)}(t,"input");const o=function(t){const e={inputKind:t.inputKind??W.inputKind,baseFilePath:t.baseFilePath,allowedRootDirectory:t.allowedRootDirectory,timeoutMs:t.timeoutMs??W.timeoutMs,maxInputBytes:t.maxInputBytes??W.maxInputBytes,maxDocumentNodes:t.maxDocumentNodes??W.maxDocumentNodes,maxDocumentDepth:t.maxDocumentDepth??W.maxDocumentDepth,maxValidationErrors:t.maxValidationErrors??W.maxValidationErrors,maxErrorMessageLength:t.maxErrorMessageLength??W.maxErrorMessageLength,signal:t.signal};if("content"!==e.inputKind&&"file"!==e.inputKind)throw new K("INVALID_OPTION",`inputKind must be "content" or "file", got: ${String(e.inputKind)}.`);return assertPositiveFiniteNumber(e.timeoutMs,"timeoutMs"),assertPositiveFiniteNumber(e.maxInputBytes,"maxInputBytes"),assertPositiveFiniteNumber(e.maxDocumentNodes,"maxDocumentNodes"),assertPositiveFiniteNumber(e.maxDocumentDepth,"maxDocumentDepth"),assertPositiveFiniteNumber(e.maxValidationErrors,"maxValidationErrors"),assertPositiveFiniteNumber(e.maxErrorMessageLength,"maxErrorMessageLength"),e}(e),r=function(t){const e=new AbortController;if(t){if(!t.aborted){const onAbort=()=>e.abort();return t.addEventListener("abort",onAbort,{once:!0}),{controller:e,dispose:()=>t.removeEventListener("abort",onAbort)}}e.abort()}return{controller:e,dispose:()=>{}}}(o.signal),n={options:o,controller:r.controller,deadline:Date.now()+o.timeoutMs};try{return await runWithDeadline(n,async()=>{if("file"===o.inputKind){const e=await async function(t,e){const o=normalizeFilePath(t);if(!e.options.allowedRootDirectory)throw new K("FILE_INPUT_FORBIDDEN","File input requires allowedRootDirectory.");return async function(t,e){const o=_(t)?t:U(e,t),r=await L(o),n=V(e,r);if(n.startsWith("..")||""===n||"\\"===B&&/^[a-zA-Z]:/.test(n))throw new K("PATH_OUTSIDE_ROOT",`File path is outside the allowed root directory: ${t}.`);return r}(o,await async function(t){if(t.rootDirectory)return t.rootDirectory;const e=t.options.allowedRootDirectory;if(!e)throw new K("INVALID_OPTION","allowedRootDirectory is required for file operations.");const o=U(e),r=await L(o);if(!(await C(r)).isDirectory())throw new K("INVALID_OPTION",`allowedRootDirectory is not a directory: ${e}.`);return t.rootDirectory=r,r}(e))}(t,n),r=await async function(t,e,o,r){throwIfCancelled(o);const n=await S(t,"r");try{const o=await n.stat();if(o.size>e)throw new K(r,`File exceeds maximum allowed size of ${e} bytes: ${t}.`);const i=Buffer.alloc(o.size);return await n.read(i,0,o.size,0),i.toString("utf8")}finally{await n.close()}}(e,o.maxInputBytes,n,"INPUT_FILE_TOO_LARGE");return validateRawContent(r,{...n,options:{...o,baseFilePath:e}})}return assertByteLength(t,o.maxInputBytes,"INPUT_TOO_LARGE"),validateRawContent(t,n)})}catch(t){throw function(t){if(t instanceof K)return t;return new K("UNKNOWN_ERROR",`OpenAPI validation failed: ${getErrorMessage5(t)}.`,t)}(t)}finally{r.dispose(),n.controller.abort()}}async function validateOpenAPIFile(t,e={}){return validateOpenAPISpec(t,{...e,inputKind:"file",baseFilePath:t})}async function validateRawContent(t,e){throwIfCancelled(e),assertByteLength(t,e.options.maxInputBytes,"INPUT_TOO_LARGE");const o=function(t){try{const e=j(t);if(null===e||"object"!=typeof e||Array.isArray(e))throw new K("INVALID_DOCUMENT_SHAPE","OpenAPI document must be a JSON object.");return e}catch(t){if(t instanceof K)throw t;throw new K("PARSE_ERROR",`Failed to parse OpenAPI document: ${getErrorMessage5(t)}.`,t)}}(t);!function(t,e){let o=0;const r=[{value:t,depth:0}];for(;r.length>0;){const{value:t,depth:n}=r.pop();if(n>e.maxDocumentDepth)throw new K("DOCUMENT_TOO_DEEP",`Document exceeds maximum nesting depth of ${e.maxDocumentDepth}.`);if(o++,o>e.maxDocumentNodes)throw new K("DOCUMENT_TOO_LARGE",`Document exceeds maximum node count of ${e.maxDocumentNodes}.`);if(Array.isArray(t))for(const e of t)r.push({value:e,depth:n+1});else if(null!==t&&"object"==typeof t)for(const e of Object.values(t))r.push({value:e,depth:n+1})}}(o,e.options),function(t){const e=t.openapi??t.swagger;if("string"!=typeof e||0===e.length)throw new K("UNSUPPORTED_VERSION","Document must declare an openapi or swagger version.");const o=Number.parseInt(e.split(".")[0]??"",10);if(!Number.isFinite(o)||2!==o&&3!==o)throw new K("UNSUPPORTED_VERSION",`Unsupported OpenAPI/Swagger version: ${e}. Supported: 2.x, 3.x.`)}(o);const r=await runWithDeadline(e,()=>async function(){return z??=import("@powerduck/openapi-parser").then(({validate:t})=>t),z}()),n=await runWithDeadline(e,()=>r(o,{throwOnError:!1}));if(!n.valid)throw new K("SPEC_VALIDATION_FAILED",`OpenAPI validation failed: ${function(t,e,o){const r=t.slice(0,e).map(t=>{const e=t.instancePath??"",r=t.message??"Unknown error",n=e?`${e}: ${r}`:r;return n.length>o?`${n.slice(0,o)}...`:n});t.length>e&&r.push(`... and ${t.length-e} more errors`);return r.join("; ")}(n.errors??[],e.options.maxValidationErrors,e.options.maxErrorMessageLength)}`);return n.specification}function assertPositiveFiniteNumber(t,e){if(!Number.isFinite(t)||t<=0)throw new K("INVALID_OPTION",`${e} must be a positive finite number, got: ${String(t)}.`)}async function runWithDeadline(t,e){throwIfCancelled(t);const o=await Promise.race([e(),createDeadlinePromise(t)]);return throwIfCancelled(t),o}function createDeadlinePromise(t){return new Promise((e,o)=>{const r=t.deadline-Date.now(),n=Math.max(0,Math.min(r,2147483647)),i=setTimeout(()=>{o(new K("OPERATION_TIMEOUT",`OpenAPI validation operation timed out after ${t.options.timeoutMs}ms.`))},n);t.controller.signal.addEventListener("abort",()=>{clearTimeout(i),o(new K("OPERATION_ABORTED","OpenAPI validation operation was aborted."))},{once:!0})})}function throwIfCancelled(t){if(t.controller.signal.aborted)throw new K("OPERATION_ABORTED","OpenAPI validation operation was aborted.");if(Date.now()>t.deadline)throw new K("OPERATION_TIMEOUT",`OpenAPI validation operation timed out after ${t.options.timeoutMs}ms.`)}function assertByteLength(t,e,o){const r=Buffer.byteLength(t,"utf8");if(r>e)throw new K(o,`Input exceeds maximum allowed size of ${e} bytes (actual: ${r} bytes).`)}function getErrorMessage5(t){return t instanceof Error?t.message:String(t)}export{K as OpenApiValidationError,t as assertConfigFormat,e as assertNonEmptyString,o as assertPatchOperations,r as assertPatchPath,n as createError,deleteConfigValue,i as deleteContentValue,detectFormat,a as getErrorMessage,normalizeFilePath,patchConfigFile,s as patchContent,readConfigFile,releaseAllLocalLocks,setConfigValue,c as setContentValue,validateOpenAPIFile,validateOpenAPISpec,withFileLock,writeConfigFile};
1
+ import{assertConfigFormat as t,assertNonEmptyString as e,assertPatchOperations as o,assertPatchPath as r,createError as n,deleteContentValue as i,getErrorMessage as a,patchContent as s,setContentValue as c}from"./chunk-YWMVVRYM.mjs";import{readFile as l}from"fs/promises";import{isAbsolute as u,resolve as f}from"path";import{fileURLToPath as m}from"url";function normalizeFilePath(t){if("string"!=typeof t||0===t.length)throw new TypeError("[confedit] filePath must be a non-empty string.");if(t.startsWith("file:"))try{return m(t)}catch(e){throw function(t,e){const o=new Error(t);try{Object.defineProperty(o,"cause",{configurable:!0,enumerable:!1,value:e,writable:!0})}catch{}return o}(`[confedit] Invalid file URL "${t}": ${function(t){return t instanceof Error?t.message:String(t)}(e)}`,e)}return u(t)?t:f(t)}async function readConfigFile(t){e(t,"filePath");const o=normalizeFilePath(t);try{return await l(o,"utf8")}catch(e){throw n(`[confedit] Failed to read "${t}": ${a(e)}`,e)}}import{mkdir as d}from"fs/promises";import{dirname as w}from"path";import{randomBytes as h}from"crypto";import{chmod as p,mkdir as y,open as g,rename as E,stat as D,unlink as b}from"fs/promises";import{basename as T,dirname as v,join as M}from"path";async function atomicWrite(t,e){if(function(t,e){if("string"!=typeof t||0===t.length)throw new TypeError(`[confedit] ${e} must be a non-empty string.`)}(t,"filePath"),"string"!=typeof e)throw new TypeError("[confedit] content must be a string.");const o=normalizeFilePath(t),r=v(o),n=T(o),i=`${process.pid}.${Date.now()}.${h(12).toString("hex")}`,a=M(r,`.${n}.${i}.tmp`),s=M(r,`.${n}.${i}.bak`);let c,l=!1,u=!1;try{await y(r,{recursive:!0});const t=await async function(t){try{return 511&(await D(t)).mode}catch(t){if("ENOENT"===getErrorCode(t))return;throw t}}(o);c=await g(a,"wx",t??384);try{await c.writeFile(e,"utf8"),await c.sync()}finally{await c.close(),c=void 0}void 0!==t&&await p(a,t);try{await E(a,o),u=!0}catch(t){if(!function(t){if("win32"!==process.platform)return!1;const e=getErrorCode(t);return"EEXIST"===e||"EPERM"===e||"EACCES"===e}(t))throw t;await E(o,s),l=!0;try{await E(a,o),u=!0}catch(t){const e=await async function(t,e){try{return await b(t).catch(t=>{if("ENOENT"!==getErrorCode(t))throw t}),await E(e,t),!0}catch{return!1}}(o,s);throw e&&(l=!1),createError3(e?`[confedit] Failed to replace "${o}", but the original file was restored.`:`[confedit] Failed to replace "${o}". The backup was retained at "${s}".`,t)}}await syncDirectory(r),l&&(await b(s),l=!1,await syncDirectory(r))}catch(e){throw void 0!==c&&await c.close().catch(()=>{}),createError3(`[confedit] Failed to atomically write "${t}": ${function(t){return t instanceof Error?t.message:String(t)}(e)}`,e)}finally{await b(a).catch(()=>{}),u&&l&&await b(s).catch(()=>{})}}async function syncDirectory(t){let e;try{e=await g(t,"r"),await e.sync()}catch(t){const e=getErrorCode(t);if("EINVAL"!==e&&"EPERM"!==e&&"EISDIR"!==e&&"ENOSYS"!==e&&"ENOTSUP"!==e)throw t}finally{await(e?.close().catch(()=>{}))}}function getErrorCode(t){if(null!==t&&"object"==typeof t&&"code"in t&&"string"==typeof t.code)return t.code}function createError3(t,e){const o=new Error(t);try{Object.defineProperty(o,"cause",{configurable:!0,enumerable:!1,value:e,writable:!0})}catch{}return o}import{randomBytes as P}from"crypto";import{open as O,readFile as I,rename as N,unlink as F,writeFile as x}from"fs/promises";import{hostname as k}from"os";var $=new Map,A=new Set;async function withFileLock(t,e,o={}){const r=normalizeFilePath(t);return function(t){if(void 0!==t.timeoutMs&&(!Number.isFinite(t.timeoutMs)||t.timeoutMs<0))throw new TypeError("[confedit] lock timeoutMs must be a non-negative finite number.");if(void 0!==t.retryDelayMs&&(!Number.isFinite(t.retryDelayMs)||t.retryDelayMs<=0))throw new TypeError("[confedit] lock retryDelayMs must be a positive finite number.");if(void 0!==t.staleThresholdMs&&(!Number.isFinite(t.staleThresholdMs)||t.staleThresholdMs<=0))throw new TypeError("[confedit] lock staleThresholdMs must be a positive finite number.")}(o),function(t,e){const o=$.get(t)??Promise.resolve();let r;const n=new Promise(t=>{r=t}),i=o.catch(()=>{}).then(()=>n);return $.set(t,i),o.catch(()=>{}).then(e).finally(()=>{r(),$.get(t)===i&&$.delete(t)})}(r,async()=>{const t=await async function(t,e){const o=e.timeoutMs??1e4,r=e.retryDelayMs??25,n=e.staleThresholdMs??Math.max(2*o,6e4),i=e.allowStaleRecovery??!1,a=`${t}.confedit.lock`,s=Date.now();let c=r;const l=createToken(),u={version:1,pid:process.pid,hostname:k(),createdAt:(new Date).toISOString(),token:l};for(;;)try{const t=await O(a,"wx",384);try{await x(t,`${JSON.stringify(u)}\n`,"utf8"),await t.sync()}finally{await t.close()}return A.add(a),createReleaseHandler(a,l)}catch(e){if("EEXIST"!==getErrorCode2(e))throw createError4(`[confedit] Failed to acquire lock "${a}": ${getErrorMessage4(e)}`,e);if(i&&await tryRecoverStaleLock(a,n),Date.now()-s>=o)throw new Error(`[confedit] Timed out waiting for lock on "${t}" after ${o}ms.`);await sleep(c),c=Math.min(Math.ceil(1.5*c),1e3)}}(r,o);try{return await e()}finally{await t()}})}async function releaseAllLocalLocks(){const t=Array.from(A).map(t=>F(t).catch(()=>{}));await Promise.allSettled(t),A.clear()}function createReleaseHandler(t,e){return async()=>{try{const o=await readLockData(t);o?.token===e&&(await F(t).catch(()=>{}),A.delete(t))}catch{}}}async function tryRecoverStaleLock(t,e){const o=await readLockData(t);if(void 0===o||!isExpiredLock(o,e))return;const r=`${t}.stale.${createToken()}`;try{await N(t,r)}catch(t){return void getErrorCode2(t)}try{const n=await readLockData(r);if(n?.token===o.token&&isExpiredLock(n,e))return void await F(r).catch(()=>{});await N(r,t).catch(()=>{})}catch{}}function isExpiredLock(t,e){const o=Date.parse(t.createdAt);return Number.isFinite(o)&&Date.now()-o>e}async function readLockData(t){try{return function(t){try{const e=JSON.parse(t);if(1!==e.version||"number"!=typeof e.pid||!Number.isSafeInteger(e.pid)||e.pid<=0||"string"!=typeof e.hostname||"string"!=typeof e.createdAt||!Number.isFinite(Date.parse(e.createdAt))||"string"!=typeof e.token||e.token.length<16)return;return{version:1,pid:e.pid,hostname:e.hostname,createdAt:e.createdAt,token:e.token}}catch{return}}(await I(t,"utf8"))}catch{return}}function createToken(){return`${process.pid}-${Date.now()}-${P(16).toString("hex")}`}function sleep(t){return new Promise(e=>setTimeout(e,t))}function getErrorCode2(t){if(null!==t&&"object"==typeof t&&"code"in t&&"string"==typeof t.code)return t.code}function getErrorMessage4(t){return t instanceof Error?t.message:String(t)}function createError4(t,e){const o=new Error(t);try{Object.defineProperty(o,"cause",{configurable:!0,enumerable:!1,value:e,writable:!0})}catch{}return o}async function writeConfigFile(t,o,r={}){if(e(t,"filePath"),"string"!=typeof o)throw new TypeError("[confedit] content must be a string.");const i=normalizeFilePath(t),writeTransaction=async()=>{try{await atomicWrite(i,o)}catch(e){throw n(`[confedit] Failed to write "${t}": ${a(e)}`,e)}};r.lock??!0?(await d(w(i),{recursive:!0}),await withFileLock(i,writeTransaction,{timeoutMs:r.lockTimeoutMs,retryDelayMs:r.lockRetryDelayMs,staleThresholdMs:r.lockStaleThresholdMs,allowStaleRecovery:r.allowStaleRecovery})):await writeTransaction()}import{extname as R}from"path";function detectFormat(t){const e=normalizeFilePath(t);switch(R(e).toLowerCase()){case".json":return"json";case".jsonc":return"jsonc";case".yaml":case".yml":return"yaml";default:throw new Error(`[confedit] Cannot detect a supported configuration format for "${t}". Expected .json, .jsonc, .yaml, or .yml.`)}}async function patchConfigFile(e,r,i={}){if("string"!=typeof e||0===e.length)throw new TypeError("[confedit] filePath must be a non-empty string.");if(o(r),0===r.length)return;const c=normalizeFilePath(e),l=i.format??detectFormat(c);t(l),function(t){if(void 0!==t.lockTimeoutMs&&(!Number.isFinite(t.lockTimeoutMs)||t.lockTimeoutMs<0))throw new TypeError("[confedit] lockTimeoutMs must be a non-negative finite number.");if(void 0!==t.lockRetryDelayMs&&(!Number.isFinite(t.lockRetryDelayMs)||t.lockRetryDelayMs<=0))throw new TypeError("[confedit] lockRetryDelayMs must be a positive finite number.");if(void 0!==t.lockStaleThresholdMs&&(!Number.isFinite(t.lockStaleThresholdMs)||t.lockStaleThresholdMs<=0))throw new TypeError("[confedit] lockStaleThresholdMs must be a positive finite number.")}(i);const u=i.strict??!0,patchTransaction=async()=>{const t=await readConfigFile(c),o=s(t,r,l,{strict:u});if(o!==t)try{await atomicWrite(c,o)}catch(t){throw n(`[confedit] Failed to write "${e}": ${a(t)}`,t)}};i.lock??!0?await withFileLock(c,patchTransaction,{timeoutMs:i.lockTimeoutMs,retryDelayMs:i.lockRetryDelayMs,staleThresholdMs:i.lockStaleThresholdMs,allowStaleRecovery:i.allowStaleRecovery}):await patchTransaction()}async function setConfigValue(t,e,o,r={}){await patchConfigFile(t,[{op:"add",path:[...e],value:o}],r)}async function deleteConfigValue(t,e,o={}){await patchConfigFile(t,[{op:"remove",path:[...e]}],o)}import{open as S,realpath as L,stat as C}from"fs/promises";import{isAbsolute as _,relative as V,resolve as U,sep as B}from"path";import{parse as j}from"yaml";var z,K=class extends Error{constructor(t,e,o){super(e),this.name="OpenApiValidationError",this.code=t,this.cause=o}},W={inputKind:"content",timeoutMs:15e3,maxInputBytes:5242880,maxDocumentNodes:1e5,maxDocumentDepth:100,maxValidationErrors:50,maxErrorMessageLength:1e3};async function validateOpenAPISpec(t,e={}){!function(t,e){if("string"!=typeof t||0===t.length)throw new TypeError(`[confedit] ${e} must be a non-empty string.`)}(t,"input");const o=function(t){const e={inputKind:t.inputKind??W.inputKind,baseFilePath:t.baseFilePath,allowedRootDirectory:t.allowedRootDirectory,timeoutMs:t.timeoutMs??W.timeoutMs,maxInputBytes:t.maxInputBytes??W.maxInputBytes,maxDocumentNodes:t.maxDocumentNodes??W.maxDocumentNodes,maxDocumentDepth:t.maxDocumentDepth??W.maxDocumentDepth,maxValidationErrors:t.maxValidationErrors??W.maxValidationErrors,maxErrorMessageLength:t.maxErrorMessageLength??W.maxErrorMessageLength,signal:t.signal};if("content"!==e.inputKind&&"file"!==e.inputKind)throw new K("INVALID_OPTION",`inputKind must be "content" or "file", got: ${String(e.inputKind)}.`);return assertPositiveFiniteNumber(e.timeoutMs,"timeoutMs"),assertPositiveFiniteNumber(e.maxInputBytes,"maxInputBytes"),assertPositiveFiniteNumber(e.maxDocumentNodes,"maxDocumentNodes"),assertPositiveFiniteNumber(e.maxDocumentDepth,"maxDocumentDepth"),assertPositiveFiniteNumber(e.maxValidationErrors,"maxValidationErrors"),assertPositiveFiniteNumber(e.maxErrorMessageLength,"maxErrorMessageLength"),e}(e),r=function(t){const e=new AbortController;if(t){if(!t.aborted){const onAbort=()=>e.abort();return t.addEventListener("abort",onAbort,{once:!0}),{controller:e,dispose:()=>t.removeEventListener("abort",onAbort)}}e.abort()}return{controller:e,dispose:()=>{}}}(o.signal),n={options:o,controller:r.controller,deadline:Date.now()+o.timeoutMs};try{return await runWithDeadline(n,async()=>{if("file"===o.inputKind){const e=await async function(t,e){const o=normalizeFilePath(t);if(!e.options.allowedRootDirectory)throw new K("FILE_INPUT_FORBIDDEN","File input requires allowedRootDirectory.");return async function(t,e){const o=_(t)?t:U(e,t),r=await L(o),n=V(e,r);if(n.startsWith("..")||""===n||"\\"===B&&/^[a-zA-Z]:/.test(n))throw new K("PATH_OUTSIDE_ROOT",`File path is outside the allowed root directory: ${t}.`);return r}(o,await async function(t){if(t.rootDirectory)return t.rootDirectory;const e=t.options.allowedRootDirectory;if(!e)throw new K("INVALID_OPTION","allowedRootDirectory is required for file operations.");const o=U(e),r=await L(o);if(!(await C(r)).isDirectory())throw new K("INVALID_OPTION",`allowedRootDirectory is not a directory: ${e}.`);return t.rootDirectory=r,r}(e))}(t,n),r=await async function(t,e,o,r){throwIfCancelled(o);const n=await S(t,"r");try{const o=await n.stat();if(o.size>e)throw new K(r,`File exceeds maximum allowed size of ${e} bytes: ${t}.`);const i=Buffer.alloc(o.size);return await n.read(i,0,o.size,0),i.toString("utf8")}finally{await n.close()}}(e,o.maxInputBytes,n,"INPUT_FILE_TOO_LARGE");return validateRawContent(r,{...n,options:{...o,baseFilePath:e}})}return assertByteLength(t,o.maxInputBytes,"INPUT_TOO_LARGE"),validateRawContent(t,n)})}catch(t){throw function(t){if(t instanceof K)return t;return new K("UNKNOWN_ERROR",`OpenAPI validation failed: ${getErrorMessage5(t)}.`,t)}(t)}finally{r.dispose(),n.controller.abort()}}async function validateOpenAPIFile(t,e={}){return validateOpenAPISpec(t,{...e,inputKind:"file",baseFilePath:t})}async function validateRawContent(t,e){throwIfCancelled(e),assertByteLength(t,e.options.maxInputBytes,"INPUT_TOO_LARGE");const o=function(t){try{const e=j(t);if(null===e||"object"!=typeof e||Array.isArray(e))throw new K("INVALID_DOCUMENT_SHAPE","OpenAPI document must be a JSON object.");return e}catch(t){if(t instanceof K)throw t;throw new K("PARSE_ERROR",`Failed to parse OpenAPI document: ${getErrorMessage5(t)}.`,t)}}(t);!function(t,e){let o=0;const r=[{value:t,depth:0}];for(;r.length>0;){const{value:t,depth:n}=r.pop();if(n>e.maxDocumentDepth)throw new K("DOCUMENT_TOO_DEEP",`Document exceeds maximum nesting depth of ${e.maxDocumentDepth}.`);if(o++,o>e.maxDocumentNodes)throw new K("DOCUMENT_TOO_LARGE",`Document exceeds maximum node count of ${e.maxDocumentNodes}.`);if(Array.isArray(t))for(const e of t)r.push({value:e,depth:n+1});else if(null!==t&&"object"==typeof t)for(const e of Object.values(t))r.push({value:e,depth:n+1})}}(o,e.options),function(t){const e=t.openapi??t.swagger;if("string"!=typeof e||0===e.length)throw new K("UNSUPPORTED_VERSION","Document must declare an openapi or swagger version.");const o=Number.parseInt(e.split(".")[0]??"",10);if(!Number.isFinite(o)||2!==o&&3!==o)throw new K("UNSUPPORTED_VERSION",`Unsupported OpenAPI/Swagger version: ${e}. Supported: 2.x, 3.x.`)}(o);const r=await runWithDeadline(e,()=>async function(){return z??=import("@powerduck/openapi-parser").then(t=>t.validate),z}()),n=await runWithDeadline(e,()=>r(o,{throwOnError:!1}));if(!n.valid)throw new K("SPEC_VALIDATION_FAILED",`OpenAPI validation failed: ${function(t,e,o){const r=t.slice(0,e).map(t=>{const e=t.instancePath??"",r=t.message??"Unknown error",n=e?`${e}: ${r}`:r;return n.length>o?`${n.slice(0,o)}...`:n});t.length>e&&r.push(`... and ${t.length-e} more errors`);return r.join("; ")}(n.errors??[],e.options.maxValidationErrors,e.options.maxErrorMessageLength)}`);return n.specification}function assertPositiveFiniteNumber(t,e){if(!Number.isFinite(t)||t<=0)throw new K("INVALID_OPTION",`${e} must be a positive finite number, got: ${String(t)}.`)}async function runWithDeadline(t,e){throwIfCancelled(t);const o=await Promise.race([e(),createDeadlinePromise(t)]);return throwIfCancelled(t),o}function createDeadlinePromise(t){return new Promise((e,o)=>{const r=t.deadline-Date.now(),n=Math.max(0,Math.min(r,2147483647)),i=setTimeout(()=>{o(new K("OPERATION_TIMEOUT",`OpenAPI validation operation timed out after ${t.options.timeoutMs}ms.`))},n);t.controller.signal.addEventListener("abort",()=>{clearTimeout(i),o(new K("OPERATION_ABORTED","OpenAPI validation operation was aborted."))},{once:!0})})}function throwIfCancelled(t){if(t.controller.signal.aborted)throw new K("OPERATION_ABORTED","OpenAPI validation operation was aborted.");if(Date.now()>t.deadline)throw new K("OPERATION_TIMEOUT",`OpenAPI validation operation timed out after ${t.options.timeoutMs}ms.`)}function assertByteLength(t,e,o){const r=Buffer.byteLength(t,"utf8");if(r>e)throw new K(o,`Input exceeds maximum allowed size of ${e} bytes (actual: ${r} bytes).`)}function getErrorMessage5(t){return t instanceof Error?t.message:String(t)}export{K as OpenApiValidationError,t as assertConfigFormat,e as assertNonEmptyString,o as assertPatchOperations,r as assertPatchPath,n as createError,deleteConfigValue,i as deleteContentValue,detectFormat,a as getErrorMessage,normalizeFilePath,patchConfigFile,s as patchContent,readConfigFile,releaseAllLocalLocks,setConfigValue,c as setContentValue,validateOpenAPIFile,validateOpenAPISpec,withFileLock,writeConfigFile};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powerduck/conf-patch",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Two-layer configuration editor: pure core for patching JSON/JSONC/YAML strings (browser-safe), plus file layer with atomic writes and locking for Node.js/Electron. RFC 6902 JSON Patch, comment and formatting preservation, OpenAPI validation.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",