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 diff --git a/API.md b/API.md index 5aaf372..a604174 100644 --- a/API.md +++ b/API.md @@ -19,25 +19,25 @@ 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, cert, key); // Authenticate -const accessToken = await RaiAcceptService.retrieveAccessTokenWithCredentials( - apiClient, +const authResult = await service.retrieveAccessTokenWithCredentials( 'username', 'password' ); +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 @@ -54,11 +54,13 @@ 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 @@ -70,12 +72,32 @@ Authenticate with username and password. - `username` (string): Username - `password` (string): Password -**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 authResult = response.object; +const accessToken = authResult?.accessToken; +const refreshToken = authResult?.refreshToken; +const accessTokenExpiresIn = authResult?.accessTokenExpiresIn; +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); ``` --- 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/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 cfcb80b..30dd0c6 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 + 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)); } @@ -79,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 a1186e1..a2310a1 100644 --- a/src/RaiAcceptService.ts +++ b/src/RaiAcceptService.ts @@ -7,7 +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 @@ -23,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; } /** @@ -174,21 +181,41 @@ export class RaiAcceptService { * Retrieve access token with credentials * @param username - Username * @param password - Password - * @returns Access token or null on error + * @returns AuthApiLoginOutput object or null on error */ - async retrieveAccessTokenWithCredentials(username: string, password: string): Promise { + async retrieveAccessTokenWithCredentials( + username: string, + password: string + ): Promise { try { const response = await this.apiClient.token(username, password); 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; } + + } + + /** + * 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 e9052c8..b7b49f4 100644 --- a/src/api/RaiAcceptAPIApi.ts +++ b/src/api/RaiAcceptAPIApi.ts @@ -1,7 +1,11 @@ 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 { 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'; @@ -22,19 +26,27 @@ 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 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[] { @@ -91,16 +103,6 @@ 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 @@ -108,54 +110,117 @@ export class RaiAcceptAPIApi { * @param password - Password * @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): Promise> { + const request = this.tokenRequest(username, password); + + return this.processRequest(request, AuthApiLoginOutput, ErrorResponse, true); } /** * Create token request - * @param authFlow - Authentication flow * @param username - Username * @param password - Password - * @param clientId - Client ID * @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): 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'); + + const loginInput = new AuthApiLoginInput(); + loginInput.username = username; + loginInput.password = password; + + const httpBody = JSON.stringify(ObjectSerializer.sanitizeForSerialization(loginInput)); + const headers = { + 'Content-Type': 'application/json', + }; + + return { + method: 'POST', + url: `${RaiAcceptAPIApi.AUTH_URL}/auth/api/login`, + headers: headers, + body: httpBody, + } as HttpRequest; + } + + /** + * 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 formParams = { - AuthFlow: authFlow, - AuthParameters: { - USERNAME: username, - PASSWORD: password, - }, - ClientId: clientId, + 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 + * @returns True if logout successful (HTTP 200), false otherwise + */ + async tokenLogout(token: string): Promise { + const request = this.tokenLogoutRequest(token); + + try { + const response = await this.client.send(request, true); + const statusCode = response.getStatusCode(); + return statusCode === 200; + } catch (error) { + return false; + } + } - const httpBody = JSON.stringify(ObjectSerializer.sanitizeForSerialization(formParams)); - const headers = RaiAcceptAPIApi.getAuthRequestHeaders(); + /** + * Create token logout request + * @param token - Token to logout + * @returns Request object + */ + tokenLogoutRequest(token: string): HttpRequest { + if (!token) { + throw new InvalidArgumentException('Missing the required parameter $token when calling tokenLogoutRequest'); + } + + const logoutInput = new AuthApiLogoutInput(); + logoutInput.refreshToken = token; + + const httpBody = JSON.stringify(ObjectSerializer.sanitizeForSerialization(logoutInput)); + const headers = { + 'Content-Type': 'application/json', + }; return { method: 'POST', - url: RaiAcceptAPIApi.AUTH_URL, + url: `${RaiAcceptAPIApi.AUTH_URL}/auth/api/logout`, headers: headers, body: httpBody, }; @@ -188,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 = { @@ -203,7 +274,9 @@ export class RaiAcceptAPIApi { url: RaiAcceptAPIApi.API_URL + resourcePath, headers: headers, body: httpBody, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } /** @@ -243,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 = { @@ -258,7 +337,9 @@ export class RaiAcceptAPIApi { url: resourcePath, headers: headers, body: httpBody, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } /** @@ -288,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}`; @@ -301,7 +388,9 @@ export class RaiAcceptAPIApi { method: 'GET', url: resourcePath, headers: headers, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } /** @@ -337,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); @@ -351,7 +446,9 @@ export class RaiAcceptAPIApi { method: 'GET', url: resourcePath, headers: headers, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } /** @@ -381,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`; @@ -394,7 +497,9 @@ export class RaiAcceptAPIApi { method: 'GET', url: resourcePath, headers: headers, - }; + cert: this.cert, + key: this.key, + } as HttpRequest; } /** @@ -436,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); @@ -454,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/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/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 42d91a6..0000000 --- a/src/models/AuthResponse.ts +++ /dev/null @@ -1,34 +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; - - constructor() { - this.authenticationResult = null; - this.challengeParameters = null; - } - - static fromObject(data: any = {}): AuthResponse { - const instance = new AuthResponse(); - instance.authenticationResult = data.AuthenticationResult - ? AuthenticationResult.fromObject(data.AuthenticationResult) - : null; - instance.challengeParameters = data.ChallengeParameters - ? ChallengeParameters.fromObject(data.ChallengeParameters) - : null; - return instance; - } - - getIdToken(): string { - 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