From 2f104d0aec4ba8d112344b274432496225f41da5 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 12:53:43 -0500 Subject: [PATCH 01/14] add .tool-versions for asdf --- .tool-versions | 1 + 1 file changed, 1 insertion(+) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..604be07 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 22.14.0 From e6b650f0a2ccfb924d0bd632bec6ef2044a5838c Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 13:14:24 -0500 Subject: [PATCH 02/14] remove unused packages --- package-lock.json | 687 ---------------------------------------------- 1 file changed, 687 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed8abbe..02cbe4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,7 @@ "@rollup/plugin-node-resolve": "^15.2.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "husky": "^9.0.11", "jest": "^29.7.0", - "lint-staged": "^15.2.2", "prettier": "^3.2.5", "rollup": "^4.12.0" } @@ -3393,39 +3391,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3529,23 +3494,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -3721,26 +3669,6 @@ "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, - "node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3988,13 +3916,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, - "license": "MIT" - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4203,19 +4124,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -4360,22 +4268,6 @@ "node": ">=10.17.0" } }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4485,19 +4377,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-generator-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", @@ -5433,19 +5312,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5453,209 +5319,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lint-staged": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.3.tgz", - "integrity": "sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.4.1", - "commander": "^13.1.0", - "debug": "^4.4.0", - "execa": "^8.0.1", - "lilconfig": "^3.1.3", - "listr2": "^8.2.5", - "micromatch": "^4.0.8", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.7.0" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/lint-staged/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/lint-staged/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/lint-staged/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", - "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5686,117 +5349,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5900,19 +5452,6 @@ "node": ">=6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6152,19 +5691,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -6529,52 +6055,6 @@ "node": ">=10" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6586,13 +6066,6 @@ "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -6752,36 +6225,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6833,16 +6276,6 @@ "node": ">=8" } }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6857,53 +6290,6 @@ "node": ">=10" } }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7219,66 +6605,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7317,19 +6643,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", From 6434f1fd0c2b9a8fad59ff32007a6bb32b6ec7e9 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 13:19:34 -0500 Subject: [PATCH 03/14] [fix] test setup --- test/index.test.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/index.test.js b/test/index.test.js index ece57be..cf9ca78 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -64,7 +64,12 @@ describe('AccessGrid SDK', () => { json: () => Promise.resolve({ message: 'Invalid credentials' }) }); - await expect(client.accessCards.provision({ cardTemplateId: '123' })) + await expect(client.accessCards.provision({ + cardTemplateId: '123', + fullName: 'Test User', + startDate: '2025-01-01T00:00:00Z', + expirationDate: '2025-12-31T00:00:00Z' + })) .rejects .toThrow(AuthenticationError); }); @@ -76,7 +81,12 @@ describe('AccessGrid SDK', () => { json: () => Promise.resolve({ message: 'Invalid input' }) }); - await expect(client.accessCards.provision({ cardTemplateId: '123' })) + await expect(client.accessCards.provision({ + cardTemplateId: '123', + fullName: 'Test User', + startDate: '2025-01-01T00:00:00Z', + expirationDate: '2025-12-31T00:00:00Z' + })) .rejects .toThrow(AccessGridError); }); From cb36e2ec45c9d05afdb9829afb4d63606110b9e6 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 13:23:24 -0500 Subject: [PATCH 04/14] add `format:check` script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a7a37b8..11ef9d2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "test": "jest", "lint": "eslint src", "prepublishOnly": "npm run build", - "format": "prettier --write 'src/**/*.js'" + "format": "prettier --write 'src/**/*.js'", + "format:check": "prettier --check 'src/**/*.js'" }, "keywords": [ "access control", From c010d02f1b66c167d5d32e371bf1c6eb2d9a1854 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 13:23:38 -0500 Subject: [PATCH 05/14] [fix] format index.js --- src/index.js | 300 +++++++++++++++++++++++++++++---------------------- 1 file changed, 170 insertions(+), 130 deletions(-) diff --git a/src/index.js b/src/index.js index 407e855..109e2dc 100644 --- a/src/index.js +++ b/src/index.js @@ -2,14 +2,14 @@ class AccessGridError extends Error { constructor(message) { super(message); - this.name = 'AccessGridError'; + this.name = "AccessGridError"; } } class AuthenticationError extends AccessGridError { - constructor(message = 'Invalid credentials') { + constructor(message = "Invalid credentials") { super(message); - this.name = 'AuthenticationError'; + this.name = "AuthenticationError"; } } @@ -62,8 +62,12 @@ class PassTemplatePair { this.id = data.id; this.name = data.name; this.createdAt = data.created_at; - this.androidTemplate = data.android_template ? new TemplateInfo(data.android_template) : null; - this.iosTemplate = data.ios_template ? new TemplateInfo(data.ios_template) : null; + this.androidTemplate = data.android_template + ? new TemplateInfo(data.android_template) + : null; + this.iosTemplate = data.ios_template + ? new TemplateInfo(data.ios_template) + : null; } } @@ -78,26 +82,34 @@ class TemplateInfo { // Base API wrapper to handle common functionality class BaseApi { - constructor(accountId, secretKey, baseUrl = 'https://api.accessgrid.com') { + constructor(accountId, secretKey, baseUrl = "https://api.accessgrid.com") { this.accountId = accountId; this.secretKey = secretKey; - this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash if present - this.version = '1.2.0'; // Should come from package.json + this.baseUrl = baseUrl.replace(/\/$/, ""); // Remove trailing slash if present + this.version = "1.2.0"; // Should come from package.json } async request(path, options = {}) { const url = `${this.baseUrl}${path}`; - const method = options.method || 'GET'; - + const method = options.method || "GET"; + try { // Extract resource ID from the endpoint if needed for signature let resourceId = null; - if (method === 'GET' || (method === 'POST' && (!options.body || Object.keys(options.body).length === 0))) { + if ( + method === "GET" || + (method === "POST" && + (!options.body || Object.keys(options.body).length === 0)) + ) { // Extract the ID from the endpoint - patterns like /resource/{id} or /resource/{id}/action - const parts = path.split('/').filter(part => part); + const parts = path.split("/").filter((part) => part); if (parts.length >= 2) { // For actions like unlink/suspend/resume, get the card ID (second to last part) - if (['suspend', 'resume', 'unlink', 'delete'].includes(parts[parts.length - 1])) { + if ( + ["suspend", "resume", "unlink", "delete"].includes( + parts[parts.length - 1], + ) + ) { resourceId = parts[parts.length - 2]; } else { // Otherwise, the ID is typically the last part of the path @@ -110,17 +122,17 @@ class BaseApi { let payload; let sigPayload; - if ((method === 'POST' && !options.body) || method === 'GET') { + if ((method === "POST" && !options.body) || method === "GET") { // For these requests, use {"id": "card_id"} as the payload for signature generation if (resourceId) { sigPayload = JSON.stringify({ id: resourceId }); } else { - payload = '{}'; + payload = "{}"; sigPayload = payload; } } else { // For normal POST/PUT/PATCH with body, use the actual payload - payload = options.body ? JSON.stringify(options.body) : ''; + payload = options.body ? JSON.stringify(options.body) : ""; sigPayload = payload; } @@ -129,19 +141,19 @@ class BaseApi { // Prepare headers const headers = { - 'Content-Type': 'application/json', - 'X-ACCT-ID': this.accountId, - 'X-PAYLOAD-SIG': signature, - 'User-Agent': `accessgrid.js @ v${this.version}`, - ...(options.headers || {}) + "Content-Type": "application/json", + "X-ACCT-ID": this.accountId, + "X-PAYLOAD-SIG": signature, + "User-Agent": `accessgrid.js @ v${this.version}`, + ...(options.headers || {}), }; // Handle query parameters for GET requests or POST with empty body let finalUrl = url; - if (method === 'GET' || (method === 'POST' && !options.body)) { + if (method === "GET" || (method === "POST" && !options.body)) { if (resourceId) { // Add sig_payload to query params - const separator = finalUrl.includes('?') ? '&' : '?'; + const separator = finalUrl.includes("?") ? "&" : "?"; finalUrl = `${finalUrl}${separator}sig_payload=${encodeURIComponent(JSON.stringify({ id: resourceId }))}`; } } @@ -150,7 +162,7 @@ class BaseApi { const response = await fetch(finalUrl, { method, headers, - body: method !== 'GET' ? payload : undefined, + body: method !== "GET" ? payload : undefined, }); const data = await response.json(); @@ -159,9 +171,9 @@ class BaseApi { if (response.status === 401) { throw new AuthenticationError(); } else if (response.status === 402) { - throw new AccessGridError('Insufficient account balance'); + throw new AccessGridError("Insufficient account balance"); } else { - throw new AccessGridError(data.message || 'Request failed'); + throw new AccessGridError(data.message || "Request failed"); } } @@ -178,29 +190,31 @@ class BaseApi { try { // Base64 encode the payload const encodedPayload = btoa(payload); - + // Generate SHA256 HMAC const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( - 'raw', + "raw", encoder.encode(this.secretKey), - { name: 'HMAC', hash: 'SHA-256' }, + { name: "HMAC", hash: "SHA-256" }, false, - ['sign'] + ["sign"], ); - + const signature = await crypto.subtle.sign( - 'HMAC', + "HMAC", key, - encoder.encode(encodedPayload) + encoder.encode(encodedPayload), ); // Convert to hex string return Array.from(new Uint8Array(signature)) - .map(b => b.toString(16).padStart(2, '0')) - .join(''); + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); } catch (error) { - throw new AccessGridError(`Failed to generate signature: ${error.message}`); + throw new AccessGridError( + `Failed to generate signature: ${error.message}`, + ); } } } @@ -213,11 +227,13 @@ class AccessCardsApi extends BaseApi { async provision(params) { // Required parameters validation - if (!params.cardTemplateId) throw new AccessGridError('card_template_id is required'); - if (!params.fullName) throw new AccessGridError('full_name is required'); - if (!params.startDate) throw new AccessGridError('start_date is required'); - if (!params.expirationDate) throw new AccessGridError('expiration_date is required'); - + if (!params.cardTemplateId) + throw new AccessGridError("card_template_id is required"); + if (!params.fullName) throw new AccessGridError("full_name is required"); + if (!params.startDate) throw new AccessGridError("start_date is required"); + if (!params.expirationDate) + throw new AccessGridError("expiration_date is required"); + // Start with required parameters const requestBody = { card_template_id: params.cardTemplateId, @@ -225,39 +241,44 @@ class AccessCardsApi extends BaseApi { start_date: params.startDate, expiration_date: params.expirationDate, }; - + // Map camelCase JS params to snake_case API params const paramMapping = { - employeeId: 'employee_id', - tagId: 'tag_id', - phoneNumber: 'phone_number', - employeePhoto: 'employee_photo', - allowOnMultipleDevices: 'allow_on_multiple_devices', - memberId: 'member_id', - membershipStatus: 'membership_status', - isPassReadyToTransact: 'is_pass_ready_to_transact', - tileData: 'tile_data', - reservations: 'reservations', - siteCode: 'site_code', - cardNumber: 'card_number', - fileData: 'file_data', - email: 'email', - classification: 'classification' + employeeId: "employee_id", + tagId: "tag_id", + phoneNumber: "phone_number", + employeePhoto: "employee_photo", + allowOnMultipleDevices: "allow_on_multiple_devices", + memberId: "member_id", + membershipStatus: "membership_status", + isPassReadyToTransact: "is_pass_ready_to_transact", + tileData: "tile_data", + reservations: "reservations", + siteCode: "site_code", + cardNumber: "card_number", + fileData: "file_data", + email: "email", + classification: "classification", }; - + // Add any params that exist to the request body - Object.keys(params).forEach(key => { - if (key !== 'cardTemplateId' && key !== 'fullName' && - key !== 'startDate' && key !== 'expirationDate' && - params[key] !== undefined && params[key] !== null) { + Object.keys(params).forEach((key) => { + if ( + key !== "cardTemplateId" && + key !== "fullName" && + key !== "startDate" && + key !== "expirationDate" && + params[key] !== undefined && + params[key] !== null + ) { const apiKey = paramMapping[key] || key; requestBody[apiKey] = params[key]; } }); - - const response = await this.request('/v1/key-cards', { - method: 'POST', - body: requestBody + + const response = await this.request("/v1/key-cards", { + method: "POST", + body: requestBody, }); return new AccessCard(response); } @@ -269,7 +290,7 @@ class AccessCardsApi extends BaseApi { async get(params) { // Required parameter validation - if (!params.cardId) throw new AccessGridError('card_id is required'); + if (!params.cardId) throw new AccessGridError("card_id is required"); const response = await this.request(`/v1/key-cards/${params.cardId}`); return new AccessCard(response); @@ -277,37 +298,41 @@ class AccessCardsApi extends BaseApi { async update(params) { // Required parameter validation - if (!params.cardId) throw new AccessGridError('card_id is required'); - + if (!params.cardId) throw new AccessGridError("card_id is required"); + // Create empty request body const requestBody = {}; - + // Map camelCase JS params to snake_case API params const paramMapping = { - employeeId: 'employee_id', - fullName: 'full_name', - classification: 'classification', - expirationDate: 'expiration_date', - employeePhoto: 'employee_photo', + employeeId: "employee_id", + fullName: "full_name", + classification: "classification", + expirationDate: "expiration_date", + employeePhoto: "employee_photo", // Hotel-specific parameters - memberId: 'member_id', - membershipStatus: 'membership_status', - isPassReadyToTransact: 'is_pass_ready_to_transact', - tileData: 'tile_data', - reservations: 'reservations' + memberId: "member_id", + membershipStatus: "membership_status", + isPassReadyToTransact: "is_pass_ready_to_transact", + tileData: "tile_data", + reservations: "reservations", }; - + // Add any params that exist to the request body - Object.keys(params).forEach(key => { - if (key !== 'cardId' && params[key] !== undefined && params[key] !== null) { + Object.keys(params).forEach((key) => { + if ( + key !== "cardId" && + params[key] !== undefined && + params[key] !== null + ) { const apiKey = paramMapping[key] || key; requestBody[apiKey] = params[key]; } }); - + const response = await this.request(`/v1/key-cards/${params.cardId}`, { - method: 'PATCH', - body: requestBody + method: "PATCH", + body: requestBody, }); return new AccessCard(response); } @@ -315,34 +340,34 @@ class AccessCardsApi extends BaseApi { async list(templateId, state = null) { const params = new URLSearchParams({ template_id: templateId }); if (state) { - params.append('state', state); + params.append("state", state); } - + const response = await this.request(`/v1/key-cards?${params.toString()}`); - return (response.keys || []).map(item => new AccessCard(item)); + return (response.keys || []).map((item) => new AccessCard(item)); } async manage(cardId, action) { const response = await this.request(`/v1/key-cards/${cardId}/${action}`, { - method: 'POST' + method: "POST", }); return new AccessCard(response); } async suspend(params) { - return this.manage(params.cardId, 'suspend'); + return this.manage(params.cardId, "suspend"); } async resume(params) { - return this.manage(params.cardId, 'resume'); + return this.manage(params.cardId, "resume"); } async unlink(params) { - return this.manage(params.cardId, 'unlink'); + return this.manage(params.cardId, "unlink"); } async delete(params) { - return this.manage(params.cardId, 'delete'); + return this.manage(params.cardId, "delete"); } } @@ -353,8 +378,8 @@ class ConsoleApi extends BaseApi { } async createTemplate(params) { - const response = await this.request('/v1/console/card-templates', { - method: 'POST', + const response = await this.request("/v1/console/card-templates", { + method: "POST", body: { name: params.name, platform: params.platform, @@ -370,45 +395,56 @@ class ConsoleApi extends BaseApi { support_phone_number: params.supportInfo?.supportPhoneNumber, support_email: params.supportInfo?.supportEmail, privacy_policy_url: params.supportInfo?.privacyPolicyUrl, - terms_and_conditions_url: params.supportInfo?.termsAndConditionsUrl - } + terms_and_conditions_url: params.supportInfo?.termsAndConditionsUrl, + }, }); return new Template(response); } async updateTemplate(params) { - const response = await this.request(`/v1/console/card-templates/${params.cardTemplateId}`, { - method: 'PUT', - body: { - name: params.name, - allow_on_multiple_devices: params.allowOnMultipleDevices, - watch_count: params.watchCount, - iphone_count: params.iphoneCount, - support_url: params.supportInfo?.supportUrl, - support_phone_number: params.supportInfo?.supportPhoneNumber, - support_email: params.supportInfo?.supportEmail, - privacy_policy_url: params.supportInfo?.privacyPolicyUrl, - terms_and_conditions_url: params.supportInfo?.termsAndConditionsUrl - } - }); + const response = await this.request( + `/v1/console/card-templates/${params.cardTemplateId}`, + { + method: "PUT", + body: { + name: params.name, + allow_on_multiple_devices: params.allowOnMultipleDevices, + watch_count: params.watchCount, + iphone_count: params.iphoneCount, + support_url: params.supportInfo?.supportUrl, + support_phone_number: params.supportInfo?.supportPhoneNumber, + support_email: params.supportInfo?.supportEmail, + privacy_policy_url: params.supportInfo?.privacyPolicyUrl, + terms_and_conditions_url: params.supportInfo?.termsAndConditionsUrl, + }, + }, + ); return new Template(response); } async readTemplate(params) { - const response = await this.request(`/v1/console/card-templates/${params.cardTemplateId}`); + const response = await this.request( + `/v1/console/card-templates/${params.cardTemplateId}`, + ); return new Template(response); } async getEventLogs(params) { const queryParams = new URLSearchParams(); if (params.filters) { - if (params.filters.device) queryParams.append('filters[device]', params.filters.device); - if (params.filters.startDate) queryParams.append('filters[start_date]', params.filters.startDate); - if (params.filters.endDate) queryParams.append('filters[end_date]', params.filters.endDate); - if (params.filters.eventType) queryParams.append('filters[event_type]', params.filters.eventType); + if (params.filters.device) + queryParams.append("filters[device]", params.filters.device); + if (params.filters.startDate) + queryParams.append("filters[start_date]", params.filters.startDate); + if (params.filters.endDate) + queryParams.append("filters[end_date]", params.filters.endDate); + if (params.filters.eventType) + queryParams.append("filters[event_type]", params.filters.eventType); } - - return this.request(`/v1/console/card-templates/${params.cardTemplateId}/logs?${queryParams}`); + + return this.request( + `/v1/console/card-templates/${params.cardTemplateId}/logs?${queryParams}`, + ); } // Alias for getEventLogs for backwards compatibility @@ -418,16 +454,20 @@ class ConsoleApi extends BaseApi { async listPassTemplatePairs(params = {}) { const queryParams = new URLSearchParams(); - if (params.page) queryParams.append('page', params.page); - if (params.perPage) queryParams.append('per_page', params.perPage); + if (params.page) queryParams.append("page", params.page); + if (params.perPage) queryParams.append("per_page", params.perPage); const queryString = queryParams.toString(); - const path = queryString ? `/v1/console/pass-template-pairs?${queryString}` : '/v1/console/pass-template-pairs'; + const path = queryString + ? `/v1/console/pass-template-pairs?${queryString}` + : "/v1/console/pass-template-pairs"; const response = await this.request(path); if (response.pass_template_pairs) { - response.passTemplatePairs = response.pass_template_pairs.map(pair => new PassTemplatePair(pair)); + response.passTemplatePairs = response.pass_template_pairs.map( + (pair) => new PassTemplatePair(pair), + ); delete response.pass_template_pairs; } @@ -438,11 +478,11 @@ class ConsoleApi extends BaseApi { // Main AccessGrid class class AccessGrid { constructor(accountId, secretKey, options = {}) { - if (!accountId) throw new Error('Account ID is required'); - if (!secretKey) throw new Error('Secret Key is required'); - - const baseUrl = options.baseUrl || 'https://api.accessgrid.com'; - + if (!accountId) throw new Error("Account ID is required"); + if (!secretKey) throw new Error("Secret Key is required"); + + const baseUrl = options.baseUrl || "https://api.accessgrid.com"; + this.accessCards = new AccessCardsApi(accountId, secretKey, baseUrl); this.console = new ConsoleApi(accountId, secretKey, baseUrl); } @@ -456,8 +496,8 @@ export { AccessCard, Template, PassTemplatePair, - TemplateInfo + TemplateInfo, }; // Default export -export default AccessGrid; \ No newline at end of file +export default AccessGrid; From c516b4f436b3beecfdf9e67acdc8fa58fd17c685 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 13:35:45 -0500 Subject: [PATCH 06/14] Create ci.yml --- .github/workflows/ci.yml | 69 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1880f33 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node: ['20', '22', '24'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Lint code + run: npm run lint + + format: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Check formatting + run: npm run format:check From ca9603a5c0a38b116b7dc9aeec6bc950e81707ba Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 13:35:52 -0500 Subject: [PATCH 07/14] Create node-versions.test.js --- test/config/node-versions.test.js | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 test/config/node-versions.test.js diff --git a/test/config/node-versions.test.js b/test/config/node-versions.test.js new file mode 100644 index 0000000..0e9cd70 --- /dev/null +++ b/test/config/node-versions.test.js @@ -0,0 +1,101 @@ +const fs = require('fs'); +const path = require('path'); + +// ══════════════════════════════════════════════════════════════════════════════ +// TARGET VERSION - Update this when upgrading Node.js +// ══════════════════════════════════════════════════════════════════════════════ +const TARGET_NODE = '22.14.0'; + +const root = path.resolve(__dirname, '..', '..'); + +function readToolVersions() { + const content = fs.readFileSync(path.join(root, '.tool-versions'), 'utf-8'); + const versions = {}; + content.split('\n').filter(Boolean).forEach(line => { + const [tool, version] = line.split(/\s+/, 2); + versions[tool] = version; + }); + return versions; +} + +function readCIJobs() { + const content = fs.readFileSync( + path.join(root, '.github', 'workflows', 'ci.yml'), + 'utf-8' + ); + + // Split into jobs by matching top-level job keys (2-space indented) + const jobBlocks = content.split(/\n (?=\w[\w-]*:)/); + const jobs = {}; + + jobBlocks.forEach(block => { + const nameMatch = block.match(/^([\w-]+):/m); + if (!nameMatch) return; + const jobName = nameMatch[1]; + + const versions = []; + + // Matrix arrays: node: ['20', '22', '24'] + const matrixMatch = block.match(/node:\s*\[([^\]]+)\]/); + if (matrixMatch) { + matrixMatch[1].split(',').forEach(v => { + versions.push(v.trim().replace(/['"]/g, '')); + }); + } + + // Standalone: node-version: '22' + const standaloneMatches = [...block.matchAll(/node-version:\s*['"]?(\d+)['"]?/g)]; + standaloneMatches.forEach(m => { + if (!versions.includes(m[1])) { + versions.push(m[1]); + } + }); + + if (versions.length > 0) { + jobs[jobName] = versions; + } + }); + + return jobs; +} + +function majorVersion(version) { + return version.split('.')[0]; +} + +describe('Node.js versions', () => { + describe('.tool-versions', () => { + test('node version matches target', () => { + const toolVersions = readToolVersions(); + expect(toolVersions.nodejs).toBe(TARGET_NODE); + }); + }); + + describe('CI workflow', () => { + const jobs = readCIJobs(); + const targetMajor = majorVersion(TARGET_NODE); + + test.each(Object.entries(jobs))( + '%s job includes target major version (%s)', + (jobName, versions) => { + expect(versions).toContain(targetMajor); + } + ); + + test.each(Object.entries(jobs))( + '%s job only contains valid major versions', + (jobName, versions) => { + versions.forEach(v => { + expect(v).toMatch(/^\d+$/); + }); + } + ); + + test('.tool-versions major is tested in every CI job', () => { + const toolMajor = majorVersion(readToolVersions().nodejs); + Object.entries(jobs).forEach(([jobName, versions]) => { + expect(versions).toContain(toolMajor); + }); + }); + }); +}); From 5b563d02d648dbf34218003c1c83da87e45f2f0a Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 13:37:16 -0500 Subject: [PATCH 08/14] verbose test output in ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1880f33..1dbc175 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: run: npm ci - name: Run tests - run: npm test + run: npm test -- --verbose lint: runs-on: ubuntu-latest From b292bef888a7266a0b9f5d8ff7397438dc6bafb8 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 13:51:26 -0500 Subject: [PATCH 09/14] [reorg] --- test/index.test.js | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/test/index.test.js b/test/index.test.js index cf9ca78..c9721f9 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,6 +1,9 @@ import AccessGrid, { AccessGridError, AuthenticationError, AccessCard, Template } from '../src/index'; -// Mock fetch globally +// ══════════════════════════════════════════════════════════════════════════════ +// Global mocks +// ══════════════════════════════════════════════════════════════════════════════ + global.fetch = jest.fn(); global.crypto = { subtle: { @@ -17,20 +20,17 @@ describe('AccessGrid SDK', () => { let client; const mockAccountId = 'test-account-id'; const mockSecretKey = 'test-secret-key'; - + beforeEach(() => { client = new AccessGrid(mockAccountId, mockSecretKey); - // Clear all mocks before each test jest.clearAllMocks(); - - // Setup default crypto mocks + global.crypto.subtle.importKey.mockResolvedValue('mockKey'); global.crypto.subtle.sign.mockResolvedValue(new Uint8Array([1, 2, 3])); - - // Setup default successful response + global.fetch.mockResolvedValue({ ok: true, - json: () => Promise.resolve({ + json: () => Promise.resolve({ id: 'mock-id', install_url: 'https://example.com/install', state: 'active', @@ -39,6 +39,10 @@ describe('AccessGrid SDK', () => { }); }); + // ════════════════════════════════════════════════════════════════════════════ + // Constructor + // ════════════════════════════════════════════════════════════════════════════ + describe('Constructor', () => { test('should throw error if accountId is missing', () => { expect(() => new AccessGrid()).toThrow('Account ID is required'); @@ -56,6 +60,10 @@ describe('AccessGrid SDK', () => { }); }); + // ════════════════════════════════════════════════════════════════════════════ + // Error handling + // ════════════════════════════════════════════════════════════════════════════ + describe('Error Handling', () => { test('should throw AuthenticationError on 401', async () => { global.fetch.mockResolvedValueOnce({ @@ -92,6 +100,10 @@ describe('AccessGrid SDK', () => { }); }); + // ════════════════════════════════════════════════════════════════════════════ + // AccessCards API + // ════════════════════════════════════════════════════════════════════════════ + describe('AccessCards API', () => { describe('provision', () => { const mockProvisionParams = { @@ -165,7 +177,7 @@ describe('AccessGrid SDK', () => { }); const result = await client.accessCards.list(templateId); - + expect(fetch).toHaveBeenCalledWith( expect.stringContaining(`/v1/key-cards?template_id=${templateId}`), expect.anything() @@ -183,7 +195,7 @@ describe('AccessGrid SDK', () => { }); await client.accessCards.list(templateId, state); - + expect(fetch).toHaveBeenCalledWith( expect.stringContaining(`template_id=${templateId}&state=${state}`), expect.anything() @@ -240,6 +252,10 @@ describe('AccessGrid SDK', () => { }); }); + // ════════════════════════════════════════════════════════════════════════════ + // Console API + // ════════════════════════════════════════════════════════════════════════════ + describe('Console API', () => { describe('createTemplate', () => { const mockTemplateParams = { @@ -358,6 +374,10 @@ describe('AccessGrid SDK', () => { }); }); + // ════════════════════════════════════════════════════════════════════════════ + // Models + // ════════════════════════════════════════════════════════════════════════════ + describe('Model classes', () => { test('AccessCard should have correct properties', () => { const card = new AccessCard({ @@ -398,4 +418,4 @@ describe('AccessGrid SDK', () => { expect(template.activeKeysCount).toBe(8); }); }); -}); \ No newline at end of file +}); From 51677bf3704ef6cb7c3e7638316863bc7a7ebb5c Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 14:25:14 -0500 Subject: [PATCH 10/14] add tests for listPassTemplatePairs --- test/index.test.js | 126 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/test/index.test.js b/test/index.test.js index c9721f9..12eeea5 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,4 +1,4 @@ -import AccessGrid, { AccessGridError, AuthenticationError, AccessCard, Template } from '../src/index'; +import AccessGrid, { AccessGridError, AuthenticationError, AccessCard, Template, PassTemplatePair, TemplateInfo } from '../src/index'; // ══════════════════════════════════════════════════════════════════════════════ // Global mocks @@ -372,6 +372,130 @@ describe('AccessGrid SDK', () => { expect(spy).toHaveBeenCalledWith(mockEventParams); }); }); + + describe('listPassTemplatePairs', () => { + const mockPairsResponse = { + pass_template_pairs: [ + { + id: 'pair-1', + name: 'Employee Badge Pair', + created_at: '2025-01-15T00:00:00Z', + ios_template: { id: 'ios-1', name: 'iOS Badge', platform: 'apple' }, + android_template: { id: 'android-1', name: 'Android Badge', platform: 'google' } + }, + { + id: 'pair-2', + name: 'Visitor Pass Pair', + created_at: '2025-02-01T00:00:00Z', + ios_template: { id: 'ios-2', name: 'iOS Visitor', platform: 'apple' }, + android_template: null + } + ] + }; + + test('should make correct API call', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockPairsResponse) + }); + + await client.console.listPassTemplatePairs(); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/console/pass-template-pairs'), + expect.objectContaining({ + method: 'GET' + }) + ); + }); + + test('should pass pagination params', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockPairsResponse) + }); + + await client.console.listPassTemplatePairs({ page: 2, perPage: 10 }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('page=2'), + expect.anything() + ); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('per_page=10'), + expect.anything() + ); + }); + + test('should not include query string when no params given', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockPairsResponse) + }); + + await client.console.listPassTemplatePairs(); + + const calledUrl = fetch.mock.calls[0][0]; + expect(calledUrl).toMatch(/\/pass-template-pairs(\?sig_payload=|$)/); + }); + + test('should return PassTemplatePair instances', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockPairsResponse) + }); + + const result = await client.console.listPassTemplatePairs(); + + expect(result.passTemplatePairs).toHaveLength(2); + expect(result.passTemplatePairs[0]).toBeInstanceOf(PassTemplatePair); + expect(result.passTemplatePairs[1]).toBeInstanceOf(PassTemplatePair); + }); + + test('should remove snake_case key from response', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockPairsResponse) + }); + + const result = await client.console.listPassTemplatePairs(); + + expect(result.pass_template_pairs).toBeUndefined(); + }); + + test('should deserialize nested TemplateInfo models', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockPairsResponse) + }); + + const result = await client.console.listPassTemplatePairs(); + const pair = result.passTemplatePairs[0]; + + expect(pair.id).toBe('pair-1'); + expect(pair.name).toBe('Employee Badge Pair'); + expect(pair.createdAt).toBe('2025-01-15T00:00:00Z'); + expect(pair.iosTemplate).toBeInstanceOf(TemplateInfo); + expect(pair.iosTemplate.id).toBe('ios-1'); + expect(pair.iosTemplate.platform).toBe('apple'); + expect(pair.androidTemplate).toBeInstanceOf(TemplateInfo); + expect(pair.androidTemplate.id).toBe('android-1'); + expect(pair.androidTemplate.platform).toBe('google'); + }); + + test('should handle null template in a pair', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockPairsResponse) + }); + + const result = await client.console.listPassTemplatePairs(); + const pair = result.passTemplatePairs[1]; + + expect(pair.iosTemplate).toBeInstanceOf(TemplateInfo); + expect(pair.androidTemplate).toBeNull(); + }); + }); }); // ════════════════════════════════════════════════════════════════════════════ From 498a33e15b939bb9c3c6a08b414d43c1bfd30c82 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 14:36:02 -0500 Subject: [PATCH 11/14] add tests for `accessCards.get()` --- test/index.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/index.test.js b/test/index.test.js index 12eeea5..1426320 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -143,6 +143,33 @@ describe('AccessGrid SDK', () => { }); }); + describe('get', () => { + const mockCardId = '0xc4rd1d'; + + test('should make correct API call', async () => { + await client.accessCards.get({ cardId: mockCardId }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining(`/v1/key-cards/${mockCardId}`), + expect.objectContaining({ + method: 'GET' + }) + ); + }); + + test('should return an AccessCard instance', async () => { + const result = await client.accessCards.get({ cardId: mockCardId }); + expect(result).toBeInstanceOf(AccessCard); + expect(result.id).toBe('mock-id'); + }); + + test('should throw when cardId is missing', async () => { + await expect(client.accessCards.get({})) + .rejects + .toThrow('card_id is required'); + }); + }); + describe('update', () => { const mockUpdateParams = { cardId: '0xc4rd1d', From a02f6d872e4f7f9fb4f451e93ec6abd6c39034d8 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 14:42:02 -0500 Subject: [PATCH 12/14] add tests for `updateTemplate` --- test/index.test.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/index.test.js b/test/index.test.js index 1426320..229c9d6 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -332,6 +332,75 @@ describe('AccessGrid SDK', () => { }); }); + describe('updateTemplate', () => { + const mockTemplateId = '0xt3mpl4t3'; + const mockUpdateParams = { + cardTemplateId: mockTemplateId, + name: 'Updated Badge', + allowOnMultipleDevices: false, + watchCount: 1, + iphoneCount: 2, + supportInfo: { + supportUrl: 'https://help.example.com', + supportPhoneNumber: '+1-555-999-0000', + supportEmail: 'help@example.com', + privacyPolicyUrl: 'https://example.com/privacy', + termsAndConditionsUrl: 'https://example.com/terms' + } + }; + + test('should make correct API call with PUT', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ id: mockTemplateId, name: 'Updated Badge' }) + }); + + await client.console.updateTemplate(mockUpdateParams); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining(`/v1/console/card-templates/${mockTemplateId}`), + expect.objectContaining({ + method: 'PUT', + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + 'X-ACCT-ID': mockAccountId + }) + }) + ); + }); + + test('should map camelCase params to snake_case body', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ id: mockTemplateId }) + }); + + await client.console.updateTemplate(mockUpdateParams); + + const callBody = JSON.parse(fetch.mock.calls[0][1].body); + expect(callBody.name).toBe('Updated Badge'); + expect(callBody.allow_on_multiple_devices).toBe(false); + expect(callBody.watch_count).toBe(1); + expect(callBody.iphone_count).toBe(2); + expect(callBody.support_url).toBe('https://help.example.com'); + expect(callBody.support_phone_number).toBe('+1-555-999-0000'); + expect(callBody.support_email).toBe('help@example.com'); + expect(callBody.privacy_policy_url).toBe('https://example.com/privacy'); + expect(callBody.terms_and_conditions_url).toBe('https://example.com/terms'); + }); + + test('should return a Template instance', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ id: mockTemplateId, name: 'Updated Badge' }) + }); + + const result = await client.console.updateTemplate(mockUpdateParams); + expect(result).toBeInstanceOf(Template); + expect(result.id).toBe(mockTemplateId); + }); + }); + describe('readTemplate', () => { const mockTemplateId = '0xd3adb00b5'; From f92ccc6c869370d2414574f794183c646fe3eac9 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 14:48:01 -0500 Subject: [PATCH 13/14] [backfill] --- test/index.test.js | 70 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/index.test.js b/test/index.test.js index 229c9d6..e9f31bd 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -82,6 +82,23 @@ describe('AccessGrid SDK', () => { .toThrow(AuthenticationError); }); + test('should throw AccessGridError with message on 402', async () => { + global.fetch.mockResolvedValueOnce({ + ok: false, + status: 402, + json: () => Promise.resolve({ message: 'Insufficient account balance' }) + }); + + await expect(client.accessCards.provision({ + cardTemplateId: '123', + fullName: 'Test User', + startDate: '2025-01-01T00:00:00Z', + expirationDate: '2025-12-31T00:00:00Z' + })) + .rejects + .toThrow('Insufficient account balance'); + }); + test('should throw AccessGridError on other errors', async () => { global.fetch.mockResolvedValueOnce({ ok: false, @@ -143,6 +160,20 @@ describe('AccessGrid SDK', () => { }); }); + describe('issue', () => { + test('issue is an alias for provision', async () => { + const spy = jest.spyOn(client.accessCards, 'provision'); + const params = { + cardTemplateId: '123', + fullName: 'Test User', + startDate: '2025-01-01T00:00:00Z', + expirationDate: '2025-12-31T00:00:00Z' + }; + await client.accessCards.issue(params); + expect(spy).toHaveBeenCalledWith(params); + }); + }); + describe('get', () => { const mockCardId = '0xc4rd1d'; @@ -637,5 +668,44 @@ describe('AccessGrid SDK', () => { expect(template.issuedKeysCount).toBe(10); expect(template.activeKeysCount).toBe(8); }); + + test('PassTemplatePair should have correct properties', () => { + const pair = new PassTemplatePair({ + id: 'pair-1', + name: 'Badge Pair', + created_at: '2025-03-01T00:00:00Z', + ios_template: { id: 'ios-1', name: 'iOS Badge', platform: 'apple' }, + android_template: { id: 'android-1', name: 'Android Badge', platform: 'google' } + }); + + expect(pair.id).toBe('pair-1'); + expect(pair.name).toBe('Badge Pair'); + expect(pair.createdAt).toBe('2025-03-01T00:00:00Z'); + expect(pair.iosTemplate).toBeInstanceOf(TemplateInfo); + expect(pair.androidTemplate).toBeInstanceOf(TemplateInfo); + }); + + test('PassTemplatePair handles missing templates', () => { + const pair = new PassTemplatePair({ + id: 'pair-2', + name: 'iOS Only', + created_at: '2025-03-01T00:00:00Z' + }); + + expect(pair.iosTemplate).toBeNull(); + expect(pair.androidTemplate).toBeNull(); + }); + + test('TemplateInfo should have correct properties', () => { + const info = new TemplateInfo({ + id: 'tmpl-1', + name: 'Employee Badge', + platform: 'apple' + }); + + expect(info.id).toBe('tmpl-1'); + expect(info.name).toBe('Employee Badge'); + expect(info.platform).toBe('apple'); + }); }); }); From 68bdb77d57b5fa54a42576d6a8a598731fa2a782 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Tue, 3 Mar 2026 14:51:50 -0500 Subject: [PATCH 14/14] [fix] limit CI --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1dbc175..228c4cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,9 @@ name: CI -on: [push, pull_request] +on: + pull_request: + push: + branches: main concurrency: group: ${{ github.workflow }}-${{ github.ref }}