@icib.dev/api-client 1.0.1 → 1.0.3

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.
@@ -0,0 +1,49 @@
1
+ import { createHash } from "crypto";
2
+ import { readFileSync, readdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ /** Recursively sort object keys for deterministic JSON hashing */
5
+ export function sortKeysRecursive(obj) {
6
+ if (obj === null || typeof obj !== "object")
7
+ return obj;
8
+ if (Array.isArray(obj))
9
+ return obj.map(sortKeysRecursive);
10
+ return Object.keys(obj)
11
+ .sort()
12
+ .reduce((acc, k) => {
13
+ acc[k] = sortKeysRecursive(obj[k]);
14
+ return acc;
15
+ }, {});
16
+ }
17
+ /** SHA256 hash of normalized JSON (deterministic regardless of key order) */
18
+ export function normalizedJsonHash(obj) {
19
+ const str = JSON.stringify(sortKeysRecursive(obj));
20
+ return createHash("sha256").update(str).digest("hex");
21
+ }
22
+ /** Deterministic file list for client hash: types/index.ts, client.ts, apiClient.ts, index.ts, contexts/*.ts (sorted) */
23
+ const CLIENT_FILE_ORDER = [
24
+ "types/index.ts",
25
+ "client.ts",
26
+ "apiClient.ts",
27
+ "index.ts",
28
+ ];
29
+ /** Compute SHA256 hash of all generated client files in deterministic order */
30
+ export function computeClientHash(baseDir, outSubdir) {
31
+ const outDir = join(baseDir, outSubdir);
32
+ const hash = createHash("sha256");
33
+ for (const relPath of CLIENT_FILE_ORDER) {
34
+ const fullPath = join(outDir, relPath);
35
+ if (existsSync(fullPath)) {
36
+ hash.update(readFileSync(fullPath, "utf-8"));
37
+ }
38
+ }
39
+ const contextsDir = join(outDir, "contexts");
40
+ if (existsSync(contextsDir)) {
41
+ const contextFiles = readdirSync(contextsDir)
42
+ .filter((f) => f.endsWith(".ts"))
43
+ .sort();
44
+ for (const f of contextFiles) {
45
+ hash.update(readFileSync(join(contextsDir, f), "utf-8"));
46
+ }
47
+ }
48
+ return hash.digest("hex");
49
+ }
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { normalizedJsonHash, computeClientHash, } from "./hash.js";
6
+ async function loadRawSpec(urlOrPath) {
7
+ if (urlOrPath.startsWith("http://") || urlOrPath.startsWith("https://")) {
8
+ const res = await fetch(urlOrPath);
9
+ if (!res.ok) {
10
+ throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
11
+ }
12
+ return res.json();
13
+ }
14
+ return JSON.parse(readFileSync(urlOrPath, "utf-8"));
15
+ }
16
+ export async function verify(options = {}) {
17
+ const cwd = options.cwd ?? process.cwd();
18
+ const manifestPath = options.manifestPath ?? join(cwd, "api-client.manifest.json");
19
+ if (!existsSync(manifestPath)) {
20
+ throw new Error("No manifest found. Run `npm run generate` first.");
21
+ }
22
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
23
+ if (!manifest.docsSource ||
24
+ !manifest.docsHash ||
25
+ !manifest.clientHash ||
26
+ !manifest.out) {
27
+ throw new Error("Invalid manifest: missing docsSource, docsHash, clientHash, or out.");
28
+ }
29
+ const rawSpec = await loadRawSpec(manifest.docsSource);
30
+ const currentDocsHash = normalizedJsonHash(rawSpec);
31
+ if (currentDocsHash !== manifest.docsHash) {
32
+ throw new Error("API docs have changed. Run `npm run generate` to regenerate the client, then update your application.");
33
+ }
34
+ const currentClientHash = computeClientHash(cwd, manifest.out);
35
+ if (currentClientHash !== manifest.clientHash) {
36
+ throw new Error("Generated client files were modified. Run `npm run generate` to regenerate.");
37
+ }
38
+ }
39
+ async function main() {
40
+ try {
41
+ await verify();
42
+ }
43
+ catch (err) {
44
+ const message = err instanceof Error ? err.message : String(err);
45
+ console.error(message);
46
+ process.exit(1);
47
+ }
48
+ }
49
+ const __filename = fileURLToPath(import.meta.url);
50
+ if (process.argv[1] === __filename) {
51
+ main();
52
+ }
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@icib.dev/api-client",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Strictly-typed TypeScript API client for ICIB API",
5
5
  "type": "module",
6
6
  "main": "./dist/api/index.js",
7
7
  "types": "./dist/api/index.d.ts",
8
+ "bin": {
9
+ "api-client-generate": "./dist/scripts/generate.js"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./dist/api/index.d.ts",
@@ -17,17 +20,20 @@
17
20
  ],
18
21
  "scripts": {
19
22
  "generate": "tsx scripts/generate.ts",
20
- "build": "tsc -p tsconfig.build.json",
21
- "prepublishOnly": "npm run generate && npm run build"
23
+ "build": "tsx scripts/verify.ts && tsc -p tsconfig.build.json && tsc -p tsconfig.scripts.json",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "prepublishOnly": "npm run build"
22
27
  },
23
28
  "dependencies": {
24
- "axios": "^1.7.0"
29
+ "@apidevtools/swagger-parser": "^12.1.0",
30
+ "axios": "^1.13.6"
25
31
  },
26
32
  "devDependencies": {
27
- "@apidevtools/swagger-parser": "^10.0.3",
28
- "@types/node": "^22.0.0",
29
- "tsx": "^4.19.0",
30
- "typescript": "^5.6.0"
33
+ "@types/node": "^25.5.0",
34
+ "tsx": "^4.21.0",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^4.1.0"
31
37
  },
32
38
  "keywords": [
33
39
  "api-client",