From 3c74cb08ed7be2784d72d295b5c6c3e8c7faa250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=BDubom=C3=ADr=20Samotn=C3=BD?= Date: Tue, 10 Feb 2026 12:58:27 +0100 Subject: [PATCH 1/3] feat: new auth api prep --- API.md | 32 ++++--- GETTING_STARTED.md | 14 ++- README.md | 22 +++-- examples/README.md | 29 ++++-- src/HttpClient.ts | 12 +++ src/RaiAcceptService.ts | 24 +++-- src/api/RaiAcceptAPIApi.ts | 149 ++++++++++++++++++++++--------- src/models/AuthApiLoginInput.ts | 20 +++++ src/models/AuthApiLoginOutput.ts | 26 ++++++ src/models/AuthApiLogoutInput.ts | 17 ++++ src/models/AuthResponse.ts | 16 ++++ 11 files changed, 282 insertions(+), 79 deletions(-) create mode 100644 src/models/AuthApiLoginInput.ts create mode 100644 src/models/AuthApiLoginOutput.ts create mode 100644 src/models/AuthApiLogoutInput.ts diff --git a/API.md b/API.md index 5aaf372..a9ecb87 100644 --- a/API.md +++ b/API.md @@ -19,25 +19,27 @@ npm install @smartbase-js/raiaccept-api-client ## Quick Start ```javascript -import { RaiAcceptAPIApi, RaiAcceptService, HttpClient } from '@smartbase-js/raiaccept-api-client'; +import { RaiAcceptService, HttpClient } from '@smartbase-js/raiaccept-api-client'; // Initialize client const httpClient = new HttpClient(); -const apiClient = new RaiAcceptAPIApi(httpClient); +const service = new RaiAcceptService(httpClient); // Authenticate -const accessToken = await RaiAcceptService.retrieveAccessTokenWithCredentials( - apiClient, +const authResult = await service.retrieveAccessTokenWithCredentials( 'username', - 'password' + 'password', + cert, // Client certificate for mTLS + key // Client private key for mTLS ); +const accessToken = authResult?.accessToken; // Step 1: Create order entry -const orderResponse = await apiClient.createOrderEntry(accessToken, orderRequest); +const orderResponse = await service.createOrderEntry(accessToken, orderRequest); const orderIdentification = orderResponse.object.getOrderIdentification(); // Step 2: Create payment session for the order -const paymentSessionResponse = await apiClient.createPaymentSession( +const paymentSessionResponse = await service.createPaymentSession( accessToken, orderRequest, orderIdentification @@ -62,20 +64,26 @@ const apiClient = new RaiAcceptAPIApi(httpClient); #### Methods -##### `token(username, password)` +##### `token(username, password, cert, key)` -Authenticate with username and password. +Authenticate with username and password using mTLS. **Parameters:** - `username` (string): Username - `password` (string): Password +- `cert` (string | Buffer): Client certificate for mTLS +- `key` (string | Buffer): Client private key for mTLS -**Returns:** `Promise` - Authentication response with access token +**Returns:** `Promise>` - Authentication response with access token, refresh token, and expiration times **Example:** ```javascript -const response = await apiClient.token('username', 'password'); -const accessToken = response.object.getIdToken(); +const response = await apiClient.token('username', 'password', cert, key); +const authResult = response.object; +const accessToken = authResult?.accessToken; +const refreshToken = authResult?.refreshToken; +const accessTokenExpiresIn = authResult?.accessTokenExpiresIn; +const refreshTokenExpiresIn = authResult?.refreshTokenExpiresIn; ``` --- diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 1ae618b..026964d 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -39,14 +39,22 @@ const client = new RaiAcceptService(httpClient); ### 3. Authenticate ```javascript -const accessToken = await client.retrieveAccessTokenWithCredentials( +const authResult = await client.retrieveAccessTokenWithCredentials( 'your-username', - 'your-password' + 'your-password', + cert, // Client certificate for mTLS + key // Client private key for mTLS ); -if (!accessToken) { +if (!authResult) { throw new Error('Authentication failed'); } + +const accessToken = authResult.accessToken; +// You can also access: +// - authResult.accessTokenExpiresIn +// - authResult.refreshToken +// - authResult.refreshTokenExpiresIn ``` ### 4. Create Your First Payment diff --git a/README.md b/README.md index 003239d..d7af1ef 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,13 @@ import { RaiAcceptService } from '@smartbase-js/raiaccept-api-client'; const service = new RaiAcceptService(); // Authenticate with your credentials -const accessToken = await service.retrieveAccessTokenWithCredentials( +const authResult = await service.retrieveAccessTokenWithCredentials( 'your-username', // Replace with your actual username - 'your-password' // Replace with your actual password + 'your-password', // Replace with your actual password + cert, // Client certificate for mTLS + key // Client private key for mTLS ); +const accessToken = authResult?.accessToken; const response = await service.createOrderEntry(accessToken, orderRequest); ``` @@ -44,10 +47,13 @@ const httpClient = new HttpClient({ const service = new RaiAcceptService(httpClient); // Authenticate -const accessToken = await service.retrieveAccessTokenWithCredentials( +const authResult = await service.retrieveAccessTokenWithCredentials( 'your-username', - 'your-password' + 'your-password', + cert, // Client certificate for mTLS + key // Client private key for mTLS ); +const accessToken = authResult?.accessToken; // Create an order and payment session (two-step process) const orderRequest = { @@ -114,10 +120,14 @@ const client = new RaiAcceptService(); ### Authentication ```typescript -const accessToken = await client.retrieveAccessTokenWithCredentials( +const authResult = await client.retrieveAccessTokenWithCredentials( username, - password + password, + cert, // Client certificate for mTLS + key // Client private key for mTLS ); +const accessToken = authResult?.accessToken; +// Also available: authResult.refreshToken, authResult.accessTokenExpiresIn, authResult.refreshTokenExpiresIn ``` ### Order Operations diff --git a/examples/README.md b/examples/README.md index 45af9f2..891a53a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -42,11 +42,13 @@ npm install 2. Update credentials in the examples: ```javascript -const accessToken = await RaiAcceptService.retrieveAccessTokenWithCredentials( - apiClient, +const authResult = await service.retrieveAccessTokenWithCredentials( 'your-username', // <- Replace with your credentials - 'your-password' // <- Replace with your credentials + 'your-password', // <- Replace with your credentials + cert, // <- Client certificate for mTLS + key // <- Client private key for mTLS ); +const accessToken = authResult?.accessToken; ``` 3. Run the examples: @@ -67,10 +69,13 @@ const app = express(); app.post('/api/create-payment', async (req, res) => { // Create service and authenticate const service = new RaiAcceptService(); - const accessToken = await service.retrieveAccessTokenWithCredentials( + const authResult = await service.retrieveAccessTokenWithCredentials( 'your-username', // Replace with your actual credentials - 'your-password' // Replace with your actual credentials + 'your-password', // Replace with your actual credentials + cert, // Client certificate for mTLS + key // Client private key for mTLS ); + const accessToken = authResult?.accessToken; // Step 1: Create order entry const orderResponse = await service.createOrderEntry(accessToken, req.body); @@ -99,10 +104,13 @@ import { RaiAcceptService } from '@smartbase-js/raiaccept-api-client'; export async function createPayment(orderData) { // Create service and authenticate const service = new RaiAcceptService(); - const accessToken = await service.retrieveAccessTokenWithCredentials( + const authResult = await service.retrieveAccessTokenWithCredentials( 'your-username', // Replace with your actual credentials - 'your-password' // Replace with your actual credentials + 'your-password', // Replace with your actual credentials + cert, // Client certificate for mTLS + key // Client private key for mTLS ); + const accessToken = authResult?.accessToken; // Step 1: Create order entry const orderResponse = await service.createOrderEntry(accessToken, orderData); @@ -131,10 +139,13 @@ import { RaiAcceptService } from '@smartbase-js/raiaccept-api-client'; export const handler = async (event) => { // Create service and authenticate const service = new RaiAcceptService(); - const accessToken = await service.retrieveAccessTokenWithCredentials( + const authResult = await service.retrieveAccessTokenWithCredentials( 'your-username', // Replace with your actual credentials - 'your-password' // Replace with your actual credentials + 'your-password', // Replace with your actual credentials + cert, // Client certificate for mTLS + key // Client private key for mTLS ); + const accessToken = authResult?.accessToken; const orderData = JSON.parse(event.body); diff --git a/src/HttpClient.ts b/src/HttpClient.ts index cfcb80b..fbe14da 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -1,4 +1,5 @@ import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; +import https from 'https'; export interface Logger { log(message: string, data?: any): void; @@ -15,6 +16,8 @@ export interface HttpRequest { url: string; headers?: Record; body?: string; + cert?: string | Buffer; + key?: string | Buffer; } export interface HttpResponse { @@ -51,6 +54,15 @@ export class HttpClient { validateStatus: () => true, // Don't throw on any status }; + // Add mTLS certificate and key (required for authentication requests) + if (request.cert && request.key) { + const httpsAgent = new https.Agent({ + cert: request.cert, + key: request.key, + }); + axiosConfig.httpsAgent = httpsAgent; + } + if (this.logger && !omitLogging) { this.logger.log('Request:', this._sanitizeForLog(request)); } diff --git a/src/RaiAcceptService.ts b/src/RaiAcceptService.ts index a1186e1..04928ac 100644 --- a/src/RaiAcceptService.ts +++ b/src/RaiAcceptService.ts @@ -8,6 +8,7 @@ import { GetOrderTransactionsResponse } from './models/GetOrderTransactionsRespo import { GetTransactionDetailsResponse } from './models/GetTransactionDetailsResponse.js'; import { RefundResponse } from './models/RefundResponse.js'; import { AuthResponse } from './models/AuthResponse.js'; +import { AuthApiLoginOutput } from './models/AuthApiLoginOutput.js'; /** * RaiAcceptService @@ -174,21 +175,32 @@ export class RaiAcceptService { * Retrieve access token with credentials * @param username - Username * @param password - Password - * @returns Access token or null on error + * @param cert - Client certificate for mTLS + * @param key - Client private key for mTLS + * @returns AuthApiLoginOutput object or null on error */ - async retrieveAccessTokenWithCredentials(username: string, password: string): Promise { + async retrieveAccessTokenWithCredentials( + username: string, + password: string, + cert: string | Buffer, + key: string | Buffer + ): Promise { + // Temporary: Return hardcoded token for testing + // return 'temp-test-token-hardcoded-for-testing'; + + // TODO: Remove hardcoded token and uncomment below for production + try { - const response = await this.apiClient.token(username, password); + const response = await this.apiClient.token(username, password, cert, key); if (!response || !response.object) { return null; } - const responseObj = response.object; - const accessToken = responseObj.getIdToken(); - return accessToken || null; + return response.object; } catch (error) { // Re-throw the error so the caller can handle it appropriately throw error; } + } /** diff --git a/src/api/RaiAcceptAPIApi.ts b/src/api/RaiAcceptAPIApi.ts index e9052c8..b8551ed 100644 --- a/src/api/RaiAcceptAPIApi.ts +++ b/src/api/RaiAcceptAPIApi.ts @@ -1,7 +1,9 @@ import { ApiException } from '../exceptions/ApiException.js'; import { InvalidArgumentException } from '../exceptions/InvalidArgumentException.js'; import { ObjectSerializer } from '../utils/ObjectSerializer.js'; -import { AuthResponse } from '../models/AuthResponse.js'; +import { AuthApiLoginOutput } from '../models/AuthApiLoginOutput.js'; +import { AuthApiLoginInput } from '../models/AuthApiLoginInput.js'; +import { AuthApiLogoutInput } from '../models/AuthApiLogoutInput.js'; import { CreateOrderEntryResponse } from '../models/CreateOrderEntryResponse.js'; import { CreatePaymentSessionResponse } from '../models/CreatePaymentSessionResponse.js'; import { GetOrderDetailsResponse } from '../models/GetOrderDetailsResponse.js'; @@ -22,9 +24,7 @@ export interface ApiResponse { * Main API client for RaiAccept payment gateway */ export class RaiAcceptAPIApi { - static AUTH_URL = 'https://authenticate.raiaccept.com'; - static AUTH_FLOW = 'USER_PASSWORD_AUTH'; - static AUTH_CLIENT_ID = 'kr2gs4117arvbnaperqff5dml'; + static AUTH_URL = 'https://api.test.raiaccept.com'; static API_URL = 'https://trapi.raiaccept.com'; static ACCEPTED_LANGUAGES = [ @@ -91,74 +91,137 @@ export class RaiAcceptAPIApi { } } - /** - * Get authentication request headers - * @returns Headers object - */ - static getAuthRequestHeaders(): Record { - return { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'AWSCognitoIdentityProviderService.InitiateAuth', - }; - } /** * Authenticate with username and password * @param username - Username * @param password - Password + * @param cert - Client certificate for mTLS + * @param key - Client private key for mTLS * @returns Authentication response */ - async token(username: string, password: string): Promise> { - const request = this.tokenRequest( - RaiAcceptAPIApi.AUTH_FLOW, - username, - password, - RaiAcceptAPIApi.AUTH_CLIENT_ID - ); - - return this.processRequest(request, AuthResponse, ErrorResponse, true); + async token( + username: string, + password: string, + cert: string | Buffer, + key: string | Buffer + ): Promise> { + const request = this.tokenRequest(username, password, cert, key); + + return this.processRequest(request, AuthApiLoginOutput, ErrorResponse, true); } /** * Create token request - * @param authFlow - Authentication flow * @param username - Username * @param password - Password - * @param clientId - Client ID + * @param cert - Client certificate for mTLS + * @param key - Client private key for mTLS * @returns Request object */ - tokenRequest(authFlow: string, username: string, password: string, clientId: string): HttpRequest { - if (!authFlow) { - throw new InvalidArgumentException('Missing the required parameter $authFlow when calling tokenRequest'); - } + tokenRequest( + username: string, + password: string, + cert: string | Buffer, + key: string | Buffer + ): HttpRequest { if (!username) { throw new InvalidArgumentException('Missing the required parameter $username when calling tokenRequest'); } if (!password) { throw new InvalidArgumentException('Missing the required parameter $password when calling tokenRequest'); } - if (!clientId) { - throw new InvalidArgumentException('Missing the required parameter $clientId when calling tokenRequest'); + if (!cert) { + throw new InvalidArgumentException('Missing the required parameter $cert when calling tokenRequest'); + } + if (!key) { + throw new InvalidArgumentException('Missing the required parameter $key when calling tokenRequest'); } - const formParams = { - AuthFlow: authFlow, - AuthParameters: { - USERNAME: username, - PASSWORD: password, - }, - ClientId: clientId, - }; + const loginInput = new AuthApiLoginInput(); + loginInput.username = username; + loginInput.password = password; - const httpBody = JSON.stringify(ObjectSerializer.sanitizeForSerialization(formParams)); - const headers = RaiAcceptAPIApi.getAuthRequestHeaders(); + const httpBody = JSON.stringify(ObjectSerializer.sanitizeForSerialization(loginInput)); + const headers = { + 'Content-Type': 'application/json', + }; - return { + const request: HttpRequest = { method: 'POST', - url: RaiAcceptAPIApi.AUTH_URL, + url: `${RaiAcceptAPIApi.AUTH_URL}/auth/api/login`, headers: headers, body: httpBody, + cert: cert, + key: key, + } as HttpRequest; + + return request; + } + + /** + * Logout with token + * @param token - Token to logout + * @param cert - Client certificate for mTLS + * @param key - Client private key for mTLS + * @returns True if logout successful (HTTP 200), false otherwise + */ + async tokenLogout( + token: string, + cert: string | Buffer, + key: string | Buffer + ): Promise { + const request = this.tokenLogoutRequest(token, cert, key); + + try { + const response = await this.client.send(request, true); + const statusCode = response.getStatusCode(); + return statusCode === 200; + } catch (error) { + return false; + } + } + + /** + * Create token logout request + * @param token - Token to logout + * @param cert - Client certificate for mTLS + * @param key - Client private key for mTLS + * @returns Request object + */ + tokenLogoutRequest( + token: string, + cert: string | Buffer, + key: string | Buffer + ): HttpRequest { + if (!token) { + throw new InvalidArgumentException('Missing the required parameter $token when calling tokenLogoutRequest'); + } + if (!cert) { + throw new InvalidArgumentException('Missing the required parameter $cert when calling tokenLogoutRequest'); + } + if (!key) { + throw new InvalidArgumentException('Missing the required parameter $key when calling tokenLogoutRequest'); + } + + const logoutInput = new AuthApiLogoutInput(); + logoutInput.refreshToken = token; + + const httpBody = JSON.stringify(ObjectSerializer.sanitizeForSerialization(logoutInput)); + const headers = { + 'Content-Type': 'application/json', }; + + const request: HttpRequest = { + method: 'POST', + url: `${RaiAcceptAPIApi.AUTH_URL}/auth/api/logout`, + headers: headers, + body: httpBody, + cert: cert, + key: key, + } as HttpRequest; + + return request; } /** diff --git a/src/models/AuthApiLoginInput.ts b/src/models/AuthApiLoginInput.ts new file mode 100644 index 0000000..6a0ff6f --- /dev/null +++ b/src/models/AuthApiLoginInput.ts @@ -0,0 +1,20 @@ +/** + * AuthApiLoginInput + * @category Model + */ +export class AuthApiLoginInput { + username: string = ''; + password: string = ''; + + constructor() { + this.username = ''; + this.password = ''; + } + + static fromObject(data: any = {}): AuthApiLoginInput { + const instance = new AuthApiLoginInput(); + instance.username = data.username || ''; + instance.password = data.password || ''; + return instance; + } +} diff --git a/src/models/AuthApiLoginOutput.ts b/src/models/AuthApiLoginOutput.ts new file mode 100644 index 0000000..d16893c --- /dev/null +++ b/src/models/AuthApiLoginOutput.ts @@ -0,0 +1,26 @@ +/** + * AuthApiLoginOutput + * @category Model + */ +export class AuthApiLoginOutput { + accessToken: string = ''; + accessTokenExpiresIn: number = 0; + refreshToken: string = ''; + refreshTokenExpiresIn: number = 0; + + constructor() { + this.accessToken = ''; + this.accessTokenExpiresIn = 0; + this.refreshToken = ''; + this.refreshTokenExpiresIn = 0; + } + + static fromObject(data: any = {}): AuthApiLoginOutput { + const instance = new AuthApiLoginOutput(); + instance.accessToken = data.accessToken || ''; + instance.accessTokenExpiresIn = data.accessTokenExpiresIn || 0; + instance.refreshToken = data.refreshToken || ''; + instance.refreshTokenExpiresIn = data.refreshTokenExpiresIn || 0; + return instance; + } +} diff --git a/src/models/AuthApiLogoutInput.ts b/src/models/AuthApiLogoutInput.ts new file mode 100644 index 0000000..87b0d97 --- /dev/null +++ b/src/models/AuthApiLogoutInput.ts @@ -0,0 +1,17 @@ +/** + * AuthApiLogoutInput + * @category Model + */ +export class AuthApiLogoutInput { + refreshToken: string = ''; + + constructor() { + this.refreshToken = ''; + } + + static fromObject(data: any = {}): AuthApiLogoutInput { + const instance = new AuthApiLogoutInput(); + instance.refreshToken = data.refreshToken || ''; + return instance; + } +} diff --git a/src/models/AuthResponse.ts b/src/models/AuthResponse.ts index 42d91a6..297dc37 100644 --- a/src/models/AuthResponse.ts +++ b/src/models/AuthResponse.ts @@ -8,14 +8,24 @@ import { ChallengeParameters } from './ChallengeParameters.js'; export class AuthResponse { authenticationResult: AuthenticationResult | null = null; challengeParameters: ChallengeParameters | null = null; + accessToken: string = ''; constructor() { this.authenticationResult = null; this.challengeParameters = null; + this.accessToken = ''; } static fromObject(data: any = {}): AuthResponse { const instance = new AuthResponse(); + + // Handle new API format: { accessToken: "..." } + if (data.accessToken) { + instance.accessToken = data.accessToken; + return instance; + } + + // Handle legacy AWS Cognito format for backward compatibility instance.authenticationResult = data.AuthenticationResult ? AuthenticationResult.fromObject(data.AuthenticationResult) : null; @@ -26,6 +36,12 @@ export class AuthResponse { } getIdToken(): string { + // Return accessToken from new API format if available + if (this.accessToken) { + return this.accessToken; + } + + // Fall back to legacy format if (!this.authenticationResult) { return ''; } From 5307f8a09abdb98de49427c4871ef3e52ef1c3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=BDubom=C3=ADr=20Samotn=C3=BD?= Date: Mon, 16 Feb 2026 16:46:14 +0100 Subject: [PATCH 2/3] feat: use new API for all requests --- API.md | 34 ++++-- examples/basic-usage.ts | 11 +- package-lock.json | 15 +-- src/HttpClient.ts | 28 ++++- src/RaiAcceptService.ts | 45 +++++--- src/api/RaiAcceptAPIApi.ts | 176 ++++++++++++++++++----------- src/index.ts | 4 +- src/models/AuthApiRefreshInput.ts | 17 +++ src/models/AuthApiRefreshOutput.ts | 20 ++++ src/models/AuthResponse.ts | 50 -------- src/models/AuthenticationResult.ts | 33 ------ src/models/ChallengeParameters.ts | 17 --- tests/README.md | 10 ++ tests/integration.test.js | 72 ++++++++++-- 14 files changed, 320 insertions(+), 212 deletions(-) create mode 100644 src/models/AuthApiRefreshInput.ts create mode 100644 src/models/AuthApiRefreshOutput.ts delete mode 100644 src/models/AuthResponse.ts delete mode 100644 src/models/AuthenticationResult.ts delete mode 100644 src/models/ChallengeParameters.ts diff --git a/API.md b/API.md index a9ecb87..a604174 100644 --- a/API.md +++ b/API.md @@ -23,14 +23,12 @@ import { RaiAcceptService, HttpClient } from '@smartbase-js/raiaccept-api-client // Initialize client const httpClient = new HttpClient(); -const service = new RaiAcceptService(httpClient); +const service = new RaiAcceptService(httpClient, cert, key); // Authenticate const authResult = await service.retrieveAccessTokenWithCredentials( 'username', - 'password', - cert, // Client certificate for mTLS - key // Client private key for mTLS + 'password' ); const accessToken = authResult?.accessToken; @@ -56,29 +54,29 @@ Main API client for interacting with RaiAccept services. #### Constructor ```javascript -const apiClient = new RaiAcceptAPIApi(httpClient); +const apiClient = new RaiAcceptAPIApi(httpClient, cert, key); ``` **Parameters:** - `httpClient` (HttpClient): HTTP client instance for making requests +- `cert` (string | Buffer): Client certificate for mTLS +- `key` (string | Buffer): Client private key for mTLS #### Methods -##### `token(username, password, cert, key)` +##### `token(username, password)` -Authenticate with username and password using mTLS. +Authenticate with username and password. **Parameters:** - `username` (string): Username - `password` (string): Password -- `cert` (string | Buffer): Client certificate for mTLS -- `key` (string | Buffer): Client private key for mTLS **Returns:** `Promise>` - Authentication response with access token, refresh token, and expiration times **Example:** ```javascript -const response = await apiClient.token('username', 'password', cert, key); +const response = await apiClient.token('username', 'password'); const authResult = response.object; const accessToken = authResult?.accessToken; const refreshToken = authResult?.refreshToken; @@ -88,6 +86,22 @@ const refreshTokenExpiresIn = authResult?.refreshTokenExpiresIn; --- +##### `tokenLogout(token)` + +Logout with refresh token. Uses AUTH_URL; cert and key not required. + +**Parameters:** +- `token` (string): Refresh token to logout + +**Returns:** `Promise` - True if logout successful (HTTP 200), false otherwise + +**Example:** +```javascript +const success = await apiClient.tokenLogout(refreshToken); +``` + +--- + ##### `createOrderEntry(accessToken, createOrderRequest)` Create a new order entry. diff --git a/examples/basic-usage.ts b/examples/basic-usage.ts index dd8366c..9e83b92 100644 --- a/examples/basic-usage.ts +++ b/examples/basic-usage.ts @@ -10,15 +10,20 @@ async function main(): Promise { try { console.log('Creating service and authenticating...'); - // Create service instance - const client = new RaiAcceptService(); + // Load mTLS cert and key (from env, files, or secure storage) + const cert = process.env.RAIACCEPT_CERT || ''; // Replace with your cert + const key = process.env.RAIACCEPT_KEY || ''; // Replace with your key + + // Create service instance with cert and key + const client = new RaiAcceptService(null, cert, key); // Authenticate with your credentials - const accessToken = await client.retrieveAccessTokenWithCredentials( + const authResult = await client.retrieveAccessTokenWithCredentials( 'your-username', // Replace with your actual username 'your-password' // Replace with your actual password ); + const accessToken = authResult?.accessToken; if (!accessToken) { throw new Error('Authentication failed'); } diff --git a/package-lock.json b/package-lock.json index 0ee37ee..4ab53f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@smartbase-js/raiaccept-api-client", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@smartbase-js/raiaccept-api-client", - "version": "0.9.0", + "version": "0.9.1", "license": "OSL-3.0", "dependencies": { "axios": "^1.6.0" @@ -1067,12 +1067,13 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, diff --git a/src/HttpClient.ts b/src/HttpClient.ts index fbe14da..30dd0c6 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -54,7 +54,7 @@ export class HttpClient { validateStatus: () => true, // Don't throw on any status }; - // Add mTLS certificate and key (required for authentication requests) + // Add mTLS certificate and key if (request.cert && request.key) { const httpsAgent = new https.Agent({ cert: request.cert, @@ -91,19 +91,43 @@ export class HttpClient { } } + private static readonly SENSITIVE_BODY_KEYS = ['cert', 'key', 'username', 'password']; + /** - * Sanitize request for logging (hide sensitive headers) + * Sanitize request for logging (hide sensitive headers and body fields) * @param request - Request object * @returns Sanitized request */ private _sanitizeForLog(request: HttpRequest): HttpRequest { const sanitized: HttpRequest = { ...request }; + + // Hide sensitive headers if (sanitized.headers) { sanitized.headers = { ...sanitized.headers }; if (sanitized.headers.Authorization) { sanitized.headers.Authorization = 'HIDDEN'; } } + + // Hide cert and key + if (sanitized.cert) sanitized.cert = 'HIDDEN'; + if (sanitized.key) sanitized.key = 'HIDDEN'; + + // Hide sensitive fields in request body (username, password, cert, key) + if (sanitized.body) { + try { + const parsed = JSON.parse(sanitized.body); + if (typeof parsed === 'object' && parsed !== null) { + for (const key of HttpClient.SENSITIVE_BODY_KEYS) { + if (key in parsed) parsed[key] = 'HIDDEN'; + } + sanitized.body = JSON.stringify(parsed); + } + } catch { + // Not JSON, leave body as-is + } + } + return sanitized; } } diff --git a/src/RaiAcceptService.ts b/src/RaiAcceptService.ts index 04928ac..a2310a1 100644 --- a/src/RaiAcceptService.ts +++ b/src/RaiAcceptService.ts @@ -7,8 +7,8 @@ import { GetOrderDetailsResponse } from './models/GetOrderDetailsResponse.js'; import { GetOrderTransactionsResponse } from './models/GetOrderTransactionsResponse.js'; import { GetTransactionDetailsResponse } from './models/GetTransactionDetailsResponse.js'; import { RefundResponse } from './models/RefundResponse.js'; -import { AuthResponse } from './models/AuthResponse.js'; import { AuthApiLoginOutput } from './models/AuthApiLoginOutput.js'; +import { AuthApiRefreshOutput } from './models/AuthApiRefreshOutput.js'; /** * RaiAcceptService @@ -24,13 +24,19 @@ export class RaiAcceptService { static STATUS_ABANDONED = 'ABANDONED'; private apiClient: RaiAcceptAPIApi; + private cert?: string | Buffer; + private key?: string | Buffer; /** * Create a new RaiAcceptService instance * @param httpClient - HTTP client instance (optional) + * @param cert - Client certificate for mTLS (optional, required for retrieveAccessTokenWithCredentials) + * @param key - Client private key for mTLS (optional, required for retrieveAccessTokenWithCredentials) */ - constructor(httpClient: HttpClient | null = null) { - this.apiClient = new RaiAcceptAPIApi(httpClient); + constructor(httpClient: HttpClient | null = null, cert?: string | Buffer, key?: string | Buffer) { + this.apiClient = new RaiAcceptAPIApi(httpClient, cert, key); + this.cert = cert; + this.key = key; } /** @@ -175,23 +181,14 @@ export class RaiAcceptService { * Retrieve access token with credentials * @param username - Username * @param password - Password - * @param cert - Client certificate for mTLS - * @param key - Client private key for mTLS * @returns AuthApiLoginOutput object or null on error */ async retrieveAccessTokenWithCredentials( username: string, - password: string, - cert: string | Buffer, - key: string | Buffer + password: string ): Promise { - // Temporary: Return hardcoded token for testing - // return 'temp-test-token-hardcoded-for-testing'; - - // TODO: Remove hardcoded token and uncomment below for production - try { - const response = await this.apiClient.token(username, password, cert, key); + const response = await this.apiClient.token(username, password); if (!response || !response.object) { return null; } @@ -200,7 +197,25 @@ export class RaiAcceptService { // Re-throw the error so the caller can handle it appropriately throw error; } - + + } + + /** + * Logout with token + * @param token - Token to logout (refresh token) + * @returns True if logout successful (HTTP 200), false otherwise + */ + async tokenLogout(token: string): Promise { + return await this.apiClient.tokenLogout(token); + } + + /** + * Refresh access token using refresh token + * @param refreshToken - Refresh token + * @returns Authentication response with new access token and expiration + */ + async tokenRefresh(refreshToken: string): Promise> { + return await this.apiClient.tokenRefresh(refreshToken); } /** diff --git a/src/api/RaiAcceptAPIApi.ts b/src/api/RaiAcceptAPIApi.ts index b8551ed..b7b49f4 100644 --- a/src/api/RaiAcceptAPIApi.ts +++ b/src/api/RaiAcceptAPIApi.ts @@ -4,6 +4,8 @@ import { ObjectSerializer } from '../utils/ObjectSerializer.js'; import { AuthApiLoginOutput } from '../models/AuthApiLoginOutput.js'; import { AuthApiLoginInput } from '../models/AuthApiLoginInput.js'; import { AuthApiLogoutInput } from '../models/AuthApiLogoutInput.js'; +import { AuthApiRefreshInput } from '../models/AuthApiRefreshInput.js'; +import { AuthApiRefreshOutput } from '../models/AuthApiRefreshOutput.js'; import { CreateOrderEntryResponse } from '../models/CreateOrderEntryResponse.js'; import { CreatePaymentSessionResponse } from '../models/CreatePaymentSessionResponse.js'; import { GetOrderDetailsResponse } from '../models/GetOrderDetailsResponse.js'; @@ -24,17 +26,27 @@ export interface ApiResponse { * Main API client for RaiAccept payment gateway */ export class RaiAcceptAPIApi { - static AUTH_URL = 'https://api.test.raiaccept.com'; - static API_URL = 'https://trapi.raiaccept.com'; + static AUTH_URL = 'https://auth.raiaccept.com'; + static API_URL = 'https://api.raiaccept.com'; static ACCEPTED_LANGUAGES = [ 'en', 'de', 'fr', 'cs', 'sk', 'sr', 'al', 'ro', 'pl', 'hr' ]; private client: HttpClient; + private cert?: string | Buffer; + private key?: string | Buffer; - constructor(client: HttpClient | null = null) { + /** + * Create a new RaiAcceptAPIApi instance + * @param client - HTTP client instance (optional) + * @param cert - Client certificate for mTLS (optional, required for API_URL endpoints: createOrderEntry, createPaymentSession, getOrderDetails, getOrderTransactions, getTransactionDetails, refund) + * @param key - Client private key for mTLS (optional, required for API_URL endpoints) + */ + constructor(client: HttpClient | null = null, cert?: string | Buffer, key?: string | Buffer) { this.client = client || new HttpClient(); + this.cert = cert; + this.key = key; } getAcceptedLanguages(): string[] { @@ -96,17 +108,10 @@ export class RaiAcceptAPIApi { * Authenticate with username and password * @param username - Username * @param password - Password - * @param cert - Client certificate for mTLS - * @param key - Client private key for mTLS * @returns Authentication response */ - async token( - username: string, - password: string, - cert: string | Buffer, - key: string | Buffer - ): Promise> { - const request = this.tokenRequest(username, password, cert, key); + async token(username: string, password: string): Promise> { + const request = this.tokenRequest(username, password); return this.processRequest(request, AuthApiLoginOutput, ErrorResponse, true); } @@ -115,28 +120,15 @@ export class RaiAcceptAPIApi { * Create token request * @param username - Username * @param password - Password - * @param cert - Client certificate for mTLS - * @param key - Client private key for mTLS * @returns Request object */ - tokenRequest( - username: string, - password: string, - cert: string | Buffer, - key: string | Buffer - ): HttpRequest { + tokenRequest(username: string, password: string): HttpRequest { if (!username) { throw new InvalidArgumentException('Missing the required parameter $username when calling tokenRequest'); } if (!password) { throw new InvalidArgumentException('Missing the required parameter $password when calling tokenRequest'); } - if (!cert) { - throw new InvalidArgumentException('Missing the required parameter $cert when calling tokenRequest'); - } - if (!key) { - throw new InvalidArgumentException('Missing the required parameter $key when calling tokenRequest'); - } const loginInput = new AuthApiLoginInput(); loginInput.username = username; @@ -147,31 +139,57 @@ export class RaiAcceptAPIApi { 'Content-Type': 'application/json', }; - const request: HttpRequest = { + return { method: 'POST', url: `${RaiAcceptAPIApi.AUTH_URL}/auth/api/login`, headers: headers, body: httpBody, - cert: cert, - key: key, } as HttpRequest; + } - return request; + /** + * Refresh access token using refresh token + * @param refreshToken - Refresh token + * @returns Authentication response with new access token and expiration + */ + async tokenRefresh(refreshToken: string): Promise> { + const request = this.tokenRefreshRequest(refreshToken); + return this.processRequest(request, AuthApiRefreshOutput, ErrorResponse, true); + } + + /** + * Create token refresh request + * @param refreshToken - Refresh token + * @returns Request object + */ + tokenRefreshRequest(refreshToken: string): HttpRequest { + if (!refreshToken) { + throw new InvalidArgumentException('Missing the required parameter $refreshToken when calling tokenRefreshRequest'); + } + + const refreshInput = new AuthApiRefreshInput(); + refreshInput.refreshToken = refreshToken; + + const httpBody = JSON.stringify(ObjectSerializer.sanitizeForSerialization(refreshInput)); + const headers = { + 'Content-Type': 'application/json', + }; + + return { + method: 'POST', + url: `${RaiAcceptAPIApi.AUTH_URL}/auth/api/refresh`, + headers: headers, + body: httpBody, + }; } /** * Logout with token * @param token - Token to logout - * @param cert - Client certificate for mTLS - * @param key - Client private key for mTLS * @returns True if logout successful (HTTP 200), false otherwise */ - async tokenLogout( - token: string, - cert: string | Buffer, - key: string | Buffer - ): Promise { - const request = this.tokenLogoutRequest(token, cert, key); + async tokenLogout(token: string): Promise { + const request = this.tokenLogoutRequest(token); try { const response = await this.client.send(request, true); @@ -185,24 +203,12 @@ export class RaiAcceptAPIApi { /** * Create token logout request * @param token - Token to logout - * @param cert - Client certificate for mTLS - * @param key - Client private key for mTLS * @returns Request object */ - tokenLogoutRequest( - token: string, - cert: string | Buffer, - key: string | Buffer - ): HttpRequest { + tokenLogoutRequest(token: string): HttpRequest { if (!token) { throw new InvalidArgumentException('Missing the required parameter $token when calling tokenLogoutRequest'); } - if (!cert) { - throw new InvalidArgumentException('Missing the required parameter $cert when calling tokenLogoutRequest'); - } - if (!key) { - throw new InvalidArgumentException('Missing the required parameter $key when calling tokenLogoutRequest'); - } const logoutInput = new AuthApiLogoutInput(); logoutInput.refreshToken = token; @@ -212,16 +218,12 @@ export class RaiAcceptAPIApi { 'Content-Type': 'application/json', }; - const request: HttpRequest = { + return { method: 'POST', url: `${RaiAcceptAPIApi.AUTH_URL}/auth/api/logout`, headers: headers, body: httpBody, - cert: cert, - key: key, - } as HttpRequest; - - return request; + }; } /** @@ -251,6 +253,12 @@ export class RaiAcceptAPIApi { if (!createOrderRequest) { throw new InvalidArgumentException('Missing the required parameter $createOrderRequest when calling createOrderEntry'); } + if (!this.cert) { + throw new InvalidArgumentException('Missing the required parameter $cert when calling createOrderEntry (provide in constructor)'); + } + if (!this.key) { + throw new InvalidArgumentException('Missing the required parameter $key when calling createOrderEntry (provide in constructor)'); + } const resourcePath = '/orders'; const headers = { @@ -266,7 +274,9 @@ export class RaiAcceptAPIApi { url: RaiAcceptAPIApi.API_URL + resourcePath, headers: headers, body: httpBody, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } /** @@ -306,6 +316,12 @@ export class RaiAcceptAPIApi { if (!paymentSessionRequest) { throw new InvalidArgumentException('Missing the required parameter $paymentSessionRequest when calling createPaymentSession'); } + if (!this.cert) { + throw new InvalidArgumentException('Missing the required parameter $cert when calling createPaymentSession (provide in constructor)'); + } + if (!this.key) { + throw new InvalidArgumentException('Missing the required parameter $key when calling createPaymentSession (provide in constructor)'); + } const resourcePath = `${RaiAcceptAPIApi.API_URL}/orders/${externalOrderId}/checkout`; const headers = { @@ -321,7 +337,9 @@ export class RaiAcceptAPIApi { url: resourcePath, headers: headers, body: httpBody, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } /** @@ -351,6 +369,12 @@ export class RaiAcceptAPIApi { if (!accessToken) { throw new InvalidArgumentException('Missing the required parameter $accessToken when calling getOrderDetailsRequest'); } + if (!this.cert) { + throw new InvalidArgumentException('Missing the required parameter $cert when calling getOrderDetails (provide in constructor)'); + } + if (!this.key) { + throw new InvalidArgumentException('Missing the required parameter $key when calling getOrderDetails (provide in constructor)'); + } const encodedPaymentId = ObjectSerializer.toPathValue(paymentId); const resourcePath = `${RaiAcceptAPIApi.API_URL}/orders/${encodedPaymentId}`; @@ -364,7 +388,9 @@ export class RaiAcceptAPIApi { method: 'GET', url: resourcePath, headers: headers, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } /** @@ -400,6 +426,12 @@ export class RaiAcceptAPIApi { if (!accessToken) { throw new InvalidArgumentException('Missing the required parameter $accessToken when calling getTransactionDetailsRequest'); } + if (!this.cert) { + throw new InvalidArgumentException('Missing the required parameter $cert when calling getTransactionDetails (provide in constructor)'); + } + if (!this.key) { + throw new InvalidArgumentException('Missing the required parameter $key when calling getTransactionDetails (provide in constructor)'); + } const encodedOrderId = ObjectSerializer.toPathValue(orderId); const encodedTransactionId = ObjectSerializer.toPathValue(transactionId); @@ -414,7 +446,9 @@ export class RaiAcceptAPIApi { method: 'GET', url: resourcePath, headers: headers, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } /** @@ -444,6 +478,12 @@ export class RaiAcceptAPIApi { if (!accessToken) { throw new InvalidArgumentException('Missing the required parameter $accessToken when calling getOrderTransactionsRequest'); } + if (!this.cert) { + throw new InvalidArgumentException('Missing the required parameter $cert when calling getOrderTransactions (provide in constructor)'); + } + if (!this.key) { + throw new InvalidArgumentException('Missing the required parameter $key when calling getOrderTransactions (provide in constructor)'); + } const encodedOrderId = ObjectSerializer.toPathValue(orderId); const resourcePath = `${RaiAcceptAPIApi.API_URL}/orders/${encodedOrderId}/transactions`; @@ -457,7 +497,9 @@ export class RaiAcceptAPIApi { method: 'GET', url: resourcePath, headers: headers, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } /** @@ -499,6 +541,12 @@ export class RaiAcceptAPIApi { if (!requestObj) { throw new InvalidArgumentException('Missing the required parameter $requestObj when calling getRefundRequest'); } + if (!this.cert) { + throw new InvalidArgumentException('Missing the required parameter $cert when calling refund (provide in constructor)'); + } + if (!this.key) { + throw new InvalidArgumentException('Missing the required parameter $key when calling refund (provide in constructor)'); + } const encodedOrderId = ObjectSerializer.toPathValue(orderId); const encodedTransactionId = ObjectSerializer.toPathValue(transactionId); @@ -517,6 +565,8 @@ export class RaiAcceptAPIApi { url: resourcePath, headers: headers, body: httpBody, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } } diff --git a/src/index.ts b/src/index.ts index 2fd17e9..cf6ca5c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,10 +21,8 @@ export { InvalidArgumentException } from './exceptions/InvalidArgumentException. // Models export { Address } from './models/Address.js'; -export { AuthenticationResult } from './models/AuthenticationResult.js'; -export { AuthResponse } from './models/AuthResponse.js'; +export { AuthApiLoginOutput } from './models/AuthApiLoginOutput.js'; export { Card } from './models/Card.js'; -export { ChallengeParameters } from './models/ChallengeParameters.js'; export { Consumer } from './models/Consumer.js'; export { CreateOrderEntryRequest } from './models/CreateOrderEntryRequest.js'; export { CreateOrderEntryResponse } from './models/CreateOrderEntryResponse.js'; diff --git a/src/models/AuthApiRefreshInput.ts b/src/models/AuthApiRefreshInput.ts new file mode 100644 index 0000000..818af56 --- /dev/null +++ b/src/models/AuthApiRefreshInput.ts @@ -0,0 +1,17 @@ +/** + * AuthApiRefreshInput + * @category Model + */ +export class AuthApiRefreshInput { + refreshToken: string = ''; + + constructor() { + this.refreshToken = ''; + } + + static fromObject(data: any = {}): AuthApiRefreshInput { + const instance = new AuthApiRefreshInput(); + instance.refreshToken = data.refreshToken || ''; + return instance; + } +} diff --git a/src/models/AuthApiRefreshOutput.ts b/src/models/AuthApiRefreshOutput.ts new file mode 100644 index 0000000..6e1f923 --- /dev/null +++ b/src/models/AuthApiRefreshOutput.ts @@ -0,0 +1,20 @@ +/** + * AuthApiRefreshOutput + * @category Model + */ +export class AuthApiRefreshOutput { + accessToken: string = ''; + accessTokenExpiresIn: number = 0; + + constructor() { + this.accessToken = ''; + this.accessTokenExpiresIn = 0; + } + + static fromObject(data: any = {}): AuthApiRefreshOutput { + const instance = new AuthApiRefreshOutput(); + instance.accessToken = data.accessToken || ''; + instance.accessTokenExpiresIn = data.accessTokenExpiresIn || 0; + return instance; + } +} diff --git a/src/models/AuthResponse.ts b/src/models/AuthResponse.ts deleted file mode 100644 index 297dc37..0000000 --- a/src/models/AuthResponse.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AuthenticationResult } from './AuthenticationResult.js'; -import { ChallengeParameters } from './ChallengeParameters.js'; - -/** - * AuthResponse - * @category Model - */ -export class AuthResponse { - authenticationResult: AuthenticationResult | null = null; - challengeParameters: ChallengeParameters | null = null; - accessToken: string = ''; - - constructor() { - this.authenticationResult = null; - this.challengeParameters = null; - this.accessToken = ''; - } - - static fromObject(data: any = {}): AuthResponse { - const instance = new AuthResponse(); - - // Handle new API format: { accessToken: "..." } - if (data.accessToken) { - instance.accessToken = data.accessToken; - return instance; - } - - // Handle legacy AWS Cognito format for backward compatibility - instance.authenticationResult = data.AuthenticationResult - ? AuthenticationResult.fromObject(data.AuthenticationResult) - : null; - instance.challengeParameters = data.ChallengeParameters - ? ChallengeParameters.fromObject(data.ChallengeParameters) - : null; - return instance; - } - - getIdToken(): string { - // Return accessToken from new API format if available - if (this.accessToken) { - return this.accessToken; - } - - // Fall back to legacy format - if (!this.authenticationResult) { - return ''; - } - return this.authenticationResult.getIdToken(); - } -} diff --git a/src/models/AuthenticationResult.ts b/src/models/AuthenticationResult.ts deleted file mode 100644 index 816360f..0000000 --- a/src/models/AuthenticationResult.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * AuthenticationResult - * @category Model - */ -export class AuthenticationResult { - AccessToken: string = ''; - ExpiresIn: number = 0; - IdToken: string = ''; - RefreshToken: string = ''; - TokenType: string = ''; - - constructor() { - this.AccessToken = ''; - this.ExpiresIn = 0; - this.IdToken = ''; - this.RefreshToken = ''; - this.TokenType = ''; - } - - static fromObject(data: any = {}): AuthenticationResult { - const instance = new AuthenticationResult(); - instance.AccessToken = data.AccessToken || ''; - instance.ExpiresIn = data.ExpiresIn || 0; - instance.IdToken = data.IdToken || ''; - instance.RefreshToken = data.RefreshToken || ''; - instance.TokenType = data.TokenType || ''; - return instance; - } - - getIdToken(): string { - return this.IdToken; - } -} diff --git a/src/models/ChallengeParameters.ts b/src/models/ChallengeParameters.ts deleted file mode 100644 index 14c48a9..0000000 --- a/src/models/ChallengeParameters.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * ChallengeParameters - * @category Model - */ -export class ChallengeParameters { - parameters: Record = {}; - - constructor() { - this.parameters = {}; - } - - static fromObject(data: any = {}): ChallengeParameters { - const instance = new ChallengeParameters(); - instance.parameters = data || {}; - return instance; - } -} diff --git a/tests/README.md b/tests/README.md index fc036c5..6b0fbcf 100644 --- a/tests/README.md +++ b/tests/README.md @@ -119,6 +119,16 @@ RAIACCEPT_TEST_USERNAME=your_username RAIACCEPT_TEST_PASSWORD=your_password ``` +**Cert/key** use either decoded input from files or base64 encoded strings directly: + +```bash +RAIACCEPT_CERT_PATH=../local_env/decoded.pem +RAIACCEPT_KEY_PATH=../local_env/decoded.key +# or +RAIACCEPT_TEST_CERT_BASE64= +RAIACCEPT_TEST_KEY_BASE64= +``` + ### Running Tests **Unit Tests (Mocked):** diff --git a/tests/integration.test.js b/tests/integration.test.js index 96464f3..6b9815c 100644 --- a/tests/integration.test.js +++ b/tests/integration.test.js @@ -1,4 +1,5 @@ import 'dotenv/config' +import { readFileSync, existsSync } from 'fs' import { describe, it, expect } from 'vitest' import { RaiAcceptService } from '../src/RaiAcceptService.ts' import { HttpClient } from '../src/HttpClient.ts' @@ -7,6 +8,33 @@ import { Consumer } from '../src/models/Consumer.ts' import { Invoice } from '../src/models/Invoice.ts' import { Urls } from '../src/models/Urls.ts' +function loadCertAndKey() { + const certPath = process.env.RAIACCEPT_CERT_PATH || process.env.RAIACCEPT_TEST_CERT_PATH + const keyPath = process.env.RAIACCEPT_KEY_PATH || process.env.RAIACCEPT_TEST_KEY_PATH + if (certPath && keyPath && existsSync(certPath) && existsSync(keyPath)) { + const cert = readFileSync(certPath, 'utf-8').replace(/\r\n/g, '\n').trim() + const key = readFileSync(keyPath, 'utf-8').replace(/\r\n/g, '\n').trim() + return { cert, key } + } + const certBase64 = (process.env.RAIACCEPT_CERT_BASE64 || process.env.RAIACCEPT_TEST_CERT_BASE64 || '') + .replace(/\\n/g, '') + .replace(/\s/g, '') + .trim() + const keyBase64 = (process.env.RAIACCEPT_KEY_BASE64 || process.env.RAIACCEPT_TEST_KEY_BASE64 || '') + .replace(/\\n/g, '') + .replace(/\s/g, '') + .trim() + if (!certBase64 || !keyBase64) return null + try { + const cert = Buffer.from(certBase64, 'base64').toString('utf-8').replace(/\r\n/g, '\n').trim() + const key = Buffer.from(keyBase64, 'base64').toString('utf-8').replace(/\r\n/g, '\n').trim() + if (!cert.startsWith('-----BEGIN') || !key.startsWith('-----BEGIN')) return null + return { cert, key } + } catch { + return null + } +} + describe('RaiAcceptService Integration Tests', () => { describe('Complete Payment Creation Flow', () => { it('should authenticate, create order entry, and create payment session', async () => { @@ -17,18 +45,30 @@ describe('RaiAcceptService Integration Tests', () => { throw new Error('Test credentials required: Set RAIACCEPT_TEST_USERNAME/RAIACCEPT_TEST_PASSWORD or RAIACCEPT_USERNAME/RAIACCEPT_PASSWORD environment variables') } + const certKey = loadCertAndKey() + if (!certKey) { + throw new Error( + 'Test cert/key required. Use either:\n' + + ' - RAIACCEPT_CERT_PATH + RAIACCEPT_KEY_PATH (paths to PEM files)\n' + + ' - RAIACCEPT_CERT_BASE64 + RAIACCEPT_KEY_BASE64 (base64 of full PEM files, e.g. base64 -w 0 cert.pem)' + ) + } + const { cert, key } = certKey + const httpClient = new HttpClient() - const realService = new RaiAcceptService(httpClient) + const realService = new RaiAcceptService(httpClient, cert, key) // Step 1: Authenticate to get access token - const accessToken = await realService.retrieveAccessTokenWithCredentials(username, password) + console.log('[Step 1] Authenticating...') + const authResult = await realService.retrieveAccessTokenWithCredentials(username, password) + const accessToken = authResult?.accessToken expect(accessToken).toBeTruthy() expect(typeof accessToken).toBe('string') expect(accessToken.length).toBeGreaterThan(10) expect(accessToken).toMatch(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/) - console.log('Authentication successful') // Step 2: Create order entry + console.log('[Step 2] Creating order entry...') const consumer = Consumer.fromObject({ email: 'test@example.com', firstName: 'John', @@ -74,9 +114,9 @@ describe('RaiAcceptService Integration Tests', () => { expect(typeof orderResponse.isProduction).toBe('boolean') const orderId = orderResponse.orderIdentification - console.log('Order entry created successfully:', orderId) // Step 3: Create payment session for the order + console.log('[Step 3] Creating payment session...') const paymentSessionRequest = orderRequest const paymentResult = await realService.createPaymentSession(accessToken, paymentSessionRequest, orderId) @@ -96,9 +136,8 @@ describe('RaiAcceptService Integration Tests', () => { expect(typeof paymentResponse.paymentRedirectURL).toBe('string') expect(paymentResponse.paymentRedirectURL).toMatch(/^https?:\/\//) - console.log('Payment session created successfully:', paymentResponse.sessionId) - // Step 4: Get order details + console.log('[Step 4] Getting order details...') const orderDetailsResult = await realService.getOrderDetails(accessToken, orderId) // Verify order details response @@ -118,9 +157,8 @@ describe('RaiAcceptService Integration Tests', () => { expect(orderDetailsResponse.invoice.currency).toBe(invoice.currency) expect(orderDetailsResponse.invoice.merchantOrderReference).toBe(invoice.merchantOrderReference) - console.log('Order details retrieved successfully - status:', orderDetailsResponse.status, 'for order:', orderId) - // Step 5: Verify no transactions exist for newly created payment session + console.log('[Step 5] Getting order transactions...') const transactionsResult = await realService.getOrderTransactions(accessToken, orderId) expect(transactionsResult).toBeDefined() @@ -134,7 +172,23 @@ describe('RaiAcceptService Integration Tests', () => { // For a newly created payment session, there should be no transactions yet expect(transactionsResponse.transactions.length).toBe(0) - console.log('Success: No transactions found for new payment session') + // Step 6: Refresh access token + console.log('[Step 6] Refreshing token...') + const refreshToken = authResult?.refreshToken + expect(refreshToken).toBeTruthy() + const refreshResult = await realService.tokenRefresh(refreshToken) + expect(refreshResult).toBeDefined() + expect(refreshResult).toHaveProperty('object') + const refreshOutput = refreshResult.object + expect(refreshOutput).toBeDefined() + expect(refreshOutput.accessToken).toBeTruthy() + expect(typeof refreshOutput.accessToken).toBe('string') + expect(typeof refreshOutput.accessTokenExpiresIn).toBe('number') + + // Step 7: Logout with refresh token (expects HTTP 200) + console.log('[Step 7] Logging out...') + const logoutSuccess = await realService.tokenLogout(refreshToken) + expect(logoutSuccess).toBe(true) }, 60000) // 60 second timeout for complete payment flow }) }) \ No newline at end of file From 99957e94ba79c89b12f70cc98850dc61977138f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=BDubom=C3=ADr=20Samotn=C3=BD?= Date: Mon, 16 Feb 2026 16:56:50 +0100 Subject: [PATCH 3/3] triv: update ci.yml with new secrets for testing --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8fa039..7ac0b5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: - name: Check if credentials are available id: check-creds run: | - if [ -z "${{ secrets.RAIACCEPT_TEST_USERNAME }}" ] || [ -z "${{ secrets.RAIACCEPT_TEST_PASSWORD }}" ]; then + if [ -z "${{ secrets.RAIACCEPT_TEST_USERNAME }}" ] || [ -z "${{ secrets.RAIACCEPT_TEST_PASSWORD }}" ] || [ -z "${{ secrets.RAIACCEPT_TEST_CERT_BASE64 }}" ] || [ -z "${{ secrets.RAIACCEPT_TEST_KEY_BASE64 }}" ]; then echo "skip=true" >> $GITHUB_OUTPUT echo "⚠️ Integration tests skipped: GitHub Secrets not configured" echo "See .github/SETUP_CI.md for setup instructions" @@ -69,4 +69,6 @@ jobs: env: RAIACCEPT_TEST_USERNAME: ${{ secrets.RAIACCEPT_TEST_USERNAME }} RAIACCEPT_TEST_PASSWORD: ${{ secrets.RAIACCEPT_TEST_PASSWORD }} + RAIACCEPT_TEST_CERT_BASE64: ${{ secrets.RAIACCEPT_TEST_CERT_BASE64 }} + RAIACCEPT_TEST_KEY_BASE64: ${{ secrets.RAIACCEPT_TEST_KEY_BASE64 }} run: npm run integration-tests