@eik/core 2.1.72 → 2.1.74

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 CHANGED
@@ -1,3 +1,17 @@
1
+ ## [2.1.74](https://github.com/eik-lib/core/compare/v2.1.73...v2.1.74) (2026-08-14)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * prevent process crash on second busboy error and fix UTF-8 chunk corruption ([#633](https://github.com/eik-lib/core/issues/633)) ([9f5e3ec](https://github.com/eik-lib/core/commit/9f5e3ecab5ea8ceed16a0826c0c9c1824e68beac)), closes [#9](https://github.com/eik-lib/core/issues/9) [#11](https://github.com/eik-lib/core/issues/11)
7
+
8
+ ## [2.1.73](https://github.com/eik-lib/core/compare/v2.1.72...v2.1.73) (2026-08-14)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * write eik.json server-side as the final committed marker for package uploads ([#632](https://github.com/eik-lib/core/issues/632)) ([4461f76](https://github.com/eik-lib/core/commit/4461f76881886992db70848d42c7d6a199397c8f)), closes [#4](https://github.com/eik-lib/core/issues/4) [#5](https://github.com/eik-lib/core/issues/5)
14
+
1
15
  ## [2.1.72](https://github.com/eik-lib/core/compare/v2.1.71...v2.1.72) (2026-08-14)
2
16
 
3
17
 
@@ -82,19 +82,22 @@ const PkgPut = class PkgPut {
82
82
  .parse(incoming)
83
83
  .then((result) => {
84
84
  const pkg = new Package(incoming);
85
+ /** @type {Record<string,any>|null} */
86
+ let eikJson = null;
85
87
  result.forEach((/** @type {any} */ obj) => {
86
88
  if (obj.constructor.name === "FormField") {
87
89
  pkg.setMeta(obj);
88
90
  }
89
91
  if (obj.constructor.name === "FormFile") {
92
+ eikJson = obj.eikJson;
90
93
  obj.value.forEach((/** @type {any} */ o) => {
91
94
  pkg.setAsset(o);
92
95
  });
93
96
  }
94
97
  });
95
- return pkg;
98
+ return { pkg, eikJson };
96
99
  })
97
- .then(async (pkg) => {
100
+ .then(async ({ pkg, eikJson }) => {
98
101
  const path = createFilePathToPackage(pkg);
99
102
  await writeJSON(this._sink, path, pkg, "application/json");
100
103
 
@@ -102,7 +105,7 @@ const PkgPut = class PkgPut {
102
105
  `pkg:put - Successfully wrote package meta file to sink - Pathname: ${path}`,
103
106
  );
104
107
 
105
- resolve(pkg);
108
+ resolve({ pkg, eikJson });
106
109
  })
107
110
  .catch((error) => {
108
111
  reject(error);
@@ -110,6 +113,29 @@ const PkgPut = class PkgPut {
110
113
  });
111
114
  }
112
115
 
116
+ /**
117
+ * Writes eik.json to the sink as the final committed marker for a version.
118
+ * Merges any fields from the client's eik.json (captured from the tar) with
119
+ * server-authoritative values, ensuring the file is always server-written
120
+ * regardless of what the client's tar contained.
121
+ *
122
+ * @param {any} incoming
123
+ * @param {Record<string,any>|null} capturedEikJson
124
+ */
125
+ async _writeEikJson(incoming, capturedEikJson) {
126
+ const content = {
127
+ ...capturedEikJson,
128
+ name: incoming.name,
129
+ version: incoming.version,
130
+ type: incoming.type,
131
+ };
132
+ const eikJsonPath = createFilePathToEikJson(incoming);
133
+ await writeJSON(this._sink, eikJsonPath, content, "application/json");
134
+ this._log.info(
135
+ `pkg:put - Successfully wrote eik.json to sink - Pathname: ${eikJsonPath}`,
136
+ );
137
+ }
138
+
113
139
  /**
114
140
  * @param {any} incoming
115
141
  */
@@ -226,7 +252,7 @@ const PkgPut = class PkgPut {
226
252
  }
227
253
 
228
254
  const versions = await this._readVersions(incoming);
229
- const pkg = await this._parser(incoming);
255
+ const { pkg, eikJson } = await this._parser(incoming);
230
256
  versions.setVersion(pVersion, pkg.integrity);
231
257
 
232
258
  try {
@@ -238,6 +264,15 @@ const PkgPut = class PkgPut {
238
264
  throw e;
239
265
  }
240
266
 
267
+ try {
268
+ await this._writeEikJson(incoming, eikJson);
269
+ // eslint-disable-next-line no-unused-vars
270
+ } catch (error) {
271
+ const e = new HttpError.BadGateway();
272
+ end({ labels: { success: false, status: e.status, type } });
273
+ throw e;
274
+ }
275
+
241
276
  const outgoing = new HttpOutgoing();
242
277
  outgoing.cacheControl = this._cacheControl || "";
243
278
  outgoing.statusCode = 303;
@@ -1,12 +1,13 @@
1
1
  const FormFile = class FormFile {
2
2
  /**
3
- * @param {{ value?: any[], name?: string }} [options]
3
+ * @param {{ value?: any[], name?: string, eikJson?: Record<string,any>|null }} [options]
4
4
  */
5
- constructor({ value = [], name = "" } = {}) {
5
+ constructor({ value = [], name = "", eikJson = null } = {}) {
6
6
  if (!Array.isArray(value))
7
7
  throw new TypeError('The argument "value" must be of type Array');
8
8
  this._value = value;
9
9
  this._name = name;
10
+ this._eikJson = eikJson;
10
11
  }
11
12
 
12
13
  get value() {
@@ -17,6 +18,11 @@ const FormFile = class FormFile {
17
18
  return this._name;
18
19
  }
19
20
 
21
+ /** @returns {Record<string,any>|null} */
22
+ get eikJson() {
23
+ return this._eikJson;
24
+ }
25
+
20
26
  toJSON() {
21
27
  return {
22
28
  value: this.value,
@@ -1,4 +1,5 @@
1
1
  import { pipeline } from "node:stream";
2
+ import path from "node:path";
2
3
  import HttpError from "http-errors";
3
4
  import Busboy from "busboy";
4
5
  import abslog from "abslog";
@@ -105,7 +106,13 @@ const MultipartParser = class MultipartParser {
105
106
  },
106
107
  );
107
108
 
108
- busboy.once("error", (/** @type {any} */ error) => {
109
+ // Use .on() instead of .once() so that a second error emitted
110
+ // by a concurrent _handleFile failure (after the first error has
111
+ // already removed a .once() listener) does not hit zero listeners
112
+ // and crash the process with an unhandled EventEmitter throw.
113
+ // Calling reject() a second time is a safe no-op on an already-
114
+ // settled Promise.
115
+ busboy.on("error", (/** @type {any} */ error) => {
109
116
  reject(error);
110
117
  });
111
118
 
@@ -148,6 +155,8 @@ const MultipartParser = class MultipartParser {
148
155
  const ac = new AbortController();
149
156
  /** @type {Promise<any>[]} */
150
157
  const queue = [];
158
+ /** @type {Record<string,any>|null} */
159
+ let capturedEikJson = null;
151
160
 
152
161
  file.once("limit", () => {
153
162
  this._log.warn(
@@ -166,6 +175,30 @@ const MultipartParser = class MultipartParser {
166
175
  entry.resume();
167
176
  return;
168
177
  }
178
+
179
+ // eik.json is captured in memory and written server-side
180
+ // after the upload is fully committed, so that it can serve
181
+ // as a reliable committed marker independent of what the
182
+ // client includes in the tar.
183
+ if (path.normalize(entry.path) === "eik.json") {
184
+ /** @type {Buffer[]} */
185
+ const chunks = [];
186
+ entry.on("data", (/** @type {Buffer} */ chunk) => {
187
+ chunks.push(chunk);
188
+ });
189
+ entry.once("end", () => {
190
+ try {
191
+ capturedEikJson = JSON.parse(
192
+ Buffer.concat(chunks).toString("utf8"),
193
+ );
194
+ } catch {
195
+ // Malformed eik.json in tar — proceed without it.
196
+ }
197
+ });
198
+ entry.once("error", () => {});
199
+ return;
200
+ }
201
+
169
202
  const persist = this._persistFile({
170
203
  incoming,
171
204
  entry,
@@ -205,7 +238,11 @@ const MultipartParser = class MultipartParser {
205
238
 
206
239
  Promise.all(queue)
207
240
  .then((result) => {
208
- const formFile = new FormFile({ name: fieldname, value: result });
241
+ const formFile = new FormFile({
242
+ name: fieldname,
243
+ value: result,
244
+ eikJson: capturedEikJson,
245
+ });
209
246
  resolve(formFile);
210
247
  })
211
248
  .catch((err) => {
@@ -22,7 +22,7 @@ const readJSON = (sink, path) =>
22
22
 
23
23
  pipeline(from.stream, to, (error) => {
24
24
  if (error) return reject(error);
25
- const str = buffer.join("").toString();
25
+ const str = Buffer.concat(buffer).toString("utf8");
26
26
  try {
27
27
  const obj = JSON.parse(str);
28
28
  return resolve(obj);
@@ -86,7 +86,7 @@ const streamCollector = (from) =>
86
86
 
87
87
  pipeline(from, to, (error) => {
88
88
  if (error) return reject(error);
89
- return resolve(buffer.join("").toString());
89
+ return resolve(Buffer.concat(buffer).toString("utf8"));
90
90
  });
91
91
  });
92
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eik/core",
3
- "version": "2.1.72",
3
+ "version": "2.1.74",
4
4
  "description": "Core server package",
5
5
  "main": "lib/main.js",
6
6
  "types": "./types/main.d.ts",
@@ -66,6 +66,16 @@ declare const PkgPut: {
66
66
  * @param {any} incoming
67
67
  */
68
68
  _parser(incoming: any): Promise<any>;
69
+ /**
70
+ * Writes eik.json to the sink as the final committed marker for a version.
71
+ * Merges any fields from the client's eik.json (captured from the tar) with
72
+ * server-authoritative values, ensuring the file is always server-written
73
+ * regardless of what the client's tar contained.
74
+ *
75
+ * @param {any} incoming
76
+ * @param {Record<string,any>|null} capturedEikJson
77
+ */
78
+ _writeEikJson(incoming: any, capturedEikJson: Record<string, any> | null): Promise<void>;
69
79
  /**
70
80
  * @param {any} incoming
71
81
  */
@@ -1,13 +1,17 @@
1
1
  export default FormFile;
2
2
  declare const FormFile: {
3
- new ({ value, name }?: {
3
+ new ({ value, name, eikJson }?: {
4
4
  value?: any[];
5
5
  name?: string;
6
+ eikJson?: Record<string, any> | null;
6
7
  }): {
7
8
  _value: any[];
8
9
  _name: string;
10
+ _eikJson: Record<string, any> | null;
9
11
  get value(): any[];
10
12
  get name(): string;
13
+ /** @returns {Record<string,any>|null} */
14
+ get eikJson(): Record<string, any> | null;
11
15
  toJSON(): {
12
16
  value: any[];
13
17
  name: string;