Initial Commit
Some checks failed
Publish to Private NPM Registry / publish (push) Failing after 1m0s

This commit is contained in:
Alan Bridgeman 2026-02-17 13:44:22 -06:00
commit 18cc482293
12 changed files with 4353 additions and 0 deletions

View file

@ -0,0 +1,109 @@
name: Publish to Private NPM Registry
on:
push:
branches:
- main
workflow_dispatch:
jobs:
publish:
runs-on: default
steps:
# Checkout the repository
- name: Checkout code
uses: actions/checkout@v3
# Set up NPM Auth Token
- name: Set up NPM Auth Token
run: echo "NODE_AUTH_TOKEN=${{ secrets.NPM_TOKEN }}" >> $GITHUB_ENV
# Set up Node.js
- name: Set up Node.js version
uses: actions/setup-node@v3
with:
# Taken from [Repo README](https://github.com/actions/setup-node#readme)
#
# > Version Spec of the version to use in SemVer notation.
# > It also admits such aliases as lts/*, latest, nightly and canary builds
# > Examples: 12.x, 10.15.1, >=10.15.0, lts/Hydrogen, 16-nightly, latest, node
node-version: '20.x'
# Taken from [Repo README](https://github.com/actions/setup-node#readme)
#
# > Optional registry to set up for auth. Will set the registry in a project level .npmrc and .yarnrc file,
# > and set up auth to read in from env.NODE_AUTH_TOKEN.
# > Default: ''
registry-url: 'https://npm.pkg.bridgemanaccessible.ca'
# Taken from [Repo README](https://github.com/actions/setup-node#readme)
#
# > Optional scope for authenticating against scoped registries.
# > Will fall back to the repository owner when using the GitHub Packages registry (https://npm.pkg.github.com/).
scope: '@BridgemanAccessible'
# Transpile/Build the package (TypeScript -> JavaScript)
- name: Transpile/Build the package (TypeScript -> JavaScript)
run: |
# Because Yarn is used locally better to install and use it than have to debug weird inconsistencies
npm install --global yarn
# Install needed dependencies
yarn install
# Run tests to ensure the package is working as expected before publishing
yarn test
# Build the package
yarn build
- name: Determine Version and Increment (if needed)
id: version_check
run: |
VERSION=$(node -p "require('./package.json').version")
echo "Version: $VERSION"
NAME=$(node -p "require('./package.json').name")
LATEST_VERSION=$(npm show $NAME version --registry https://npm.pkg.bridgemanaccessible.ca)
echo "Latest version: $LATEST_VERSION"
if [ "$LATEST_VERSION" != "$VERSION" ]; then
echo "Manually updated version detected: $VERSION"
else
NEW_VERSION=$(npm version patch --no-git-tag-version)
echo "New version: $NEW_VERSION"
echo "new_version=$NEW_VERSION" >> $GITHUB_ENV
echo "version_changed=true" >> $GITHUB_OUTPUT
fi
- name: Commit Version Change (if needed)
if: steps.version_check.outputs.version_changed == 'true'
run: |
# Update remote URL to use the GITHUB_TOKEN for authentication
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@git.bridgemanaccessible.ca/${{ github.repository }}.git
# Setup git user details for committing the version change
git config user.name "Forgejo Actions"
git config user.email "actions@git.bridgemanaccessible.ca"
# Commit the version change to the `package.json` file
git add package.json
git commit -m "[Forgejo Actions] Update version to ${{ env.new_version }}"
# Push the changes to the repository
git push origin HEAD:main
# Publish to private NPM registry
- name: Publish the package
run: |
# Copy over the files to the build output (`dist`) folder
cp package.json dist/package.json
cp README.md dist/README.md
cp LICENSE dist/LICENSE
# Change directory to the build output (`dist`) folder
cd dist
# Publish the package to the private NPM registry
npm publish --registry http://npm.pkg.bridgemanaccessible.ca/

16
.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
# Node modules
node_modules
# Yarn stuff
.yarn/
.yarnrc.yml
# Credentials/Secrets
.npmrc
# Build output
dist
# Automation Scripts
unpublish-publish.ps1
unpublish-publish.sh

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Bridgeman Accessible
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# File based Implementation of bridgeman Accessible Auth Keystore
This is the file based implementation of the Bridgeman Accessible Auth Keystore.

36
jest.config.cjs Normal file
View file

@ -0,0 +1,36 @@
module.exports = {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
// Tell Jest where to look for source code and tests
roots: ['<rootDir>/src', '<rootDir>/tests'],
// Explicitly match files in the tests folder
testMatch: [
'<rootDir>/tests/**/*.test.ts',
'<rootDir>/tests/**/*.spec.ts'
],
// Tell Jest to treat .ts files as ESM modules
extensionsToTreatAsEsm: ['.ts'],
// Fixes the "Cannot find module" error
moduleNameMapper: {
// If the import starts with . or .. and ends with .js, strip the .js
'^(\\.{1,2}/.*)\\.js$': '$1',
// Map the library to mock file
// This tells Jest to load simple JS file instead of parsing the complex node_module
//'^@BridgemanAccessible/ba-auth$': '<rootDir>/tests/__mocks__/ba-auth.js'
},
// Configure the transformer to enable ESM support
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
}
]
}
};

36
package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "@BridgemanAccessible/ba-auth_keystore_file",
"version": "1.0.0",
"description": "A file based Keystore implementation for use in combination with the Bridgeman Accessible Auth Package",
"main": "index.js",
"types": "index.d.ts",
"repository": "https://git.bridgemanaccessible.ca/Bridgeman-Accessible/ba-auth_keystore_file.git",
"author": "Bridgeman Accessible<info@bridgemanaccessible.ca>",
"type": "module",
"exports": {
".": {
"default": "./index.js",
"types": "./index.d.ts"
}
},
"license": "MIT",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^25.2.3",
"@types/node-jose": "^1.1.13",
"cross-env": "^10.1.0",
"jest": "^30.2.0",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3"
},
"dependencies": {
"@BridgemanAccessible/ba-auth": "^1.0.30",
"@BridgemanAccessible/ba-logging": "^1.0.1",
"node-jose": "^2.2.0"
},
"packageManager": "yarn@1.22.22"
}

89
src/FileKeystore.ts Normal file
View file

@ -0,0 +1,89 @@
import path from 'path';
import fs from 'fs';
import NodeJose from 'node-jose';
import type { JWK as JWKTypes } from 'node-jose';
import { BaseKeystore, Keystore } from '@BridgemanAccessible/ba-auth/keystore';
const { JWK } = NodeJose;
/**
* File implementation for the keystore
*/
export class FileKeystore extends BaseKeystore {
private keystoreFilePath: string | undefined;
/**
* Create a new keystore from a file
*
* @param keystoreFile The name of the keystore file
* @param certDir The directory that contains the keystore file
*/
private constructor() {
// Call the super constructor
super();
}
/** Get the file path of the keystore */
getKeystoreFilePath(): string {
if(typeof this.keystoreFilePath === 'undefined') {
throw new Error('Keystore file path not set');
}
return this.keystoreFilePath;
}
/**
* Setup/Load the keystore
*
* If the keystore file doesn't exist, it will be created (including generating an initial signing and encryption key pair)
* If the keystore file does exist, it will be loaded
*
* @param keystoreFile The name of the keystore file
* @param certDir The directory to save the keystore file in
* @returns The keystore object
*/
private async setupKeystore(keystoreFile: string, certDir: string): Promise<JWKTypes.KeyStore> {
// Combine the directory and the file into a singular file path
this.keystoreFilePath = path.join(certDir, keystoreFile);
// Create a variable to hold the keystore
let keystore: JWKTypes.KeyStore;
// Check if the keystore file already exists
if (!fs.existsSync(this.keystoreFilePath)) {
// Because the keystore file doesn't exist, check if the cert directory exists
if (!fs.existsSync(certDir)) {
// Because the directory doesn't exist, create it
fs.mkdirSync(certDir)
}
// Create a new keystore
keystore = JWK.createKeyStore();
// Generate two new RSA key pairs (one for signing the other foe encryption)
const signingKey = await keystore.generate('RSA', 2048, { alg: 'RS256', use: 'sig' });
//const encryptionKey = await keystore.generate('RSA', 2048, { enc: 'A128CBC-HS256', use: 'enc' });
const encryptionKey = await keystore.generate('RSA', 2048, { /*alg: 'RS-OAEP', enc: 'A128CBC-HS256',*/ use: 'enc' });
// Save the keystore to a file
fs.writeFileSync(this.keystoreFilePath, JSON.stringify(keystore.toJSON(true), null, 4));
}
else {
// Load the keystore from the file
keystore = await JWK.asKeyStore(JSON.parse(fs.readFileSync(this.keystoreFilePath, 'utf-8')));
}
return keystore;
}
static async create(keystoreFile: string = 'keystore.json', certDir: string = '.cert') {
const keystore = new FileKeystore();
keystore.keystore = await keystore.setupKeystore(keystoreFile, certDir);
return keystore;
}
}
Keystore.addKeystoreType('file', FileKeystore.create);

3
src/index.ts Normal file
View file

@ -0,0 +1,3 @@
import { FileKeystore } from './FileKeystore.js';
export { FileKeystore };

147
tests/FileKeystore.test.ts Normal file
View file

@ -0,0 +1,147 @@
import path from 'path';
import NodeJose from 'node-jose';
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
import type { FileKeystore } from '../src/FileKeystore.js'; // Import TYPE only (safe in ESM)
// We use unstable_mockModule because standard jest.mock struggles to intercept
// core Node modules (like fs) in strict ESM mode.
jest.unstable_mockModule('fs', () => ({
// We must mock the 'default' export because your code uses: import fs from 'fs'
default: {
existsSync: jest.fn(),
mkdirSync: jest.fn(),
writeFileSync: jest.fn(),
readFileSync: jest.fn(),
},
// We also mock named exports purely for safety, though 'default' is the key one here
existsSync: jest.fn(),
mkdirSync: jest.fn(),
writeFileSync: jest.fn(),
readFileSync: jest.fn(),
}));
// Mock dependency
jest.unstable_mockModule('@BridgemanAccessible/ba-auth/keystore', () => ({
BaseKeystore: class {},
Keystore: {
addKeystoreType: jest.fn(),
},
}));
describe('FileKeystore', () => {
// Variables to hold our dynamically imported modules
let FileKeystoreClass: typeof FileKeystore;
let mockFs: any;
const TEST_DIR = '.test-cert';
const TEST_FILE = 'test-keystore.json';
const FULL_PATH = path.join(TEST_DIR, TEST_FILE);
beforeEach(async () => {
jest.clearAllMocks();
// 3. DYNAMICALLY IMPORT THE MODULES
// This is the magic. We import AFTER the mock is defined.
// This ensures 'fs' inside FileKeystore.ts is actually our mock.
const fsModule = await import('fs');
mockFs = fsModule.default; // Access the default export we mocked above
// Import your class under test
// Note: You might need to add '.js' to the path if your resolving is strict
const module = await import('../src/FileKeystore.js');
FileKeystoreClass = module.FileKeystore;
});
test('should create a NEW keystore if the file does not exist', async () => {
// SETUP:
// 1. Pretend the file does NOT exist
mockFs.existsSync.mockReturnValue(false);
// 2. Mock writeFileSync to do nothing (just verify it was called)
mockFs.writeFileSync.mockImplementation(() => {});
// 3. Mock mkdirSync
mockFs.mkdirSync.mockImplementation(() => undefined);
// EXECUTE
const keystoreInstance = await FileKeystoreClass.create(TEST_FILE, TEST_DIR);
// VERIFY
// 1. Verify it checked for file existence
expect(mockFs.existsSync).toHaveBeenCalledWith(FULL_PATH);
// 2. Verify it checked for directory existence (and created it)
expect(mockFs.mkdirSync).toHaveBeenCalledWith(TEST_DIR);
// 3. Verify it wrote the new keystore to disk
// We expect the first argument to be the path, and the second to be a JSON string
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
FULL_PATH,
expect.stringContaining('"keys":') // Simple check to ensure it looks like JSON
);
// 4. Verify the instance path is set correctly
expect(keystoreInstance.getKeystoreFilePath()).toBe(FULL_PATH);
// 5. Verify internal keystore state (node-jose should have generated keys)
// @ts-ignore - accessing public property 'keystore' which might be protected in your real usage,
// but implied public in the provided snippet logic
const keys = keystoreInstance.keystore.all();
expect(keys.length).toBeGreaterThanOrEqual(1);
});
test('should LOAD an existing keystore if the file exists', async () => {
// SETUP:
// 1. Create a real temporary keystore using node-jose to mimic a file on disk
const realKeystore = await NodeJose.JWK.createKeyStore().generate('RSA', 2048);
const keystoreJson = JSON.stringify(realKeystore.keystore.toJSON(true));
// 2. Pretend the file DOES exist
mockFs.existsSync.mockReturnValue(true);
// 3. Pretend readFileSync returns our JSON string
mockFs.readFileSync.mockReturnValue(keystoreJson);
// EXECUTE
const keystoreInstance = await FileKeystoreClass.create(TEST_FILE, TEST_DIR);
// VERIFY
// 1. It should check existence
expect(mockFs.existsSync).toHaveBeenCalledWith(FULL_PATH);
// 2. It should READ the file
expect(mockFs.readFileSync).toHaveBeenCalledWith(FULL_PATH, 'utf-8');
// 3. CRITICAL: It should NOT overwrite the file
expect(mockFs.writeFileSync).not.toHaveBeenCalled();
// 4. CRITICAL: It should NOT try to create the directory again
// (The code checks !existsSync(file), so it goes into the else block)
expect(mockFs.mkdirSync).not.toHaveBeenCalled();
// 5. Verify the loaded keys match what we "mocked" on disk
// @ts-ignore
const loadedKeys = keystoreInstance.keystore.all();
expect(loadedKeys.length).toBe(1);
});
test('should create directory only if it is missing', async () => {
// Scenario: File missing, Directory also missing
mockFs.existsSync
.mockReturnValueOnce(false) // First check: File exists? No.
.mockReturnValueOnce(false); // Second check: Dir exists? No.
await FileKeystoreClass.create(TEST_FILE, TEST_DIR);
expect(mockFs.mkdirSync).toHaveBeenCalledWith(TEST_DIR);
});
test('should NOT create directory if it already exists', async () => {
// Scenario: File missing, Directory exists
mockFs.existsSync
.mockReturnValueOnce(false) // First check: File exists? No.
.mockReturnValueOnce(true); // Second check: Dir exists? Yes.
await FileKeystoreClass.create(TEST_FILE, TEST_DIR);
expect(mockFs.mkdirSync).not.toHaveBeenCalled();
});
});

17
tsconfig.build.json Normal file
View file

@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
"rootDir": "./src",
"types": [],
},
"exclude": [
"node_modules",
"dist",
"tests"
],
"include": [
"./src/**/*.ts",
"./src/**/*.tsx"
]
}

50
tsconfig.json Normal file
View file

@ -0,0 +1,50 @@
{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
// "rootDir": "./src",
"outDir": "./dist",
// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "nodenext",
"target": "esnext",
"types": ["node", "jest"],
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
},
"exclude": ["node_modules", "dist"],
"include": [
"./src/**/*.ts",
"./src/**/*.tsx",
"./tests/**/*.ts"
]
}

3827
yarn.lock Normal file

File diff suppressed because it is too large Load diff