@fulmenhq/tsfulmen 0.3.3 → 0.4.1
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/CHANGELOG.md +76 -1
- package/README.md +30 -1
- package/config/crucible-ts/taxonomy/metrics.yaml +1 -1
- package/dist/appidentity/index.js +9 -2209
- package/dist/appidentity/index.js.map +1 -1
- package/dist/assets/index.d.ts +184 -0
- package/dist/assets/index.js +4 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/bin/prometheus-cli.js +15 -9139
- package/dist/bin/prometheus-cli.js.map +1 -1
- package/dist/bin/schema-cli.js +4 -6098
- package/dist/bin/schema-cli.js.map +1 -1
- package/dist/bin/signals-cli.js +15 -1910
- package/dist/bin/signals-cli.js.map +1 -1
- package/dist/chunk-24BAF63H.js +194 -0
- package/dist/chunk-24BAF63H.js.map +1 -0
- package/dist/chunk-3HISR2XI.js +36 -0
- package/dist/chunk-3HISR2XI.js.map +1 -0
- package/dist/chunk-56F2EEAG.js +328 -0
- package/dist/chunk-56F2EEAG.js.map +1 -0
- package/dist/chunk-5HZEJGZ5.js +725 -0
- package/dist/chunk-5HZEJGZ5.js.map +1 -0
- package/dist/chunk-7YWACZEJ.js +837 -0
- package/dist/chunk-7YWACZEJ.js.map +1 -0
- package/dist/chunk-BCO3FUUO.js +682 -0
- package/dist/chunk-BCO3FUUO.js.map +1 -0
- package/dist/chunk-GOTEQYAD.js +410 -0
- package/dist/chunk-GOTEQYAD.js.map +1 -0
- package/dist/chunk-HY6JQOFN.js +32 -0
- package/dist/chunk-HY6JQOFN.js.map +1 -0
- package/dist/chunk-LMNAUTW7.js +43 -0
- package/dist/chunk-LMNAUTW7.js.map +1 -0
- package/dist/chunk-LWAH2UG7.js +103 -0
- package/dist/chunk-LWAH2UG7.js.map +1 -0
- package/dist/chunk-OHE2AOH4.js +273 -0
- package/dist/chunk-OHE2AOH4.js.map +1 -0
- package/dist/chunk-P3DNOZOG.js +1488 -0
- package/dist/chunk-P3DNOZOG.js.map +1 -0
- package/dist/chunk-PQ6TKPUN.js +27 -0
- package/dist/chunk-PQ6TKPUN.js.map +1 -0
- package/dist/chunk-QCCN6POA.js +211 -0
- package/dist/chunk-QCCN6POA.js.map +1 -0
- package/dist/chunk-QGTNV4U2.js +985 -0
- package/dist/chunk-QGTNV4U2.js.map +1 -0
- package/dist/chunk-RSLFR4TO.js +631 -0
- package/dist/chunk-RSLFR4TO.js.map +1 -0
- package/dist/chunk-SNV5WG5V.js +9412 -0
- package/dist/chunk-SNV5WG5V.js.map +1 -0
- package/dist/chunk-UP2VWCW5.js +33 -0
- package/dist/chunk-UP2VWCW5.js.map +1 -0
- package/dist/chunk-VJIGX7MH.js +208 -0
- package/dist/chunk-VJIGX7MH.js.map +1 -0
- package/dist/chunk-WCCMRVZF.js +130 -0
- package/dist/chunk-WCCMRVZF.js.map +1 -0
- package/dist/chunk-WSMVLWLB.js +269 -0
- package/dist/chunk-WSMVLWLB.js.map +1 -0
- package/dist/config/index.js +6 -1220
- package/dist/config/index.js.map +1 -1
- package/dist/crucible/fulpack/index.js +2 -25
- package/dist/crucible/fulpack/index.js.map +1 -1
- package/dist/crucible/index.js +13 -1129
- package/dist/crucible/index.js.map +1 -1
- package/dist/docscribe/index.js +2 -680
- package/dist/docscribe/index.js.map +1 -1
- package/dist/errors/index.js +5 -1831
- package/dist/errors/index.js.map +1 -1
- package/dist/errors-D7KXV3QW.js +4 -0
- package/dist/errors-D7KXV3QW.js.map +1 -0
- package/dist/errors-R7ZODJXS.js +8 -0
- package/dist/errors-R7ZODJXS.js.map +1 -0
- package/dist/export-EA3RNQNS.js +6 -0
- package/dist/export-EA3RNQNS.js.map +1 -0
- package/dist/foundry/index.js +340 -3949
- package/dist/foundry/index.js.map +1 -1
- package/dist/foundry/similarity/index.js +3 -134
- package/dist/foundry/similarity/index.js.map +1 -1
- package/dist/fulencode/index.js +3 -319
- package/dist/fulencode/index.js.map +1 -1
- package/dist/fulhash/index.js +3 -611
- package/dist/fulhash/index.js.map +1 -1
- package/dist/fulpack/index.js +3 -1515
- package/dist/fulpack/index.js.map +1 -1
- package/dist/hash-YOJYVOWH.js +4 -0
- package/dist/hash-YOJYVOWH.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +14 -4395
- package/dist/index.js.map +1 -1
- package/dist/loader-VP2ZIC4O.js +9 -0
- package/dist/loader-VP2ZIC4O.js.map +1 -0
- package/dist/logging/index.js +2 -723
- package/dist/logging/index.js.map +1 -1
- package/dist/pathfinder/index.js +18 -2341
- package/dist/pathfinder/index.js.map +1 -1
- package/dist/prom-client-PNEBGD3G.js +4801 -0
- package/dist/prom-client-PNEBGD3G.js.map +1 -0
- package/dist/reports/license-inventory.csv +1 -1
- package/dist/schema/index.d.ts +19 -6
- package/dist/schema/index.js +4 -6148
- package/dist/schema/index.js.map +1 -1
- package/dist/signals/index.js +8 -2767
- package/dist/signals/index.js.map +1 -1
- package/dist/similarity/index.js +3 -134
- package/dist/similarity/index.js.map +1 -1
- package/dist/telemetry/http/index.js +9 -930
- package/dist/telemetry/http/index.js.map +1 -1
- package/dist/telemetry/index.js +4 -1561
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/prometheus/index.js +7 -8422
- package/dist/telemetry/prometheus/index.js.map +1 -1
- package/dist/validator-I3ASDVDB.js +6 -0
- package/dist/validator-I3ASDVDB.js.map +1 -0
- package/package.json +5 -1
- package/schemas/crucible-ts/library/module-manifest/v1.0.0/module-manifest.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/definitions.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/log-event.schema.json +13 -13
- package/schemas/crucible-ts/observability/logging/v1.0.0/logger-config.schema.json +2 -2
- package/schemas/crucible-ts/observability/logging/v1.0.0/logging-policy.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/middleware-config.schema.json +2 -2
- package/schemas/crucible-ts/observability/logging/v1.0.0/severity-filter.schema.json +3 -3
- package/schemas/crucible-ts/upstream/3leaps/crucible/PROVENANCE.md +3 -3
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/catalog/classifiers/README.md +1 -1
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/standards/classifiers-framework.md +1 -1
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/standards/data-sensitivity-classification.md +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4400 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import '
|
|
13
|
-
import 'crypto';
|
|
14
|
-
import { existsSync, mkdirSync, statSync, createReadStream, createWriteStream, readdirSync, lstatSync } from 'fs';
|
|
15
|
-
import { pipeline } from 'stream';
|
|
16
|
-
import { createGunzip, createGzip } from 'zlib';
|
|
17
|
-
import { TarArchive, ZipArchive } from 'archiver';
|
|
18
|
-
|
|
19
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
20
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
21
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
22
|
-
}) : x)(function(x) {
|
|
23
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
24
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
25
|
-
});
|
|
26
|
-
var __esm = (fn, res) => function __init() {
|
|
27
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// src/appidentity/constants.ts
|
|
31
|
-
var APP_IDENTITY_FILENAME, APP_IDENTITY_DIR, APP_IDENTITY_ENV_VAR, APP_IDENTITY_SCHEMA_ID, MAX_ANCESTOR_SEARCH_DEPTH;
|
|
32
|
-
var init_constants = __esm({
|
|
33
|
-
"src/appidentity/constants.ts"() {
|
|
34
|
-
APP_IDENTITY_FILENAME = "app.yaml";
|
|
35
|
-
APP_IDENTITY_DIR = ".fulmen";
|
|
36
|
-
APP_IDENTITY_ENV_VAR = "FULMEN_APP_IDENTITY_PATH";
|
|
37
|
-
APP_IDENTITY_SCHEMA_ID = "config/repository/app-identity/v1.0.0/app-identity";
|
|
38
|
-
MAX_ANCESTOR_SEARCH_DEPTH = 20;
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
function applyFulmenAjvFormats(ajv, options = {}) {
|
|
42
|
-
const mode = options.mode ?? "fast";
|
|
43
|
-
const formats = options.formats ?? DEFAULT_FORMATS;
|
|
44
|
-
addFormats(ajv, { mode, formats });
|
|
45
|
-
return ajv;
|
|
46
|
-
}
|
|
47
|
-
var DEFAULT_FORMATS;
|
|
48
|
-
var init_ajv_formats = __esm({
|
|
49
|
-
"src/schema/ajv-formats.ts"() {
|
|
50
|
-
DEFAULT_FORMATS = [
|
|
51
|
-
"date-time",
|
|
52
|
-
"email",
|
|
53
|
-
"hostname",
|
|
54
|
-
"ipv4",
|
|
55
|
-
"ipv6",
|
|
56
|
-
"uri",
|
|
57
|
-
"uri-reference",
|
|
58
|
-
"uuid"
|
|
59
|
-
];
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// src/schema/errors.ts
|
|
64
|
-
var SchemaValidationError;
|
|
65
|
-
var init_errors = __esm({
|
|
66
|
-
"src/schema/errors.ts"() {
|
|
67
|
-
SchemaValidationError = class _SchemaValidationError extends Error {
|
|
68
|
-
constructor(message, schemaId, diagnostics = [], source, cause) {
|
|
69
|
-
super(message);
|
|
70
|
-
this.schemaId = schemaId;
|
|
71
|
-
this.diagnostics = diagnostics;
|
|
72
|
-
this.source = source;
|
|
73
|
-
this.cause = cause;
|
|
74
|
-
this.name = "SchemaValidationError";
|
|
75
|
-
if (Error.captureStackTrace) {
|
|
76
|
-
Error.captureStackTrace(this, _SchemaValidationError);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Create error for schema not found
|
|
81
|
-
*/
|
|
82
|
-
static schemaNotFound(schemaId) {
|
|
83
|
-
return new _SchemaValidationError(`Schema not found: ${schemaId}`, schemaId);
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Create error for invalid schema input
|
|
87
|
-
*/
|
|
88
|
-
static invalidSchemaInput(source, details) {
|
|
89
|
-
return new _SchemaValidationError(`Invalid schema input: ${details}`, void 0, [], source);
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Create error for validation failure
|
|
93
|
-
*/
|
|
94
|
-
static validationFailed(schemaId, diagnostics, source) {
|
|
95
|
-
const errorCount = diagnostics.filter((d) => d.severity === "ERROR").length;
|
|
96
|
-
const warningCount = diagnostics.filter((d) => d.severity === "WARN").length;
|
|
97
|
-
const message = `Schema validation failed: ${errorCount} error(s), ${warningCount} warning(s)`;
|
|
98
|
-
return new _SchemaValidationError(message, schemaId, diagnostics, source);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Create error for goneat binary not found
|
|
102
|
-
*/
|
|
103
|
-
static goneatNotFound(goneatPath) {
|
|
104
|
-
const pathInfo = goneatPath ? ` at ${goneatPath}` : "";
|
|
105
|
-
return new _SchemaValidationError(
|
|
106
|
-
`Goneat binary not found${pathInfo}. Falling back to AJV validation.`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Create error for goneat execution failure
|
|
111
|
-
*/
|
|
112
|
-
static goneatExecutionFailed(error) {
|
|
113
|
-
return new _SchemaValidationError(
|
|
114
|
-
"Goneat execution failed. Falling back to AJV validation.",
|
|
115
|
-
void 0,
|
|
116
|
-
[],
|
|
117
|
-
void 0,
|
|
118
|
-
error
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Create error for empty schema input
|
|
123
|
-
*/
|
|
124
|
-
static emptySchemaInput(source) {
|
|
125
|
-
return new _SchemaValidationError("Schema content is empty", void 0, [], source);
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Create error for parse failure
|
|
129
|
-
*/
|
|
130
|
-
static parseFailed(source, error) {
|
|
131
|
-
return new _SchemaValidationError(
|
|
132
|
-
`Failed to parse schema: ${error.message}`,
|
|
133
|
-
void 0,
|
|
134
|
-
[],
|
|
135
|
-
source,
|
|
136
|
-
error
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Create error for encoding failure
|
|
141
|
-
*/
|
|
142
|
-
static encodingFailed(source, error) {
|
|
143
|
-
return new _SchemaValidationError(
|
|
144
|
-
`Failed to encode schema: ${error.message}`,
|
|
145
|
-
void 0,
|
|
146
|
-
[],
|
|
147
|
-
source,
|
|
148
|
-
error
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Create error for registry operation failure
|
|
153
|
-
*/
|
|
154
|
-
static registryError(operation, details) {
|
|
155
|
-
return new _SchemaValidationError(`Schema registry ${operation} failed: ${details}`);
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Format error for display
|
|
159
|
-
*/
|
|
160
|
-
format() {
|
|
161
|
-
let output = this.message;
|
|
162
|
-
if (this.schemaId) {
|
|
163
|
-
output += `
|
|
164
|
-
Schema ID: ${this.schemaId}`;
|
|
165
|
-
}
|
|
166
|
-
if (this.diagnostics.length > 0) {
|
|
167
|
-
output += "\n\nValidation Issues:";
|
|
168
|
-
this.diagnostics.forEach((diag, index) => {
|
|
169
|
-
output += `
|
|
170
|
-
${index + 1}. [${diag.severity}] ${diag.message}`;
|
|
171
|
-
if (diag.pointer) {
|
|
172
|
-
output += ` at ${diag.pointer}`;
|
|
173
|
-
}
|
|
174
|
-
if (diag.keyword) {
|
|
175
|
-
output += ` (keyword: ${diag.keyword})`;
|
|
176
|
-
}
|
|
177
|
-
if (diag.source) {
|
|
178
|
-
output += ` [${diag.source}]`;
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
if (this.source) {
|
|
183
|
-
output += `
|
|
184
|
-
|
|
185
|
-
Source: ${this.source.type}`;
|
|
186
|
-
if (this.source.id) {
|
|
187
|
-
output += ` (${this.source.id})`;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return output;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Convert to JSON representation
|
|
194
|
-
*/
|
|
195
|
-
toJSON() {
|
|
196
|
-
return {
|
|
197
|
-
name: this.name,
|
|
198
|
-
message: this.message,
|
|
199
|
-
schemaId: this.schemaId,
|
|
200
|
-
diagnostics: this.diagnostics,
|
|
201
|
-
source: this.source,
|
|
202
|
-
cause: this.cause?.message
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// src/schema/utils.ts
|
|
210
|
-
function createDiagnostic(pointer, message, keyword, severity = "ERROR", source = "ajv", data) {
|
|
211
|
-
return {
|
|
212
|
-
pointer,
|
|
213
|
-
message,
|
|
214
|
-
keyword,
|
|
215
|
-
severity,
|
|
216
|
-
source,
|
|
217
|
-
data
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
var init_utils = __esm({
|
|
221
|
-
"src/schema/utils.ts"() {
|
|
222
|
-
init_errors();
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
var init_goneat_bridge = __esm({
|
|
226
|
-
"src/schema/goneat-bridge.ts"() {
|
|
227
|
-
init_utils();
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
var init_normalizer = __esm({
|
|
231
|
-
"src/schema/normalizer.ts"() {
|
|
232
|
-
init_errors();
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
function optionsChanged(newOptions) {
|
|
236
|
-
if (!globalRegistryOptions) return false;
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
function getSchemaRegistry(options) {
|
|
240
|
-
if (!globalRegistry || optionsChanged()) {
|
|
241
|
-
globalRegistry = new SchemaRegistry(options);
|
|
242
|
-
globalRegistryOptions = options;
|
|
243
|
-
}
|
|
244
|
-
return globalRegistry;
|
|
245
|
-
}
|
|
246
|
-
var DEFAULT_PATTERNS, SchemaRegistry, globalRegistry, globalRegistryOptions;
|
|
247
|
-
var init_registry = __esm({
|
|
248
|
-
"src/schema/registry.ts"() {
|
|
249
|
-
init_errors();
|
|
250
|
-
DEFAULT_PATTERNS = ["**/*.schema.json", "**/*.schema.yaml", "**/*.schema.yml"];
|
|
251
|
-
SchemaRegistry = class {
|
|
252
|
-
schemas = /* @__PURE__ */ new Map();
|
|
253
|
-
options;
|
|
254
|
-
constructor(options = {}) {
|
|
255
|
-
this.options = {
|
|
256
|
-
baseDir: options.baseDir || this.getDefaultSchemaDir(),
|
|
257
|
-
patterns: options.patterns || DEFAULT_PATTERNS,
|
|
258
|
-
followSymlinks: options.followSymlinks ?? false,
|
|
259
|
-
maxDepth: options.maxDepth ?? 10
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Get default schema directory using import.meta.url
|
|
264
|
-
*/
|
|
265
|
-
getDefaultSchemaDir() {
|
|
266
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
267
|
-
const __dirname2 = dirname(__filename);
|
|
268
|
-
return join(__dirname2, "..", "..", "schemas", "crucible-ts");
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Build logical schema ID from file path
|
|
272
|
-
*/
|
|
273
|
-
buildSchemaId(filePath, baseDir) {
|
|
274
|
-
const relativePath = relative(baseDir, filePath);
|
|
275
|
-
const withoutExt = relativePath.replace(/\.(schema\.(json|yaml|yml))$/, "");
|
|
276
|
-
return withoutExt.replace(/\\/g, "/");
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Extract schema format from file extension
|
|
280
|
-
*/
|
|
281
|
-
getSchemaFormat(filePath) {
|
|
282
|
-
const ext = extname(filePath).toLowerCase();
|
|
283
|
-
switch (ext) {
|
|
284
|
-
case ".json":
|
|
285
|
-
return "json";
|
|
286
|
-
case ".yaml":
|
|
287
|
-
case ".yml":
|
|
288
|
-
return "yaml";
|
|
289
|
-
default:
|
|
290
|
-
return "json";
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Extract metadata from schema file
|
|
295
|
-
*/
|
|
296
|
-
async extractMetadata(filePath) {
|
|
297
|
-
try {
|
|
298
|
-
const content = await readFile(filePath, "utf-8");
|
|
299
|
-
const format = this.getSchemaFormat(filePath);
|
|
300
|
-
let parsed;
|
|
301
|
-
if (format === "yaml") {
|
|
302
|
-
parsed = parse(content);
|
|
303
|
-
} else {
|
|
304
|
-
parsed = JSON.parse(content);
|
|
305
|
-
}
|
|
306
|
-
const baseDir = this.options.baseDir ?? "";
|
|
307
|
-
const relativePath = relative(baseDir, filePath);
|
|
308
|
-
return {
|
|
309
|
-
id: this.buildSchemaId(filePath, baseDir),
|
|
310
|
-
path: filePath,
|
|
311
|
-
relativePath,
|
|
312
|
-
format,
|
|
313
|
-
version: parsed.$schema || parsed.version,
|
|
314
|
-
description: parsed.title || parsed.description,
|
|
315
|
-
schemaDraft: parsed.$schema
|
|
316
|
-
};
|
|
317
|
-
} catch (error) {
|
|
318
|
-
throw SchemaValidationError.registryError(
|
|
319
|
-
"metadata extraction",
|
|
320
|
-
`Failed to process ${filePath}: ${error.message}`
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Discover and index all available schemas
|
|
326
|
-
*/
|
|
327
|
-
async discoverSchemas() {
|
|
328
|
-
try {
|
|
329
|
-
const baseDir = this.options.baseDir ?? "";
|
|
330
|
-
const patterns = this.options.patterns ?? [];
|
|
331
|
-
if (patterns.length === 0) {
|
|
332
|
-
this.schemas.clear();
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
const pattern = patterns.map((p) => join(baseDir, p));
|
|
336
|
-
try {
|
|
337
|
-
await access(baseDir);
|
|
338
|
-
} catch {
|
|
339
|
-
this.schemas.clear();
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
const files = await glob(pattern, {
|
|
343
|
-
absolute: true,
|
|
344
|
-
followSymbolicLinks: this.options.followSymlinks,
|
|
345
|
-
deep: this.options.maxDepth,
|
|
346
|
-
onlyFiles: true,
|
|
347
|
-
suppressErrors: true
|
|
348
|
-
// Don't throw on permission errors
|
|
349
|
-
});
|
|
350
|
-
this.schemas.clear();
|
|
351
|
-
for (const filePath of files) {
|
|
352
|
-
try {
|
|
353
|
-
const metadata = await this.extractMetadata(filePath);
|
|
354
|
-
this.schemas.set(metadata.id, metadata);
|
|
355
|
-
} catch (error) {
|
|
356
|
-
console.warn(`Warning: Failed to process schema ${filePath}:`, error);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
} catch (error) {
|
|
360
|
-
throw SchemaValidationError.registryError("discovery", error.message);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* List available schemas with optional prefix filtering
|
|
365
|
-
*/
|
|
366
|
-
async listSchemas(prefix) {
|
|
367
|
-
if (this.schemas.size === 0) {
|
|
368
|
-
await this.discoverSchemas();
|
|
369
|
-
}
|
|
370
|
-
const schemas = Array.from(this.schemas.values());
|
|
371
|
-
if (prefix) {
|
|
372
|
-
return schemas.filter((schema) => schema.id.startsWith(prefix));
|
|
373
|
-
}
|
|
374
|
-
return schemas;
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Get schema by logical ID
|
|
378
|
-
*/
|
|
379
|
-
async getSchema(id) {
|
|
380
|
-
if (this.schemas.size === 0) {
|
|
381
|
-
await this.discoverSchemas();
|
|
382
|
-
}
|
|
383
|
-
const schema = this.schemas.get(id);
|
|
384
|
-
if (!schema) {
|
|
385
|
-
throw SchemaValidationError.schemaNotFound(id);
|
|
386
|
-
}
|
|
387
|
-
return schema;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Get schema by file path
|
|
391
|
-
*/
|
|
392
|
-
async getSchemaByPath(filePath) {
|
|
393
|
-
if (this.schemas.size === 0) {
|
|
394
|
-
await this.discoverSchemas();
|
|
395
|
-
}
|
|
396
|
-
const absolutePath = filePath.startsWith("/") ? filePath : join(process.cwd(), filePath);
|
|
397
|
-
for (const schema of this.schemas.values()) {
|
|
398
|
-
if (schema.path === absolutePath) {
|
|
399
|
-
return schema;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
throw SchemaValidationError.schemaNotFound(filePath);
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Check if schema exists
|
|
406
|
-
*/
|
|
407
|
-
async hasSchema(id) {
|
|
408
|
-
if (this.schemas.size === 0) {
|
|
409
|
-
await this.discoverSchemas();
|
|
410
|
-
}
|
|
411
|
-
return this.schemas.has(id);
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Get registry size
|
|
415
|
-
*/
|
|
416
|
-
get size() {
|
|
417
|
-
return this.schemas.size;
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Clear registry cache
|
|
421
|
-
*/
|
|
422
|
-
clear() {
|
|
423
|
-
this.schemas.clear();
|
|
424
|
-
}
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// src/telemetry/counter.ts
|
|
430
|
-
var Counter;
|
|
431
|
-
var init_counter = __esm({
|
|
432
|
-
"src/telemetry/counter.ts"() {
|
|
433
|
-
Counter = class {
|
|
434
|
-
constructor(name) {
|
|
435
|
-
this.name = name;
|
|
436
|
-
}
|
|
437
|
-
value = 0;
|
|
438
|
-
labeledValues = /* @__PURE__ */ new Map();
|
|
439
|
-
/**
|
|
440
|
-
* Increment counter by delta (default: 1)
|
|
441
|
-
*
|
|
442
|
-
* @param delta - Amount to increment (must be non-negative)
|
|
443
|
-
* @param labels - Optional label dimensions for this observation
|
|
444
|
-
* @throws {Error} If delta is negative
|
|
445
|
-
*
|
|
446
|
-
* @example
|
|
447
|
-
* ```typescript
|
|
448
|
-
* counter.inc(); // Increment unlabeled by 1
|
|
449
|
-
* counter.inc(5); // Increment unlabeled by 5
|
|
450
|
-
* counter.inc(1, { status: '200' }); // Increment labeled instance
|
|
451
|
-
* counter.inc(1, { result: 'success' }); // Different label set
|
|
452
|
-
* ```
|
|
453
|
-
*/
|
|
454
|
-
inc(delta = 1, labels) {
|
|
455
|
-
if (delta < 0) {
|
|
456
|
-
throw new Error(`Counter delta must be non-negative, got: ${delta}`);
|
|
457
|
-
}
|
|
458
|
-
if (labels && Object.keys(labels).length > 0) {
|
|
459
|
-
const labelKey = this.serializeLabels(labels);
|
|
460
|
-
const current = this.labeledValues.get(labelKey) || 0;
|
|
461
|
-
this.labeledValues.set(labelKey, current + delta);
|
|
462
|
-
} else {
|
|
463
|
-
this.value += delta;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Get current counter value (unlabeled)
|
|
468
|
-
*/
|
|
469
|
-
getValue() {
|
|
470
|
-
return this.value;
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Get all labeled values
|
|
474
|
-
* @returns Map of serialized label keys to values
|
|
475
|
-
*/
|
|
476
|
-
getLabeledValues() {
|
|
477
|
-
return new Map(this.labeledValues);
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Get value for specific label combination
|
|
481
|
-
*/
|
|
482
|
-
getValueForLabels(labels) {
|
|
483
|
-
const labelKey = this.serializeLabels(labels);
|
|
484
|
-
return this.labeledValues.get(labelKey) || 0;
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Reset counter to zero (all label combinations)
|
|
488
|
-
*/
|
|
489
|
-
reset() {
|
|
490
|
-
this.value = 0;
|
|
491
|
-
this.labeledValues.clear();
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Serialize labels to deterministic string key
|
|
495
|
-
* Format: key1=value1,key2=value2 (sorted by key)
|
|
496
|
-
*/
|
|
497
|
-
serializeLabels(labels) {
|
|
498
|
-
return Object.entries(labels).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join(",");
|
|
499
|
-
}
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
// src/telemetry/gauge.ts
|
|
505
|
-
var Gauge;
|
|
506
|
-
var init_gauge = __esm({
|
|
507
|
-
"src/telemetry/gauge.ts"() {
|
|
508
|
-
Gauge = class {
|
|
509
|
-
constructor(name) {
|
|
510
|
-
this.name = name;
|
|
511
|
-
}
|
|
512
|
-
value = 0;
|
|
513
|
-
labeledValues = /* @__PURE__ */ new Map();
|
|
514
|
-
/**
|
|
515
|
-
* Set gauge to specific value
|
|
516
|
-
*
|
|
517
|
-
* @param value - New gauge value (can be any number, including negative)
|
|
518
|
-
* @param labels - Optional label dimensions for this observation
|
|
519
|
-
*
|
|
520
|
-
* @example
|
|
521
|
-
* ```typescript
|
|
522
|
-
* gauge.set(42); // Set unlabeled to 42
|
|
523
|
-
* gauge.set(-10); // Negative values allowed
|
|
524
|
-
* gauge.set(1, { phase: 'collect' }); // Set labeled instance
|
|
525
|
-
* ```
|
|
526
|
-
*/
|
|
527
|
-
set(value, labels) {
|
|
528
|
-
if (labels && Object.keys(labels).length > 0) {
|
|
529
|
-
const labelKey = this.serializeLabels(labels);
|
|
530
|
-
this.labeledValues.set(labelKey, value);
|
|
531
|
-
} else {
|
|
532
|
-
this.value = value;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Increment gauge by delta (default: 1)
|
|
537
|
-
*
|
|
538
|
-
* @param delta - Amount to increment (can be negative)
|
|
539
|
-
* @param labels - Optional label dimensions for this observation
|
|
540
|
-
*/
|
|
541
|
-
inc(delta = 1, labels) {
|
|
542
|
-
if (labels && Object.keys(labels).length > 0) {
|
|
543
|
-
const labelKey = this.serializeLabels(labels);
|
|
544
|
-
const current = this.labeledValues.get(labelKey) || 0;
|
|
545
|
-
this.labeledValues.set(labelKey, current + delta);
|
|
546
|
-
} else {
|
|
547
|
-
this.value += delta;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
/**
|
|
551
|
-
* Decrement gauge by delta (default: 1)
|
|
552
|
-
*
|
|
553
|
-
* @param delta - Amount to decrement (can be negative)
|
|
554
|
-
* @param labels - Optional label dimensions for this observation
|
|
555
|
-
*/
|
|
556
|
-
dec(delta = 1, labels) {
|
|
557
|
-
if (labels && Object.keys(labels).length > 0) {
|
|
558
|
-
const labelKey = this.serializeLabels(labels);
|
|
559
|
-
const current = this.labeledValues.get(labelKey) || 0;
|
|
560
|
-
this.labeledValues.set(labelKey, current - delta);
|
|
561
|
-
} else {
|
|
562
|
-
this.value -= delta;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Get current gauge value (unlabeled)
|
|
567
|
-
*/
|
|
568
|
-
getValue() {
|
|
569
|
-
return this.value;
|
|
570
|
-
}
|
|
571
|
-
/**
|
|
572
|
-
* Get all labeled values
|
|
573
|
-
* @returns Map of serialized label keys to values
|
|
574
|
-
*/
|
|
575
|
-
getLabeledValues() {
|
|
576
|
-
return new Map(this.labeledValues);
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Get value for specific label combination
|
|
580
|
-
*/
|
|
581
|
-
getValueForLabels(labels) {
|
|
582
|
-
const labelKey = this.serializeLabels(labels);
|
|
583
|
-
return this.labeledValues.get(labelKey) || 0;
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Reset gauge to zero (all label combinations)
|
|
587
|
-
*/
|
|
588
|
-
reset() {
|
|
589
|
-
this.value = 0;
|
|
590
|
-
this.labeledValues.clear();
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Serialize labels to deterministic string key
|
|
594
|
-
* Format: key1=value1,key2=value2 (sorted by key)
|
|
595
|
-
*/
|
|
596
|
-
serializeLabels(labels) {
|
|
597
|
-
return Object.entries(labels).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join(",");
|
|
598
|
-
}
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
async function getDefaultUnit(name) {
|
|
603
|
-
return TaxonomyLoader.getInstance().getDefaultUnit(name);
|
|
604
|
-
}
|
|
605
|
-
var DEFAULT_MS_BUCKETS, TaxonomyLoader;
|
|
606
|
-
var init_taxonomy = __esm({
|
|
607
|
-
"src/telemetry/taxonomy.ts"() {
|
|
608
|
-
DEFAULT_MS_BUCKETS = [1, 5, 10, 50, 100, 500, 1e3, 5e3, 1e4];
|
|
609
|
-
TaxonomyLoader = class _TaxonomyLoader {
|
|
610
|
-
static instance;
|
|
611
|
-
taxonomy = null;
|
|
612
|
-
loadPromise = null;
|
|
613
|
-
loadError = null;
|
|
614
|
-
constructor() {
|
|
615
|
-
}
|
|
616
|
-
/**
|
|
617
|
-
* Get singleton instance
|
|
618
|
-
*/
|
|
619
|
-
static getInstance() {
|
|
620
|
-
if (!_TaxonomyLoader.instance) {
|
|
621
|
-
_TaxonomyLoader.instance = new _TaxonomyLoader();
|
|
622
|
-
}
|
|
623
|
-
return _TaxonomyLoader.instance;
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Load taxonomy from YAML file
|
|
627
|
-
*/
|
|
628
|
-
async load() {
|
|
629
|
-
if (this.taxonomy !== null) {
|
|
630
|
-
return this.taxonomy;
|
|
631
|
-
}
|
|
632
|
-
if (this.loadError !== null) {
|
|
633
|
-
throw this.loadError;
|
|
634
|
-
}
|
|
635
|
-
if (this.loadPromise) {
|
|
636
|
-
return this.loadPromise;
|
|
637
|
-
}
|
|
638
|
-
this.loadPromise = (async () => {
|
|
639
|
-
try {
|
|
640
|
-
const taxonomyPath = join(
|
|
641
|
-
__dirname,
|
|
642
|
-
"..",
|
|
643
|
-
"..",
|
|
644
|
-
"config",
|
|
645
|
-
"crucible-ts",
|
|
646
|
-
"taxonomy",
|
|
647
|
-
"metrics.yaml"
|
|
648
|
-
);
|
|
649
|
-
const content = await readFile(taxonomyPath, "utf-8");
|
|
650
|
-
this.taxonomy = parse(content);
|
|
651
|
-
return this.taxonomy;
|
|
652
|
-
} catch (err) {
|
|
653
|
-
this.loadError = err instanceof Error ? err : new Error(String(err));
|
|
654
|
-
throw new Error(`Failed to load metrics taxonomy: ${this.loadError.message}`);
|
|
655
|
-
}
|
|
656
|
-
})();
|
|
657
|
-
return this.loadPromise;
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* Get taxonomy (async)
|
|
661
|
-
*/
|
|
662
|
-
async getTaxonomy() {
|
|
663
|
-
return this.load();
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Get metric definition by name
|
|
667
|
-
*/
|
|
668
|
-
async getMetric(name) {
|
|
669
|
-
const taxonomy = await this.load();
|
|
670
|
-
return taxonomy.metrics.find((m) => m.name === name);
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Get default unit for metric
|
|
674
|
-
*/
|
|
675
|
-
async getDefaultUnit(name) {
|
|
676
|
-
const metric = await this.getMetric(name);
|
|
677
|
-
return metric?.unit;
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* Get default histogram buckets for metric
|
|
681
|
-
* Returns ADR-0007 buckets for _ms metrics, undefined for others
|
|
682
|
-
*/
|
|
683
|
-
async getDefaultBuckets(name) {
|
|
684
|
-
if (name.endsWith("_ms")) {
|
|
685
|
-
const taxonomy = await this.load();
|
|
686
|
-
return taxonomy.defaults.histogram_buckets.ms_metrics;
|
|
687
|
-
}
|
|
688
|
-
return void 0;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Check if metric name is valid (exists in taxonomy)
|
|
692
|
-
*/
|
|
693
|
-
async isValidMetricName(name) {
|
|
694
|
-
try {
|
|
695
|
-
const taxonomy = await this.load();
|
|
696
|
-
return taxonomy.metrics.some((m) => m.name === name);
|
|
697
|
-
} catch {
|
|
698
|
-
return false;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Reset loader state (for testing)
|
|
703
|
-
* @internal
|
|
704
|
-
*/
|
|
705
|
-
static _reset() {
|
|
706
|
-
_TaxonomyLoader.instance = new _TaxonomyLoader();
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
// src/telemetry/histogram.ts
|
|
713
|
-
var Histogram;
|
|
714
|
-
var init_histogram = __esm({
|
|
715
|
-
"src/telemetry/histogram.ts"() {
|
|
716
|
-
init_taxonomy();
|
|
717
|
-
Histogram = class {
|
|
718
|
-
constructor(name, options) {
|
|
719
|
-
this.name = name;
|
|
720
|
-
if (options?.buckets) {
|
|
721
|
-
this.buckets = [...options.buckets].sort((a, b) => a - b);
|
|
722
|
-
} else if (name.endsWith("_ms") || name.endsWith("_seconds")) {
|
|
723
|
-
this.buckets = [...DEFAULT_MS_BUCKETS];
|
|
724
|
-
} else {
|
|
725
|
-
this.buckets = [];
|
|
726
|
-
}
|
|
727
|
-
for (const bucket of this.buckets) {
|
|
728
|
-
this.bucketCounts.set(bucket, 0);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
count = 0;
|
|
732
|
-
sum = 0;
|
|
733
|
-
bucketCounts = /* @__PURE__ */ new Map();
|
|
734
|
-
labeledStates = /* @__PURE__ */ new Map();
|
|
735
|
-
buckets;
|
|
736
|
-
/**
|
|
737
|
-
* Record an observation
|
|
738
|
-
*
|
|
739
|
-
* @param value - Value to observe (typically a duration in ms or seconds)
|
|
740
|
-
* @param labels - Optional label dimensions for this observation
|
|
741
|
-
*
|
|
742
|
-
* @example
|
|
743
|
-
* ```typescript
|
|
744
|
-
* const start = performance.now();
|
|
745
|
-
* // ... operation ...
|
|
746
|
-
* histogram.observe(performance.now() - start);
|
|
747
|
-
* histogram.observe(duration, { phase: 'collect', result: 'success' });
|
|
748
|
-
* ```
|
|
749
|
-
*/
|
|
750
|
-
observe(value, labels) {
|
|
751
|
-
if (labels && Object.keys(labels).length > 0) {
|
|
752
|
-
const labelKey = this.serializeLabels(labels);
|
|
753
|
-
let state = this.labeledStates.get(labelKey);
|
|
754
|
-
if (!state) {
|
|
755
|
-
state = {
|
|
756
|
-
count: 0,
|
|
757
|
-
sum: 0,
|
|
758
|
-
bucketCounts: /* @__PURE__ */ new Map()
|
|
759
|
-
};
|
|
760
|
-
for (const bucket of this.buckets) {
|
|
761
|
-
state.bucketCounts.set(bucket, 0);
|
|
762
|
-
}
|
|
763
|
-
this.labeledStates.set(labelKey, state);
|
|
764
|
-
}
|
|
765
|
-
state.count++;
|
|
766
|
-
state.sum += value;
|
|
767
|
-
for (const bucket of this.buckets) {
|
|
768
|
-
if (value <= bucket) {
|
|
769
|
-
state.bucketCounts.set(bucket, (state.bucketCounts.get(bucket) || 0) + 1);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
} else {
|
|
773
|
-
this.count++;
|
|
774
|
-
this.sum += value;
|
|
775
|
-
for (const bucket of this.buckets) {
|
|
776
|
-
if (value <= bucket) {
|
|
777
|
-
this.bucketCounts.set(bucket, (this.bucketCounts.get(bucket) || 0) + 1);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Get histogram summary
|
|
784
|
-
*
|
|
785
|
-
* Returns OTLP-compatible histogram summary with cumulative bucket counts.
|
|
786
|
-
*/
|
|
787
|
-
getSummary() {
|
|
788
|
-
const buckets = this.buckets.map((le) => ({
|
|
789
|
-
le,
|
|
790
|
-
count: this.bucketCounts.get(le) || 0
|
|
791
|
-
}));
|
|
792
|
-
return {
|
|
793
|
-
count: this.count,
|
|
794
|
-
sum: this.sum,
|
|
795
|
-
buckets
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* Get current observation count
|
|
800
|
-
*/
|
|
801
|
-
getCount() {
|
|
802
|
-
return this.count;
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Get sum of all observed values
|
|
806
|
-
*/
|
|
807
|
-
getSum() {
|
|
808
|
-
return this.sum;
|
|
809
|
-
}
|
|
810
|
-
/**
|
|
811
|
-
* Get average of observed values
|
|
812
|
-
*/
|
|
813
|
-
getAverage() {
|
|
814
|
-
return this.count > 0 ? this.sum / this.count : 0;
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Get all labeled summaries
|
|
818
|
-
* @returns Map of serialized label keys to histogram summaries
|
|
819
|
-
*/
|
|
820
|
-
getLabeledSummaries() {
|
|
821
|
-
const summaries = /* @__PURE__ */ new Map();
|
|
822
|
-
for (const [labelKey, state] of this.labeledStates) {
|
|
823
|
-
const buckets = this.buckets.map((le) => ({
|
|
824
|
-
le,
|
|
825
|
-
count: state.bucketCounts.get(le) || 0
|
|
826
|
-
}));
|
|
827
|
-
summaries.set(labelKey, {
|
|
828
|
-
count: state.count,
|
|
829
|
-
sum: state.sum,
|
|
830
|
-
buckets
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
return summaries;
|
|
834
|
-
}
|
|
835
|
-
/**
|
|
836
|
-
* Get summary for specific label combination
|
|
837
|
-
*/
|
|
838
|
-
getSummaryForLabels(labels) {
|
|
839
|
-
const labelKey = this.serializeLabels(labels);
|
|
840
|
-
const state = this.labeledStates.get(labelKey);
|
|
841
|
-
if (!state) {
|
|
842
|
-
return null;
|
|
843
|
-
}
|
|
844
|
-
const buckets = this.buckets.map((le) => ({
|
|
845
|
-
le,
|
|
846
|
-
count: state.bucketCounts.get(le) || 0
|
|
847
|
-
}));
|
|
848
|
-
return {
|
|
849
|
-
count: state.count,
|
|
850
|
-
sum: state.sum,
|
|
851
|
-
buckets
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Reset histogram to initial state (all label combinations)
|
|
856
|
-
*/
|
|
857
|
-
reset() {
|
|
858
|
-
this.count = 0;
|
|
859
|
-
this.sum = 0;
|
|
860
|
-
for (const bucket of this.buckets) {
|
|
861
|
-
this.bucketCounts.set(bucket, 0);
|
|
862
|
-
}
|
|
863
|
-
this.labeledStates.clear();
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Serialize labels to deterministic string key
|
|
867
|
-
* Format: key1=value1,key2=value2 (sorted by key)
|
|
868
|
-
*/
|
|
869
|
-
serializeLabels(labels) {
|
|
870
|
-
return Object.entries(labels).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join(",");
|
|
871
|
-
}
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
// src/telemetry/registry.ts
|
|
877
|
-
var MetricsRegistry;
|
|
878
|
-
var init_registry2 = __esm({
|
|
879
|
-
"src/telemetry/registry.ts"() {
|
|
880
|
-
init_counter();
|
|
881
|
-
init_gauge();
|
|
882
|
-
init_histogram();
|
|
883
|
-
init_taxonomy();
|
|
884
|
-
MetricsRegistry = class {
|
|
885
|
-
counters = /* @__PURE__ */ new Map();
|
|
886
|
-
gauges = /* @__PURE__ */ new Map();
|
|
887
|
-
histograms = /* @__PURE__ */ new Map();
|
|
888
|
-
/**
|
|
889
|
-
* Get or create a counter
|
|
890
|
-
*
|
|
891
|
-
* @param name - Metric name from taxonomy
|
|
892
|
-
* @returns Counter instance
|
|
893
|
-
*
|
|
894
|
-
* @example
|
|
895
|
-
* ```typescript
|
|
896
|
-
* const counter = registry.counter('schema_validations');
|
|
897
|
-
* counter.inc();
|
|
898
|
-
* ```
|
|
899
|
-
*/
|
|
900
|
-
counter(name) {
|
|
901
|
-
let counter = this.counters.get(name);
|
|
902
|
-
if (!counter) {
|
|
903
|
-
counter = new Counter(name);
|
|
904
|
-
this.counters.set(name, counter);
|
|
905
|
-
}
|
|
906
|
-
return counter;
|
|
907
|
-
}
|
|
908
|
-
/**
|
|
909
|
-
* Get or create a gauge
|
|
910
|
-
*
|
|
911
|
-
* @param name - Metric name from taxonomy
|
|
912
|
-
* @returns Gauge instance
|
|
913
|
-
*
|
|
914
|
-
* @example
|
|
915
|
-
* ```typescript
|
|
916
|
-
* const gauge = registry.gauge('foundry_lookup_count');
|
|
917
|
-
* gauge.set(42);
|
|
918
|
-
* ```
|
|
919
|
-
*/
|
|
920
|
-
gauge(name) {
|
|
921
|
-
let gauge = this.gauges.get(name);
|
|
922
|
-
if (!gauge) {
|
|
923
|
-
gauge = new Gauge(name);
|
|
924
|
-
this.gauges.set(name, gauge);
|
|
925
|
-
}
|
|
926
|
-
return gauge;
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* Get or create a histogram
|
|
930
|
-
*
|
|
931
|
-
* @param name - Metric name from taxonomy
|
|
932
|
-
* @param options - Optional histogram options
|
|
933
|
-
* @returns Histogram instance
|
|
934
|
-
*
|
|
935
|
-
* @example
|
|
936
|
-
* ```typescript
|
|
937
|
-
* // Auto-applies ADR-0007 buckets for _ms metrics
|
|
938
|
-
* const histogram = registry.histogram('config_load_ms');
|
|
939
|
-
* histogram.observe(42.5);
|
|
940
|
-
*
|
|
941
|
-
* // Custom buckets
|
|
942
|
-
* const custom = registry.histogram('custom_metric', {
|
|
943
|
-
* buckets: [10, 50, 100, 500, 1000]
|
|
944
|
-
* });
|
|
945
|
-
* ```
|
|
946
|
-
*/
|
|
947
|
-
histogram(name, options) {
|
|
948
|
-
let histogram = this.histograms.get(name);
|
|
949
|
-
if (!histogram) {
|
|
950
|
-
histogram = new Histogram(name, options);
|
|
951
|
-
this.histograms.set(name, histogram);
|
|
952
|
-
}
|
|
953
|
-
return histogram;
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* Export all metrics as events
|
|
957
|
-
*
|
|
958
|
-
* Returns array of schema-compliant MetricsEvent objects.
|
|
959
|
-
* Does not clear metrics (use flush() to clear after export).
|
|
960
|
-
*
|
|
961
|
-
* @returns Promise resolving to array of metrics events
|
|
962
|
-
*
|
|
963
|
-
* @example
|
|
964
|
-
* ```typescript
|
|
965
|
-
* const events = await registry.export();
|
|
966
|
-
* console.log(JSON.stringify(events, null, 2));
|
|
967
|
-
* ```
|
|
968
|
-
*/
|
|
969
|
-
async export() {
|
|
970
|
-
const events = [];
|
|
971
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
972
|
-
for (const [name, counter] of this.counters) {
|
|
973
|
-
const unit = await getDefaultUnit(name);
|
|
974
|
-
events.push({
|
|
975
|
-
timestamp,
|
|
976
|
-
name,
|
|
977
|
-
value: counter.getValue(),
|
|
978
|
-
unit
|
|
979
|
-
});
|
|
980
|
-
for (const [labelKey, value] of counter.getLabeledValues()) {
|
|
981
|
-
if (value > 0) {
|
|
982
|
-
const tags = this.deserializeLabels(labelKey);
|
|
983
|
-
events.push({
|
|
984
|
-
timestamp,
|
|
985
|
-
name,
|
|
986
|
-
value,
|
|
987
|
-
tags,
|
|
988
|
-
unit
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
for (const [name, gauge] of this.gauges) {
|
|
994
|
-
const unit = await getDefaultUnit(name);
|
|
995
|
-
events.push({
|
|
996
|
-
timestamp,
|
|
997
|
-
name,
|
|
998
|
-
value: gauge.getValue(),
|
|
999
|
-
unit
|
|
1000
|
-
});
|
|
1001
|
-
for (const [labelKey, value] of gauge.getLabeledValues()) {
|
|
1002
|
-
const tags = this.deserializeLabels(labelKey);
|
|
1003
|
-
events.push({
|
|
1004
|
-
timestamp,
|
|
1005
|
-
name,
|
|
1006
|
-
value,
|
|
1007
|
-
tags,
|
|
1008
|
-
unit
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
for (const [name, histogram] of this.histograms) {
|
|
1013
|
-
const unit = await getDefaultUnit(name);
|
|
1014
|
-
events.push({
|
|
1015
|
-
timestamp,
|
|
1016
|
-
name,
|
|
1017
|
-
value: histogram.getSummary(),
|
|
1018
|
-
unit
|
|
1019
|
-
});
|
|
1020
|
-
for (const [labelKey, summary] of histogram.getLabeledSummaries()) {
|
|
1021
|
-
if (summary.count > 0) {
|
|
1022
|
-
const tags = this.deserializeLabels(labelKey);
|
|
1023
|
-
events.push({
|
|
1024
|
-
timestamp,
|
|
1025
|
-
name,
|
|
1026
|
-
value: summary,
|
|
1027
|
-
tags,
|
|
1028
|
-
unit
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
return events;
|
|
1034
|
-
}
|
|
1035
|
-
/**
|
|
1036
|
-
* Deserialize label key back to tags object
|
|
1037
|
-
* Format: key1=value1,key2=value2 → {key1: "value1", key2: "value2"}
|
|
1038
|
-
*/
|
|
1039
|
-
deserializeLabels(labelKey) {
|
|
1040
|
-
if (!labelKey) {
|
|
1041
|
-
return {};
|
|
1042
|
-
}
|
|
1043
|
-
const tags = {};
|
|
1044
|
-
for (const pair of labelKey.split(",")) {
|
|
1045
|
-
const [key, value] = pair.split("=");
|
|
1046
|
-
if (key && value) {
|
|
1047
|
-
tags[key] = value;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
return tags;
|
|
1051
|
-
}
|
|
1052
|
-
/**
|
|
1053
|
-
* Export and clear all metrics
|
|
1054
|
-
*
|
|
1055
|
-
* Exports metrics as events, optionally emits them via logger,
|
|
1056
|
-
* then resets all metrics to zero.
|
|
1057
|
-
*
|
|
1058
|
-
* @param options - Flush options
|
|
1059
|
-
* @returns Promise resolving to array of exported events
|
|
1060
|
-
*
|
|
1061
|
-
* @example
|
|
1062
|
-
* ```typescript
|
|
1063
|
-
* // Export and clear
|
|
1064
|
-
* const events = await registry.flush();
|
|
1065
|
-
*
|
|
1066
|
-
* // Export, emit to logger, and clear
|
|
1067
|
-
* const events = await registry.flush({
|
|
1068
|
-
* emit: (events) => console.log(JSON.stringify(events))
|
|
1069
|
-
* });
|
|
1070
|
-
* ```
|
|
1071
|
-
*/
|
|
1072
|
-
async flush(options) {
|
|
1073
|
-
const events = await this.export();
|
|
1074
|
-
try {
|
|
1075
|
-
if (options?.emit) {
|
|
1076
|
-
options.emit(events);
|
|
1077
|
-
}
|
|
1078
|
-
} finally {
|
|
1079
|
-
this.clear();
|
|
1080
|
-
}
|
|
1081
|
-
return events;
|
|
1082
|
-
}
|
|
1083
|
-
/**
|
|
1084
|
-
* Clear all metrics (reset to zero)
|
|
1085
|
-
*
|
|
1086
|
-
* Resets all counters, gauges, and histograms to their initial state.
|
|
1087
|
-
*/
|
|
1088
|
-
clear() {
|
|
1089
|
-
for (const counter of this.counters.values()) {
|
|
1090
|
-
counter.reset();
|
|
1091
|
-
}
|
|
1092
|
-
for (const gauge of this.gauges.values()) {
|
|
1093
|
-
gauge.reset();
|
|
1094
|
-
}
|
|
1095
|
-
for (const histogram of this.histograms.values()) {
|
|
1096
|
-
histogram.reset();
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Get all registered metric names
|
|
1101
|
-
*
|
|
1102
|
-
* Returns array of all metric names that have been accessed
|
|
1103
|
-
* (counters, gauges, or histograms).
|
|
1104
|
-
*/
|
|
1105
|
-
getMetricNames() {
|
|
1106
|
-
const names = /* @__PURE__ */ new Set();
|
|
1107
|
-
for (const name of this.counters.keys()) {
|
|
1108
|
-
names.add(name);
|
|
1109
|
-
}
|
|
1110
|
-
for (const name of this.gauges.keys()) {
|
|
1111
|
-
names.add(name);
|
|
1112
|
-
}
|
|
1113
|
-
for (const name of this.histograms.keys()) {
|
|
1114
|
-
names.add(name);
|
|
1115
|
-
}
|
|
1116
|
-
return Array.from(names);
|
|
1117
|
-
}
|
|
1118
|
-
/**
|
|
1119
|
-
* Get total count of registered metrics
|
|
1120
|
-
*/
|
|
1121
|
-
getMetricCount() {
|
|
1122
|
-
return this.counters.size + this.gauges.size + this.histograms.size;
|
|
1123
|
-
}
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1126
|
-
});
|
|
1127
|
-
|
|
1128
|
-
// src/telemetry/types.ts
|
|
1129
|
-
var init_types = __esm({
|
|
1130
|
-
"src/telemetry/types.ts"() {
|
|
1131
|
-
}
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
|
-
// src/telemetry/validators.ts
|
|
1135
|
-
var init_validators = __esm({
|
|
1136
|
-
"src/telemetry/validators.ts"() {
|
|
1137
|
-
init_schema();
|
|
1138
|
-
}
|
|
1139
|
-
});
|
|
1140
|
-
|
|
1141
|
-
// src/telemetry/index.ts
|
|
1142
|
-
var metrics;
|
|
1143
|
-
var init_telemetry = __esm({
|
|
1144
|
-
"src/telemetry/index.ts"() {
|
|
1145
|
-
init_registry2();
|
|
1146
|
-
init_registry2();
|
|
1147
|
-
init_counter();
|
|
1148
|
-
init_gauge();
|
|
1149
|
-
init_histogram();
|
|
1150
|
-
init_taxonomy();
|
|
1151
|
-
init_types();
|
|
1152
|
-
init_validators();
|
|
1153
|
-
metrics = new MetricsRegistry();
|
|
1154
|
-
}
|
|
1155
|
-
});
|
|
1156
|
-
async function loadMetaSchema(draft) {
|
|
1157
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
1158
|
-
const __dirname2 = dirname(__filename);
|
|
1159
|
-
const metaSchemaPath = join(
|
|
1160
|
-
__dirname2,
|
|
1161
|
-
"..",
|
|
1162
|
-
"..",
|
|
1163
|
-
"schemas",
|
|
1164
|
-
"crucible-ts",
|
|
1165
|
-
"meta",
|
|
1166
|
-
draft,
|
|
1167
|
-
"schema.json"
|
|
1168
|
-
);
|
|
1169
|
-
const content = await readFile(metaSchemaPath, "utf-8");
|
|
1170
|
-
return JSON.parse(content);
|
|
1171
|
-
}
|
|
1172
|
-
async function loadVocabularySchemas(draft) {
|
|
1173
|
-
if (draft !== "draft-2019-09" && draft !== "draft-2020-12") {
|
|
1174
|
-
return [];
|
|
1175
|
-
}
|
|
1176
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
1177
|
-
const __dirname2 = dirname(__filename);
|
|
1178
|
-
const vocabDir = join(__dirname2, "..", "..", "schemas", "crucible-ts", "meta", draft, "meta");
|
|
1179
|
-
const vocabFiles = draft === "draft-2020-12" ? [
|
|
1180
|
-
"core.json",
|
|
1181
|
-
"applicator.json",
|
|
1182
|
-
"unevaluated.json",
|
|
1183
|
-
"validation.json",
|
|
1184
|
-
"meta-data.json",
|
|
1185
|
-
"format-annotation.json",
|
|
1186
|
-
"content.json"
|
|
1187
|
-
] : [
|
|
1188
|
-
"core.json",
|
|
1189
|
-
"applicator.json",
|
|
1190
|
-
"validation.json",
|
|
1191
|
-
"meta-data.json",
|
|
1192
|
-
"format.json",
|
|
1193
|
-
"content.json"
|
|
1194
|
-
];
|
|
1195
|
-
const schemas = [];
|
|
1196
|
-
for (const file of vocabFiles) {
|
|
1197
|
-
try {
|
|
1198
|
-
const content = await readFile(join(vocabDir, file), "utf-8");
|
|
1199
|
-
schemas.push(JSON.parse(content));
|
|
1200
|
-
} catch {
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
return schemas;
|
|
1204
|
-
}
|
|
1205
|
-
async function loadReferencedSchema(uri) {
|
|
1206
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
1207
|
-
const __dirname2 = dirname(__filename);
|
|
1208
|
-
const repoRoot = join(__dirname2, "..", "..");
|
|
1209
|
-
let resolvedPath;
|
|
1210
|
-
if (uri.startsWith("https://schemas.fulmenhq.dev/")) {
|
|
1211
|
-
let relativePath = uri.replace("https://schemas.fulmenhq.dev/", "");
|
|
1212
|
-
if (relativePath.startsWith("crucible/")) {
|
|
1213
|
-
relativePath = relativePath.slice("crucible/".length);
|
|
1214
|
-
}
|
|
1215
|
-
if (relativePath.startsWith("config/taxonomy/")) {
|
|
1216
|
-
resolvedPath = join(
|
|
1217
|
-
repoRoot,
|
|
1218
|
-
"config",
|
|
1219
|
-
"crucible-ts",
|
|
1220
|
-
"taxonomy",
|
|
1221
|
-
relativePath.split("/").pop() || ""
|
|
1222
|
-
);
|
|
1223
|
-
} else {
|
|
1224
|
-
resolvedPath = join(repoRoot, "schemas", "crucible-ts", relativePath);
|
|
1225
|
-
}
|
|
1226
|
-
} else if (uri.startsWith("../../") || uri.startsWith("../")) {
|
|
1227
|
-
const schemaBase = join(
|
|
1228
|
-
repoRoot,
|
|
1229
|
-
"schemas",
|
|
1230
|
-
"crucible-ts",
|
|
1231
|
-
"observability",
|
|
1232
|
-
"metrics",
|
|
1233
|
-
"v1.0.0"
|
|
1234
|
-
);
|
|
1235
|
-
resolvedPath = join(schemaBase, uri);
|
|
1236
|
-
} else if (uri.startsWith("file://")) {
|
|
1237
|
-
resolvedPath = fileURLToPath(uri);
|
|
1238
|
-
} else {
|
|
1239
|
-
throw new Error(`Cannot load remote schema: ${uri}`);
|
|
1240
|
-
}
|
|
1241
|
-
const content = await readFile(resolvedPath, "utf-8");
|
|
1242
|
-
const ext = resolvedPath.split(".").pop()?.toLowerCase();
|
|
1243
|
-
if (ext === "yaml" || ext === "yml") {
|
|
1244
|
-
return parse(content);
|
|
1245
|
-
}
|
|
1246
|
-
return JSON.parse(content);
|
|
1247
|
-
}
|
|
1248
|
-
function detectDialect(schema) {
|
|
1249
|
-
if (schema && typeof schema === "object" && !Array.isArray(schema)) {
|
|
1250
|
-
const maybeSchema = schema;
|
|
1251
|
-
const declared = maybeSchema.$schema;
|
|
1252
|
-
if (typeof declared === "string") {
|
|
1253
|
-
if (declared.includes("draft-04")) return "draft-04";
|
|
1254
|
-
if (declared.includes("draft-06")) return "draft-06";
|
|
1255
|
-
if (declared.includes("draft-07")) return "draft-07";
|
|
1256
|
-
if (declared.includes("draft/2019-09")) return "draft-2019-09";
|
|
1257
|
-
if (declared.includes("draft/2020-12")) return "draft-2020-12";
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
return "draft-2020-12";
|
|
1261
|
-
}
|
|
1262
|
-
function createAjv(dialect) {
|
|
1263
|
-
const AjvCtor = dialect === "draft-2020-12" ? Ajv2020 : dialect === "draft-2019-09" ? Ajv2019 : dialect === "draft-04" ? AjvDraft04 : Ajv;
|
|
1264
|
-
const ajv = new AjvCtor({
|
|
1265
|
-
strict: false,
|
|
1266
|
-
allErrors: true,
|
|
1267
|
-
verbose: true,
|
|
1268
|
-
// Allow schemas with $id to be added without replacing existing ones
|
|
1269
|
-
addUsedSchema: false,
|
|
1270
|
-
// draft-04 uses "id"; later drafts use "$id"
|
|
1271
|
-
schemaId: dialect === "draft-04" ? "id" : "$id",
|
|
1272
|
-
// Enable async schema loading for YAML references
|
|
1273
|
-
loadSchema: loadReferencedSchema
|
|
1274
|
-
});
|
|
1275
|
-
applyFulmenAjvFormats(ajv);
|
|
1276
|
-
return ajv;
|
|
1277
|
-
}
|
|
1278
|
-
async function getAjv(dialect) {
|
|
1279
|
-
const existing = ajvInstances.get(dialect);
|
|
1280
|
-
if (existing) {
|
|
1281
|
-
const ready = metaschemaReady.get(dialect);
|
|
1282
|
-
if (ready) await ready;
|
|
1283
|
-
return existing;
|
|
1284
|
-
}
|
|
1285
|
-
const ajv = createAjv(dialect);
|
|
1286
|
-
ajvInstances.set(dialect, ajv);
|
|
1287
|
-
const readyPromise = Promise.all([loadVocabularySchemas(dialect), loadMetaSchema(dialect)]).then(([vocabSchemas, metaSchema]) => {
|
|
1288
|
-
for (const vocabSchema of vocabSchemas) {
|
|
1289
|
-
try {
|
|
1290
|
-
ajv.addMetaSchema(vocabSchema);
|
|
1291
|
-
} catch {
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
try {
|
|
1295
|
-
ajv.addMetaSchema(metaSchema);
|
|
1296
|
-
} catch {
|
|
1297
|
-
}
|
|
1298
|
-
}).catch((error) => {
|
|
1299
|
-
throw new Error(`Failed to load metaschemas (${dialect}): ${error}`);
|
|
1300
|
-
});
|
|
1301
|
-
metaschemaReady.set(dialect, readyPromise);
|
|
1302
|
-
await readyPromise;
|
|
1303
|
-
return ajv;
|
|
1304
|
-
}
|
|
1305
|
-
async function compileSchema(schema, options = {}) {
|
|
1306
|
-
const baseKey = typeof schema === "string" ? schema : JSON.stringify(schema);
|
|
1307
|
-
let parsedSchema;
|
|
1308
|
-
if (typeof schema === "string") {
|
|
1309
|
-
try {
|
|
1310
|
-
parsedSchema = JSON.parse(schema);
|
|
1311
|
-
} catch {
|
|
1312
|
-
parsedSchema = parse(schema);
|
|
1313
|
-
}
|
|
1314
|
-
} else if (Buffer.isBuffer(schema)) {
|
|
1315
|
-
const content = schema.toString("utf-8");
|
|
1316
|
-
try {
|
|
1317
|
-
parsedSchema = JSON.parse(content);
|
|
1318
|
-
} catch {
|
|
1319
|
-
parsedSchema = parse(content);
|
|
1320
|
-
}
|
|
1321
|
-
} else {
|
|
1322
|
-
parsedSchema = schema;
|
|
1323
|
-
}
|
|
1324
|
-
const dialect = detectDialect(parsedSchema);
|
|
1325
|
-
const ajv = await getAjv(dialect);
|
|
1326
|
-
const cacheKey = `${dialect}:${baseKey}`;
|
|
1327
|
-
const cached = schemaCache.get(cacheKey);
|
|
1328
|
-
if (cached !== void 0) {
|
|
1329
|
-
return cached;
|
|
1330
|
-
}
|
|
1331
|
-
try {
|
|
1332
|
-
if (options.aliases && options.aliases.length > 0) {
|
|
1333
|
-
for (const alias of options.aliases) {
|
|
1334
|
-
if (alias && ajv.getSchema(alias) === void 0) {
|
|
1335
|
-
try {
|
|
1336
|
-
if (typeof parsedSchema === "object" && parsedSchema !== null) {
|
|
1337
|
-
ajv.addSchema(parsedSchema, alias);
|
|
1338
|
-
}
|
|
1339
|
-
} catch {
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
const validator = typeof parsedSchema === "boolean" ? ajv.compile(parsedSchema) : await ajv.compileAsync(parsedSchema);
|
|
1345
|
-
schemaCache.set(cacheKey, validator);
|
|
1346
|
-
return validator;
|
|
1347
|
-
} catch (error) {
|
|
1348
|
-
throw SchemaValidationError.parseFailed(
|
|
1349
|
-
{
|
|
1350
|
-
type: "string",
|
|
1351
|
-
content: typeof schema === "string" ? schema : JSON.stringify(schema)
|
|
1352
|
-
},
|
|
1353
|
-
error
|
|
1354
|
-
);
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
function validateData(data, validator) {
|
|
1358
|
-
const valid = validator(data);
|
|
1359
|
-
const result = {
|
|
1360
|
-
valid,
|
|
1361
|
-
diagnostics: [],
|
|
1362
|
-
source: "ajv"
|
|
1363
|
-
};
|
|
1364
|
-
if (!valid && validator.errors) {
|
|
1365
|
-
const errors = validator.errors;
|
|
1366
|
-
if (Array.isArray(errors)) {
|
|
1367
|
-
result.diagnostics = errors.map(
|
|
1368
|
-
(error) => createDiagnostic(
|
|
1369
|
-
error.instancePath || "",
|
|
1370
|
-
error.message || "Validation failed",
|
|
1371
|
-
error.keyword || "unknown",
|
|
1372
|
-
"ERROR",
|
|
1373
|
-
"ajv"
|
|
1374
|
-
)
|
|
1375
|
-
);
|
|
1376
|
-
}
|
|
1377
|
-
metrics.counter("schema_validation_errors").inc();
|
|
1378
|
-
} else {
|
|
1379
|
-
metrics.counter("schema_validations").inc();
|
|
1380
|
-
}
|
|
1381
|
-
return result;
|
|
1382
|
-
}
|
|
1383
|
-
async function compileSchemaById(schemaId, registryOptions) {
|
|
1384
|
-
try {
|
|
1385
|
-
const registry = getSchemaRegistry(registryOptions);
|
|
1386
|
-
const metadata = await registry.getSchema(schemaId);
|
|
1387
|
-
const content = await readFile(metadata.path, "utf-8");
|
|
1388
|
-
const aliases = [];
|
|
1389
|
-
const normalizedRelativePath = metadata.relativePath.replace(/\\/g, "/");
|
|
1390
|
-
if (normalizedRelativePath) {
|
|
1391
|
-
aliases.push(
|
|
1392
|
-
new URL(`crucible/${normalizedRelativePath}`, "https://schemas.fulmenhq.dev/").toString()
|
|
1393
|
-
);
|
|
1394
|
-
}
|
|
1395
|
-
return compileSchema(content, { aliases });
|
|
1396
|
-
} catch (error) {
|
|
1397
|
-
metrics.counter("schema_validation_errors").inc();
|
|
1398
|
-
throw error;
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
async function validateDataBySchemaId(data, schemaId, registryOptions) {
|
|
1402
|
-
try {
|
|
1403
|
-
const validator = await compileSchemaById(schemaId, registryOptions);
|
|
1404
|
-
return validateData(data, validator);
|
|
1405
|
-
} catch (error) {
|
|
1406
|
-
metrics.counter("schema_validation_errors").inc();
|
|
1407
|
-
throw error;
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
var ajvInstances, metaschemaReady, schemaCache;
|
|
1411
|
-
var init_validator = __esm({
|
|
1412
|
-
"src/schema/validator.ts"() {
|
|
1413
|
-
init_telemetry();
|
|
1414
|
-
init_ajv_formats();
|
|
1415
|
-
init_errors();
|
|
1416
|
-
init_registry();
|
|
1417
|
-
init_utils();
|
|
1418
|
-
ajvInstances = /* @__PURE__ */ new Map();
|
|
1419
|
-
metaschemaReady = /* @__PURE__ */ new Map();
|
|
1420
|
-
schemaCache = /* @__PURE__ */ new Map();
|
|
1421
|
-
}
|
|
1422
|
-
});
|
|
1423
|
-
var init_cli = __esm({
|
|
1424
|
-
"src/schema/cli.ts"() {
|
|
1425
|
-
init_goneat_bridge();
|
|
1426
|
-
init_normalizer();
|
|
1427
|
-
init_registry();
|
|
1428
|
-
init_utils();
|
|
1429
|
-
init_validator();
|
|
1430
|
-
}
|
|
1431
|
-
});
|
|
1432
|
-
var init_export = __esm({
|
|
1433
|
-
"src/schema/export.ts"() {
|
|
1434
|
-
init_errors();
|
|
1435
|
-
init_registry();
|
|
1436
|
-
init_validator();
|
|
1437
|
-
}
|
|
1438
|
-
});
|
|
1439
|
-
|
|
1440
|
-
// src/schema/index.ts
|
|
1441
|
-
var init_schema = __esm({
|
|
1442
|
-
"src/schema/index.ts"() {
|
|
1443
|
-
init_ajv_formats();
|
|
1444
|
-
init_cli();
|
|
1445
|
-
init_errors();
|
|
1446
|
-
init_export();
|
|
1447
|
-
init_goneat_bridge();
|
|
1448
|
-
init_normalizer();
|
|
1449
|
-
init_registry();
|
|
1450
|
-
init_utils();
|
|
1451
|
-
init_validator();
|
|
1452
|
-
}
|
|
1453
|
-
});
|
|
1454
|
-
var init_correlation = __esm({
|
|
1455
|
-
"src/errors/correlation.ts"() {
|
|
1456
|
-
}
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
// src/errors/severity.ts
|
|
1460
|
-
function getDefaultSeverity() {
|
|
1461
|
-
return {
|
|
1462
|
-
name: Severity.MEDIUM,
|
|
1463
|
-
level: 2
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
var Severity, SEVERITY_LEVELS;
|
|
1467
|
-
var init_severity = __esm({
|
|
1468
|
-
"src/errors/severity.ts"() {
|
|
1469
|
-
Severity = {
|
|
1470
|
-
INFO: "info",
|
|
1471
|
-
LOW: "low",
|
|
1472
|
-
MEDIUM: "medium",
|
|
1473
|
-
HIGH: "high",
|
|
1474
|
-
CRITICAL: "critical"
|
|
1475
|
-
};
|
|
1476
|
-
SEVERITY_LEVELS = {
|
|
1477
|
-
info: 0,
|
|
1478
|
-
low: 1,
|
|
1479
|
-
medium: 2,
|
|
1480
|
-
high: 3,
|
|
1481
|
-
critical: 4
|
|
1482
|
-
};
|
|
1483
|
-
}
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
// src/errors/serialization.ts
|
|
1487
|
-
function extractErrorMessage(error) {
|
|
1488
|
-
if (error instanceof Error) {
|
|
1489
|
-
return error.message;
|
|
1490
|
-
}
|
|
1491
|
-
if (isErrorLike(error)) {
|
|
1492
|
-
return error.message;
|
|
1493
|
-
}
|
|
1494
|
-
if (typeof error === "string") {
|
|
1495
|
-
return error;
|
|
1496
|
-
}
|
|
1497
|
-
return String(error);
|
|
1498
|
-
}
|
|
1499
|
-
function extractStackTrace(error) {
|
|
1500
|
-
if (error instanceof Error) {
|
|
1501
|
-
return error.stack;
|
|
1502
|
-
}
|
|
1503
|
-
if (isErrorLike(error) && typeof error.stack === "string") {
|
|
1504
|
-
return error.stack;
|
|
1505
|
-
}
|
|
1506
|
-
return void 0;
|
|
1507
|
-
}
|
|
1508
|
-
function isErrorLike(value) {
|
|
1509
|
-
return typeof value === "object" && value !== null && "message" in value && typeof value.message === "string";
|
|
1510
|
-
}
|
|
1511
|
-
var init_serialization = __esm({
|
|
1512
|
-
"src/errors/serialization.ts"() {
|
|
1513
|
-
init_severity();
|
|
1514
|
-
}
|
|
1515
|
-
});
|
|
1516
|
-
|
|
1517
|
-
// src/errors/validators.ts
|
|
1518
|
-
async function validateErrorData(data) {
|
|
1519
|
-
return ErrorValidator.getInstance().validate(data);
|
|
1520
|
-
}
|
|
1521
|
-
var ErrorValidator;
|
|
1522
|
-
var init_validators2 = __esm({
|
|
1523
|
-
"src/errors/validators.ts"() {
|
|
1524
|
-
init_schema();
|
|
1525
|
-
ErrorValidator = class _ErrorValidator {
|
|
1526
|
-
static instance;
|
|
1527
|
-
validateFn = null;
|
|
1528
|
-
initPromise = null;
|
|
1529
|
-
initError = null;
|
|
1530
|
-
constructor() {
|
|
1531
|
-
}
|
|
1532
|
-
/**
|
|
1533
|
-
* Get singleton instance
|
|
1534
|
-
*/
|
|
1535
|
-
static getInstance() {
|
|
1536
|
-
if (!_ErrorValidator.instance) {
|
|
1537
|
-
_ErrorValidator.instance = new _ErrorValidator();
|
|
1538
|
-
}
|
|
1539
|
-
return _ErrorValidator.instance;
|
|
1540
|
-
}
|
|
1541
|
-
/**
|
|
1542
|
-
* Initialize validator (lazy load, async)
|
|
1543
|
-
*/
|
|
1544
|
-
async init() {
|
|
1545
|
-
if (this.validateFn !== null || this.initError !== null) {
|
|
1546
|
-
return;
|
|
1547
|
-
}
|
|
1548
|
-
if (this.initPromise) {
|
|
1549
|
-
return this.initPromise;
|
|
1550
|
-
}
|
|
1551
|
-
this.initPromise = (async () => {
|
|
1552
|
-
try {
|
|
1553
|
-
await compileSchemaById("pathfinder/v1.0.0/error-response");
|
|
1554
|
-
await compileSchemaById("assessment/v1.0.0/severity-definitions");
|
|
1555
|
-
this.validateFn = await compileSchemaById("error-handling/v1.0.0/error-response");
|
|
1556
|
-
} catch (err) {
|
|
1557
|
-
this.initError = err instanceof Error ? err : new Error(String(err));
|
|
1558
|
-
throw new Error(`Failed to initialize error validator: ${this.initError.message}`);
|
|
1559
|
-
}
|
|
1560
|
-
})();
|
|
1561
|
-
return this.initPromise;
|
|
1562
|
-
}
|
|
1563
|
-
/**
|
|
1564
|
-
* Validate error data against schema
|
|
1565
|
-
*
|
|
1566
|
-
* @param data - Data to validate
|
|
1567
|
-
* @returns Promise resolving to true if valid, false otherwise
|
|
1568
|
-
* @throws {Error} If validator failed to initialize
|
|
1569
|
-
*/
|
|
1570
|
-
async validate(data) {
|
|
1571
|
-
if (this.validateFn === null) {
|
|
1572
|
-
await this.init();
|
|
1573
|
-
}
|
|
1574
|
-
if (this.initError) {
|
|
1575
|
-
throw this.initError;
|
|
1576
|
-
}
|
|
1577
|
-
if (!this.validateFn) {
|
|
1578
|
-
throw new Error("Validator not initialized");
|
|
1579
|
-
}
|
|
1580
|
-
return this.validateFn(data);
|
|
1581
|
-
}
|
|
1582
|
-
/**
|
|
1583
|
-
* Get validation errors from last validation
|
|
1584
|
-
*
|
|
1585
|
-
* @returns Validation errors or null
|
|
1586
|
-
*/
|
|
1587
|
-
getErrors() {
|
|
1588
|
-
if (!this.validateFn) {
|
|
1589
|
-
return null;
|
|
1590
|
-
}
|
|
1591
|
-
return this.validateFn.errors;
|
|
1592
|
-
}
|
|
1593
|
-
/**
|
|
1594
|
-
* Reset validator state (for testing)
|
|
1595
|
-
* @internal
|
|
1596
|
-
*/
|
|
1597
|
-
static _reset() {
|
|
1598
|
-
_ErrorValidator.instance = new _ErrorValidator();
|
|
1599
|
-
}
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
|
-
});
|
|
1603
|
-
|
|
1604
|
-
// src/errors/fulmen-error.ts
|
|
1605
|
-
function isFulmenErrorData(value) {
|
|
1606
|
-
return typeof value === "object" && value !== null && "code" in value && typeof value.code === "string" && "message" in value && typeof value.message === "string";
|
|
1607
|
-
}
|
|
1608
|
-
var FulmenError;
|
|
1609
|
-
var init_fulmen_error = __esm({
|
|
1610
|
-
"src/errors/fulmen-error.ts"() {
|
|
1611
|
-
init_serialization();
|
|
1612
|
-
init_severity();
|
|
1613
|
-
init_validators2();
|
|
1614
|
-
FulmenError = class _FulmenError extends Error {
|
|
1615
|
-
data;
|
|
1616
|
-
constructor(data) {
|
|
1617
|
-
super(data.message);
|
|
1618
|
-
this.name = "FulmenError";
|
|
1619
|
-
this.data = Object.freeze({ ...data });
|
|
1620
|
-
Error.captureStackTrace(this, _FulmenError);
|
|
1621
|
-
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Serialize to JSON (schema-compliant)
|
|
1624
|
-
*/
|
|
1625
|
-
toJSON() {
|
|
1626
|
-
return this.data;
|
|
1627
|
-
}
|
|
1628
|
-
/**
|
|
1629
|
-
* Check equality with another FulmenError
|
|
1630
|
-
*/
|
|
1631
|
-
equals(other) {
|
|
1632
|
-
return JSON.stringify(this.data) === JSON.stringify(other.data);
|
|
1633
|
-
}
|
|
1634
|
-
/**
|
|
1635
|
-
* Get severity level for comparison
|
|
1636
|
-
*/
|
|
1637
|
-
getSeverityLevel() {
|
|
1638
|
-
return this.data.severity_level ?? SEVERITY_LEVELS[this.data.severity ?? "medium"];
|
|
1639
|
-
}
|
|
1640
|
-
/**
|
|
1641
|
-
* Wrap an existing error with FulmenError structure
|
|
1642
|
-
*
|
|
1643
|
-
* @param error - Error to wrap (Error instance or FulmenErrorData)
|
|
1644
|
-
* @param options - Additional error options
|
|
1645
|
-
* @returns New FulmenError instance
|
|
1646
|
-
*
|
|
1647
|
-
* @example
|
|
1648
|
-
* ```typescript
|
|
1649
|
-
* try {
|
|
1650
|
-
* throw new Error('Config invalid');
|
|
1651
|
-
* } catch (err) {
|
|
1652
|
-
* const fulmenErr = FulmenError.wrap(err, {
|
|
1653
|
-
* code: 'CONFIG_INVALID',
|
|
1654
|
-
* severity: 'high',
|
|
1655
|
-
* exit_code: 2
|
|
1656
|
-
* });
|
|
1657
|
-
* throw fulmenErr;
|
|
1658
|
-
* }
|
|
1659
|
-
* ```
|
|
1660
|
-
*/
|
|
1661
|
-
static wrap(error, options = {}) {
|
|
1662
|
-
if (error instanceof _FulmenError) {
|
|
1663
|
-
const effectiveSeverity = options.severity ?? error.data.severity ?? Severity.MEDIUM;
|
|
1664
|
-
const effectiveSeverityLevel = SEVERITY_LEVELS[effectiveSeverity];
|
|
1665
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1666
|
-
return new _FulmenError({
|
|
1667
|
-
...error.data,
|
|
1668
|
-
...options,
|
|
1669
|
-
code: options.code ?? error.data.code,
|
|
1670
|
-
message: error.data.message,
|
|
1671
|
-
severity: effectiveSeverity,
|
|
1672
|
-
// Consistent severity
|
|
1673
|
-
severity_level: effectiveSeverityLevel,
|
|
1674
|
-
// Recomputed level
|
|
1675
|
-
timestamp
|
|
1676
|
-
// Updated timestamp
|
|
1677
|
-
});
|
|
1678
|
-
}
|
|
1679
|
-
if (isFulmenErrorData(error)) {
|
|
1680
|
-
const defaultSeverity = getDefaultSeverity();
|
|
1681
|
-
const effectiveSeverity = options.severity ?? error.severity ?? defaultSeverity.name;
|
|
1682
|
-
const effectiveSeverityLevel = SEVERITY_LEVELS[effectiveSeverity];
|
|
1683
|
-
return new _FulmenError({
|
|
1684
|
-
...error,
|
|
1685
|
-
...options,
|
|
1686
|
-
severity: effectiveSeverity,
|
|
1687
|
-
severity_level: effectiveSeverityLevel,
|
|
1688
|
-
// Recomputed, not from error.severity_level
|
|
1689
|
-
timestamp: error.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
return _FulmenError.fromError(error, options);
|
|
1693
|
-
}
|
|
1694
|
-
/**
|
|
1695
|
-
* Create FulmenError from native Error object
|
|
1696
|
-
*
|
|
1697
|
-
* @param err - Native Error instance
|
|
1698
|
-
* @param options - Error options
|
|
1699
|
-
* @returns New FulmenError instance
|
|
1700
|
-
*
|
|
1701
|
-
* @example
|
|
1702
|
-
* ```typescript
|
|
1703
|
-
* const err = new TypeError('Invalid type');
|
|
1704
|
-
* const fulmenErr = FulmenError.fromError(err, {
|
|
1705
|
-
* code: 'TYPE_ERROR',
|
|
1706
|
-
* severity: 'medium'
|
|
1707
|
-
* });
|
|
1708
|
-
* ```
|
|
1709
|
-
*/
|
|
1710
|
-
static fromError(err, options = {}) {
|
|
1711
|
-
const code = options.code ?? "UNKNOWN_ERROR";
|
|
1712
|
-
const severity = options.severity ?? Severity.MEDIUM;
|
|
1713
|
-
const severityLevel = SEVERITY_LEVELS[severity];
|
|
1714
|
-
const message = extractErrorMessage(err);
|
|
1715
|
-
const stack = extractStackTrace(err);
|
|
1716
|
-
const data = {
|
|
1717
|
-
code,
|
|
1718
|
-
message,
|
|
1719
|
-
severity,
|
|
1720
|
-
severity_level: severityLevel,
|
|
1721
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1722
|
-
...options,
|
|
1723
|
-
context: {
|
|
1724
|
-
...options.context,
|
|
1725
|
-
originalName: err instanceof Error ? err.name : typeof err,
|
|
1726
|
-
stack
|
|
1727
|
-
},
|
|
1728
|
-
original: stack || message
|
|
1729
|
-
};
|
|
1730
|
-
return new _FulmenError(data);
|
|
1731
|
-
}
|
|
1732
|
-
/**
|
|
1733
|
-
* Validate error data against schema
|
|
1734
|
-
*
|
|
1735
|
-
* @param data - Error data to validate
|
|
1736
|
-
* @returns Promise resolving to true if valid
|
|
1737
|
-
*
|
|
1738
|
-
* @example
|
|
1739
|
-
* ```typescript
|
|
1740
|
-
* const data = { code: 'TEST', message: 'Test error' };
|
|
1741
|
-
* if (await FulmenError.validate(data)) {
|
|
1742
|
-
* const err = new FulmenError(data);
|
|
1743
|
-
* }
|
|
1744
|
-
* ```
|
|
1745
|
-
*/
|
|
1746
|
-
static async validate(data) {
|
|
1747
|
-
return validateErrorData(data);
|
|
1748
|
-
}
|
|
1749
|
-
/**
|
|
1750
|
-
* Exit process with structured error
|
|
1751
|
-
*
|
|
1752
|
-
* Logs error as JSON and exits with specified exit code.
|
|
1753
|
-
* Mockable for testing (override process.exit).
|
|
1754
|
-
*
|
|
1755
|
-
* @param error - FulmenError instance
|
|
1756
|
-
* @param options - Exit options
|
|
1757
|
-
*
|
|
1758
|
-
* @example
|
|
1759
|
-
* ```typescript
|
|
1760
|
-
* const err = FulmenError.fromError(new Error('Fatal'), {
|
|
1761
|
-
* code: 'FATAL_ERROR',
|
|
1762
|
-
* exit_code: 1
|
|
1763
|
-
* });
|
|
1764
|
-
* FulmenError.exitWithError(err); // Exits with code 1
|
|
1765
|
-
* ```
|
|
1766
|
-
*/
|
|
1767
|
-
static exitWithError(error, options = {}) {
|
|
1768
|
-
const logger = options.logger ?? console.error;
|
|
1769
|
-
const exitCode = error.data.exit_code ?? 1;
|
|
1770
|
-
logger(JSON.stringify(error.toJSON(), null, 2));
|
|
1771
|
-
process.exit(exitCode);
|
|
1772
|
-
}
|
|
1773
|
-
};
|
|
1774
|
-
}
|
|
1775
|
-
});
|
|
1776
|
-
|
|
1777
|
-
// src/errors/index.ts
|
|
1778
|
-
var init_errors2 = __esm({
|
|
1779
|
-
"src/errors/index.ts"() {
|
|
1780
|
-
init_correlation();
|
|
1781
|
-
init_fulmen_error();
|
|
1782
|
-
init_serialization();
|
|
1783
|
-
init_severity();
|
|
1784
|
-
init_validators2();
|
|
1785
|
-
}
|
|
1786
|
-
});
|
|
1787
|
-
|
|
1788
|
-
// src/appidentity/errors.ts
|
|
1789
|
-
var AppIdentityError;
|
|
1790
|
-
var init_errors3 = __esm({
|
|
1791
|
-
"src/appidentity/errors.ts"() {
|
|
1792
|
-
init_errors2();
|
|
1793
|
-
AppIdentityError = class _AppIdentityError extends FulmenError {
|
|
1794
|
-
identityPath;
|
|
1795
|
-
constructor(message, identityPath, cause) {
|
|
1796
|
-
let errorData;
|
|
1797
|
-
if (cause) {
|
|
1798
|
-
errorData = FulmenError.fromError(cause, {
|
|
1799
|
-
code: "APP_IDENTITY_ERROR",
|
|
1800
|
-
severity: "high",
|
|
1801
|
-
context: { identityPath }
|
|
1802
|
-
}).data;
|
|
1803
|
-
} else {
|
|
1804
|
-
errorData = {
|
|
1805
|
-
code: "APP_IDENTITY_ERROR",
|
|
1806
|
-
message,
|
|
1807
|
-
severity: "high",
|
|
1808
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1809
|
-
context: { identityPath }
|
|
1810
|
-
};
|
|
1811
|
-
}
|
|
1812
|
-
super(errorData);
|
|
1813
|
-
this.name = "AppIdentityError";
|
|
1814
|
-
this.identityPath = identityPath;
|
|
1815
|
-
if (Error.captureStackTrace) {
|
|
1816
|
-
Error.captureStackTrace(this, _AppIdentityError);
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
/**
|
|
1820
|
-
* Create error for identity not found
|
|
1821
|
-
*/
|
|
1822
|
-
static notFound(searchedPaths) {
|
|
1823
|
-
const message = `App identity not found
|
|
1824
|
-
Searched paths:
|
|
1825
|
-
${searchedPaths.map((p) => ` - ${p}`).join("\n")}`;
|
|
1826
|
-
return new _AppIdentityError(message);
|
|
1827
|
-
}
|
|
1828
|
-
/**
|
|
1829
|
-
* Create error for schema validation failure
|
|
1830
|
-
*/
|
|
1831
|
-
static validationFailed(path, diagnostics) {
|
|
1832
|
-
const errorCount = diagnostics.filter((d) => d.severity === "ERROR").length;
|
|
1833
|
-
const warningCount = diagnostics.filter((d) => d.severity === "WARN").length;
|
|
1834
|
-
let message = `Invalid app identity: ${path}
|
|
1835
|
-
`;
|
|
1836
|
-
message += `Validation errors: ${errorCount} error(s), ${warningCount} warning(s)
|
|
1837
|
-
`;
|
|
1838
|
-
const displayDiagnostics = diagnostics.slice(0, 3);
|
|
1839
|
-
for (const diag of displayDiagnostics) {
|
|
1840
|
-
message += ` - ${diag.message}`;
|
|
1841
|
-
if (diag.pointer) {
|
|
1842
|
-
message += ` at ${diag.pointer}`;
|
|
1843
|
-
}
|
|
1844
|
-
message += "\n";
|
|
1845
|
-
}
|
|
1846
|
-
if (diagnostics.length > 3) {
|
|
1847
|
-
message += ` ... and ${diagnostics.length - 3} more
|
|
1848
|
-
`;
|
|
1849
|
-
}
|
|
1850
|
-
return new _AppIdentityError(message, path);
|
|
1851
|
-
}
|
|
1852
|
-
/**
|
|
1853
|
-
* Create error for environment variable override pointing to missing file
|
|
1854
|
-
*/
|
|
1855
|
-
static envOverrideMissing(envPath) {
|
|
1856
|
-
const message = `FULMEN_APP_IDENTITY_PATH points to missing file: ${envPath}
|
|
1857
|
-
`;
|
|
1858
|
-
return new _AppIdentityError(message, envPath);
|
|
1859
|
-
}
|
|
1860
|
-
/**
|
|
1861
|
-
* Create error for YAML parsing failure
|
|
1862
|
-
*/
|
|
1863
|
-
static parseFailed(path, cause) {
|
|
1864
|
-
const message = `Failed to parse identity file: ${path}
|
|
1865
|
-
${cause.message}`;
|
|
1866
|
-
return new _AppIdentityError(message, path, cause);
|
|
1867
|
-
}
|
|
1868
|
-
/**
|
|
1869
|
-
* Create error for file read failure
|
|
1870
|
-
*/
|
|
1871
|
-
static readFailed(path, cause) {
|
|
1872
|
-
const message = `Failed to read identity file: ${path}
|
|
1873
|
-
${cause.message}`;
|
|
1874
|
-
return new _AppIdentityError(message, path, cause);
|
|
1875
|
-
}
|
|
1876
|
-
/**
|
|
1877
|
-
* Create error for embedded identity already registered
|
|
1878
|
-
*
|
|
1879
|
-
* Uses first-wins semantics - once registered, cannot be replaced
|
|
1880
|
-
*/
|
|
1881
|
-
static alreadyRegistered() {
|
|
1882
|
-
const message = "Embedded identity already registered. Registration uses first-wins semantics and cannot be replaced.";
|
|
1883
|
-
return new _AppIdentityError(message);
|
|
1884
|
-
}
|
|
1885
|
-
/**
|
|
1886
|
-
* Create error for embedded identity YAML parsing failure
|
|
1887
|
-
*/
|
|
1888
|
-
static embeddedParseFailed(cause) {
|
|
1889
|
-
const message = `Failed to parse embedded identity YAML: ${cause.message}`;
|
|
1890
|
-
return new _AppIdentityError(message, void 0, cause);
|
|
1891
|
-
}
|
|
1892
|
-
/**
|
|
1893
|
-
* Create error for embedded identity schema validation failure
|
|
1894
|
-
*/
|
|
1895
|
-
static embeddedValidationFailed(diagnostics) {
|
|
1896
|
-
const errorCount = diagnostics.filter((d) => d.severity === "ERROR").length;
|
|
1897
|
-
const warningCount = diagnostics.filter((d) => d.severity === "WARN").length;
|
|
1898
|
-
let message = "Invalid embedded identity\n";
|
|
1899
|
-
message += `Validation errors: ${errorCount} error(s), ${warningCount} warning(s)
|
|
1900
|
-
`;
|
|
1901
|
-
const displayDiagnostics = diagnostics.slice(0, 3);
|
|
1902
|
-
for (const diag of displayDiagnostics) {
|
|
1903
|
-
message += ` - ${diag.message}`;
|
|
1904
|
-
if (diag.pointer) {
|
|
1905
|
-
message += ` at ${diag.pointer}`;
|
|
1906
|
-
}
|
|
1907
|
-
message += "\n";
|
|
1908
|
-
}
|
|
1909
|
-
if (diagnostics.length > 3) {
|
|
1910
|
-
message += ` ... and ${diagnostics.length - 3} more
|
|
1911
|
-
`;
|
|
1912
|
-
}
|
|
1913
|
-
return new _AppIdentityError(message);
|
|
1914
|
-
}
|
|
1915
|
-
};
|
|
1916
|
-
}
|
|
1917
|
-
});
|
|
1918
|
-
function deepFreeze(obj) {
|
|
1919
|
-
Object.freeze(obj);
|
|
1920
|
-
Object.getOwnPropertyNames(obj).forEach((prop) => {
|
|
1921
|
-
const value = obj[prop];
|
|
1922
|
-
if (value !== null && (typeof value === "object" || typeof value === "function") && !Object.isFrozen(value)) {
|
|
1923
|
-
deepFreeze(value);
|
|
1924
|
-
}
|
|
1925
|
-
});
|
|
1926
|
-
return obj;
|
|
1927
|
-
}
|
|
1928
|
-
async function registerEmbeddedIdentity(data, options) {
|
|
1929
|
-
if (isRegistered) {
|
|
1930
|
-
throw AppIdentityError.alreadyRegistered();
|
|
1931
|
-
}
|
|
1932
|
-
const skipValidation = options?.skipValidation ?? false;
|
|
1933
|
-
let identity;
|
|
1934
|
-
if (typeof data === "string") {
|
|
1935
|
-
let parsed;
|
|
1936
|
-
try {
|
|
1937
|
-
parsed = parse(data);
|
|
1938
|
-
} catch (error) {
|
|
1939
|
-
throw AppIdentityError.embeddedParseFailed(
|
|
1940
|
-
error instanceof Error ? error : new Error(String(error))
|
|
1941
|
-
);
|
|
1942
|
-
}
|
|
1943
|
-
if (!skipValidation) {
|
|
1944
|
-
const result = await validateDataBySchemaId(parsed, APP_IDENTITY_SCHEMA_ID);
|
|
1945
|
-
if (!result.valid) {
|
|
1946
|
-
throw AppIdentityError.embeddedValidationFailed(result.diagnostics);
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
identity = parsed;
|
|
1950
|
-
} else {
|
|
1951
|
-
if (!skipValidation) {
|
|
1952
|
-
const result = await validateDataBySchemaId(data, APP_IDENTITY_SCHEMA_ID);
|
|
1953
|
-
if (!result.valid) {
|
|
1954
|
-
throw AppIdentityError.embeddedValidationFailed(result.diagnostics);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
identity = data;
|
|
1958
|
-
}
|
|
1959
|
-
embeddedIdentity = deepFreeze(structuredClone(identity));
|
|
1960
|
-
isRegistered = true;
|
|
1961
|
-
}
|
|
1962
|
-
function hasEmbeddedIdentity() {
|
|
1963
|
-
return isRegistered;
|
|
1964
|
-
}
|
|
1965
|
-
function getEmbeddedIdentity() {
|
|
1966
|
-
return embeddedIdentity;
|
|
1967
|
-
}
|
|
1968
|
-
function clearEmbeddedIdentity() {
|
|
1969
|
-
embeddedIdentity = null;
|
|
1970
|
-
isRegistered = false;
|
|
1971
|
-
}
|
|
1972
|
-
var embeddedIdentity, isRegistered;
|
|
1973
|
-
var init_embedded = __esm({
|
|
1974
|
-
"src/appidentity/embedded.ts"() {
|
|
1975
|
-
init_schema();
|
|
1976
|
-
init_constants();
|
|
1977
|
-
init_errors3();
|
|
1978
|
-
embeddedIdentity = null;
|
|
1979
|
-
isRegistered = false;
|
|
1980
|
-
}
|
|
1981
|
-
});
|
|
1982
|
-
|
|
1983
|
-
// src/appidentity/cache.ts
|
|
1984
|
-
function getCachedIdentity() {
|
|
1985
|
-
return cachedIdentity;
|
|
1986
|
-
}
|
|
1987
|
-
function setCachedIdentity(identity) {
|
|
1988
|
-
cachedIdentity = identity;
|
|
1989
|
-
}
|
|
1990
|
-
function clearIdentityCache() {
|
|
1991
|
-
cachedIdentity = null;
|
|
1992
|
-
}
|
|
1993
|
-
var cachedIdentity;
|
|
1994
|
-
var init_cache = __esm({
|
|
1995
|
-
"src/appidentity/cache.ts"() {
|
|
1996
|
-
cachedIdentity = null;
|
|
1997
|
-
}
|
|
1998
|
-
});
|
|
1999
|
-
async function discoverIdentityPath(options) {
|
|
2000
|
-
if (options?.path) {
|
|
2001
|
-
const exists = await fileExists(options.path);
|
|
2002
|
-
if (!exists) {
|
|
2003
|
-
throw AppIdentityError.notFound([options.path]);
|
|
2004
|
-
}
|
|
2005
|
-
return { path: options.path, source: "explicit" };
|
|
2006
|
-
}
|
|
2007
|
-
const envPath = process.env[APP_IDENTITY_ENV_VAR];
|
|
2008
|
-
if (envPath) {
|
|
2009
|
-
const exists = await fileExists(envPath);
|
|
2010
|
-
if (!exists) {
|
|
2011
|
-
throw AppIdentityError.envOverrideMissing(envPath);
|
|
2012
|
-
}
|
|
2013
|
-
return { path: envPath, source: "env" };
|
|
2014
|
-
}
|
|
2015
|
-
const startDir = options?.startDir || process.cwd();
|
|
2016
|
-
const result = await searchAncestors(startDir);
|
|
2017
|
-
if (result) {
|
|
2018
|
-
return { path: result, source: "ancestor" };
|
|
2019
|
-
}
|
|
2020
|
-
return null;
|
|
2021
|
-
}
|
|
2022
|
-
async function searchAncestors(startDir) {
|
|
2023
|
-
let currentDir = startDir;
|
|
2024
|
-
const searchedPaths = [];
|
|
2025
|
-
for (let i = 0; i < MAX_ANCESTOR_SEARCH_DEPTH; i++) {
|
|
2026
|
-
const candidatePath = join(currentDir, APP_IDENTITY_DIR, APP_IDENTITY_FILENAME);
|
|
2027
|
-
searchedPaths.push(candidatePath);
|
|
2028
|
-
if (await fileExists(candidatePath)) {
|
|
2029
|
-
return candidatePath;
|
|
2030
|
-
}
|
|
2031
|
-
const parentDir = dirname(currentDir);
|
|
2032
|
-
if (parentDir === currentDir) {
|
|
2033
|
-
throw AppIdentityError.notFound(searchedPaths);
|
|
2034
|
-
}
|
|
2035
|
-
currentDir = parentDir;
|
|
2036
|
-
}
|
|
2037
|
-
throw AppIdentityError.notFound(searchedPaths);
|
|
2038
|
-
}
|
|
2039
|
-
async function fileExists(path) {
|
|
2040
|
-
try {
|
|
2041
|
-
await access(path);
|
|
2042
|
-
return true;
|
|
2043
|
-
} catch {
|
|
2044
|
-
return false;
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
var init_discovery = __esm({
|
|
2048
|
-
"src/appidentity/discovery.ts"() {
|
|
2049
|
-
init_constants();
|
|
2050
|
-
init_errors3();
|
|
2051
|
-
}
|
|
2052
|
-
});
|
|
2053
|
-
function deepFreeze2(obj) {
|
|
2054
|
-
Object.freeze(obj);
|
|
2055
|
-
Object.getOwnPropertyNames(obj).forEach((prop) => {
|
|
2056
|
-
const value = obj[prop];
|
|
2057
|
-
if (value !== null && (typeof value === "object" || typeof value === "function") && !Object.isFrozen(value)) {
|
|
2058
|
-
deepFreeze2(value);
|
|
2059
|
-
}
|
|
2060
|
-
});
|
|
2061
|
-
return obj;
|
|
2062
|
-
}
|
|
2063
|
-
async function loadIdentity(options) {
|
|
2064
|
-
if (options?.identity) {
|
|
2065
|
-
return deepFreeze2(structuredClone(options.identity));
|
|
2066
|
-
}
|
|
2067
|
-
if (!options?.skipCache) {
|
|
2068
|
-
const cached = getCachedIdentity();
|
|
2069
|
-
if (cached) {
|
|
2070
|
-
return cached;
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
let discovery;
|
|
2074
|
-
try {
|
|
2075
|
-
discovery = await discoverIdentityPath({
|
|
2076
|
-
path: options?.path,
|
|
2077
|
-
startDir: options?.startDir
|
|
2078
|
-
});
|
|
2079
|
-
} catch (error) {
|
|
2080
|
-
const hasExplicitPath = Boolean(options?.path);
|
|
2081
|
-
const hasEnvOverride = Boolean(process.env[APP_IDENTITY_ENV_VAR]);
|
|
2082
|
-
if (!hasExplicitPath && !hasEnvOverride && error instanceof AppIdentityError) {
|
|
2083
|
-
const embedded = getEmbeddedIdentity();
|
|
2084
|
-
if (embedded) {
|
|
2085
|
-
setCachedIdentity(embedded);
|
|
2086
|
-
return embedded;
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
throw error;
|
|
2090
|
-
}
|
|
2091
|
-
if (!discovery) {
|
|
2092
|
-
const embedded = getEmbeddedIdentity();
|
|
2093
|
-
if (embedded) {
|
|
2094
|
-
setCachedIdentity(embedded);
|
|
2095
|
-
return embedded;
|
|
2096
|
-
}
|
|
2097
|
-
throw AppIdentityError.notFound([]);
|
|
2098
|
-
}
|
|
2099
|
-
let content;
|
|
2100
|
-
try {
|
|
2101
|
-
content = await readFile(discovery.path, "utf-8");
|
|
2102
|
-
} catch (error) {
|
|
2103
|
-
throw AppIdentityError.readFailed(
|
|
2104
|
-
discovery.path,
|
|
2105
|
-
error instanceof Error ? error : new Error(String(error))
|
|
2106
|
-
);
|
|
2107
|
-
}
|
|
2108
|
-
let parsed;
|
|
2109
|
-
try {
|
|
2110
|
-
parsed = parse(content);
|
|
2111
|
-
} catch (error) {
|
|
2112
|
-
throw AppIdentityError.parseFailed(
|
|
2113
|
-
discovery.path,
|
|
2114
|
-
error instanceof Error ? error : new Error(String(error))
|
|
2115
|
-
);
|
|
2116
|
-
}
|
|
2117
|
-
if (!options?.skipValidation) {
|
|
2118
|
-
const result = await validateDataBySchemaId(parsed, APP_IDENTITY_SCHEMA_ID);
|
|
2119
|
-
if (!result.valid) {
|
|
2120
|
-
throw AppIdentityError.validationFailed(discovery.path, result.diagnostics);
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
const identity = deepFreeze2(structuredClone(parsed));
|
|
2124
|
-
setCachedIdentity(identity);
|
|
2125
|
-
return identity;
|
|
2126
|
-
}
|
|
2127
|
-
var init_loader = __esm({
|
|
2128
|
-
"src/appidentity/loader.ts"() {
|
|
2129
|
-
init_schema();
|
|
2130
|
-
init_cache();
|
|
2131
|
-
init_constants();
|
|
2132
|
-
init_discovery();
|
|
2133
|
-
init_embedded();
|
|
2134
|
-
init_errors3();
|
|
2135
|
-
}
|
|
2136
|
-
});
|
|
2137
|
-
|
|
2138
|
-
// src/appidentity/runtime.ts
|
|
2139
|
-
function detectRuntime() {
|
|
2140
|
-
const versions = process.versions;
|
|
2141
|
-
if (typeof versions.bun === "string" && versions.bun.length > 0) {
|
|
2142
|
-
return { name: "bun", version: versions.bun };
|
|
2143
|
-
}
|
|
2144
|
-
if (typeof versions.node === "string" && versions.node.length > 0) {
|
|
2145
|
-
return { name: "node", version: versions.node };
|
|
2146
|
-
}
|
|
2147
|
-
return { name: "unknown" };
|
|
2148
|
-
}
|
|
2149
|
-
function buildRuntimeInfo(options = {}) {
|
|
2150
|
-
const runtime = detectRuntime();
|
|
2151
|
-
const serviceName = options.serviceName ?? options.identity?.app.binary_name ?? "unknown-service";
|
|
2152
|
-
const vendor = options.vendor ?? options.identity?.app.vendor;
|
|
2153
|
-
return {
|
|
2154
|
-
service: {
|
|
2155
|
-
name: serviceName,
|
|
2156
|
-
vendor,
|
|
2157
|
-
version: options.version
|
|
2158
|
-
},
|
|
2159
|
-
runtime,
|
|
2160
|
-
platform: {
|
|
2161
|
-
os: process.platform,
|
|
2162
|
-
arch: process.arch
|
|
2163
|
-
}
|
|
2164
|
-
};
|
|
2165
|
-
}
|
|
2166
|
-
var init_runtime = __esm({
|
|
2167
|
-
"src/appidentity/runtime.ts"() {
|
|
2168
|
-
}
|
|
2169
|
-
});
|
|
2170
|
-
|
|
2171
|
-
// src/appidentity/index.ts
|
|
2172
|
-
init_constants();
|
|
2173
|
-
init_embedded();
|
|
2174
|
-
init_errors3();
|
|
2175
|
-
|
|
2176
|
-
// src/appidentity/helpers.ts
|
|
2177
|
-
init_loader();
|
|
2178
|
-
async function getBinaryName(options) {
|
|
2179
|
-
const identity = await loadIdentity(options);
|
|
2180
|
-
return identity.app.binary_name;
|
|
2181
|
-
}
|
|
2182
|
-
async function getVendor(options) {
|
|
2183
|
-
const identity = await loadIdentity(options);
|
|
2184
|
-
return identity.app.vendor;
|
|
2185
|
-
}
|
|
2186
|
-
async function getEnvPrefix(options) {
|
|
2187
|
-
const identity = await loadIdentity(options);
|
|
2188
|
-
return identity.app.env_prefix;
|
|
2189
|
-
}
|
|
2190
|
-
async function getConfigName(options) {
|
|
2191
|
-
const identity = await loadIdentity(options);
|
|
2192
|
-
return identity.app.config_name;
|
|
2193
|
-
}
|
|
2194
|
-
async function getTelemetryNamespace(options) {
|
|
2195
|
-
const identity = await loadIdentity(options);
|
|
2196
|
-
return identity.metadata?.telemetry_namespace ?? identity.app.binary_name;
|
|
2197
|
-
}
|
|
2198
|
-
async function getConfigIdentifiers(options) {
|
|
2199
|
-
const identity = await loadIdentity(options);
|
|
2200
|
-
return Object.freeze({
|
|
2201
|
-
vendor: identity.app.vendor,
|
|
2202
|
-
configName: identity.app.config_name
|
|
2203
|
-
});
|
|
2204
|
-
}
|
|
2205
|
-
async function buildEnvVar(key, options) {
|
|
2206
|
-
const envPrefix = await getEnvPrefix(options);
|
|
2207
|
-
const normalizedKey = key.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
2208
|
-
return `${envPrefix}${normalizedKey}`;
|
|
2209
|
-
}
|
|
2210
|
-
async function getEnvVar(key, options) {
|
|
2211
|
-
const envVarName = await buildEnvVar(key, options);
|
|
2212
|
-
return process.env[envVarName];
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
// src/appidentity/index.ts
|
|
2216
|
-
init_loader();
|
|
2217
|
-
init_runtime();
|
|
2218
|
-
|
|
2219
|
-
// src/docscribe/normalize.ts
|
|
2220
|
-
var decoder = new TextDecoder("utf-8", { fatal: false, ignoreBOM: true });
|
|
2221
|
-
function stripBom(value) {
|
|
2222
|
-
return value.charCodeAt(0) === 65279 ? value.slice(1) : value;
|
|
2223
|
-
}
|
|
2224
|
-
function normalizeNewlines(value) {
|
|
2225
|
-
return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2226
|
-
}
|
|
2227
|
-
function normalizeInput(input) {
|
|
2228
|
-
let content;
|
|
2229
|
-
if (typeof input === "string") {
|
|
2230
|
-
content = input;
|
|
2231
|
-
} else if (input instanceof Uint8Array) {
|
|
2232
|
-
content = decoder.decode(input);
|
|
2233
|
-
} else {
|
|
2234
|
-
content = decoder.decode(new Uint8Array(input));
|
|
2235
|
-
}
|
|
2236
|
-
const normalized = normalizeNewlines(stripBom(content));
|
|
2237
|
-
return { content: normalized, original: input };
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
// src/docscribe/types.ts
|
|
2241
|
-
var DocScribeError = class extends Error {
|
|
2242
|
-
constructor(message, options) {
|
|
2243
|
-
super(message, options);
|
|
2244
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
2245
|
-
this.name = "DocScribeError";
|
|
2246
|
-
}
|
|
2247
|
-
};
|
|
2248
|
-
var DocScribeParseError = class extends DocScribeError {
|
|
2249
|
-
constructor(message, options) {
|
|
2250
|
-
super(message, options);
|
|
2251
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
2252
|
-
this.name = "DocScribeParseError";
|
|
2253
|
-
}
|
|
2254
|
-
};
|
|
2255
|
-
var DocScribeUnsupportedFormatError = class extends DocScribeError {
|
|
2256
|
-
constructor(message, options) {
|
|
2257
|
-
super(message, options);
|
|
2258
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
2259
|
-
this.name = "DocScribeUnsupportedFormatError";
|
|
2260
|
-
}
|
|
2261
|
-
};
|
|
2262
|
-
|
|
2263
|
-
// src/docscribe/frontmatter.ts
|
|
2264
|
-
var FRONTMATTER_DELIMITER = "---";
|
|
2265
|
-
var STRING_KEYS = /* @__PURE__ */ new Set([
|
|
2266
|
-
"title",
|
|
2267
|
-
"description",
|
|
2268
|
-
"author",
|
|
2269
|
-
"date",
|
|
2270
|
-
"lastUpdated",
|
|
2271
|
-
"status",
|
|
2272
|
-
"version"
|
|
2273
|
-
]);
|
|
2274
|
-
function parseFrontmatter(input) {
|
|
2275
|
-
const { content } = normalizeInput(input);
|
|
2276
|
-
const bounds = locateFrontmatter(content);
|
|
2277
|
-
if (!bounds) {
|
|
2278
|
-
return { metadata: null, body: content, range: null, bodyStartLine: 1 };
|
|
2279
|
-
}
|
|
2280
|
-
const yamlBlock = content.slice(bounds.openEnd, bounds.closingStart);
|
|
2281
|
-
const metadata = yamlBlock.trim().length === 0 ? {} : parseYamlMetadata(yamlBlock);
|
|
2282
|
-
const bodyRaw = content.slice(bounds.closingEnd);
|
|
2283
|
-
const leadingNewlinesMatch = bodyRaw.match(/^\n+/);
|
|
2284
|
-
const leadingNewlinesLength = leadingNewlinesMatch ? leadingNewlinesMatch[0].length : 0;
|
|
2285
|
-
const bodyStartIndex = bounds.closingEnd + leadingNewlinesLength;
|
|
2286
|
-
const prefix = content.slice(0, bodyStartIndex);
|
|
2287
|
-
const bodyStartLine = prefix.length === 0 ? 1 : prefix.split("\n").length;
|
|
2288
|
-
const body = bodyRaw.slice(leadingNewlinesLength);
|
|
2289
|
-
return {
|
|
2290
|
-
metadata,
|
|
2291
|
-
body,
|
|
2292
|
-
range: { start: bounds.start, end: bounds.closingEnd },
|
|
2293
|
-
bodyStartLine
|
|
2294
|
-
};
|
|
2295
|
-
}
|
|
2296
|
-
function stripFrontmatter(input) {
|
|
2297
|
-
return parseFrontmatter(input).body;
|
|
2298
|
-
}
|
|
2299
|
-
function extractMetadata(input) {
|
|
2300
|
-
return parseFrontmatter(input).metadata;
|
|
2301
|
-
}
|
|
2302
|
-
function locateFrontmatter(content) {
|
|
2303
|
-
let cursor = 0;
|
|
2304
|
-
while (cursor < content.length) {
|
|
2305
|
-
const newlineIndex = content.indexOf("\n", cursor);
|
|
2306
|
-
const lineEnd = newlineIndex === -1 ? content.length : newlineIndex;
|
|
2307
|
-
const line = content.slice(cursor, lineEnd);
|
|
2308
|
-
if (line.trim().length === 0) {
|
|
2309
|
-
if (newlineIndex === -1) {
|
|
2310
|
-
return null;
|
|
2311
|
-
}
|
|
2312
|
-
cursor = newlineIndex + 1;
|
|
2313
|
-
continue;
|
|
2314
|
-
}
|
|
2315
|
-
if (line.trim() !== FRONTMATTER_DELIMITER) {
|
|
2316
|
-
return null;
|
|
2317
|
-
}
|
|
2318
|
-
const openStart = cursor;
|
|
2319
|
-
const openEnd = newlineIndex === -1 ? content.length : newlineIndex + 1;
|
|
2320
|
-
let search = openEnd;
|
|
2321
|
-
while (search <= content.length) {
|
|
2322
|
-
const nextNewline = content.indexOf("\n", search);
|
|
2323
|
-
const segmentEnd = nextNewline === -1 ? content.length : nextNewline;
|
|
2324
|
-
const candidate = content.slice(search, segmentEnd).trim();
|
|
2325
|
-
if (candidate === FRONTMATTER_DELIMITER) {
|
|
2326
|
-
const closingStart = search;
|
|
2327
|
-
const closingEnd = nextNewline === -1 ? segmentEnd : nextNewline + 1;
|
|
2328
|
-
return {
|
|
2329
|
-
start: openStart,
|
|
2330
|
-
openEnd,
|
|
2331
|
-
closingStart,
|
|
2332
|
-
closingEnd
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
if (nextNewline === -1) {
|
|
2336
|
-
break;
|
|
2337
|
-
}
|
|
2338
|
-
search = nextNewline + 1;
|
|
2339
|
-
}
|
|
2340
|
-
return null;
|
|
2341
|
-
}
|
|
2342
|
-
return null;
|
|
2343
|
-
}
|
|
2344
|
-
function parseYamlMetadata(source) {
|
|
2345
|
-
const document = parseDocument(source, { prettyErrors: true });
|
|
2346
|
-
if (document.errors.length > 0) {
|
|
2347
|
-
const error = document.errors[0];
|
|
2348
|
-
const location = error.linePos && error.linePos.length > 0 ? error.linePos[0] : null;
|
|
2349
|
-
const position = location ? ` (line ${location.line + 1}, column ${location.col + 1})` : "";
|
|
2350
|
-
throw new DocScribeParseError(`Invalid YAML frontmatter${position}: ${error.message}`, {
|
|
2351
|
-
cause: error
|
|
2352
|
-
});
|
|
2353
|
-
}
|
|
2354
|
-
let raw;
|
|
2355
|
-
try {
|
|
2356
|
-
raw = document.toJS({ mapAsMap: false });
|
|
2357
|
-
} catch (error) {
|
|
2358
|
-
throw new DocScribeParseError("Failed to parse YAML frontmatter", {
|
|
2359
|
-
cause: error instanceof Error ? error : void 0
|
|
2360
|
-
});
|
|
2361
|
-
}
|
|
2362
|
-
if (raw == null) {
|
|
2363
|
-
return {};
|
|
2364
|
-
}
|
|
2365
|
-
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
2366
|
-
throw new DocScribeParseError("YAML frontmatter must be a mapping at the top level");
|
|
2367
|
-
}
|
|
2368
|
-
return normalizeMetadata(raw);
|
|
2369
|
-
}
|
|
2370
|
-
function normalizeMetadata(raw) {
|
|
2371
|
-
const normalized = {};
|
|
2372
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
2373
|
-
normalized[key] = normalizeMetadataValue(value);
|
|
2374
|
-
}
|
|
2375
|
-
const base = {};
|
|
2376
|
-
for (const [key, value] of Object.entries(normalized)) {
|
|
2377
|
-
if (STRING_KEYS.has(key) || key === "tags" || key === "relatedDocs") {
|
|
2378
|
-
continue;
|
|
2379
|
-
}
|
|
2380
|
-
base[key] = value;
|
|
2381
|
-
}
|
|
2382
|
-
const metadata = {
|
|
2383
|
-
...base,
|
|
2384
|
-
...normalized.title !== void 0 ? { title: ensureStringValue(normalized.title) } : {},
|
|
2385
|
-
...normalized.description !== void 0 ? { description: ensureStringValue(normalized.description) } : {},
|
|
2386
|
-
...normalized.author !== void 0 ? { author: ensureStringValue(normalized.author) } : {},
|
|
2387
|
-
...normalized.date !== void 0 ? { date: ensureStringValue(normalized.date) } : {},
|
|
2388
|
-
...normalized.lastUpdated !== void 0 ? { lastUpdated: ensureStringValue(normalized.lastUpdated) } : {},
|
|
2389
|
-
...normalized.status !== void 0 ? { status: ensureStringValue(normalized.status) } : {},
|
|
2390
|
-
...normalized.version !== void 0 ? { version: ensureStringValue(normalized.version) } : {},
|
|
2391
|
-
...normalized.tags !== void 0 ? (() => {
|
|
2392
|
-
const tags = ensureStringArray(normalized.tags);
|
|
2393
|
-
return tags.length > 0 ? { tags } : {};
|
|
2394
|
-
})() : {},
|
|
2395
|
-
...normalized.relatedDocs !== void 0 ? (() => {
|
|
2396
|
-
const related = ensureStringArray(normalized.relatedDocs);
|
|
2397
|
-
return related.length > 0 ? { relatedDocs: related } : {};
|
|
2398
|
-
})() : {}
|
|
2399
|
-
};
|
|
2400
|
-
return metadata;
|
|
2401
|
-
}
|
|
2402
|
-
function normalizeMetadataValue(value) {
|
|
2403
|
-
if (value === null) {
|
|
2404
|
-
return null;
|
|
2405
|
-
}
|
|
2406
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
2407
|
-
return value;
|
|
2408
|
-
}
|
|
2409
|
-
if (value instanceof Date) {
|
|
2410
|
-
return value.toISOString();
|
|
2411
|
-
}
|
|
2412
|
-
if (Array.isArray(value)) {
|
|
2413
|
-
return value.map(
|
|
2414
|
-
(item) => normalizeMetadataValue(item)
|
|
2415
|
-
);
|
|
2416
|
-
}
|
|
2417
|
-
if (typeof value === "object") {
|
|
2418
|
-
const result = {};
|
|
2419
|
-
for (const [key, nested] of Object.entries(value)) {
|
|
2420
|
-
result[key] = normalizeMetadataValue(nested);
|
|
2421
|
-
}
|
|
2422
|
-
return result;
|
|
2423
|
-
}
|
|
2424
|
-
return String(value);
|
|
2425
|
-
}
|
|
2426
|
-
function ensureStringValue(value) {
|
|
2427
|
-
if (value === null) {
|
|
2428
|
-
return "";
|
|
2429
|
-
}
|
|
2430
|
-
if (typeof value === "string") {
|
|
2431
|
-
return value;
|
|
2432
|
-
}
|
|
2433
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
2434
|
-
return String(value);
|
|
2435
|
-
}
|
|
2436
|
-
if (Array.isArray(value)) {
|
|
2437
|
-
return value.flatMap((item) => ensureStringArrayItem(item)).join(", ");
|
|
2438
|
-
}
|
|
2439
|
-
return JSON.stringify(value);
|
|
2440
|
-
}
|
|
2441
|
-
function ensureStringArray(value) {
|
|
2442
|
-
return ensureStringArrayItem(value);
|
|
2443
|
-
}
|
|
2444
|
-
function ensureStringArrayItem(value) {
|
|
2445
|
-
if (value === null) {
|
|
2446
|
-
return [];
|
|
2447
|
-
}
|
|
2448
|
-
if (typeof value === "string") {
|
|
2449
|
-
return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
2450
|
-
}
|
|
2451
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
2452
|
-
return [String(value)];
|
|
2453
|
-
}
|
|
2454
|
-
if (Array.isArray(value)) {
|
|
2455
|
-
return value.flatMap((item) => ensureStringArrayItem(item));
|
|
2456
|
-
}
|
|
2457
|
-
return [JSON.stringify(value)];
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
// src/docscribe/headers.ts
|
|
2461
|
-
var ATX_PATTERN = /^ {0,3}(#{1,6})(?:[ \t]+|$)(.*)$/;
|
|
2462
|
-
var SETEXT_PATTERN = /^ {0,3}(=+|-+)\s*$/;
|
|
2463
|
-
function extractHeaders(input, options) {
|
|
2464
|
-
const { content } = normalizeInput(input);
|
|
2465
|
-
const parsed = parseFrontmatter(content);
|
|
2466
|
-
const body = parsed.body;
|
|
2467
|
-
const lineOffset = parsed.bodyStartLine;
|
|
2468
|
-
const slugify = options?.slugify ?? defaultSlugify;
|
|
2469
|
-
const maxDepth = options?.maxDepth ?? 6;
|
|
2470
|
-
const lines = body.split("\n");
|
|
2471
|
-
const headers = [];
|
|
2472
|
-
let inFence = false;
|
|
2473
|
-
let fenceMarker = null;
|
|
2474
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
2475
|
-
const line = lines[index];
|
|
2476
|
-
const trimmed = line.trimEnd();
|
|
2477
|
-
const lineNumber = lineOffset + index;
|
|
2478
|
-
const fenceMatch = line.match(/^ {0,3}(```+|~~~+)/);
|
|
2479
|
-
if (fenceMatch) {
|
|
2480
|
-
const marker = fenceMatch[1][0];
|
|
2481
|
-
if (!inFence) {
|
|
2482
|
-
inFence = true;
|
|
2483
|
-
fenceMarker = marker;
|
|
2484
|
-
} else if (fenceMarker === marker) {
|
|
2485
|
-
inFence = false;
|
|
2486
|
-
fenceMarker = null;
|
|
2487
|
-
}
|
|
2488
|
-
continue;
|
|
2489
|
-
}
|
|
2490
|
-
if (inFence) {
|
|
2491
|
-
continue;
|
|
2492
|
-
}
|
|
2493
|
-
if (/^( {4}|\t)/.test(line)) {
|
|
2494
|
-
continue;
|
|
2495
|
-
}
|
|
2496
|
-
const atxMatch = trimmed.match(ATX_PATTERN);
|
|
2497
|
-
if (atxMatch) {
|
|
2498
|
-
const level = atxMatch[1].length;
|
|
2499
|
-
if (level <= maxDepth) {
|
|
2500
|
-
const rawText = atxMatch[2].replace(/#+\s*$/, "").trim();
|
|
2501
|
-
const text = rawText.length > 0 ? rawText : "";
|
|
2502
|
-
const slug = ensureSlug(slugify, text, lineNumber);
|
|
2503
|
-
headers.push({ level, text, slug, line: lineNumber });
|
|
2504
|
-
}
|
|
2505
|
-
continue;
|
|
2506
|
-
}
|
|
2507
|
-
if (trimmed.length === 0) {
|
|
2508
|
-
continue;
|
|
2509
|
-
}
|
|
2510
|
-
if (index + 1 >= lines.length) {
|
|
2511
|
-
continue;
|
|
2512
|
-
}
|
|
2513
|
-
const nextLine = lines[index + 1];
|
|
2514
|
-
const setextMatch = SETEXT_PATTERN.exec(nextLine.trimEnd());
|
|
2515
|
-
if (setextMatch) {
|
|
2516
|
-
const underline = setextMatch[1];
|
|
2517
|
-
const level = underline.startsWith("=") ? 1 : 2;
|
|
2518
|
-
if (level <= maxDepth) {
|
|
2519
|
-
const text = trimmed.trim();
|
|
2520
|
-
if (text.length > 0 && !/^ {0,3}(```|~~~)/.test(text)) {
|
|
2521
|
-
const slug = ensureSlug(slugify, text, lineNumber);
|
|
2522
|
-
headers.push({ level, text, slug, line: lineNumber });
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
index += 1;
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
return headers;
|
|
2529
|
-
}
|
|
2530
|
-
function defaultSlugify(value) {
|
|
2531
|
-
return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-");
|
|
2532
|
-
}
|
|
2533
|
-
function ensureSlug(slugify, text, lineNumber) {
|
|
2534
|
-
const candidate = slugify(text);
|
|
2535
|
-
if (candidate && candidate.trim().length > 0) {
|
|
2536
|
-
return candidate;
|
|
2537
|
-
}
|
|
2538
|
-
return `section-${lineNumber}`;
|
|
2539
|
-
}
|
|
2540
|
-
|
|
2541
|
-
// src/docscribe/format.ts
|
|
2542
|
-
var JSON_PATTERN = /^[\s\t]*[[{]/;
|
|
2543
|
-
var TOML_ASSIGNMENT = /^[A-Za-z0-9_"'\-.]+\s*=\s*.+$/;
|
|
2544
|
-
var TOML_TABLE = /^\s*\[[^\]]+\]\s*$/;
|
|
2545
|
-
var YAML_KEY_PATTERN = /^\s*[\w"'-]+\s*:\s*.+$/;
|
|
2546
|
-
var MARKDOWN_HEADING = /(\n|^)\s{0,3}#{1,6}\s+/;
|
|
2547
|
-
var MARKDOWN_LIST = /(\n|^)\s{0,3}(?:-|\*|\+)\s+/;
|
|
2548
|
-
var MARKDOWN_SETEXT = /(\n|^).+\n\s{0,3}(?:=+|-+)\s*(\n|$)/;
|
|
2549
|
-
function detectFormat(input) {
|
|
2550
|
-
const { content } = normalizeInput(input);
|
|
2551
|
-
return detectFormatFromContent(content);
|
|
2552
|
-
}
|
|
2553
|
-
function inspectDocument(input, options) {
|
|
2554
|
-
const { content } = normalizeInput(input);
|
|
2555
|
-
const format = detectFormatFromContent(content);
|
|
2556
|
-
const frontmatter = parseFrontmatter(content);
|
|
2557
|
-
const headers = extractHeaders(content, options);
|
|
2558
|
-
const headerCount = headers.length;
|
|
2559
|
-
const estimatedSections = headerCount > 0 ? headerCount : frontmatter.body.trim().length > 0 ? 1 : 0;
|
|
2560
|
-
const lineCount = content.length === 0 ? 0 : content.split("\n").length;
|
|
2561
|
-
const size = content.length;
|
|
2562
|
-
return {
|
|
2563
|
-
format,
|
|
2564
|
-
hasFrontmatter: frontmatter.metadata !== null,
|
|
2565
|
-
frontmatterRange: frontmatter.range,
|
|
2566
|
-
metadata: frontmatter.metadata,
|
|
2567
|
-
headerCount,
|
|
2568
|
-
estimatedSections,
|
|
2569
|
-
bodyStartLine: frontmatter.bodyStartLine,
|
|
2570
|
-
lineCount,
|
|
2571
|
-
size,
|
|
2572
|
-
headers
|
|
2573
|
-
};
|
|
2574
|
-
}
|
|
2575
|
-
function detectFormatFromContent(content) {
|
|
2576
|
-
const trimmed = content.trim();
|
|
2577
|
-
if (trimmed.length === 0) {
|
|
2578
|
-
return "plain";
|
|
2579
|
-
}
|
|
2580
|
-
if (looksLikeJson(trimmed)) {
|
|
2581
|
-
return "json";
|
|
2582
|
-
}
|
|
2583
|
-
if (looksLikeToml(trimmed)) {
|
|
2584
|
-
return "toml";
|
|
2585
|
-
}
|
|
2586
|
-
const yamlSeparatorCount = countYamlSeparators(content);
|
|
2587
|
-
if (yamlSeparatorCount >= 1 && isLikelyYamlStream(content)) {
|
|
2588
|
-
return "yaml-stream";
|
|
2589
|
-
}
|
|
2590
|
-
const markdownIndicators = hasMarkdownIndicators(content);
|
|
2591
|
-
if (looksLikeYaml(trimmed) && !markdownIndicators) {
|
|
2592
|
-
return "yaml";
|
|
2593
|
-
}
|
|
2594
|
-
const frontmatter = parseFrontmatter(content);
|
|
2595
|
-
if (frontmatter.metadata !== null) {
|
|
2596
|
-
const body = frontmatter.body.trim();
|
|
2597
|
-
if (markdownIndicators || MARKDOWN_HEADING.test(body) || MARKDOWN_LIST.test(body)) {
|
|
2598
|
-
return "markdown";
|
|
2599
|
-
}
|
|
2600
|
-
if (!body) {
|
|
2601
|
-
return "markdown";
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
if (markdownIndicators) {
|
|
2605
|
-
return "markdown";
|
|
2606
|
-
}
|
|
2607
|
-
return "plain";
|
|
2608
|
-
}
|
|
2609
|
-
function looksLikeJson(trimmed) {
|
|
2610
|
-
if (!JSON_PATTERN.test(trimmed)) {
|
|
2611
|
-
return false;
|
|
2612
|
-
}
|
|
2613
|
-
try {
|
|
2614
|
-
JSON.parse(trimmed);
|
|
2615
|
-
return true;
|
|
2616
|
-
} catch (_error) {
|
|
2617
|
-
return false;
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
function looksLikeToml(content) {
|
|
2621
|
-
const lines = content.split("\n");
|
|
2622
|
-
let tableCount = 0;
|
|
2623
|
-
let assignmentCount = 0;
|
|
2624
|
-
for (const rawLine of lines) {
|
|
2625
|
-
const line = rawLine.trim();
|
|
2626
|
-
if (line === "" || line.startsWith("#")) {
|
|
2627
|
-
continue;
|
|
2628
|
-
}
|
|
2629
|
-
if (TOML_TABLE.test(line)) {
|
|
2630
|
-
tableCount += 1;
|
|
2631
|
-
continue;
|
|
2632
|
-
}
|
|
2633
|
-
if (TOML_ASSIGNMENT.test(line)) {
|
|
2634
|
-
assignmentCount += 1;
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
return tableCount > 0 || assignmentCount >= 3;
|
|
2638
|
-
}
|
|
2639
|
-
function looksLikeYaml(trimmed) {
|
|
2640
|
-
if (trimmed.startsWith("%YAML")) {
|
|
2641
|
-
return true;
|
|
2642
|
-
}
|
|
2643
|
-
const lines = trimmed.split("\n");
|
|
2644
|
-
let signal = 0;
|
|
2645
|
-
for (const raw of lines.slice(0, 20)) {
|
|
2646
|
-
const line = raw.trim();
|
|
2647
|
-
if (line === "" || line.startsWith("#")) {
|
|
2648
|
-
continue;
|
|
2649
|
-
}
|
|
2650
|
-
if (line === "---") {
|
|
2651
|
-
signal += 1;
|
|
2652
|
-
continue;
|
|
2653
|
-
}
|
|
2654
|
-
if (YAML_KEY_PATTERN.test(line) || /^-\s/.test(line)) {
|
|
2655
|
-
signal += 1;
|
|
2656
|
-
}
|
|
2657
|
-
}
|
|
2658
|
-
return signal >= 2;
|
|
2659
|
-
}
|
|
2660
|
-
function countYamlSeparators(content) {
|
|
2661
|
-
const matches = content.match(/(^|\n)---\s*(\n|$)/g);
|
|
2662
|
-
return matches ? matches.length : 0;
|
|
2663
|
-
}
|
|
2664
|
-
function isLikelyYamlStream(content) {
|
|
2665
|
-
const lines = content.split("\n");
|
|
2666
|
-
let yamlSignals = 0;
|
|
2667
|
-
let markdownSignals = 0;
|
|
2668
|
-
for (const rawLine of lines) {
|
|
2669
|
-
const line = rawLine.trim();
|
|
2670
|
-
if (line === "" || line === "---") {
|
|
2671
|
-
continue;
|
|
2672
|
-
}
|
|
2673
|
-
if (/^#/.test(line) || /^\s{0,3}(?:-|\*|\+)\s+/.test(line) || /(\[[^\]]+\]\([^)]+\))/.test(line)) {
|
|
2674
|
-
markdownSignals += 1;
|
|
2675
|
-
}
|
|
2676
|
-
if (/:\s/.test(line) || /^-\s/.test(line)) {
|
|
2677
|
-
yamlSignals += 1;
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
return yamlSignals >= 2 && markdownSignals === 0;
|
|
2681
|
-
}
|
|
2682
|
-
function hasMarkdownIndicators(content) {
|
|
2683
|
-
return MARKDOWN_HEADING.test(content) || MARKDOWN_LIST.test(content) || MARKDOWN_SETEXT.test(content) || /(\n|^)\s{0,3}>\s+/.test(content) || /(\[[^\]]+\]\([^)]+\))/.test(content);
|
|
2684
|
-
}
|
|
2685
|
-
var __internal = {
|
|
2686
|
-
detectFormatFromContent,
|
|
2687
|
-
looksLikeJson,
|
|
2688
|
-
looksLikeToml,
|
|
2689
|
-
looksLikeYaml,
|
|
2690
|
-
isLikelyYamlStream
|
|
2691
|
-
};
|
|
2692
|
-
|
|
2693
|
-
// src/docscribe/split.ts
|
|
2694
|
-
function splitDocuments(input, options) {
|
|
2695
|
-
const { content } = normalizeInput(input);
|
|
2696
|
-
const trimmed = content.trim();
|
|
2697
|
-
if (trimmed.length === 0) {
|
|
2698
|
-
return [];
|
|
2699
|
-
}
|
|
2700
|
-
const maxDocuments = options?.maxDocuments ?? Number.POSITIVE_INFINITY;
|
|
2701
|
-
const format = detectFormat(content);
|
|
2702
|
-
let splits;
|
|
2703
|
-
if (format === "yaml-stream") {
|
|
2704
|
-
splits = withIndices(splitYamlStream(content));
|
|
2705
|
-
} else if (format === "markdown") {
|
|
2706
|
-
const markdownSplits = splitMarkdownDocuments(content);
|
|
2707
|
-
splits = markdownSplits.length > 0 ? withIndices(markdownSplits) : [withIndex(buildSingleSplit(content, format, 1), 0)];
|
|
2708
|
-
} else {
|
|
2709
|
-
splits = [withIndex(buildSingleSplit(content, format, 1), 0)];
|
|
2710
|
-
}
|
|
2711
|
-
return applyDocumentLimit(splits, maxDocuments);
|
|
2712
|
-
}
|
|
2713
|
-
function splitYamlStream(content) {
|
|
2714
|
-
const lines = content.split("\n");
|
|
2715
|
-
const docs = [];
|
|
2716
|
-
let current = { lines: [], startLine: 1 };
|
|
2717
|
-
let delimiterCount = 0;
|
|
2718
|
-
const flush = () => {
|
|
2719
|
-
if (current.lines.join("\n").trim().length === 0) {
|
|
2720
|
-
current = { lines: [], startLine: current.startLine };
|
|
2721
|
-
return;
|
|
2722
|
-
}
|
|
2723
|
-
docs.push({ ...current, lines: [...current.lines] });
|
|
2724
|
-
current = { lines: [], startLine: current.startLine };
|
|
2725
|
-
};
|
|
2726
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
2727
|
-
const lineNumber = index + 1;
|
|
2728
|
-
const line = lines[index];
|
|
2729
|
-
const trimmed = line.trim();
|
|
2730
|
-
if (trimmed === "---") {
|
|
2731
|
-
delimiterCount += 1;
|
|
2732
|
-
flush();
|
|
2733
|
-
current = { lines: [], startLine: lineNumber + 1 };
|
|
2734
|
-
continue;
|
|
2735
|
-
}
|
|
2736
|
-
if (delimiterCount === 0) {
|
|
2737
|
-
current.startLine = 1;
|
|
2738
|
-
}
|
|
2739
|
-
current.lines.push(line);
|
|
2740
|
-
}
|
|
2741
|
-
flush();
|
|
2742
|
-
if (docs.length === 0) {
|
|
2743
|
-
return [buildRawSplit(content, "yaml", null, 1, content.split("\n").length)];
|
|
2744
|
-
}
|
|
2745
|
-
return docs.map((doc) => {
|
|
2746
|
-
const joined = doc.lines.join("\n");
|
|
2747
|
-
const lineCount = doc.lines.length;
|
|
2748
|
-
return buildRawSplit(
|
|
2749
|
-
joined,
|
|
2750
|
-
"yaml",
|
|
2751
|
-
null,
|
|
2752
|
-
doc.startLine,
|
|
2753
|
-
doc.startLine + Math.max(lineCount - 1, 0)
|
|
2754
|
-
);
|
|
2755
|
-
}).filter((split) => split.content.trim().length > 0);
|
|
2756
|
-
}
|
|
2757
|
-
function splitMarkdownDocuments(content) {
|
|
2758
|
-
const frontmatter = parseFrontmatter(content);
|
|
2759
|
-
const lines = content.split("\n");
|
|
2760
|
-
const docs = [];
|
|
2761
|
-
const frontmatterLimit = frontmatter.metadata ? Math.max(frontmatter.bodyStartLine - 1, 0) : 0;
|
|
2762
|
-
let current = { lines: [], startLine: 1 };
|
|
2763
|
-
let inFence = false;
|
|
2764
|
-
let fenceMarker = null;
|
|
2765
|
-
const flush = () => {
|
|
2766
|
-
if (current.lines.join("\n").trim().length === 0) {
|
|
2767
|
-
current = { lines: [], startLine: current.startLine };
|
|
2768
|
-
return;
|
|
2769
|
-
}
|
|
2770
|
-
docs.push({ ...current, lines: [...current.lines] });
|
|
2771
|
-
current = { lines: [], startLine: current.startLine };
|
|
2772
|
-
};
|
|
2773
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
2774
|
-
const lineNumber = index + 1;
|
|
2775
|
-
const line = lines[index];
|
|
2776
|
-
const trimmed = line.trimEnd();
|
|
2777
|
-
const fenceMatch = line.match(/^ {0,3}(```+|~~~+)/);
|
|
2778
|
-
if (fenceMatch) {
|
|
2779
|
-
const marker = fenceMatch[1][0];
|
|
2780
|
-
if (!inFence) {
|
|
2781
|
-
inFence = true;
|
|
2782
|
-
fenceMarker = marker;
|
|
2783
|
-
} else if (fenceMarker === marker) {
|
|
2784
|
-
inFence = false;
|
|
2785
|
-
fenceMarker = null;
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
|
-
const isPrimaryFrontmatter = frontmatterLimit > 0 && lineNumber <= frontmatterLimit;
|
|
2789
|
-
if (!inFence && !isPrimaryFrontmatter && trimmed === "---") {
|
|
2790
|
-
const prev = findPrevNonEmpty(lines, index - 1);
|
|
2791
|
-
const next = findNextNonEmpty(lines, index + 1);
|
|
2792
|
-
const prevLooksLikeMetadata = prev ? /:\s*/.test(prev.line) : false;
|
|
2793
|
-
const nextIsHeading = next ? next.line.trim().startsWith("#") : false;
|
|
2794
|
-
if (next && !prevLooksLikeMetadata && nextIsHeading) {
|
|
2795
|
-
flush();
|
|
2796
|
-
current = { lines: [], startLine: next.lineNumber };
|
|
2797
|
-
continue;
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
current.lines.push(line);
|
|
2801
|
-
}
|
|
2802
|
-
flush();
|
|
2803
|
-
return docs.map((doc) => {
|
|
2804
|
-
const contentValue = doc.lines.join("\n");
|
|
2805
|
-
const parsed = parseFrontmatter(contentValue);
|
|
2806
|
-
const lineCount = doc.lines.length;
|
|
2807
|
-
return buildRawSplit(
|
|
2808
|
-
contentValue,
|
|
2809
|
-
"markdown",
|
|
2810
|
-
parsed.metadata,
|
|
2811
|
-
doc.startLine,
|
|
2812
|
-
doc.startLine + Math.max(lineCount - 1, 0),
|
|
2813
|
-
parsed.metadata !== null
|
|
2814
|
-
);
|
|
2815
|
-
}).filter((split) => split.content.trim().length > 0);
|
|
2816
|
-
}
|
|
2817
|
-
function buildSingleSplit(content, format, startLine) {
|
|
2818
|
-
const parsed = parseFrontmatter(content);
|
|
2819
|
-
const lineCount = content.length === 0 ? 0 : content.split("\n").length;
|
|
2820
|
-
return buildRawSplit(
|
|
2821
|
-
content,
|
|
2822
|
-
format,
|
|
2823
|
-
parsed.metadata,
|
|
2824
|
-
startLine,
|
|
2825
|
-
startLine + Math.max(lineCount - 1, 0),
|
|
2826
|
-
parsed.metadata !== null
|
|
2827
|
-
);
|
|
2828
|
-
}
|
|
2829
|
-
function buildRawSplit(content, format, metadata, startLine, endLine, hasFrontmatter = false) {
|
|
2830
|
-
return {
|
|
2831
|
-
content,
|
|
2832
|
-
format,
|
|
2833
|
-
hasFrontmatter,
|
|
2834
|
-
metadata: metadata ?? null,
|
|
2835
|
-
startLine,
|
|
2836
|
-
endLine
|
|
2837
|
-
};
|
|
2838
|
-
}
|
|
2839
|
-
function applyDocumentLimit(splits, maxDocuments) {
|
|
2840
|
-
if (maxDocuments <= 0) {
|
|
2841
|
-
return [];
|
|
2842
|
-
}
|
|
2843
|
-
if (splits.length <= maxDocuments) {
|
|
2844
|
-
return splits.map((split, idx) => ({ ...split, index: idx }));
|
|
2845
|
-
}
|
|
2846
|
-
const cutoff = Math.max(maxDocuments - 1, 0);
|
|
2847
|
-
const allowed = splits.slice(0, cutoff);
|
|
2848
|
-
const remaining = splits.slice(cutoff);
|
|
2849
|
-
const merged = mergeSplitGroup(remaining);
|
|
2850
|
-
const combined = cutoff === 0 ? [merged] : [...allowed, merged];
|
|
2851
|
-
return combined.map((split, idx) => ({ ...split, index: idx }));
|
|
2852
|
-
}
|
|
2853
|
-
function mergeSplitGroup(splits) {
|
|
2854
|
-
const first = splits[0];
|
|
2855
|
-
const last = splits[splits.length - 1];
|
|
2856
|
-
const combinedContent = splits.map((split) => split.content).join("\n---\n");
|
|
2857
|
-
return {
|
|
2858
|
-
content: combinedContent,
|
|
2859
|
-
format: first.format,
|
|
2860
|
-
hasFrontmatter: first.hasFrontmatter,
|
|
2861
|
-
metadata: first.metadata,
|
|
2862
|
-
startLine: first.startLine,
|
|
2863
|
-
endLine: last.endLine,
|
|
2864
|
-
index: 0
|
|
2865
|
-
};
|
|
2866
|
-
}
|
|
2867
|
-
function withIndices(rawSplits) {
|
|
2868
|
-
return rawSplits.map((split, idx) => withIndex(split, idx));
|
|
2869
|
-
}
|
|
2870
|
-
function withIndex(split, index) {
|
|
2871
|
-
return { ...split, index };
|
|
2872
|
-
}
|
|
2873
|
-
function findNextNonEmpty(lines, startIndex) {
|
|
2874
|
-
for (let index = startIndex; index < lines.length; index += 1) {
|
|
2875
|
-
const candidate = lines[index];
|
|
2876
|
-
if (candidate.trim().length === 0) {
|
|
2877
|
-
continue;
|
|
2878
|
-
}
|
|
2879
|
-
return { line: candidate, lineNumber: index + 1 };
|
|
2880
|
-
}
|
|
2881
|
-
return null;
|
|
2882
|
-
}
|
|
2883
|
-
function findPrevNonEmpty(lines, startIndex) {
|
|
2884
|
-
for (let index = startIndex; index >= 0; index -= 1) {
|
|
2885
|
-
const candidate = lines[index];
|
|
2886
|
-
if (candidate.trim().length === 0) {
|
|
2887
|
-
continue;
|
|
2888
|
-
}
|
|
2889
|
-
return { line: candidate, lineNumber: index + 1 };
|
|
2890
|
-
}
|
|
2891
|
-
return null;
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
// src/crucible/fulpack/types.ts
|
|
2895
|
-
var ArchiveFormat = /* @__PURE__ */ ((ArchiveFormat2) => {
|
|
2896
|
-
ArchiveFormat2["TAR"] = "tar";
|
|
2897
|
-
ArchiveFormat2["TAR_GZ"] = "tar.gz";
|
|
2898
|
-
ArchiveFormat2["ZIP"] = "zip";
|
|
2899
|
-
ArchiveFormat2["GZIP"] = "gzip";
|
|
2900
|
-
return ArchiveFormat2;
|
|
2901
|
-
})(ArchiveFormat || {});
|
|
2902
|
-
var EntryType = /* @__PURE__ */ ((EntryType2) => {
|
|
2903
|
-
EntryType2["FILE"] = "file";
|
|
2904
|
-
EntryType2["DIRECTORY"] = "directory";
|
|
2905
|
-
EntryType2["SYMLINK"] = "symlink";
|
|
2906
|
-
return EntryType2;
|
|
2907
|
-
})(EntryType || {});
|
|
2908
|
-
var Operation = /* @__PURE__ */ ((Operation2) => {
|
|
2909
|
-
Operation2["CREATE"] = "create";
|
|
2910
|
-
Operation2["EXTRACT"] = "extract";
|
|
2911
|
-
Operation2["SCAN"] = "scan";
|
|
2912
|
-
Operation2["VERIFY"] = "verify";
|
|
2913
|
-
Operation2["INFO"] = "info";
|
|
2914
|
-
return Operation2;
|
|
2915
|
-
})(Operation || {});
|
|
2916
|
-
|
|
2917
|
-
// src/fulpack/errors.ts
|
|
2918
|
-
function createFulpackError(code, message, operation, context) {
|
|
2919
|
-
return {
|
|
2920
|
-
code,
|
|
2921
|
-
message,
|
|
2922
|
-
operation,
|
|
2923
|
-
...context?.path && { path: context.path },
|
|
2924
|
-
...context?.archive && { archive: context.archive },
|
|
2925
|
-
...context?.details && { details: context.details }
|
|
2926
|
-
};
|
|
2927
|
-
}
|
|
2928
|
-
var ERROR_CODES = {
|
|
2929
|
-
// Validation Errors
|
|
2930
|
-
INVALID_ARCHIVE_FORMAT: "INVALID_ARCHIVE_FORMAT",
|
|
2931
|
-
INVALID_PATH: "INVALID_PATH",
|
|
2932
|
-
INVALID_OPTIONS: "INVALID_OPTIONS",
|
|
2933
|
-
// Security Errors
|
|
2934
|
-
PATH_TRAVERSAL: "PATH_TRAVERSAL",
|
|
2935
|
-
ABSOLUTE_PATH: "ABSOLUTE_PATH",
|
|
2936
|
-
SYMLINK_ESCAPE: "SYMLINK_ESCAPE",
|
|
2937
|
-
DECOMPRESSION_BOMB: "DECOMPRESSION_BOMB",
|
|
2938
|
-
CHECKSUM_MISMATCH: "CHECKSUM_MISMATCH",
|
|
2939
|
-
// Runtime Errors
|
|
2940
|
-
ARCHIVE_NOT_FOUND: "ARCHIVE_NOT_FOUND",
|
|
2941
|
-
ARCHIVE_CORRUPT: "ARCHIVE_CORRUPT",
|
|
2942
|
-
EXTRACTION_FAILED: "EXTRACTION_FAILED",
|
|
2943
|
-
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
2944
|
-
DISK_FULL: "DISK_FULL",
|
|
2945
|
-
SOURCE_NOT_FOUND: "SOURCE_NOT_FOUND"
|
|
2946
|
-
};
|
|
2947
|
-
var FulpackOperationError = class extends Error {
|
|
2948
|
-
code;
|
|
2949
|
-
operation;
|
|
2950
|
-
path;
|
|
2951
|
-
archive;
|
|
2952
|
-
details;
|
|
2953
|
-
constructor(error) {
|
|
2954
|
-
super(error.message);
|
|
2955
|
-
this.name = "FulpackOperationError";
|
|
2956
|
-
this.code = error.code;
|
|
2957
|
-
this.operation = error.operation;
|
|
2958
|
-
this.path = error.path;
|
|
2959
|
-
this.archive = error.archive;
|
|
2960
|
-
this.details = error.details;
|
|
2961
|
-
}
|
|
2962
|
-
/**
|
|
2963
|
-
* Convert back to canonical error format
|
|
2964
|
-
*/
|
|
2965
|
-
toCanonical() {
|
|
2966
|
-
return {
|
|
2967
|
-
code: this.code,
|
|
2968
|
-
message: this.message,
|
|
2969
|
-
operation: this.operation,
|
|
2970
|
-
...this.path && { path: this.path },
|
|
2971
|
-
...this.archive && { archive: this.archive },
|
|
2972
|
-
...this.details && { details: this.details }
|
|
2973
|
-
};
|
|
2974
|
-
}
|
|
2975
|
-
};
|
|
2976
|
-
function hasPathTraversal(path) {
|
|
2977
|
-
return path.includes("..") || path.includes("\\..");
|
|
2978
|
-
}
|
|
2979
|
-
function isAbsolutePath(path) {
|
|
2980
|
-
return path.startsWith("/") || /^[a-zA-Z]:/.test(path);
|
|
2981
|
-
}
|
|
2982
|
-
function validatePath(path, operation, allowAbsolute = false) {
|
|
2983
|
-
if (!allowAbsolute && isAbsolutePath(path)) {
|
|
2984
|
-
return createFulpackError(
|
|
2985
|
-
ERROR_CODES.ABSOLUTE_PATH,
|
|
2986
|
-
`Absolute path not allowed: ${path}`,
|
|
2987
|
-
operation,
|
|
2988
|
-
{ path }
|
|
2989
|
-
);
|
|
2990
|
-
}
|
|
2991
|
-
if (hasPathTraversal(path)) {
|
|
2992
|
-
return createFulpackError(
|
|
2993
|
-
ERROR_CODES.PATH_TRAVERSAL,
|
|
2994
|
-
`Path traversal detected: ${path}`,
|
|
2995
|
-
operation,
|
|
2996
|
-
{ path }
|
|
2997
|
-
);
|
|
2998
|
-
}
|
|
2999
|
-
return null;
|
|
3000
|
-
}
|
|
3001
|
-
function checkDecompressionBomb(uncompressedSize, compressedSize, maxSize, maxEntries, currentEntries) {
|
|
3002
|
-
if (uncompressedSize > maxSize) {
|
|
3003
|
-
return createFulpackError(
|
|
3004
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
3005
|
-
`Archive exceeds maximum size limit`,
|
|
3006
|
-
"extract" /* EXTRACT */,
|
|
3007
|
-
{
|
|
3008
|
-
details: {
|
|
3009
|
-
actual_size: uncompressedSize,
|
|
3010
|
-
max_size: maxSize
|
|
3011
|
-
}
|
|
3012
|
-
}
|
|
3013
|
-
);
|
|
3014
|
-
}
|
|
3015
|
-
if (currentEntries > maxEntries) {
|
|
3016
|
-
return createFulpackError(
|
|
3017
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
3018
|
-
`Archive exceeds maximum entry limit`,
|
|
3019
|
-
"extract" /* EXTRACT */,
|
|
3020
|
-
{
|
|
3021
|
-
details: {
|
|
3022
|
-
actual_size: currentEntries,
|
|
3023
|
-
max_size: maxEntries
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
);
|
|
3027
|
-
}
|
|
3028
|
-
if (compressedSize > 0 && uncompressedSize / compressedSize > 100) {
|
|
3029
|
-
return createFulpackError(
|
|
3030
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
3031
|
-
`Suspicious compression ratio detected`,
|
|
3032
|
-
"extract" /* EXTRACT */,
|
|
3033
|
-
{
|
|
3034
|
-
details: {
|
|
3035
|
-
compression_ratio: uncompressedSize / compressedSize
|
|
3036
|
-
}
|
|
3037
|
-
}
|
|
3038
|
-
);
|
|
3039
|
-
}
|
|
3040
|
-
return null;
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
|
-
// src/fulpack/core.ts
|
|
3044
|
-
var DEFAULTS = {
|
|
3045
|
-
CREATE: {
|
|
3046
|
-
compression_level: 6,
|
|
3047
|
-
checksum_algorithm: "sha256",
|
|
3048
|
-
preserve_permissions: true,
|
|
3049
|
-
follow_symlinks: false
|
|
3050
|
-
},
|
|
3051
|
-
EXTRACT: {
|
|
3052
|
-
overwrite: "error",
|
|
3053
|
-
verify_checksums: true,
|
|
3054
|
-
preserve_permissions: true,
|
|
3055
|
-
max_size: 1024 * 1024 * 1024,
|
|
3056
|
-
// 1GB
|
|
3057
|
-
max_entries: 1e5
|
|
3058
|
-
},
|
|
3059
|
-
SCAN: {
|
|
3060
|
-
include_metadata: true,
|
|
3061
|
-
max_entries: 1e5
|
|
3062
|
-
}
|
|
3063
|
-
};
|
|
3064
|
-
async function create(source, output, format, options) {
|
|
3065
|
-
const opts = { ...DEFAULTS.CREATE, ...options };
|
|
3066
|
-
const sources = Array.isArray(source) ? source : [source];
|
|
3067
|
-
if (format !== "tar" /* TAR */ && format !== "tar.gz" /* TAR_GZ */ && format !== "zip" /* ZIP */ && format !== "gzip" /* GZIP */) {
|
|
3068
|
-
throw new FulpackOperationError(
|
|
3069
|
-
createFulpackError(
|
|
3070
|
-
ERROR_CODES.INVALID_ARCHIVE_FORMAT,
|
|
3071
|
-
`Format ${format} not yet supported`,
|
|
3072
|
-
"create" /* CREATE */
|
|
3073
|
-
)
|
|
3074
|
-
);
|
|
3075
|
-
}
|
|
3076
|
-
const outputDir = dirname(output);
|
|
3077
|
-
if (!existsSync(outputDir)) {
|
|
3078
|
-
mkdirSync(outputDir, { recursive: true });
|
|
3079
|
-
}
|
|
3080
|
-
if (format === "zip" /* ZIP */) {
|
|
3081
|
-
return await createZipArchive(sources, output, opts);
|
|
3082
|
-
} else if (format === "tar.gz" /* TAR_GZ */) {
|
|
3083
|
-
return await createTarGzArchive(sources, output, opts);
|
|
3084
|
-
} else if (format === "tar" /* TAR */) {
|
|
3085
|
-
return await createTarArchive(sources, output, opts);
|
|
3086
|
-
} else if (format === "gzip" /* GZIP */) {
|
|
3087
|
-
return await createGzipFile(sources, output, opts);
|
|
3088
|
-
} else {
|
|
3089
|
-
throw new FulpackOperationError(
|
|
3090
|
-
createFulpackError(
|
|
3091
|
-
ERROR_CODES.INVALID_ARCHIVE_FORMAT,
|
|
3092
|
-
`Unsupported format for creation: ${format}`,
|
|
3093
|
-
"create" /* CREATE */
|
|
3094
|
-
)
|
|
3095
|
-
);
|
|
3096
|
-
}
|
|
3097
|
-
}
|
|
3098
|
-
async function extract(archive, destination, options) {
|
|
3099
|
-
const opts = { ...DEFAULTS.EXTRACT, ...options };
|
|
3100
|
-
if (!existsSync(archive)) {
|
|
3101
|
-
throw new FulpackOperationError(
|
|
3102
|
-
createFulpackError(
|
|
3103
|
-
ERROR_CODES.ARCHIVE_NOT_FOUND,
|
|
3104
|
-
`Archive not found: ${archive}`,
|
|
3105
|
-
"extract" /* EXTRACT */,
|
|
3106
|
-
{ archive }
|
|
3107
|
-
)
|
|
3108
|
-
);
|
|
3109
|
-
}
|
|
3110
|
-
if (!existsSync(destination)) {
|
|
3111
|
-
mkdirSync(destination, { recursive: true });
|
|
3112
|
-
}
|
|
3113
|
-
if (archive.endsWith(".tar.gz") || archive.endsWith(".tgz")) {
|
|
3114
|
-
return await extractTarGz(archive, destination, opts);
|
|
3115
|
-
} else if (archive.endsWith(".tar")) {
|
|
3116
|
-
return await extractTar(archive, destination, opts);
|
|
3117
|
-
} else if (archive.endsWith(".zip")) {
|
|
3118
|
-
return await extractZip(archive, destination, opts);
|
|
3119
|
-
} else if (archive.endsWith(".gz")) {
|
|
3120
|
-
return await extractGzipFile(archive, destination, opts);
|
|
3121
|
-
} else {
|
|
3122
|
-
throw new FulpackOperationError(
|
|
3123
|
-
createFulpackError(
|
|
3124
|
-
ERROR_CODES.INVALID_ARCHIVE_FORMAT,
|
|
3125
|
-
`Unsupported archive format for extraction: ${archive}`,
|
|
3126
|
-
"extract" /* EXTRACT */,
|
|
3127
|
-
{ archive }
|
|
3128
|
-
)
|
|
3129
|
-
);
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3132
|
-
async function scan(archive, options) {
|
|
3133
|
-
const opts = { ...DEFAULTS.SCAN, ...options };
|
|
3134
|
-
if (!existsSync(archive)) {
|
|
3135
|
-
throw new FulpackOperationError(
|
|
3136
|
-
createFulpackError(
|
|
3137
|
-
ERROR_CODES.ARCHIVE_NOT_FOUND,
|
|
3138
|
-
`Archive not found: ${archive}`,
|
|
3139
|
-
"scan" /* SCAN */,
|
|
3140
|
-
{ archive }
|
|
3141
|
-
)
|
|
3142
|
-
);
|
|
3143
|
-
}
|
|
3144
|
-
const filename = basename(archive);
|
|
3145
|
-
if (filename.endsWith(".tar.gz") || filename.endsWith(".tgz")) {
|
|
3146
|
-
return await scanTarGz(archive, opts);
|
|
3147
|
-
} else if (filename.endsWith(".tar")) {
|
|
3148
|
-
return await scanTar(archive, opts);
|
|
3149
|
-
} else if (filename.endsWith(".zip")) {
|
|
3150
|
-
return await scanZip(archive, opts);
|
|
3151
|
-
} else if (filename.endsWith(".gz")) {
|
|
3152
|
-
return await scanGzipFile(archive, opts);
|
|
3153
|
-
} else {
|
|
3154
|
-
throw new FulpackOperationError(
|
|
3155
|
-
createFulpackError(
|
|
3156
|
-
ERROR_CODES.INVALID_ARCHIVE_FORMAT,
|
|
3157
|
-
`Unsupported archive format: ${filename}`,
|
|
3158
|
-
"scan" /* SCAN */,
|
|
3159
|
-
{ archive }
|
|
3160
|
-
)
|
|
3161
|
-
);
|
|
3162
|
-
}
|
|
3163
|
-
}
|
|
3164
|
-
async function verify(archive, _options) {
|
|
3165
|
-
if (!existsSync(archive)) {
|
|
3166
|
-
throw new FulpackOperationError(
|
|
3167
|
-
createFulpackError(
|
|
3168
|
-
ERROR_CODES.ARCHIVE_NOT_FOUND,
|
|
3169
|
-
`Archive not found: ${archive}`,
|
|
3170
|
-
"verify" /* VERIFY */,
|
|
3171
|
-
{ archive }
|
|
3172
|
-
)
|
|
3173
|
-
);
|
|
3174
|
-
}
|
|
3175
|
-
const stats = statSync(archive);
|
|
3176
|
-
const errors = [];
|
|
3177
|
-
const warnings = [];
|
|
3178
|
-
const checks_performed = [];
|
|
3179
|
-
let entry_count = 0;
|
|
3180
|
-
let checksums_verified = 0;
|
|
3181
|
-
checks_performed.push("structure_valid");
|
|
3182
|
-
if (stats.size === 0) {
|
|
3183
|
-
errors.push(
|
|
3184
|
-
createFulpackError(ERROR_CODES.ARCHIVE_CORRUPT, "Archive file is empty", "verify" /* VERIFY */, {
|
|
3185
|
-
archive
|
|
3186
|
-
})
|
|
3187
|
-
);
|
|
3188
|
-
return {
|
|
3189
|
-
valid: false,
|
|
3190
|
-
errors,
|
|
3191
|
-
warnings,
|
|
3192
|
-
entry_count: 0,
|
|
3193
|
-
checksums_verified: 0,
|
|
3194
|
-
checks_performed
|
|
3195
|
-
};
|
|
3196
|
-
}
|
|
3197
|
-
let entries;
|
|
3198
|
-
try {
|
|
3199
|
-
entries = await scan(archive, { include_metadata: true });
|
|
3200
|
-
entry_count = entries.length;
|
|
3201
|
-
} catch (error) {
|
|
3202
|
-
errors.push(
|
|
3203
|
-
createFulpackError(
|
|
3204
|
-
ERROR_CODES.ARCHIVE_CORRUPT,
|
|
3205
|
-
`Failed to scan archive: ${error instanceof Error ? error.message : String(error)}`,
|
|
3206
|
-
"verify" /* VERIFY */,
|
|
3207
|
-
{ archive, details: { original_error: error } }
|
|
3208
|
-
)
|
|
3209
|
-
);
|
|
3210
|
-
return {
|
|
3211
|
-
valid: false,
|
|
3212
|
-
errors,
|
|
3213
|
-
warnings,
|
|
3214
|
-
entry_count: 0,
|
|
3215
|
-
checksums_verified: 0,
|
|
3216
|
-
checks_performed
|
|
3217
|
-
};
|
|
3218
|
-
}
|
|
3219
|
-
checks_performed.push("no_path_traversal");
|
|
3220
|
-
for (const entry of entries) {
|
|
3221
|
-
const pathError = validatePath(entry.path, "verify" /* VERIFY */, true);
|
|
3222
|
-
if (pathError) {
|
|
3223
|
-
errors.push({ ...pathError, archive });
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
const symlinks = entries.filter((e) => e.type === "symlink");
|
|
3227
|
-
if (symlinks.length > 0) {
|
|
3228
|
-
checks_performed.push("symlinks_safe");
|
|
3229
|
-
for (const symlink of symlinks) {
|
|
3230
|
-
if (symlink.symlink_target) {
|
|
3231
|
-
const targetError = validatePath(symlink.symlink_target, "verify" /* VERIFY */, true);
|
|
3232
|
-
if (targetError) {
|
|
3233
|
-
errors.push(
|
|
3234
|
-
createFulpackError(
|
|
3235
|
-
ERROR_CODES.ARCHIVE_CORRUPT,
|
|
3236
|
-
`Symlink '${symlink.path}' targets unsafe location: ${symlink.symlink_target}`,
|
|
3237
|
-
"verify" /* VERIFY */,
|
|
3238
|
-
{ archive, path: symlink.path, details: { symlink_target: symlink.symlink_target } }
|
|
3239
|
-
)
|
|
3240
|
-
);
|
|
3241
|
-
}
|
|
3242
|
-
}
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
checks_performed.push("no_decompression_bomb");
|
|
3246
|
-
const total_size = entries.reduce((sum, entry) => sum + (entry.size || 0), 0);
|
|
3247
|
-
const compressed_size = stats.size;
|
|
3248
|
-
const compression_ratio = total_size > 0 && compressed_size > 0 ? total_size / compressed_size : 1;
|
|
3249
|
-
if (compression_ratio > 100) {
|
|
3250
|
-
warnings.push(
|
|
3251
|
-
`High compression ratio detected: ${compression_ratio.toFixed(1)}:1 (threshold: 100:1)`
|
|
3252
|
-
);
|
|
3253
|
-
}
|
|
3254
|
-
const MAX_SIZE = 1024 * 1024 * 1024;
|
|
3255
|
-
if (total_size > MAX_SIZE) {
|
|
3256
|
-
warnings.push(
|
|
3257
|
-
`Total uncompressed size (${(total_size / (1024 * 1024)).toFixed(1)}MB) exceeds recommended limit (1GB)`
|
|
3258
|
-
);
|
|
3259
|
-
}
|
|
3260
|
-
const MAX_ENTRIES = 1e5;
|
|
3261
|
-
if (entry_count > MAX_ENTRIES) {
|
|
3262
|
-
warnings.push(`Entry count (${entry_count}) exceeds recommended limit (${MAX_ENTRIES})`);
|
|
3263
|
-
}
|
|
3264
|
-
const entriesWithChecksums = entries.filter((e) => e.checksum !== void 0);
|
|
3265
|
-
if (entriesWithChecksums.length > 0) {
|
|
3266
|
-
checks_performed.push("checksums_verified");
|
|
3267
|
-
checksums_verified = entriesWithChecksums.length;
|
|
3268
|
-
} else if (entry_count > 0) {
|
|
3269
|
-
warnings.push("Archive does not contain checksums for integrity verification");
|
|
3270
|
-
}
|
|
3271
|
-
return {
|
|
3272
|
-
valid: errors.length === 0,
|
|
3273
|
-
errors,
|
|
3274
|
-
warnings,
|
|
3275
|
-
entry_count,
|
|
3276
|
-
checksums_verified,
|
|
3277
|
-
checks_performed
|
|
3278
|
-
};
|
|
3279
|
-
}
|
|
3280
|
-
async function info(archive) {
|
|
3281
|
-
if (!existsSync(archive)) {
|
|
3282
|
-
throw new FulpackOperationError(
|
|
3283
|
-
createFulpackError(
|
|
3284
|
-
ERROR_CODES.ARCHIVE_NOT_FOUND,
|
|
3285
|
-
`Archive not found: ${archive}`,
|
|
3286
|
-
"info" /* INFO */,
|
|
3287
|
-
{ archive }
|
|
3288
|
-
)
|
|
3289
|
-
);
|
|
3290
|
-
}
|
|
3291
|
-
const stats = statSync(archive);
|
|
3292
|
-
const filename = basename(archive);
|
|
3293
|
-
let format;
|
|
3294
|
-
let compression = "none";
|
|
3295
|
-
if (filename.endsWith(".tar.gz") || filename.endsWith(".tgz")) {
|
|
3296
|
-
format = "tar.gz" /* TAR_GZ */;
|
|
3297
|
-
compression = "gzip";
|
|
3298
|
-
} else if (filename.endsWith(".zip")) {
|
|
3299
|
-
format = "zip" /* ZIP */;
|
|
3300
|
-
compression = "deflate";
|
|
3301
|
-
} else if (filename.endsWith(".gz")) {
|
|
3302
|
-
format = "gzip" /* GZIP */;
|
|
3303
|
-
compression = "gzip";
|
|
3304
|
-
} else if (filename.endsWith(".tar")) {
|
|
3305
|
-
format = "tar" /* TAR */;
|
|
3306
|
-
compression = "none";
|
|
3307
|
-
} else {
|
|
3308
|
-
throw new FulpackOperationError(
|
|
3309
|
-
createFulpackError(
|
|
3310
|
-
ERROR_CODES.INVALID_ARCHIVE_FORMAT,
|
|
3311
|
-
`Unsupported archive format: ${filename}`,
|
|
3312
|
-
"info" /* INFO */,
|
|
3313
|
-
{ archive }
|
|
3314
|
-
)
|
|
3315
|
-
);
|
|
3316
|
-
}
|
|
3317
|
-
const entries = await scan(archive, { include_metadata: true });
|
|
3318
|
-
const entry_count = entries.length;
|
|
3319
|
-
const total_size = entries.reduce((sum, entry) => sum + (entry.size || 0), 0);
|
|
3320
|
-
const compressed_size = stats.size;
|
|
3321
|
-
const compression_ratio = total_size > 0 ? compressed_size / total_size : 1;
|
|
3322
|
-
const has_checksums = entries.some((entry) => entry.checksum !== void 0);
|
|
3323
|
-
return {
|
|
3324
|
-
format,
|
|
3325
|
-
compression,
|
|
3326
|
-
entry_count,
|
|
3327
|
-
total_size,
|
|
3328
|
-
compressed_size,
|
|
3329
|
-
compression_ratio,
|
|
3330
|
-
has_checksums,
|
|
3331
|
-
created: stats.mtime.toISOString()
|
|
3332
|
-
};
|
|
3333
|
-
}
|
|
3334
|
-
async function extractGzipFile(archive, destination, options) {
|
|
3335
|
-
const filename = basename(archive, ".gz");
|
|
3336
|
-
const outputPath = join(destination, filename);
|
|
3337
|
-
if (existsSync(outputPath)) {
|
|
3338
|
-
if (options.overwrite === "error") {
|
|
3339
|
-
throw new FulpackOperationError(
|
|
3340
|
-
createFulpackError(
|
|
3341
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
3342
|
-
`Output file already exists: ${outputPath}`,
|
|
3343
|
-
"extract" /* EXTRACT */,
|
|
3344
|
-
{ path: outputPath, archive }
|
|
3345
|
-
)
|
|
3346
|
-
);
|
|
3347
|
-
} else if (options.overwrite === "skip") {
|
|
3348
|
-
return {
|
|
3349
|
-
extracted_count: 0,
|
|
3350
|
-
skipped_count: 1,
|
|
3351
|
-
error_count: 0,
|
|
3352
|
-
errors: [],
|
|
3353
|
-
warnings: [`Skipped existing file: ${outputPath}`]
|
|
3354
|
-
};
|
|
3355
|
-
}
|
|
3356
|
-
}
|
|
3357
|
-
try {
|
|
3358
|
-
const readStream = createReadStream(archive);
|
|
3359
|
-
const writeStream = createWriteStream(outputPath);
|
|
3360
|
-
const gunzip = createGunzip();
|
|
3361
|
-
await new Promise((resolve, reject) => {
|
|
3362
|
-
pipeline(readStream, gunzip, writeStream, (error) => {
|
|
3363
|
-
if (error) {
|
|
3364
|
-
reject(error);
|
|
3365
|
-
} else {
|
|
3366
|
-
resolve(void 0);
|
|
3367
|
-
}
|
|
3368
|
-
});
|
|
3369
|
-
});
|
|
3370
|
-
return {
|
|
3371
|
-
extracted_count: 1,
|
|
3372
|
-
skipped_count: 0,
|
|
3373
|
-
error_count: 0,
|
|
3374
|
-
errors: [],
|
|
3375
|
-
warnings: []
|
|
3376
|
-
};
|
|
3377
|
-
} catch (error) {
|
|
3378
|
-
throw new FulpackOperationError(
|
|
3379
|
-
createFulpackError(
|
|
3380
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
3381
|
-
`Failed to extract gzip file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
3382
|
-
"extract" /* EXTRACT */,
|
|
3383
|
-
{ archive, details: { original_error: error } }
|
|
3384
|
-
)
|
|
3385
|
-
);
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
async function scanGzipFile(archive, options) {
|
|
3389
|
-
const filename = basename(archive, ".gz");
|
|
3390
|
-
const stats = statSync(archive);
|
|
3391
|
-
const entry = {
|
|
3392
|
-
path: filename,
|
|
3393
|
-
type: "file" /* FILE */,
|
|
3394
|
-
size: 0,
|
|
3395
|
-
// Would need to decompress to get actual size
|
|
3396
|
-
compressed_size: stats.size,
|
|
3397
|
-
modified: stats.mtime.toISOString(),
|
|
3398
|
-
...options.include_metadata && { mode: "0644" }
|
|
3399
|
-
// Default for gzip files
|
|
3400
|
-
};
|
|
3401
|
-
return [entry];
|
|
3402
|
-
}
|
|
3403
|
-
async function createTarGzArchive(sources, output, options) {
|
|
3404
|
-
const writeStream = createWriteStream(output);
|
|
3405
|
-
const archive = new TarArchive({
|
|
3406
|
-
gzip: true,
|
|
3407
|
-
gzipOptions: {
|
|
3408
|
-
level: options.compression_level
|
|
3409
|
-
}
|
|
3410
|
-
});
|
|
3411
|
-
let entryCount = 0;
|
|
3412
|
-
let totalSize = 0;
|
|
3413
|
-
const writePromise = new Promise((resolve, reject) => {
|
|
3414
|
-
writeStream.on("close", () => resolve());
|
|
3415
|
-
writeStream.on("error", reject);
|
|
3416
|
-
archive.on(
|
|
3417
|
-
"error",
|
|
3418
|
-
(error) => reject(
|
|
3419
|
-
new FulpackOperationError(
|
|
3420
|
-
createFulpackError(
|
|
3421
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
3422
|
-
`TAR.GZ creation failed: ${error.message}`,
|
|
3423
|
-
"create" /* CREATE */,
|
|
3424
|
-
{ details: { original_error: error } }
|
|
3425
|
-
)
|
|
3426
|
-
)
|
|
3427
|
-
)
|
|
3428
|
-
);
|
|
3429
|
-
});
|
|
3430
|
-
archive.pipe(writeStream);
|
|
3431
|
-
for (const sourcePath of sources) {
|
|
3432
|
-
if (!existsSync(sourcePath)) {
|
|
3433
|
-
throw new FulpackOperationError(
|
|
3434
|
-
createFulpackError(
|
|
3435
|
-
ERROR_CODES.SOURCE_NOT_FOUND,
|
|
3436
|
-
`Source not found: ${sourcePath}`,
|
|
3437
|
-
"create" /* CREATE */,
|
|
3438
|
-
{ details: { source: sourcePath } }
|
|
3439
|
-
)
|
|
3440
|
-
);
|
|
3441
|
-
}
|
|
3442
|
-
const stats = statSync(sourcePath);
|
|
3443
|
-
if (stats.isDirectory()) {
|
|
3444
|
-
await addDirectoryToTarGzArchive(archive, sourcePath, "", options);
|
|
3445
|
-
} else {
|
|
3446
|
-
const entryName = basename(sourcePath);
|
|
3447
|
-
archive.file(sourcePath, {
|
|
3448
|
-
name: entryName,
|
|
3449
|
-
mode: options.preserve_permissions ? stats.mode : 420
|
|
3450
|
-
});
|
|
3451
|
-
entryCount++;
|
|
3452
|
-
totalSize += stats.size;
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
archive.finalize().catch(() => {
|
|
3456
|
-
});
|
|
3457
|
-
await writePromise;
|
|
3458
|
-
const outputStats = statSync(output);
|
|
3459
|
-
return {
|
|
3460
|
-
format: "tar.gz" /* TAR_GZ */,
|
|
3461
|
-
compression: "gzip",
|
|
3462
|
-
entry_count: entryCount,
|
|
3463
|
-
total_size: totalSize,
|
|
3464
|
-
compressed_size: outputStats.size,
|
|
3465
|
-
compression_ratio: totalSize > 0 ? outputStats.size / totalSize : 0,
|
|
3466
|
-
has_checksums: false,
|
|
3467
|
-
// TODO: Add fulhash checksum integration
|
|
3468
|
-
created: outputStats.mtime.toISOString()
|
|
3469
|
-
};
|
|
3470
|
-
}
|
|
3471
|
-
async function createZipArchive(sources, output, options) {
|
|
3472
|
-
const writeStream = createWriteStream(output);
|
|
3473
|
-
const archive = new ZipArchive({
|
|
3474
|
-
zlib: { level: options.compression_level }
|
|
3475
|
-
});
|
|
3476
|
-
let entryCount = 0;
|
|
3477
|
-
let totalSize = 0;
|
|
3478
|
-
const writePromise = new Promise((resolve, reject) => {
|
|
3479
|
-
writeStream.on("close", () => resolve());
|
|
3480
|
-
writeStream.on("error", reject);
|
|
3481
|
-
archive.on(
|
|
3482
|
-
"error",
|
|
3483
|
-
(error) => reject(
|
|
3484
|
-
new FulpackOperationError(
|
|
3485
|
-
createFulpackError(
|
|
3486
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
3487
|
-
`ZIP creation failed: ${error.message}`,
|
|
3488
|
-
"create" /* CREATE */,
|
|
3489
|
-
{ details: { original_error: error } }
|
|
3490
|
-
)
|
|
3491
|
-
)
|
|
3492
|
-
)
|
|
3493
|
-
);
|
|
3494
|
-
});
|
|
3495
|
-
archive.pipe(writeStream);
|
|
3496
|
-
for (const sourcePath of sources) {
|
|
3497
|
-
if (!existsSync(sourcePath)) {
|
|
3498
|
-
throw new FulpackOperationError(
|
|
3499
|
-
createFulpackError(
|
|
3500
|
-
ERROR_CODES.SOURCE_NOT_FOUND,
|
|
3501
|
-
`Source not found: ${sourcePath}`,
|
|
3502
|
-
"create" /* CREATE */,
|
|
3503
|
-
{ details: { source: sourcePath } }
|
|
3504
|
-
)
|
|
3505
|
-
);
|
|
3506
|
-
}
|
|
3507
|
-
const stats = statSync(sourcePath);
|
|
3508
|
-
if (stats.isDirectory()) {
|
|
3509
|
-
await addDirectoryToZipArchive(archive, sourcePath, "", options);
|
|
3510
|
-
} else {
|
|
3511
|
-
const entryName = basename(sourcePath);
|
|
3512
|
-
archive.file(sourcePath, {
|
|
3513
|
-
name: entryName,
|
|
3514
|
-
mode: options.preserve_permissions ? stats.mode : 420
|
|
3515
|
-
});
|
|
3516
|
-
entryCount++;
|
|
3517
|
-
totalSize += stats.size;
|
|
3518
|
-
}
|
|
3519
|
-
}
|
|
3520
|
-
archive.finalize().catch(() => {
|
|
3521
|
-
});
|
|
3522
|
-
await writePromise;
|
|
3523
|
-
const outputStats = statSync(output);
|
|
3524
|
-
return {
|
|
3525
|
-
format: "zip" /* ZIP */,
|
|
3526
|
-
compression: "deflate",
|
|
3527
|
-
entry_count: entryCount,
|
|
3528
|
-
total_size: totalSize,
|
|
3529
|
-
compressed_size: outputStats.size,
|
|
3530
|
-
compression_ratio: totalSize > 0 ? outputStats.size / totalSize : 0,
|
|
3531
|
-
has_checksums: false,
|
|
3532
|
-
created: outputStats.mtime.toISOString()
|
|
3533
|
-
};
|
|
3534
|
-
}
|
|
3535
|
-
async function addDirectoryToTarGzArchive(archive, dirPath, archivePrefix, options) {
|
|
3536
|
-
const entries = readdirSync(dirPath);
|
|
3537
|
-
for (const entry of entries) {
|
|
3538
|
-
const fullPath = join(dirPath, entry);
|
|
3539
|
-
const archivePath = join(archivePrefix, entry);
|
|
3540
|
-
const stats = lstatSync(fullPath);
|
|
3541
|
-
if (stats.isSymbolicLink()) {
|
|
3542
|
-
if (!options.follow_symlinks) {
|
|
3543
|
-
continue;
|
|
3544
|
-
}
|
|
3545
|
-
const targetStats = statSync(fullPath);
|
|
3546
|
-
if (targetStats.isDirectory()) {
|
|
3547
|
-
await addDirectoryToTarGzArchive(archive, fullPath, archivePath, options);
|
|
3548
|
-
} else {
|
|
3549
|
-
archive.file(fullPath, {
|
|
3550
|
-
name: archivePath,
|
|
3551
|
-
mode: options.preserve_permissions ? targetStats.mode : 420
|
|
3552
|
-
});
|
|
3553
|
-
}
|
|
3554
|
-
} else if (stats.isDirectory()) {
|
|
3555
|
-
await addDirectoryToTarGzArchive(archive, fullPath, archivePath, options);
|
|
3556
|
-
} else {
|
|
3557
|
-
archive.file(fullPath, {
|
|
3558
|
-
name: archivePath,
|
|
3559
|
-
mode: options.preserve_permissions ? stats.mode : 420
|
|
3560
|
-
});
|
|
3561
|
-
}
|
|
3562
|
-
}
|
|
3563
|
-
}
|
|
3564
|
-
async function createTarArchive(sources, output, options) {
|
|
3565
|
-
const writeStream = createWriteStream(output);
|
|
3566
|
-
const archive = new TarArchive({
|
|
3567
|
-
gzip: false
|
|
3568
|
-
// Uncompressed
|
|
3569
|
-
});
|
|
3570
|
-
let entryCount = 0;
|
|
3571
|
-
let totalSize = 0;
|
|
3572
|
-
const writePromise = new Promise((resolve, reject) => {
|
|
3573
|
-
writeStream.on("close", () => resolve());
|
|
3574
|
-
writeStream.on("error", reject);
|
|
3575
|
-
archive.on(
|
|
3576
|
-
"error",
|
|
3577
|
-
(error) => reject(
|
|
3578
|
-
new FulpackOperationError(
|
|
3579
|
-
createFulpackError(
|
|
3580
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
3581
|
-
`TAR creation failed: ${error.message}`,
|
|
3582
|
-
"create" /* CREATE */,
|
|
3583
|
-
{ details: { original_error: error } }
|
|
3584
|
-
)
|
|
3585
|
-
)
|
|
3586
|
-
)
|
|
3587
|
-
);
|
|
3588
|
-
});
|
|
3589
|
-
archive.pipe(writeStream);
|
|
3590
|
-
for (const sourcePath of sources) {
|
|
3591
|
-
if (!existsSync(sourcePath)) {
|
|
3592
|
-
throw new FulpackOperationError(
|
|
3593
|
-
createFulpackError(
|
|
3594
|
-
ERROR_CODES.SOURCE_NOT_FOUND,
|
|
3595
|
-
`Source not found: ${sourcePath}`,
|
|
3596
|
-
"create" /* CREATE */,
|
|
3597
|
-
{ details: { source: sourcePath } }
|
|
3598
|
-
)
|
|
3599
|
-
);
|
|
3600
|
-
}
|
|
3601
|
-
const stats = statSync(sourcePath);
|
|
3602
|
-
if (stats.isDirectory()) {
|
|
3603
|
-
await addDirectoryToTarArchive(archive, sourcePath, "", options);
|
|
3604
|
-
} else {
|
|
3605
|
-
const entryName = basename(sourcePath);
|
|
3606
|
-
archive.file(sourcePath, {
|
|
3607
|
-
name: entryName,
|
|
3608
|
-
mode: options.preserve_permissions ? stats.mode : 420
|
|
3609
|
-
});
|
|
3610
|
-
entryCount++;
|
|
3611
|
-
totalSize += stats.size;
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
archive.finalize().catch(() => {
|
|
3615
|
-
});
|
|
3616
|
-
await writePromise;
|
|
3617
|
-
const outputStats = statSync(output);
|
|
3618
|
-
return {
|
|
3619
|
-
format: "tar" /* TAR */,
|
|
3620
|
-
compression: "none",
|
|
3621
|
-
entry_count: entryCount,
|
|
3622
|
-
total_size: totalSize,
|
|
3623
|
-
compressed_size: outputStats.size,
|
|
3624
|
-
compression_ratio: 1,
|
|
3625
|
-
// Uncompressed
|
|
3626
|
-
has_checksums: false,
|
|
3627
|
-
// TODO: Add fulhash checksum integration
|
|
3628
|
-
created: outputStats.mtime.toISOString()
|
|
3629
|
-
};
|
|
3630
|
-
}
|
|
3631
|
-
async function addDirectoryToTarArchive(archive, dirPath, archivePrefix, options) {
|
|
3632
|
-
const entries = readdirSync(dirPath);
|
|
3633
|
-
for (const entry of entries) {
|
|
3634
|
-
const fullPath = join(dirPath, entry);
|
|
3635
|
-
const archivePath = join(archivePrefix, entry);
|
|
3636
|
-
const stats = lstatSync(fullPath);
|
|
3637
|
-
if (stats.isSymbolicLink()) {
|
|
3638
|
-
if (!options.follow_symlinks) {
|
|
3639
|
-
continue;
|
|
3640
|
-
}
|
|
3641
|
-
const targetStats = statSync(fullPath);
|
|
3642
|
-
if (targetStats.isDirectory()) {
|
|
3643
|
-
await addDirectoryToTarArchive(archive, fullPath, archivePath, options);
|
|
3644
|
-
} else {
|
|
3645
|
-
archive.file(fullPath, {
|
|
3646
|
-
name: archivePath,
|
|
3647
|
-
mode: options.preserve_permissions ? targetStats.mode : 420
|
|
3648
|
-
});
|
|
3649
|
-
}
|
|
3650
|
-
} else if (stats.isDirectory()) {
|
|
3651
|
-
await addDirectoryToTarArchive(archive, fullPath, archivePath, options);
|
|
3652
|
-
} else {
|
|
3653
|
-
archive.file(fullPath, {
|
|
3654
|
-
name: archivePath,
|
|
3655
|
-
mode: options.preserve_permissions ? stats.mode : 420
|
|
3656
|
-
});
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
}
|
|
3660
|
-
async function createGzipFile(sources, output, options) {
|
|
3661
|
-
if (sources.length !== 1) {
|
|
3662
|
-
throw new FulpackOperationError(
|
|
3663
|
-
createFulpackError(
|
|
3664
|
-
ERROR_CODES.INVALID_OPTIONS,
|
|
3665
|
-
`GZIP format requires exactly one source file, got ${sources.length}`,
|
|
3666
|
-
"create" /* CREATE */
|
|
3667
|
-
)
|
|
3668
|
-
);
|
|
3669
|
-
}
|
|
3670
|
-
const sourcePath = sources[0];
|
|
3671
|
-
if (!existsSync(sourcePath)) {
|
|
3672
|
-
throw new FulpackOperationError(
|
|
3673
|
-
createFulpackError(
|
|
3674
|
-
ERROR_CODES.SOURCE_NOT_FOUND,
|
|
3675
|
-
`Source not found: ${sourcePath}`,
|
|
3676
|
-
"create" /* CREATE */,
|
|
3677
|
-
{ details: { source: sourcePath } }
|
|
3678
|
-
)
|
|
3679
|
-
);
|
|
3680
|
-
}
|
|
3681
|
-
const stats = statSync(sourcePath);
|
|
3682
|
-
if (stats.isDirectory()) {
|
|
3683
|
-
throw new FulpackOperationError(
|
|
3684
|
-
createFulpackError(
|
|
3685
|
-
ERROR_CODES.INVALID_OPTIONS,
|
|
3686
|
-
"GZIP format does not support directories",
|
|
3687
|
-
"create" /* CREATE */,
|
|
3688
|
-
{ details: { source: sourcePath } }
|
|
3689
|
-
)
|
|
3690
|
-
);
|
|
3691
|
-
}
|
|
3692
|
-
const readStream = createReadStream(sourcePath);
|
|
3693
|
-
const writeStream = createWriteStream(output);
|
|
3694
|
-
const gzip = createGzip({ level: options.compression_level });
|
|
3695
|
-
await new Promise((resolve, reject) => {
|
|
3696
|
-
pipeline(readStream, gzip, writeStream, (error) => {
|
|
3697
|
-
if (error) {
|
|
3698
|
-
reject(error);
|
|
3699
|
-
} else {
|
|
3700
|
-
resolve(void 0);
|
|
3701
|
-
}
|
|
3702
|
-
});
|
|
3703
|
-
});
|
|
3704
|
-
const outputStats = statSync(output);
|
|
3705
|
-
return {
|
|
3706
|
-
format: "gzip" /* GZIP */,
|
|
3707
|
-
compression: "gzip",
|
|
3708
|
-
entry_count: 1,
|
|
3709
|
-
total_size: stats.size,
|
|
3710
|
-
compressed_size: outputStats.size,
|
|
3711
|
-
compression_ratio: stats.size > 0 ? outputStats.size / stats.size : 0,
|
|
3712
|
-
has_checksums: false,
|
|
3713
|
-
created: outputStats.mtime.toISOString()
|
|
3714
|
-
};
|
|
3715
|
-
}
|
|
3716
|
-
async function addDirectoryToZipArchive(archive, dirPath, archivePrefix, options) {
|
|
3717
|
-
const entries = readdirSync(dirPath);
|
|
3718
|
-
for (const entry of entries) {
|
|
3719
|
-
const fullPath = join(dirPath, entry);
|
|
3720
|
-
const archivePath = join(archivePrefix, entry);
|
|
3721
|
-
const stats = lstatSync(fullPath);
|
|
3722
|
-
if (stats.isSymbolicLink()) {
|
|
3723
|
-
if (!options.follow_symlinks) {
|
|
3724
|
-
continue;
|
|
3725
|
-
}
|
|
3726
|
-
const targetStats = statSync(fullPath);
|
|
3727
|
-
if (targetStats.isDirectory()) {
|
|
3728
|
-
await addDirectoryToZipArchive(archive, fullPath, archivePath, options);
|
|
3729
|
-
} else {
|
|
3730
|
-
archive.file(fullPath, {
|
|
3731
|
-
name: archivePath,
|
|
3732
|
-
mode: options.preserve_permissions ? targetStats.mode : 420
|
|
3733
|
-
});
|
|
3734
|
-
}
|
|
3735
|
-
} else if (stats.isDirectory()) {
|
|
3736
|
-
await addDirectoryToZipArchive(archive, fullPath, archivePath, options);
|
|
3737
|
-
} else {
|
|
3738
|
-
archive.file(fullPath, {
|
|
3739
|
-
name: archivePath,
|
|
3740
|
-
mode: options.preserve_permissions ? stats.mode : 420
|
|
3741
|
-
});
|
|
3742
|
-
}
|
|
3743
|
-
}
|
|
3744
|
-
}
|
|
3745
|
-
async function extractTarGz(archive, destination, options) {
|
|
3746
|
-
const tarStream = await import('tar-stream');
|
|
3747
|
-
const extract2 = tarStream.extract();
|
|
3748
|
-
let extractedCount = 0;
|
|
3749
|
-
let skippedCount = 0;
|
|
3750
|
-
let errorCount = 0;
|
|
3751
|
-
const errors = [];
|
|
3752
|
-
const warnings = [];
|
|
3753
|
-
let totalExtractedSize = 0;
|
|
3754
|
-
let entryCount = 0;
|
|
3755
|
-
extract2.on("entry", async (header, stream, next) => {
|
|
3756
|
-
entryCount++;
|
|
3757
|
-
try {
|
|
3758
|
-
if (options.max_entries && entryCount > options.max_entries) {
|
|
3759
|
-
errors.push(
|
|
3760
|
-
createFulpackError(
|
|
3761
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
3762
|
-
`Entry count exceeds maximum (${options.max_entries})`,
|
|
3763
|
-
"extract" /* EXTRACT */,
|
|
3764
|
-
{
|
|
3765
|
-
archive,
|
|
3766
|
-
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
3767
|
-
}
|
|
3768
|
-
)
|
|
3769
|
-
);
|
|
3770
|
-
stream.resume();
|
|
3771
|
-
next();
|
|
3772
|
-
return;
|
|
3773
|
-
}
|
|
3774
|
-
const pathError = validatePath(header.name, "extract" /* EXTRACT */, false);
|
|
3775
|
-
if (pathError) {
|
|
3776
|
-
errors.push({ ...pathError, archive });
|
|
3777
|
-
errorCount++;
|
|
3778
|
-
stream.resume();
|
|
3779
|
-
next();
|
|
3780
|
-
return;
|
|
3781
|
-
}
|
|
3782
|
-
const outputPath = join(destination, header.name);
|
|
3783
|
-
if (header.type === "directory") {
|
|
3784
|
-
if (!existsSync(outputPath)) {
|
|
3785
|
-
mkdirSync(outputPath, { recursive: true });
|
|
3786
|
-
}
|
|
3787
|
-
extractedCount++;
|
|
3788
|
-
stream.resume();
|
|
3789
|
-
next();
|
|
3790
|
-
} else if (header.type === "file") {
|
|
3791
|
-
if (existsSync(outputPath)) {
|
|
3792
|
-
if (options.overwrite === "error") {
|
|
3793
|
-
errors.push(
|
|
3794
|
-
createFulpackError(
|
|
3795
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
3796
|
-
`Output file already exists: ${outputPath}`,
|
|
3797
|
-
"extract" /* EXTRACT */,
|
|
3798
|
-
{ path: header.name, archive }
|
|
3799
|
-
)
|
|
3800
|
-
);
|
|
3801
|
-
errorCount++;
|
|
3802
|
-
stream.resume();
|
|
3803
|
-
next();
|
|
3804
|
-
return;
|
|
3805
|
-
} else if (options.overwrite === "skip") {
|
|
3806
|
-
warnings.push(`Skipped existing file: ${outputPath}`);
|
|
3807
|
-
skippedCount++;
|
|
3808
|
-
stream.resume();
|
|
3809
|
-
next();
|
|
3810
|
-
return;
|
|
3811
|
-
}
|
|
3812
|
-
}
|
|
3813
|
-
const parentDir = dirname(outputPath);
|
|
3814
|
-
if (!existsSync(parentDir)) {
|
|
3815
|
-
mkdirSync(parentDir, { recursive: true });
|
|
3816
|
-
}
|
|
3817
|
-
const writeStream = createWriteStream(outputPath);
|
|
3818
|
-
stream.on("data", (chunk) => {
|
|
3819
|
-
totalExtractedSize += chunk.length;
|
|
3820
|
-
if (options.max_size && totalExtractedSize > options.max_size) {
|
|
3821
|
-
stream.destroy();
|
|
3822
|
-
writeStream.destroy();
|
|
3823
|
-
errors.push(
|
|
3824
|
-
createFulpackError(
|
|
3825
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
3826
|
-
`Total extracted size exceeds maximum (${options.max_size} bytes)`,
|
|
3827
|
-
"extract" /* EXTRACT */,
|
|
3828
|
-
{
|
|
3829
|
-
archive,
|
|
3830
|
-
path: header.name,
|
|
3831
|
-
details: { actual_size: totalExtractedSize, max_size: options.max_size }
|
|
3832
|
-
}
|
|
3833
|
-
)
|
|
3834
|
-
);
|
|
3835
|
-
errorCount++;
|
|
3836
|
-
next();
|
|
3837
|
-
return;
|
|
3838
|
-
}
|
|
3839
|
-
});
|
|
3840
|
-
stream.pipe(writeStream);
|
|
3841
|
-
writeStream.on("finish", () => {
|
|
3842
|
-
if (options.preserve_permissions && header.mode) {
|
|
3843
|
-
try {
|
|
3844
|
-
const { chmodSync } = __require("fs");
|
|
3845
|
-
chmodSync(outputPath, header.mode);
|
|
3846
|
-
} catch {
|
|
3847
|
-
}
|
|
3848
|
-
}
|
|
3849
|
-
extractedCount++;
|
|
3850
|
-
next();
|
|
3851
|
-
});
|
|
3852
|
-
writeStream.on("error", (error) => {
|
|
3853
|
-
errors.push(
|
|
3854
|
-
createFulpackError(
|
|
3855
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
3856
|
-
`Failed to write file: ${error.message}`,
|
|
3857
|
-
"extract" /* EXTRACT */,
|
|
3858
|
-
{ path: header.name, archive, details: { original_error: error } }
|
|
3859
|
-
)
|
|
3860
|
-
);
|
|
3861
|
-
errorCount++;
|
|
3862
|
-
next();
|
|
3863
|
-
});
|
|
3864
|
-
} else {
|
|
3865
|
-
warnings.push(`Skipped entry type ${header.type}: ${header.name}`);
|
|
3866
|
-
stream.resume();
|
|
3867
|
-
next();
|
|
3868
|
-
}
|
|
3869
|
-
} catch (error) {
|
|
3870
|
-
errors.push(
|
|
3871
|
-
createFulpackError(
|
|
3872
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
3873
|
-
`Error processing entry: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
3874
|
-
"extract" /* EXTRACT */,
|
|
3875
|
-
{ path: header.name, archive, details: { original_error: error } }
|
|
3876
|
-
)
|
|
3877
|
-
);
|
|
3878
|
-
errorCount++;
|
|
3879
|
-
stream.resume();
|
|
3880
|
-
next();
|
|
3881
|
-
}
|
|
3882
|
-
});
|
|
3883
|
-
const readStream = createReadStream(archive);
|
|
3884
|
-
const gunzip = createGunzip();
|
|
3885
|
-
return new Promise((resolve, reject) => {
|
|
3886
|
-
extract2.on("finish", () => {
|
|
3887
|
-
resolve({
|
|
3888
|
-
extracted_count: extractedCount,
|
|
3889
|
-
skipped_count: skippedCount,
|
|
3890
|
-
error_count: errorCount,
|
|
3891
|
-
errors: errors.length > 0 ? errors : void 0,
|
|
3892
|
-
warnings: warnings.length > 0 ? warnings : void 0,
|
|
3893
|
-
total_bytes: totalExtractedSize
|
|
3894
|
-
});
|
|
3895
|
-
});
|
|
3896
|
-
extract2.on("error", (error) => {
|
|
3897
|
-
reject(
|
|
3898
|
-
new FulpackOperationError(
|
|
3899
|
-
createFulpackError(
|
|
3900
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
3901
|
-
`TAR.GZ extraction failed: ${error.message}`,
|
|
3902
|
-
"extract" /* EXTRACT */,
|
|
3903
|
-
{ archive, details: { original_error: error } }
|
|
3904
|
-
)
|
|
3905
|
-
)
|
|
3906
|
-
);
|
|
3907
|
-
});
|
|
3908
|
-
readStream.pipe(gunzip).pipe(extract2);
|
|
3909
|
-
});
|
|
3910
|
-
}
|
|
3911
|
-
async function extractTar(archive, destination, options) {
|
|
3912
|
-
const tarStream = await import('tar-stream');
|
|
3913
|
-
const extract2 = tarStream.extract();
|
|
3914
|
-
let extractedCount = 0;
|
|
3915
|
-
let skippedCount = 0;
|
|
3916
|
-
let errorCount = 0;
|
|
3917
|
-
const errors = [];
|
|
3918
|
-
const warnings = [];
|
|
3919
|
-
let totalExtractedSize = 0;
|
|
3920
|
-
let entryCount = 0;
|
|
3921
|
-
extract2.on("entry", async (header, stream, next) => {
|
|
3922
|
-
entryCount++;
|
|
3923
|
-
try {
|
|
3924
|
-
if (options.max_entries && entryCount > options.max_entries) {
|
|
3925
|
-
errors.push(
|
|
3926
|
-
createFulpackError(
|
|
3927
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
3928
|
-
`Entry count exceeds maximum (${options.max_entries})`,
|
|
3929
|
-
"extract" /* EXTRACT */,
|
|
3930
|
-
{
|
|
3931
|
-
archive,
|
|
3932
|
-
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
3933
|
-
}
|
|
3934
|
-
)
|
|
3935
|
-
);
|
|
3936
|
-
stream.resume();
|
|
3937
|
-
next();
|
|
3938
|
-
return;
|
|
3939
|
-
}
|
|
3940
|
-
const pathError = validatePath(header.name, "extract" /* EXTRACT */, false);
|
|
3941
|
-
if (pathError) {
|
|
3942
|
-
errors.push({ ...pathError, archive });
|
|
3943
|
-
errorCount++;
|
|
3944
|
-
stream.resume();
|
|
3945
|
-
next();
|
|
3946
|
-
return;
|
|
3947
|
-
}
|
|
3948
|
-
const outputPath = join(destination, header.name);
|
|
3949
|
-
if (header.type === "directory") {
|
|
3950
|
-
if (!existsSync(outputPath)) {
|
|
3951
|
-
mkdirSync(outputPath, { recursive: true });
|
|
3952
|
-
}
|
|
3953
|
-
extractedCount++;
|
|
3954
|
-
stream.resume();
|
|
3955
|
-
next();
|
|
3956
|
-
} else if (header.type === "file") {
|
|
3957
|
-
if (existsSync(outputPath)) {
|
|
3958
|
-
if (options.overwrite === "error") {
|
|
3959
|
-
errors.push(
|
|
3960
|
-
createFulpackError(
|
|
3961
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
3962
|
-
`Output file already exists: ${outputPath}`,
|
|
3963
|
-
"extract" /* EXTRACT */,
|
|
3964
|
-
{ path: header.name, archive }
|
|
3965
|
-
)
|
|
3966
|
-
);
|
|
3967
|
-
errorCount++;
|
|
3968
|
-
stream.resume();
|
|
3969
|
-
next();
|
|
3970
|
-
return;
|
|
3971
|
-
} else if (options.overwrite === "skip") {
|
|
3972
|
-
warnings.push(`Skipped existing file: ${outputPath}`);
|
|
3973
|
-
skippedCount++;
|
|
3974
|
-
stream.resume();
|
|
3975
|
-
next();
|
|
3976
|
-
return;
|
|
3977
|
-
}
|
|
3978
|
-
}
|
|
3979
|
-
const parentDir = dirname(outputPath);
|
|
3980
|
-
if (!existsSync(parentDir)) {
|
|
3981
|
-
mkdirSync(parentDir, { recursive: true });
|
|
3982
|
-
}
|
|
3983
|
-
const writeStream = createWriteStream(outputPath);
|
|
3984
|
-
stream.on("data", (chunk) => {
|
|
3985
|
-
totalExtractedSize += chunk.length;
|
|
3986
|
-
if (options.max_size && totalExtractedSize > options.max_size) {
|
|
3987
|
-
stream.destroy();
|
|
3988
|
-
writeStream.destroy();
|
|
3989
|
-
errors.push(
|
|
3990
|
-
createFulpackError(
|
|
3991
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
3992
|
-
`Total extracted size exceeds maximum (${options.max_size} bytes)`,
|
|
3993
|
-
"extract" /* EXTRACT */,
|
|
3994
|
-
{
|
|
3995
|
-
archive,
|
|
3996
|
-
path: header.name,
|
|
3997
|
-
details: { actual_size: totalExtractedSize, max_size: options.max_size }
|
|
3998
|
-
}
|
|
3999
|
-
)
|
|
4000
|
-
);
|
|
4001
|
-
errorCount++;
|
|
4002
|
-
next();
|
|
4003
|
-
return;
|
|
4004
|
-
}
|
|
4005
|
-
});
|
|
4006
|
-
stream.pipe(writeStream);
|
|
4007
|
-
writeStream.on("finish", () => {
|
|
4008
|
-
if (options.preserve_permissions && header.mode) {
|
|
4009
|
-
try {
|
|
4010
|
-
const { chmodSync } = __require("fs");
|
|
4011
|
-
chmodSync(outputPath, header.mode);
|
|
4012
|
-
} catch {
|
|
4013
|
-
}
|
|
4014
|
-
}
|
|
4015
|
-
extractedCount++;
|
|
4016
|
-
next();
|
|
4017
|
-
});
|
|
4018
|
-
writeStream.on("error", (error) => {
|
|
4019
|
-
errors.push(
|
|
4020
|
-
createFulpackError(
|
|
4021
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
4022
|
-
`Failed to write file: ${error.message}`,
|
|
4023
|
-
"extract" /* EXTRACT */,
|
|
4024
|
-
{ path: header.name, archive, details: { original_error: error } }
|
|
4025
|
-
)
|
|
4026
|
-
);
|
|
4027
|
-
errorCount++;
|
|
4028
|
-
next();
|
|
4029
|
-
});
|
|
4030
|
-
} else {
|
|
4031
|
-
warnings.push(`Skipped entry type ${header.type}: ${header.name}`);
|
|
4032
|
-
stream.resume();
|
|
4033
|
-
next();
|
|
4034
|
-
}
|
|
4035
|
-
} catch (error) {
|
|
4036
|
-
errors.push(
|
|
4037
|
-
createFulpackError(
|
|
4038
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
4039
|
-
`Error processing entry: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
4040
|
-
"extract" /* EXTRACT */,
|
|
4041
|
-
{ path: header.name, archive, details: { original_error: error } }
|
|
4042
|
-
)
|
|
4043
|
-
);
|
|
4044
|
-
errorCount++;
|
|
4045
|
-
stream.resume();
|
|
4046
|
-
next();
|
|
4047
|
-
}
|
|
4048
|
-
});
|
|
4049
|
-
const readStream = createReadStream(archive);
|
|
4050
|
-
return new Promise((resolve, reject) => {
|
|
4051
|
-
extract2.on("finish", () => {
|
|
4052
|
-
resolve({
|
|
4053
|
-
extracted_count: extractedCount,
|
|
4054
|
-
skipped_count: skippedCount,
|
|
4055
|
-
error_count: errorCount,
|
|
4056
|
-
errors: errors.length > 0 ? errors : void 0,
|
|
4057
|
-
warnings: warnings.length > 0 ? warnings : void 0,
|
|
4058
|
-
total_bytes: totalExtractedSize
|
|
4059
|
-
});
|
|
4060
|
-
});
|
|
4061
|
-
extract2.on("error", (error) => {
|
|
4062
|
-
reject(
|
|
4063
|
-
new FulpackOperationError(
|
|
4064
|
-
createFulpackError(
|
|
4065
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
4066
|
-
`TAR extraction failed: ${error.message}`,
|
|
4067
|
-
"extract" /* EXTRACT */,
|
|
4068
|
-
{ archive, details: { original_error: error } }
|
|
4069
|
-
)
|
|
4070
|
-
)
|
|
4071
|
-
);
|
|
4072
|
-
});
|
|
4073
|
-
readStream.pipe(extract2);
|
|
4074
|
-
});
|
|
4075
|
-
}
|
|
4076
|
-
async function extractZip(archive, destination, options) {
|
|
4077
|
-
const unzipper = await import('unzipper');
|
|
4078
|
-
let extractedCount = 0;
|
|
4079
|
-
let skippedCount = 0;
|
|
4080
|
-
let errorCount = 0;
|
|
4081
|
-
const errors = [];
|
|
4082
|
-
const warnings = [];
|
|
4083
|
-
let totalExtractedSize = 0;
|
|
4084
|
-
let entryCount = 0;
|
|
4085
|
-
return new Promise((resolve, reject) => {
|
|
4086
|
-
const readStream = createReadStream(archive);
|
|
4087
|
-
readStream.pipe(unzipper.Parse()).on("entry", async (entry) => {
|
|
4088
|
-
entryCount++;
|
|
4089
|
-
try {
|
|
4090
|
-
if (options.max_entries && entryCount > options.max_entries) {
|
|
4091
|
-
errors.push(
|
|
4092
|
-
createFulpackError(
|
|
4093
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
4094
|
-
`Entry count exceeds maximum (${options.max_entries})`,
|
|
4095
|
-
"extract" /* EXTRACT */,
|
|
4096
|
-
{
|
|
4097
|
-
archive,
|
|
4098
|
-
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
4099
|
-
}
|
|
4100
|
-
)
|
|
4101
|
-
);
|
|
4102
|
-
entry.autodrain();
|
|
4103
|
-
return;
|
|
4104
|
-
}
|
|
4105
|
-
const pathError = validatePath(entry.path, "extract" /* EXTRACT */, false);
|
|
4106
|
-
if (pathError) {
|
|
4107
|
-
errors.push({ ...pathError, archive });
|
|
4108
|
-
errorCount++;
|
|
4109
|
-
entry.autodrain();
|
|
4110
|
-
return;
|
|
4111
|
-
}
|
|
4112
|
-
const outputPath = join(destination, entry.path);
|
|
4113
|
-
if (entry.type === "Directory") {
|
|
4114
|
-
if (!existsSync(outputPath)) {
|
|
4115
|
-
mkdirSync(outputPath, { recursive: true });
|
|
4116
|
-
}
|
|
4117
|
-
extractedCount++;
|
|
4118
|
-
entry.autodrain();
|
|
4119
|
-
} else if (entry.type === "File") {
|
|
4120
|
-
if (existsSync(outputPath)) {
|
|
4121
|
-
if (options.overwrite === "error") {
|
|
4122
|
-
errors.push(
|
|
4123
|
-
createFulpackError(
|
|
4124
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
4125
|
-
`Output file already exists: ${outputPath}`,
|
|
4126
|
-
"extract" /* EXTRACT */,
|
|
4127
|
-
{ path: entry.path, archive }
|
|
4128
|
-
)
|
|
4129
|
-
);
|
|
4130
|
-
errorCount++;
|
|
4131
|
-
entry.autodrain();
|
|
4132
|
-
return;
|
|
4133
|
-
} else if (options.overwrite === "skip") {
|
|
4134
|
-
warnings.push(`Skipped existing file: ${outputPath}`);
|
|
4135
|
-
skippedCount++;
|
|
4136
|
-
entry.autodrain();
|
|
4137
|
-
return;
|
|
4138
|
-
}
|
|
4139
|
-
}
|
|
4140
|
-
const parentDir = dirname(outputPath);
|
|
4141
|
-
if (!existsSync(parentDir)) {
|
|
4142
|
-
mkdirSync(parentDir, { recursive: true });
|
|
4143
|
-
}
|
|
4144
|
-
const writeStream = createWriteStream(outputPath);
|
|
4145
|
-
entry.on("data", (chunk) => {
|
|
4146
|
-
totalExtractedSize += chunk.length;
|
|
4147
|
-
if (options.max_size && totalExtractedSize > options.max_size) {
|
|
4148
|
-
entry.destroy();
|
|
4149
|
-
writeStream.destroy();
|
|
4150
|
-
errors.push(
|
|
4151
|
-
createFulpackError(
|
|
4152
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
4153
|
-
`Total extracted size exceeds maximum (${options.max_size} bytes)`,
|
|
4154
|
-
"extract" /* EXTRACT */,
|
|
4155
|
-
{
|
|
4156
|
-
archive,
|
|
4157
|
-
path: entry.path,
|
|
4158
|
-
details: { actual_size: totalExtractedSize, max_size: options.max_size }
|
|
4159
|
-
}
|
|
4160
|
-
)
|
|
4161
|
-
);
|
|
4162
|
-
errorCount++;
|
|
4163
|
-
return;
|
|
4164
|
-
}
|
|
4165
|
-
});
|
|
4166
|
-
entry.pipe(writeStream);
|
|
4167
|
-
writeStream.on("finish", () => {
|
|
4168
|
-
extractedCount++;
|
|
4169
|
-
});
|
|
4170
|
-
writeStream.on("error", (error) => {
|
|
4171
|
-
errors.push(
|
|
4172
|
-
createFulpackError(
|
|
4173
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
4174
|
-
`Failed to write file: ${error.message}`,
|
|
4175
|
-
"extract" /* EXTRACT */,
|
|
4176
|
-
{ path: entry.path, archive, details: { original_error: error } }
|
|
4177
|
-
)
|
|
4178
|
-
);
|
|
4179
|
-
errorCount++;
|
|
4180
|
-
});
|
|
4181
|
-
} else {
|
|
4182
|
-
warnings.push(`Skipped entry type ${entry.type}: ${entry.path}`);
|
|
4183
|
-
entry.autodrain();
|
|
4184
|
-
}
|
|
4185
|
-
} catch (error) {
|
|
4186
|
-
errors.push(
|
|
4187
|
-
createFulpackError(
|
|
4188
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
4189
|
-
`Error processing entry: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
4190
|
-
"extract" /* EXTRACT */,
|
|
4191
|
-
{ path: entry.path, archive, details: { original_error: error } }
|
|
4192
|
-
)
|
|
4193
|
-
);
|
|
4194
|
-
errorCount++;
|
|
4195
|
-
entry.autodrain();
|
|
4196
|
-
}
|
|
4197
|
-
}).on("close", () => {
|
|
4198
|
-
resolve({
|
|
4199
|
-
extracted_count: extractedCount,
|
|
4200
|
-
skipped_count: skippedCount,
|
|
4201
|
-
error_count: errorCount,
|
|
4202
|
-
errors: errors.length > 0 ? errors : void 0,
|
|
4203
|
-
warnings: warnings.length > 0 ? warnings : void 0,
|
|
4204
|
-
total_bytes: totalExtractedSize
|
|
4205
|
-
});
|
|
4206
|
-
}).on("error", (error) => {
|
|
4207
|
-
reject(
|
|
4208
|
-
new FulpackOperationError(
|
|
4209
|
-
createFulpackError(
|
|
4210
|
-
ERROR_CODES.EXTRACTION_FAILED,
|
|
4211
|
-
`ZIP extraction failed: ${error.message}`,
|
|
4212
|
-
"extract" /* EXTRACT */,
|
|
4213
|
-
{ archive, details: { original_error: error } }
|
|
4214
|
-
)
|
|
4215
|
-
)
|
|
4216
|
-
);
|
|
4217
|
-
});
|
|
4218
|
-
});
|
|
4219
|
-
}
|
|
4220
|
-
async function scanTar(archive, options) {
|
|
4221
|
-
const tarStream = await import('tar-stream');
|
|
4222
|
-
const extract2 = tarStream.extract();
|
|
4223
|
-
const entries = [];
|
|
4224
|
-
let entryCount = 0;
|
|
4225
|
-
return new Promise((resolve, reject) => {
|
|
4226
|
-
extract2.on("entry", async (header, stream, next) => {
|
|
4227
|
-
entryCount++;
|
|
4228
|
-
if (options.max_entries && entryCount > options.max_entries) {
|
|
4229
|
-
stream.resume();
|
|
4230
|
-
next();
|
|
4231
|
-
reject(
|
|
4232
|
-
new FulpackOperationError(
|
|
4233
|
-
createFulpackError(
|
|
4234
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
4235
|
-
`Entry count exceeds maximum (${options.max_entries})`,
|
|
4236
|
-
"scan" /* SCAN */,
|
|
4237
|
-
{
|
|
4238
|
-
archive,
|
|
4239
|
-
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
4240
|
-
}
|
|
4241
|
-
)
|
|
4242
|
-
)
|
|
4243
|
-
);
|
|
4244
|
-
return;
|
|
4245
|
-
}
|
|
4246
|
-
const entry = {
|
|
4247
|
-
path: header.name,
|
|
4248
|
-
type: header.type === "directory" ? "directory" /* DIRECTORY */ : header.type === "symlink" ? "symlink" /* SYMLINK */ : "file" /* FILE */,
|
|
4249
|
-
size: header.size || 0,
|
|
4250
|
-
modified: header.mtime ? header.mtime.toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
4251
|
-
...options.include_metadata && header.mode !== void 0 && header.mode !== null ? { mode: `0${(header.mode & 511).toString(8)}` } : {},
|
|
4252
|
-
...options.include_metadata && header.type === "symlink" && header.linkname ? { symlink_target: header.linkname } : {}
|
|
4253
|
-
};
|
|
4254
|
-
entries.push(entry);
|
|
4255
|
-
stream.resume();
|
|
4256
|
-
next();
|
|
4257
|
-
});
|
|
4258
|
-
extract2.on("finish", () => {
|
|
4259
|
-
resolve(entries);
|
|
4260
|
-
});
|
|
4261
|
-
extract2.on("error", (error) => {
|
|
4262
|
-
reject(
|
|
4263
|
-
new FulpackOperationError(
|
|
4264
|
-
createFulpackError(
|
|
4265
|
-
ERROR_CODES.ARCHIVE_CORRUPT,
|
|
4266
|
-
`TAR scanning failed: ${error.message}`,
|
|
4267
|
-
"scan" /* SCAN */,
|
|
4268
|
-
{ archive, details: { original_error: error } }
|
|
4269
|
-
)
|
|
4270
|
-
)
|
|
4271
|
-
);
|
|
4272
|
-
});
|
|
4273
|
-
const readStream = createReadStream(archive);
|
|
4274
|
-
readStream.pipe(extract2);
|
|
4275
|
-
});
|
|
4276
|
-
}
|
|
4277
|
-
async function scanTarGz(archive, options) {
|
|
4278
|
-
const tarStream = await import('tar-stream');
|
|
4279
|
-
const extract2 = tarStream.extract();
|
|
4280
|
-
const entries = [];
|
|
4281
|
-
let entryCount = 0;
|
|
4282
|
-
return new Promise((resolve, reject) => {
|
|
4283
|
-
extract2.on("entry", async (header, stream, next) => {
|
|
4284
|
-
entryCount++;
|
|
4285
|
-
if (options.max_entries && entryCount > options.max_entries) {
|
|
4286
|
-
stream.resume();
|
|
4287
|
-
next();
|
|
4288
|
-
reject(
|
|
4289
|
-
new FulpackOperationError(
|
|
4290
|
-
createFulpackError(
|
|
4291
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
4292
|
-
`Entry count exceeds maximum (${options.max_entries})`,
|
|
4293
|
-
"scan" /* SCAN */,
|
|
4294
|
-
{
|
|
4295
|
-
archive,
|
|
4296
|
-
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
4297
|
-
}
|
|
4298
|
-
)
|
|
4299
|
-
)
|
|
4300
|
-
);
|
|
4301
|
-
return;
|
|
4302
|
-
}
|
|
4303
|
-
const entry = {
|
|
4304
|
-
path: header.name,
|
|
4305
|
-
type: header.type === "directory" ? "directory" /* DIRECTORY */ : header.type === "symlink" ? "symlink" /* SYMLINK */ : "file" /* FILE */,
|
|
4306
|
-
size: header.size || 0,
|
|
4307
|
-
modified: header.mtime ? header.mtime.toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
4308
|
-
...options.include_metadata && header.mode !== void 0 && header.mode !== null ? { mode: `0${(header.mode & 511).toString(8)}` } : {},
|
|
4309
|
-
...options.include_metadata && header.type === "symlink" && header.linkname ? { symlink_target: header.linkname } : {}
|
|
4310
|
-
};
|
|
4311
|
-
entries.push(entry);
|
|
4312
|
-
stream.resume();
|
|
4313
|
-
next();
|
|
4314
|
-
});
|
|
4315
|
-
extract2.on("finish", () => {
|
|
4316
|
-
resolve(entries);
|
|
4317
|
-
});
|
|
4318
|
-
extract2.on("error", (error) => {
|
|
4319
|
-
reject(
|
|
4320
|
-
new FulpackOperationError(
|
|
4321
|
-
createFulpackError(
|
|
4322
|
-
ERROR_CODES.ARCHIVE_CORRUPT,
|
|
4323
|
-
`TAR.GZ scanning failed: ${error.message}`,
|
|
4324
|
-
"scan" /* SCAN */,
|
|
4325
|
-
{ archive, details: { original_error: error } }
|
|
4326
|
-
)
|
|
4327
|
-
)
|
|
4328
|
-
);
|
|
4329
|
-
});
|
|
4330
|
-
const readStream = createReadStream(archive);
|
|
4331
|
-
const gunzip = createGunzip();
|
|
4332
|
-
readStream.pipe(gunzip).pipe(extract2);
|
|
4333
|
-
});
|
|
4334
|
-
}
|
|
4335
|
-
async function scanZip(archive, options) {
|
|
4336
|
-
const unzipper = await import('unzipper');
|
|
4337
|
-
const entries = [];
|
|
4338
|
-
let entryCount = 0;
|
|
4339
|
-
return new Promise((resolve, reject) => {
|
|
4340
|
-
createReadStream(archive).pipe(unzipper.Parse()).on("entry", (entry) => {
|
|
4341
|
-
const typedEntry = entry;
|
|
4342
|
-
entryCount++;
|
|
4343
|
-
if (options.max_entries && entryCount > options.max_entries) {
|
|
4344
|
-
typedEntry.autodrain();
|
|
4345
|
-
reject(
|
|
4346
|
-
new FulpackOperationError(
|
|
4347
|
-
createFulpackError(
|
|
4348
|
-
ERROR_CODES.DECOMPRESSION_BOMB,
|
|
4349
|
-
`Entry count exceeds maximum (${options.max_entries})`,
|
|
4350
|
-
"scan" /* SCAN */,
|
|
4351
|
-
{
|
|
4352
|
-
archive,
|
|
4353
|
-
details: { entry_count: entryCount, max_entries: options.max_entries }
|
|
4354
|
-
}
|
|
4355
|
-
)
|
|
4356
|
-
)
|
|
4357
|
-
);
|
|
4358
|
-
return;
|
|
4359
|
-
}
|
|
4360
|
-
const uncompressedSize = typedEntry.vars?.uncompressedSize || typedEntry.props?.size || 0;
|
|
4361
|
-
const compressedSize = typedEntry.vars?.compressedSize || typedEntry.props?.compressedSize || 0;
|
|
4362
|
-
const archiveEntry = {
|
|
4363
|
-
path: typedEntry.path,
|
|
4364
|
-
type: typedEntry.type === "Directory" ? "directory" /* DIRECTORY */ : "file" /* FILE */,
|
|
4365
|
-
size: uncompressedSize,
|
|
4366
|
-
modified: typedEntry.vars?.lastModifiedTime ? new Date(typedEntry.vars.lastModifiedTime).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
4367
|
-
...options.include_metadata ? {
|
|
4368
|
-
compressed_size: compressedSize,
|
|
4369
|
-
mode: "0644"
|
|
4370
|
-
// ZIP format doesn't preserve Unix permissions consistently
|
|
4371
|
-
} : {}
|
|
4372
|
-
};
|
|
4373
|
-
entries.push(archiveEntry);
|
|
4374
|
-
typedEntry.autodrain();
|
|
4375
|
-
}).on("error", (error) => {
|
|
4376
|
-
reject(
|
|
4377
|
-
new FulpackOperationError(
|
|
4378
|
-
createFulpackError(
|
|
4379
|
-
ERROR_CODES.ARCHIVE_CORRUPT,
|
|
4380
|
-
`ZIP scanning failed: ${error.message}`,
|
|
4381
|
-
"scan" /* SCAN */,
|
|
4382
|
-
{ archive, details: { original_error: error } }
|
|
4383
|
-
)
|
|
4384
|
-
)
|
|
4385
|
-
);
|
|
4386
|
-
}).on("finish", () => {
|
|
4387
|
-
resolve(entries);
|
|
4388
|
-
});
|
|
4389
|
-
});
|
|
4390
|
-
}
|
|
4391
|
-
|
|
4392
|
-
// src/fulpack/index.ts
|
|
4393
|
-
var FULPACK_VERSION = "1.0.0";
|
|
1
|
+
export { ERROR_CODES, FULPACK_VERSION, FulpackOperationError, checkDecompressionBomb, create, createFulpackError, extract, hasPathTraversal, info, isAbsolutePath, scan, validatePath, verify } from './chunk-P3DNOZOG.js';
|
|
2
|
+
export { buildEnvVar, getBinaryName, getConfigIdentifiers, getConfigName, getEnvPrefix, getEnvVar, getTelemetryNamespace, getVendor } from './chunk-LMNAUTW7.js';
|
|
3
|
+
export { APP_IDENTITY_DIR, APP_IDENTITY_ENV_VAR, APP_IDENTITY_FILENAME, APP_IDENTITY_SCHEMA_ID, MAX_ANCESTOR_SEARCH_DEPTH, clearEmbeddedIdentity, clearIdentityCache, getCachedIdentity, getEmbeddedIdentity, hasEmbeddedIdentity, loadIdentity, registerEmbeddedIdentity } from './chunk-VJIGX7MH.js';
|
|
4
|
+
export { AppIdentityError } from './chunk-WCCMRVZF.js';
|
|
5
|
+
export { buildRuntimeInfo } from './chunk-HY6JQOFN.js';
|
|
6
|
+
export { ArchiveFormat, EntryType, Operation } from './chunk-PQ6TKPUN.js';
|
|
7
|
+
export { DocScribeError, DocScribeParseError, DocScribeUnsupportedFormatError, __internal, detectFormat, extractHeaders, extractMetadata, inspectDocument, normalizeInput, parseFrontmatter, splitDocuments, stripFrontmatter } from './chunk-BCO3FUUO.js';
|
|
8
|
+
import './chunk-GOTEQYAD.js';
|
|
9
|
+
import './chunk-SNV5WG5V.js';
|
|
10
|
+
import './chunk-QCCN6POA.js';
|
|
11
|
+
import './chunk-56F2EEAG.js';
|
|
12
|
+
import './chunk-UP2VWCW5.js';
|
|
4394
13
|
|
|
4395
14
|
// src/index.ts
|
|
4396
|
-
var VERSION = "0.
|
|
15
|
+
var VERSION = "0.4.1";
|
|
4397
16
|
|
|
4398
|
-
export {
|
|
17
|
+
export { VERSION };
|
|
4399
18
|
//# sourceMappingURL=index.js.map
|
|
4400
19
|
//# sourceMappingURL=index.js.map
|