Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hungry-pugs-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ssecd/ihs': patch
---

Remove `development` mode
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
NODE_ENV=sandbox

IHS_ORGANIZATION_ID=
IHS_CLIENT_SECRET=
IHS_SECRET_KEY=
Expand All @@ -7,4 +9,4 @@ TEST_AGENT_NIK=
TEST_AGENT_NAME=
TEST_PATIENT_NIK=
TEST_PATIENT_NAME=
TEST_PATIENT_ID=
TEST_PATIENT_ID=P02478375538
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
Indonesia Health Service API Helpers

- ✅ FHIR API
- ✅ Patient Consent API
- ✅ KYC API
- ✅ Automatic authentication and token invalidation
- ✅ TypeSafe and Autocomplete-Enabled API
- ✅ FHIR
- ✅ Patient Consent
- ✅ KYC
- ✅ KFA
- 🏗️ MSI
- 🏗️ Wilayah

## Instalasi

Expand Down Expand Up @@ -214,7 +217,7 @@ Setiap method pada API ini memiliki parameter dan nilai kembalian yang di-defini

Proses enkripsi dan dekripsi pesan dilakukan dengan menggunakan algoritma `aes-256-gcm` sedangkan untuk proses enkripsi dan dekripsi _symmetric key_ menggunakan metode RSA dengan `RSA_PKCS1_OAEP_PADDING` padding dan `sha256` hash. Semua proses tersebut sudah dilakukan secara internal sesuai dengan spesifikasi IHS pada Playbook.

Proses kriptografi pada API ini memerlukan file _server key_ atau _public key_ dengan format `.pem`. File _public key_ ini dapat disesuaikan lokasinya dengan mengatur `kycPemFile` pada config instance atau class `IHS` yang secara default bernama `publickey.dev.pem` pada mode `development` atau `publickey.pem` pada mode `production` dan berada di _working directory_ atau folder di mana API dijalankan.
Proses kriptografi pada API ini memerlukan file _server key_ atau _public key_ dengan format `.pem`. File _public key_ ini dapat disesuaikan lokasinya dengan mengatur `kycPemFile` pada config instance atau class `IHS` yang secara default bernama `publickey.sandbox.pem` pada mode `sandbox` atau `publickey.pem` pada mode `production` dan berada di _working directory_ atau folder di mana API dijalankan.

File _public key_ atau _server key_ dapat di-unduh di [sini](https://github.com/ssecd/ihs/issues/2).

Expand Down Expand Up @@ -259,17 +262,18 @@ interface IHSConfig {
secretKey: string;

/**
* Mode environment API antara `development`, `staging`, atau `production`
* Mode environment API antara `sandbox` atau `production`,
* `sandbox` secara default
*
* @default process.env.NODE_ENV || 'development'
* @default process.env.NODE_ENV || 'sandbox'
*/
mode: Mode;

/**
* Path atau lokasi public key KYC dari SatuSehat. Dapat
* menggunakan absolute atau relative path. Secara default
* akan membaca nilai environment variable IHS_KYC_PEM_FILE
* atau `publickey.dev.pem` pada mode `development` dan
* atau `publickey.sandbox.pem` pada mode `sandbox` dan
* `publickey.pem` pada mode `production`
*
* @default process.env.IHS_KYC_PEM_FILE
Expand Down
12 changes: 6 additions & 6 deletions src/ihs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ describe('ihs', () => {
expect(config.clientSecret).toBeTypeOf('string');
expect(config.secretKey).toBeDefined();
expect(config.secretKey).toBeTypeOf('string');
expect(config.mode).toBe('development');
expect(config.kycPemFile).toBe('publickey.dev.pem');
expect(config.mode).toBe('sandbox');
expect(config.kycPemFile).toBe('publickey.sandbox.pem');
});

it('config should be valid from async config', async () => {
const userConfig: Partial<IHSConfig> = {
clientSecret: 'th3-53cREt',
secretKey: 'th3_keY',
kycPemFile: 'server-key.pem',
mode: 'development'
mode: 'sandbox'
};

const ihs = new IHS(async () => {
Expand All @@ -38,11 +38,11 @@ describe('ihs', () => {
expect(config).toEqual(userConfig);
});

it('config kycPemFile should be valid between development and production', async () => {
it('config kycPemFile should be valid between sandbox and production', async () => {
const ihsDev = new IHS();
const devConfig = await ihsDev.getConfig();
expect(devConfig.mode).toBe('development');
expect(devConfig.kycPemFile).toBe('publickey.dev.pem');
expect(devConfig.mode).toBe('sandbox');
expect(devConfig.kycPemFile).toBe('publickey.sandbox.pem');

const ihsProd = new IHS({ mode: 'production' });
const prodConfig = await ihsProd.getConfig();
Expand Down
85 changes: 47 additions & 38 deletions src/ihs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { getKFASingleton } from './kfa.js';
import { getKycSingleton } from './kyc.js';

type MaybePromise<T> = T | Promise<T>;
type Mode = 'development' | 'staging' | 'production';
type Mode = 'sandbox' | 'production';
type API = 'auth' | 'fhir' | 'consent' | 'kyc' | 'kfa' | 'kfa2' | 'kfa3';
type BaseURL = Record<Mode, Record<API, string>>;
type EndpointURL = Record<Mode, Record<API, string>>;

export interface IHSConfig {
/**
Expand All @@ -23,17 +23,17 @@ export interface IHSConfig {
secretKey: string;

/**
* Mode environment API antara `development`, `staging`, atau `production`
* Mode environment API antara `sandbox` atau `production`
*
* @default process.env.NODE_ENV || 'development'
* @default process.env.NODE_ENV || 'sandbox'
*/
mode: Mode;

/**
* Path atau lokasi public key KYC dari SatuSehat. Dapat
* menggunakan absolute atau relative path. Secara default
* akan membaca nilai environment variable IHS_KYC_PEM_FILE
* atau `publickey.dev.pem` pada mode `development` dan
* atau `publickey.sandbox.pem` pada mode `sandbox` dan
* `publickey.pem` pada mode `production`
*
* @default process.env.IHS_KYC_PEM_FILE
Expand All @@ -45,7 +45,7 @@ type UserConfig = Partial<IHSConfig> | (() => MaybePromise<Partial<IHSConfig>>);

type RequestConfig = {
type: Exclude<API, 'auth'>;
path: `/${string}`;
path: string;
searchParams?: URLSearchParams | Record<string, string> | [string, string][];
} & RequestInit;

Expand All @@ -59,35 +59,38 @@ export interface AuthStore {
set(detail: AuthDetail): MaybePromise<void>;
}

const defaultBaseUrls: BaseURL = {
development: {
auth: `https://api-satusehat-dev.dto.kemkes.go.id/oauth2/v1`,
fhir: `https://api-satusehat-dev.dto.kemkes.go.id/fhir-r4/v1`,
consent: `https://api-satusehat-dev.dto.kemkes.go.id/consent/v1`,
kyc: `https://api-satusehat-dev.dto.kemkes.go.id/kyc/v1`,
kfa: `https://api-satusehat-dev.dto.kemkes.go.id/kfa`,
kfa2: `https://api-satusehat-dev.dto.kemkes.go.id/kfa-v2`,
kfa3: `https://api-satusehat-dev.dto.kemkes.go.id/kfa-v3`
},
staging: {
auth: `https://api-satusehat-stg.dto.kemkes.go.id/oauth2/v1`,
fhir: `https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1`,
consent: `https://api-satusehat-stg.dto.kemkes.go.id/consent/v1`,
kyc: `https://api-satusehat-stg.dto.kemkes.go.id/kyc/v1`,
kfa: `https://api-satusehat-stg.dto.kemkes.go.id/kfa`,
kfa2: `https://api-satusehat-stg.dto.kemkes.go.id/kfa-v2`,
kfa3: `https://api-satusehat-stg.dto.kemkes.go.id/kfa-v3`
const defaultBaseUrls: Record<Mode, string> = {
sandbox: 'https://api-satusehat-stg.dto.kemkes.go.id',
production: 'https://api-satusehat.kemkes.go.id'
};

const defaultEndpointUrls = Object.entries(defaultBaseUrls).reduce(
(acc, [mode, url]) => {
acc[mode as Mode] = {
auth: `${url}/oauth2/v1`,
fhir: `${url}/fhir-r4/v1`,
consent: `${url}/consent/v1`,
kyc: `${url}/kyc/v1`,
kfa: `${url}/kfa`,
kfa2: `${url}/kfa-v2`,
kfa3: `${url}/kfa-v3`
};
return acc;
},
production: {
auth: `https://api-satusehat.kemkes.go.id/oauth2/v1`,
fhir: `https://api-satusehat.kemkes.go.id/fhir-r4/v1`,
consent: `https://api-satusehat.kemkes.go.id/consent/v1`,
kyc: `https://api-satusehat.kemkes.go.id/kyc/v1`,
kfa: `https://api-satusehat.kemkes.go.id/kfa`,
kfa2: `https://api-satusehat.kemkes.go.id/kfa-v2`,
kfa3: `https://api-satusehat.kemkes.go.id/kfa-v3`
}
} as const;
<EndpointURL>{}
);

function buildUrl(base: string, path: string) {
// ensure base ends with a slash so it's treated as a directory
const normalizedBase = base.endsWith('/') ? base : base + '/';

/**
* URL constructor params rules
* - base must end with / otherwise it's treated as a file
* - path in input must NOT start with / otherwise it resets the path
*/
return new URL(path.replace(/^\/+/, ''), normalizedBase);
}

export default class IHS {
private config: Readonly<IHSConfig> | undefined;
Expand All @@ -97,7 +100,7 @@ export default class IHS {

private async applyUserConfig(): Promise<void> {
const defaultConfig: Readonly<IHSConfig> = {
mode: (process.env['NODE_ENV'] as Mode) || 'development',
mode: (process.env['NODE_ENV'] as Mode) || 'sandbox',
clientSecret: process.env['IHS_CLIENT_SECRET'] || '',
secretKey: process.env['IHS_SECRET_KEY'] || '',
kycPemFile: process.env['IHS_KYC_PEM_FILE'] || ''
Expand All @@ -107,9 +110,15 @@ export default class IHS {
typeof this.userConfig === 'function' ? await this.userConfig() : this.userConfig;

const mergedConfig = { ...defaultConfig, ...resolveUserConfig };

if (!(<Mode[]>['sandbox', 'production']).includes(mergedConfig.mode)) {
console.warn(`[ihs]: Invalid mode "${mergedConfig.mode}", falling back to "sandbox".`);
mergedConfig.mode = 'sandbox';
}

if (!mergedConfig.kycPemFile) {
mergedConfig.kycPemFile =
mergedConfig.mode === 'development' ? 'publickey.dev.pem' : 'publickey.pem';
mergedConfig.mode === 'sandbox' ? 'publickey.sandbox.pem' : 'publickey.pem';
}
this.config = mergedConfig;
}
Expand Down Expand Up @@ -140,7 +149,7 @@ export default class IHS {
async request(config: RequestConfig): Promise<Response> {
const { mode } = await this.getConfig();
const { type, path, searchParams, ...init } = config;
const url = new URL(defaultBaseUrls[mode][type] + path);
const url = buildUrl(defaultEndpointUrls[mode][type], path);
url.search = searchParams ? new URLSearchParams(searchParams).toString() : url.search;
const auth = await this.auth();
init.headers = {
Expand All @@ -167,7 +176,7 @@ export default class IHS {
throw new Error(message);
}

const url = defaultBaseUrls[mode]['auth'] + '/accesstoken?grant_type=client_credentials';
const url = defaultEndpointUrls[mode]['auth'] + '/accesstoken?grant_type=client_credentials';
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
Expand Down