openapi-sdkgen 8.1.1 → 8.2.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/README.md CHANGED
@@ -14,16 +14,19 @@ The npm launcher requires Node.js 22 or newer. This requirement applies only to
14
14
  the npm launcher; generated SDK source has its own runtime compatibility
15
15
  contract.
16
16
 
17
- The npm package is a small JavaScript launcher. On the first run of a package
18
- version, it downloads only the matching macOS, Linux, or Windows executable from
19
- that exact version's GitHub Release, verifies it against the release SHA-256
20
- checksums, and stores the verified executable in the user cache. Later runs reuse
21
- the verified cache entry, so they do not require network access. Go is not
22
- required by consumers.
17
+ The npm package is a small Releaseway-generated JavaScript launcher. On the
18
+ first run it selects the declared native target, downloads the matching archive
19
+ from that exact version's immutable GitHub Release, verifies the GitHub SHA-256
20
+ digest, safely extracts only the configured executable, and stores the verified
21
+ archive/executable in a content-addressed user cache. Later runs reuse the
22
+ verified cache entry, so they do not require network access. Go is not required
23
+ by consumers.
23
24
 
24
25
  Set `OPENAPI_SDKGEN_CACHE_DIR` to override the cache root, for example in an
25
- isolated CI environment. The launcher never follows a moving `latest` release;
26
- the npm package version and GitHub Release tag must match exactly.
26
+ isolated CI environment. That product-specific environment contract is
27
+ preserved through the pinned Releaseway native runtime. The launcher never
28
+ follows a moving `latest` release; the npm package version and GitHub Release
29
+ tag must match exactly.
27
30
 
28
31
  For command reference and generated SDK usage, see the
29
32
  [project documentation](https://jinyongp.github.io/openapi-sdkgen/).
@@ -1,20 +1,13 @@
1
1
  #!/usr/bin/env node
2
-
3
- import { readFile } from "node:fs/promises";
4
-
5
- import { runLauncher } from "../lib/launcher.js";
6
-
7
- const packageJSON = JSON.parse(
8
- await readFile(new URL("../package.json", import.meta.url), "utf8"),
9
- );
10
-
11
- try {
12
- process.exitCode = await runLauncher({
13
- version: packageJSON.version,
14
- args: process.argv.slice(2),
15
- });
16
- } catch (error) {
17
- const message = error instanceof Error ? error.message : String(error);
18
- console.error(`openapi-sdkgen: ${message}`);
19
- process.exitCode = 1;
20
- }
2
+ import { realpathSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const self = realpathSync(fileURLToPath(import.meta.url));
8
+ const require = createRequire(import.meta.url);
9
+ const runtime = require(resolve(dirname(self), "../.releaseway/runtime.cjs"));
10
+
11
+ runtime.main({
12
+ manifestPath: resolve(dirname(self), "../.releaseway/native.json"),
13
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openapi-sdkgen",
3
- "version": "8.1.1",
3
+ "version": "8.2.1",
4
4
  "description": "Generate application SDK source from OpenAPI documents.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -8,8 +8,6 @@
8
8
  "openapi-sdkgen": "./bin/openapi-sdkgen.js"
9
9
  },
10
10
  "files": [
11
- "bin",
12
- "lib",
13
11
  "NOTICE"
14
12
  ],
15
13
  "engines": {
@@ -26,4 +24,4 @@
26
24
  "publishConfig": {
27
25
  "access": "public"
28
26
  }
29
- }
27
+ }
package/lib/launcher.js DELETED
@@ -1,382 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
4
- import { homedir } from "node:os";
5
- import { isAbsolute, join, resolve } from "node:path";
6
- import { setTimeout as delay } from "node:timers/promises";
7
-
8
- const defaultReleaseBaseURL =
9
- "https://github.com/jinyongp/openapi-sdkgen/releases/download";
10
- const maxChecksumBytes = 1024 * 1024;
11
- const maxBinaryBytes = 128 * 1024 * 1024;
12
- const maxRedirects = 5;
13
- const downloadTimeoutMs = 30_000;
14
- const cacheLockTimeoutMs = 30_000;
15
- const staleCacheLockMs = 120_000;
16
- const semverPattern =
17
- /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+)(?:\.[0-9A-Za-z-]+)*)?(?:\+([0-9A-Za-z-]+)(?:\.[0-9A-Za-z-]+)*)?$/;
18
-
19
- const targets = {
20
- darwin: {
21
- arm64: { os: "darwin", arch: "arm64", extension: "" },
22
- x64: { os: "darwin", arch: "amd64", extension: "" },
23
- },
24
- linux: {
25
- arm64: { os: "linux", arch: "arm64", extension: "" },
26
- x64: { os: "linux", arch: "amd64", extension: "" },
27
- },
28
- win32: {
29
- arm64: { os: "windows", arch: "arm64", extension: ".exe" },
30
- x64: { os: "windows", arch: "amd64", extension: ".exe" },
31
- },
32
- };
33
-
34
- export function resolveReleaseTarget(
35
- platform = process.platform,
36
- architecture = process.arch,
37
- ) {
38
- const target = targets[platform]?.[architecture];
39
- if (target === undefined) {
40
- throw new Error(
41
- `unsupported platform ${platform}/${architecture}; supported platforms are darwin, linux, and win32 on arm64 or x64`,
42
- );
43
- }
44
- return {
45
- ...target,
46
- key: `${target.os}-${target.arch}`,
47
- executable: `openapi-sdkgen${target.extension}`,
48
- };
49
- }
50
-
51
- export function releaseAssetName(version, target) {
52
- validateVersion(version);
53
- return `openapi-sdkgen_${version}_${target.os}_${target.arch}${target.extension}`;
54
- }
55
-
56
- export function resolveCacheRoot({
57
- platform = process.platform,
58
- env = process.env,
59
- home = homedir(),
60
- } = {}) {
61
- const override = env.OPENAPI_SDKGEN_CACHE_DIR;
62
- if (override !== undefined && override !== "") {
63
- return isAbsolute(override) ? override : resolve(override);
64
- }
65
- if (platform === "win32") {
66
- const base = env.LOCALAPPDATA || join(home, "AppData", "Local");
67
- return join(base, "openapi-sdkgen", "cache");
68
- }
69
- if (platform === "darwin") {
70
- return join(home, "Library", "Caches", "openapi-sdkgen");
71
- }
72
- const base = env.XDG_CACHE_HOME || join(home, ".cache");
73
- return join(base, "openapi-sdkgen");
74
- }
75
-
76
- export function cacheEntryPaths({
77
- cacheRoot,
78
- version,
79
- platform = process.platform,
80
- architecture = process.arch,
81
- }) {
82
- validateVersion(version);
83
- const target = resolveReleaseTarget(platform, architecture);
84
- const directory = join(cacheRoot, version, target.key);
85
- return {
86
- target,
87
- directory,
88
- executable: join(directory, target.executable),
89
- checksum: join(directory, "sha256"),
90
- versionDirectory: join(cacheRoot, version),
91
- };
92
- }
93
-
94
- export async function ensureBinary({
95
- version,
96
- platform = process.platform,
97
- architecture = process.arch,
98
- env = process.env,
99
- cacheRoot = resolveCacheRoot({ platform, env }),
100
- releaseBaseURL = defaultReleaseBaseURL,
101
- fetchImpl = globalThis.fetch,
102
- } = {}) {
103
- validateVersion(version);
104
- if (typeof fetchImpl !== "function") {
105
- throw new Error("Fetch API is unavailable in this Node.js runtime");
106
- }
107
- const releaseBase = new URL(ensureTrailingSlash(releaseBaseURL));
108
- if (releaseBase.protocol !== "https:") {
109
- throw new Error("release download origin must use HTTPS");
110
- }
111
-
112
- const paths = cacheEntryPaths({ cacheRoot, version, platform, architecture });
113
- if (await hasValidCacheEntry(paths)) {
114
- return paths.executable;
115
- }
116
- await mkdir(paths.versionDirectory, { recursive: true, mode: 0o700 });
117
-
118
- return withCacheLock(paths, async () => {
119
- if (await hasValidCacheEntry(paths)) {
120
- return paths.executable;
121
- }
122
- await rm(paths.directory, { recursive: true, force: true });
123
-
124
- const tagBase = new URL(`v${version}/`, releaseBase);
125
- const asset = releaseAssetName(version, paths.target);
126
- const checksumBytes = await fetchBytes(new URL("checksums.txt", tagBase), {
127
- fetchImpl,
128
- maxBytes: maxChecksumBytes,
129
- label: "release checksums",
130
- });
131
- const expectedDigest = checksumForAsset(checksumBytes, asset);
132
- const binaryBytes = await fetchBytes(new URL(asset, tagBase), {
133
- fetchImpl,
134
- maxBytes: maxBinaryBytes,
135
- label: `release binary ${asset}`,
136
- });
137
- const actualDigest = sha256(binaryBytes);
138
- if (actualDigest !== expectedDigest) {
139
- throw new Error(
140
- `release binary checksum mismatch for ${asset}: expected ${expectedDigest}, got ${actualDigest}`,
141
- );
142
- }
143
-
144
- await publishCacheEntry(paths, binaryBytes, expectedDigest);
145
- if (!(await hasValidCacheEntry(paths))) {
146
- throw new Error(`cached release binary failed verification for ${paths.target.key}`);
147
- }
148
- return paths.executable;
149
- });
150
- }
151
-
152
- export function executeBinary(
153
- executable,
154
- args,
155
- { spawnImpl = spawnSync } = {},
156
- ) {
157
- const result = spawnImpl(executable, args, { stdio: "inherit" });
158
- if (result.error !== undefined) {
159
- throw new Error(`failed to execute cached openapi-sdkgen binary: ${result.error.message}`);
160
- }
161
- if (result.status === null) {
162
- throw new Error(
163
- `openapi-sdkgen binary terminated without an exit status${result.signal ? ` (${result.signal})` : ""}`,
164
- );
165
- }
166
- return result.status;
167
- }
168
-
169
- export async function runLauncher({
170
- version,
171
- args = process.argv.slice(2),
172
- platform = process.platform,
173
- architecture = process.arch,
174
- env = process.env,
175
- cacheRoot,
176
- releaseBaseURL,
177
- fetchImpl,
178
- spawnImpl,
179
- } = {}) {
180
- const executable = await ensureBinary({
181
- version,
182
- platform,
183
- architecture,
184
- env,
185
- ...(cacheRoot === undefined ? {} : { cacheRoot }),
186
- ...(releaseBaseURL === undefined ? {} : { releaseBaseURL }),
187
- ...(fetchImpl === undefined ? {} : { fetchImpl }),
188
- });
189
- return executeBinary(executable, args, {
190
- ...(spawnImpl === undefined ? {} : { spawnImpl }),
191
- });
192
- }
193
-
194
- function validateVersion(version) {
195
- if (typeof version !== "string" || !semverPattern.test(version)) {
196
- throw new Error(`invalid openapi-sdkgen package version: ${String(version)}`);
197
- }
198
- }
199
-
200
- async function hasValidCacheEntry(paths) {
201
- let expectedDigest;
202
- let binary;
203
- try {
204
- expectedDigest = (await readFile(paths.checksum, "utf8")).trim().toLowerCase();
205
- if (!/^[0-9a-f]{64}$/.test(expectedDigest)) return false;
206
- binary = await readFile(paths.executable);
207
- } catch (error) {
208
- if (error?.code === "ENOENT") return false;
209
- throw new Error(
210
- `cannot read openapi-sdkgen cache entry ${paths.directory}: ${error instanceof Error ? error.message : String(error)}`,
211
- );
212
- }
213
- return sha256(binary) === expectedDigest;
214
- }
215
-
216
- async function withCacheLock(paths, action) {
217
- const lockDirectory = join(paths.versionDirectory, `.${paths.target.key}.lock`);
218
- const deadline = Date.now() + cacheLockTimeoutMs;
219
-
220
- while (true) {
221
- try {
222
- await mkdir(lockDirectory, { mode: 0o700 });
223
- break;
224
- } catch (error) {
225
- if (error?.code !== "EEXIST") throw error;
226
- }
227
-
228
- if (await hasValidCacheEntry(paths)) {
229
- return paths.executable;
230
- }
231
-
232
- try {
233
- const lock = await stat(lockDirectory);
234
- if (Date.now() - lock.mtimeMs > staleCacheLockMs) {
235
- await rm(lockDirectory, { recursive: true, force: true });
236
- continue;
237
- }
238
- } catch (error) {
239
- if (error?.code === "ENOENT") continue;
240
- throw error;
241
- }
242
-
243
- if (Date.now() >= deadline) {
244
- throw new Error(`timed out waiting for openapi-sdkgen cache lock for ${paths.target.key}`);
245
- }
246
- await delay(50);
247
- }
248
-
249
- try {
250
- return await action();
251
- } finally {
252
- await rm(lockDirectory, { recursive: true, force: true });
253
- }
254
- }
255
-
256
- async function publishCacheEntry(paths, binaryBytes, digest) {
257
- const temporaryDirectory = join(
258
- paths.versionDirectory,
259
- `.${paths.target.key}.tmp-${process.pid}-${randomUUID()}`,
260
- );
261
- const temporaryExecutable = join(temporaryDirectory, paths.target.executable);
262
- const temporaryChecksum = join(temporaryDirectory, "sha256");
263
-
264
- await mkdir(temporaryDirectory, { mode: 0o700 });
265
- try {
266
- await writeFile(temporaryExecutable, binaryBytes, { mode: 0o600 });
267
- if (paths.target.os !== "windows") {
268
- await chmod(temporaryExecutable, 0o755);
269
- }
270
- await writeFile(temporaryChecksum, `${digest}\n`, { mode: 0o600 });
271
- await rename(temporaryDirectory, paths.directory);
272
- } finally {
273
- await rm(temporaryDirectory, { recursive: true, force: true });
274
- }
275
- }
276
-
277
- async function fetchBytes(url, { fetchImpl, maxBytes, label }) {
278
- let currentURL = new URL(url);
279
- for (let redirect = 0; redirect <= maxRedirects; redirect += 1) {
280
- if (currentURL.protocol !== "https:") {
281
- throw new Error(`${label} URL must use HTTPS: ${currentURL.origin}`);
282
- }
283
- let response;
284
- try {
285
- response = await fetchImpl(currentURL, {
286
- redirect: "manual",
287
- signal: AbortSignal.timeout(downloadTimeoutMs),
288
- headers: { "user-agent": "openapi-sdkgen-npm-launcher" },
289
- });
290
- } catch (error) {
291
- throw new Error(
292
- `failed to download ${label}: ${error instanceof Error ? error.message : String(error)}`,
293
- );
294
- }
295
-
296
- if (isRedirectStatus(response.status)) {
297
- if (redirect === maxRedirects) {
298
- throw new Error(`${label} exceeded ${maxRedirects} HTTPS redirects`);
299
- }
300
- const location = response.headers.get("location");
301
- if (location === null) {
302
- throw new Error(`${label} redirect is missing Location`);
303
- }
304
- currentURL = new URL(location, currentURL);
305
- continue;
306
- }
307
- if (!response.ok) {
308
- throw new Error(`failed to download ${label}: HTTP ${response.status}`);
309
- }
310
-
311
- const declaredLength = response.headers.get("content-length");
312
- if (
313
- declaredLength !== null &&
314
- /^\d+$/.test(declaredLength) &&
315
- Number(declaredLength) > maxBytes
316
- ) {
317
- throw new Error(`${label} exceeds the ${maxBytes}-byte download limit`);
318
- }
319
- if (response.body === null) {
320
- return new Uint8Array();
321
- }
322
-
323
- const reader = response.body.getReader();
324
- const chunks = [];
325
- let total = 0;
326
- while (true) {
327
- const next = await reader.read();
328
- if (next.done) break;
329
- if (next.value.byteLength > maxBytes - total) {
330
- throw new Error(`${label} exceeds the ${maxBytes}-byte download limit`);
331
- }
332
- total += next.value.byteLength;
333
- chunks.push(next.value);
334
- }
335
- const bytes = new Uint8Array(total);
336
- let offset = 0;
337
- for (const chunk of chunks) {
338
- bytes.set(chunk, offset);
339
- offset += chunk.byteLength;
340
- }
341
- return bytes;
342
- }
343
- throw new Error(`failed to download ${label}`);
344
- }
345
-
346
- function checksumForAsset(checksumBytes, asset) {
347
- let text;
348
- try {
349
- text = new TextDecoder("utf-8", { fatal: true }).decode(checksumBytes);
350
- } catch {
351
- throw new Error("release checksums are not valid UTF-8");
352
- }
353
- let digest;
354
- for (const line of text.split(/\r?\n/)) {
355
- if (line.trim() === "") continue;
356
- const match = line.match(/^([0-9A-Fa-f]{64})\s+(.+)$/);
357
- if (match === null) {
358
- throw new Error("release checksums contain a malformed entry");
359
- }
360
- if (match[2] !== asset) continue;
361
- if (digest !== undefined) {
362
- throw new Error(`release checksums contain duplicate entries for ${asset}`);
363
- }
364
- digest = match[1].toLowerCase();
365
- }
366
- if (digest === undefined) {
367
- throw new Error(`release checksums do not contain ${asset}`);
368
- }
369
- return digest;
370
- }
371
-
372
- function sha256(value) {
373
- return createHash("sha256").update(value).digest("hex");
374
- }
375
-
376
- function ensureTrailingSlash(value) {
377
- return value.endsWith("/") ? value : `${value}/`;
378
- }
379
-
380
- function isRedirectStatus(status) {
381
- return [301, 302, 303, 307, 308].includes(status);
382
- }