Skip to content

Commit fb4558f

Browse files
authored
Release v1.1.0: Organizations Resource and Enhanced Sources Authentication (#4)
Major release introducing Organizations resource, dynamic API key management, and enhanced Sources authentication. ✨ New: Organizations API, runtime API key switching, improved test utilities 🔧 Enhanced: Lazy PK authentication for Sources, comprehensive TypeScript support ⚠️ Breaking: Removed Sources.create() method and related types 📊 Stats: 17 files changed, 236 tests passing, clean build See CHANGELOG.md for detailed migration guide and full feature documentation.
2 parents 564c681 + c016350 commit fb4558f

File tree

16 files changed

+829
-365
lines changed

16 files changed

+829
-365
lines changed

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.0] - 2025-09-05
9+
10+
### Added
11+
12+
- **Organization Resource** - New resource for retrieving organization information including branding, payment method settings, and payout configurations
13+
- **createTestOrganization()** utility function for testing with realistic organization data
14+
- **Organization Types** - Complete TypeScript definitions for Organization, OrganizationBranding, PaymentMethodSettings, PaymentGateway, PaymentMethodRate, and PayoutSettings
15+
16+
### Changed
17+
18+
- **BREAKING: SourcesResource** - Removed `create()` method as sources should only be created client-side for security
19+
- **SourcesResource Authentication** - Implemented lazy public key (PK) authentication that automatically switches from secret key to public key when retrieving sources
20+
- **BaseClient** - Added `setApiKey()` and `getApiKey()` methods for dynamic API key switching
21+
- **Enhanced Test Utilities** - Extended SpyableMagpie with `mockRequest()` and `mockNetworkError()` methods for better test coverage
22+
23+
### Fixed
24+
25+
- **Sources Security** - Sources now correctly use public key authentication as required by the API
26+
- **Test Coverage** - Updated sources tests to remove create method tests and add PK authentication tests
27+
28+
### Technical Improvements
29+
30+
- Lazy authentication switching prevents unnecessary HTTP calls during SDK initialization
31+
- Public key caching eliminates redundant organization API calls
32+
- Enhanced mock testing infrastructure with request tracking and custom response handling
33+
834
## [1.0.0] - 2025-09-04
935

1036
### Added

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2024 Magpie IM
3+
Copyright (c) 2025 Magpie IM
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

docs/plan.md

Lines changed: 0 additions & 60 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@magpieim/magpie-node",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "The Magpie API Library for NodeJS enables you to work with Magpie APIs.",
55
"keywords": [
66
"magpie",

src/__tests__/testUtils.ts

Lines changed: 136 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export interface LastRequest {
2222
*/
2323
interface SpyableMagpie extends Magpie {
2424
LAST_REQUEST: LastRequest | null;
25+
REQUESTS: LastRequest[];
26+
mockRequest(method: string, url: string, responseData: any, status?: number, headers?: Record<string, string>): void;
27+
mockNetworkError(method: string, url: string): void;
2528
}
2629

2730
/**
@@ -39,6 +42,22 @@ export function getSpyableMagpie(
3942

4043
// Initialize the LAST_REQUEST tracker
4144
magpie.LAST_REQUEST = null;
45+
magpie.REQUESTS = [];
46+
47+
// Store mock responses and errors
48+
const mockResponses: Map<string, { data: any; status: number; headers: Record<string, string> }> = new Map();
49+
const mockErrors: Map<string, Error> = new Map();
50+
51+
// Add mock methods
52+
magpie.mockRequest = (method: string, url: string, responseData: any, status = 200, headers = {}) => {
53+
const key = `${method.toUpperCase()}:${url}`;
54+
mockResponses.set(key, { data: responseData, status, headers });
55+
};
56+
57+
magpie.mockNetworkError = (method: string, url: string) => {
58+
const key = `${method.toUpperCase()}:${url}`;
59+
mockErrors.set(key, new Error('Network Error'));
60+
};
4261

4362
// Mock the HTTP client to capture requests
4463
(magpie as any).http.request = jest.fn().mockImplementation((requestConfig: AxiosRequestConfig) => {
@@ -89,7 +108,7 @@ export function getSpyableMagpie(
89108
};
90109

91110
// Capture request details
92-
magpie.LAST_REQUEST = {
111+
const requestDetails = {
93112
method: requestConfig.method?.toUpperCase() ?? 'GET',
94113
url: requestConfig.url ?? '',
95114
data: requestConfig.data,
@@ -103,17 +122,52 @@ export function getSpyableMagpie(
103122
}
104123
};
105124

106-
// Return a mock successful response
107-
const mockResponse: AxiosResponse = {
125+
magpie.LAST_REQUEST = requestDetails;
126+
magpie.REQUESTS.push(requestDetails);
127+
128+
// Check for mock responses and errors
129+
const key = `${requestDetails.method}:${requestDetails.url}`;
130+
131+
// Check for network error first
132+
if (mockErrors.has(key)) {
133+
const error = mockErrors.get(key);
134+
return Promise.reject(error ?? new Error('Network Error'));
135+
}
136+
137+
// Check for custom mock response
138+
if (mockResponses.has(key)) {
139+
const mockConfig = mockResponses.get(key)!;
140+
const mockResponse: AxiosResponse = {
141+
data: mockConfig.data,
142+
status: mockConfig.status,
143+
statusText: mockConfig.status >= 400 ? 'Error' : 'OK',
144+
headers: { 'request-id': 'req_test_123', ...mockConfig.headers },
145+
config: requestConfig as any,
146+
request: {}
147+
};
148+
149+
// Throw error for 4xx and 5xx status codes
150+
if (mockConfig.status >= 400) {
151+
const error = new Error(`HTTP ${mockConfig.status} Error`) as any;
152+
error.response = mockResponse;
153+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
154+
return Promise.reject(error);
155+
}
156+
157+
return Promise.resolve(mockResponse);
158+
}
159+
160+
// Return default mock response
161+
const defaultResponse: AxiosResponse = {
108162
data: { id: 'mock_response', object: 'test' },
109163
status: 200,
110164
statusText: 'OK',
111-
headers: {},
165+
headers: { 'request-id': 'req_default_123' },
112166
config: requestConfig as any,
113167
request: {}
114168
};
115169

116-
return Promise.resolve(mockResponse);
170+
return Promise.resolve(defaultResponse);
117171
});
118172

119173
return magpie;
@@ -259,29 +313,6 @@ export function createTestCharge(overrides: Record<string, any> = {}): Record<st
259313
};
260314
}
261315

262-
/**
263-
* Utility to create test source data
264-
*/
265-
export function createTestSource(overrides: Record<string, any> = {}): Record<string, any> {
266-
return {
267-
type: 'card',
268-
card: {
269-
number: '4242424242424242',
270-
exp_month: 12,
271-
exp_year: 2025,
272-
cvc: '123'
273-
},
274-
redirect: {
275-
success: 'https://example.com/success',
276-
fail: 'https://example.com/fail'
277-
},
278-
billing: {
279-
name: 'Test User',
280-
email: 'test@example.com'
281-
},
282-
...overrides
283-
};
284-
}
285316

286317
/**
287318
* Utility to create test line item data
@@ -415,6 +446,82 @@ export function createTestWebhookHeaders(signature: string, timestamp?: number,
415446
return headers;
416447
}
417448

449+
/**
450+
* Utility to create test organization data
451+
*/
452+
export function createTestOrganization(overrides: Record<string, any> = {}): Record<string, any> {
453+
return {
454+
object: 'organization',
455+
id: `org_test_${getRandomString()}`,
456+
title: 'Test Organization',
457+
account_name: 'Test Org Account',
458+
statement_descriptor: 'TEST_ORG',
459+
pk_test_key: `pk_test_${getRandomString()}`,
460+
sk_test_key: `sk_test_${getRandomString()}`,
461+
pk_live_key: `pk_live_${getRandomString()}`,
462+
sk_live_key: `sk_live_${getRandomString()}`,
463+
branding: {
464+
icon: 'https://example.com/icon.png',
465+
logo: null,
466+
use_logo: false,
467+
brand_color: '#fffefd',
468+
accent_color: '#1a3da6'
469+
},
470+
status: 'approved',
471+
created_at: '2021-08-15T23:13:11.682944+08:00',
472+
updated_at: '2025-09-05T11:35:51.957059+08:00',
473+
business_address: null,
474+
payment_method_settings: {
475+
card: {
476+
mid: null,
477+
gateway: {
478+
id: 'default',
479+
name: 'Magpie Gateway'
480+
},
481+
rate: {
482+
mdr: 0.029,
483+
fixed_fee: 1000,
484+
formula: 'mdr_plus_fixed'
485+
},
486+
status: 'approved'
487+
},
488+
gcash: {
489+
mid: '217020000038029496672',
490+
gateway: null,
491+
rate: {
492+
mdr: 0.022,
493+
fixed_fee: 0,
494+
formula: 'mdr_plus_fixed'
495+
},
496+
status: 'approved'
497+
}
498+
},
499+
rates: {
500+
card: {
501+
mdr: 0.029,
502+
fixed_fee: 1000
503+
},
504+
gcash: {
505+
mdr: 0.022,
506+
fixed_fee: 0
507+
}
508+
},
509+
payout_settings: {
510+
schedule: 'automatic',
511+
delivery_type: 'standard',
512+
bank_code: 'BPI/BPI Family Savings Bank',
513+
account_number: '3259442965',
514+
account_name: 'Test Account'
515+
},
516+
metadata: {
517+
business_website: 'https://test.com',
518+
support_phone: '917 513 4281',
519+
support_email: 'support@test.com'
520+
},
521+
...overrides
522+
};
523+
}
524+
418525
/**
419526
* Utility to create test webhook signature configuration
420527
*/
@@ -427,4 +534,4 @@ export function createTestWebhookConfig(overrides: Record<string, any> = {}): Re
427534
prefix: 'v1=',
428535
...overrides
429536
};
430-
}
537+
}

src/base-client.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,59 @@ export class BaseClient {
510510
this.config.debug = debug;
511511
}
512512

513-
// Test the connection
513+
/**
514+
* Updates the API key used for authentication.
515+
*
516+
* This method allows switching between different API keys (e.g., from secret key
517+
* to public key) for specific requests. The key validation ensures only valid
518+
* Magpie API keys are accepted.
519+
*
520+
* @param apiKey - New API key (must start with 'sk_' or 'pk_')
521+
*
522+
* @throws {Error} When apiKey is missing, invalid, or malformed
523+
*
524+
* @example
525+
* ```typescript
526+
* // Switch to public key for sources operations
527+
* client.setApiKey('pk_test_123');
528+
*
529+
* // Switch back to secret key
530+
* client.setApiKey('sk_test_456');
531+
* ```
532+
*/
533+
public setApiKey(apiKey: string): void {
534+
if (!apiKey || typeof apiKey !== 'string') {
535+
throw new Error('Missing or invalid API key');
536+
}
537+
538+
if (!apiKey.startsWith('sk_') && !apiKey.startsWith('pk_')) {
539+
throw new Error('Invalid API key - must start with sk_ or pk_');
540+
}
541+
542+
this.secretKey = apiKey;
543+
544+
// Update the Axios instance with the new authentication
545+
this.http.defaults.auth = {
546+
username: apiKey,
547+
password: '',
548+
};
549+
}
550+
551+
/**
552+
* Gets the currently configured API key.
553+
*
554+
* @returns The current API key (secret or public)
555+
*
556+
* @example
557+
* ```typescript
558+
* const currentKey = client.getApiKey();
559+
* console.log(currentKey.startsWith('sk_') ? 'Secret Key' : 'Public Key');
560+
* ```
561+
*/
562+
public getApiKey(): string {
563+
return this.secretKey;
564+
}
565+
514566
/**
515567
* Tests connectivity to the Magpie API.
516568
*

0 commit comments

Comments
 (0)