@bpmlib/sauth-frontend
HTTP client dengan auto-token lifecycle dan permission utilities untuk sauth ecosystem.
Versi: 0.1.0
Kategori: Pure Utils
TL;DR
Library ini menyediakan dua hal: (1) HTTP client (RequestInstance) yang menangani token lifecycle secara otomatis — mendapatkan, menyimpan, dan me-refresh access token dari sauth ecosystem; (2) permission utilities untuk mengecek izin user berbasis Inertia PageProps atau JWT token langsung.
Exports:
// Entry point utama
import { RequestInstance, ServiceCodes } from '@bpmlib/sauth-frontend'
import type { RequestOptions, SauthTokenClaims, RouteDict, RouteParams } from '@bpmlib/sauth-frontend'
// Sub-path exports
import RequestInstance from '@bpmlib/sauth-frontend/http'
import { getUserPermissions, permissionCheck } from '@bpmlib/sauth-frontend/auth/inertia'
import { decodeTokenClaims, getPermissionsFromToken, permissionCheckFromToken } from '@bpmlib/sauth-frontend/auth/token'
import { ServiceCodes } from '@bpmlib/sauth-frontend/constants'Installation & Setup
Requirements
- Node.js 18+
- TypeScript 5.0+
Peer Dependencies
| Dependency | Versi | Status | Deskripsi |
|---|---|---|---|
axios | ^1.13.0 | Required | HTTP client |
@inertiajs/core | ^2.0.0 | Optional | Diperlukan untuk auth/inertia utilities |
npm install axios
# Jika menggunakan auth/inertia:
npm install @inertiajs/corePackage Installation
Library dipublish ke private registry. Konfigurasi .npmrc terlebih dahulu:
# .npmrc
@bpmlib:registry=https://js.pkg.ppsdmmigas.id/npm install @bpmlib/sauth-frontendyarn add @bpmlib/sauth-frontendpnpm add @bpmlib/sauth-frontendbun add @bpmlib/sauth-frontendImport
Library menyediakan beberapa entry points — gunakan sub-path import untuk bundle lebih ringan:
// Semua dari entry point utama
import { RequestInstance, getUserPermissions, permissionCheck, ServiceCodes } from '@bpmlib/sauth-frontend'
// Sub-path (lebih ringan jika hanya butuh satu fitur)
import RequestInstance from '@bpmlib/sauth-frontend/http'
import { getUserPermissions, permissionCheck } from '@bpmlib/sauth-frontend/auth/inertia'
import { decodeTokenClaims, permissionCheckFromToken } from '@bpmlib/sauth-frontend/auth/token'
import { ServiceCodes } from '@bpmlib/sauth-frontend/constants'Quick Start
Session Mode (Inertia/Breeze)
Untuk aplikasi Laravel + Inertia.js. Token diperoleh otomatis dari ticketbooth setiap kali dibutuhkan — tidak perlu konfigurasi khusus.
// api/gwa.ts
import RequestInstance from '@bpmlib/sauth-frontend/http'
import routes from './gwa-routes.json'
const gwaApi = new RequestInstance(
'https://gwa.ppsdmmigas.id',
routes,
'gwa'
// mode: 'session' adalah default
)
// Token dikelola otomatis
const { data } = await gwaApi.get('training.index')
const { data: detail } = await gwaApi.get('training.show', 123)Permission Check (Inertia)
import { permissionCheck } from '@bpmlib/sauth-frontend/auth/inertia'
import { usePage } from '@inertiajs/vue3' // atau @inertiajs/react
const page = usePage()
// Cek satu permission
if (permissionCheck('training.read', page)) {
// tampilkan fitur
}
// OR logic — true jika salah satu terpenuhi
if (permissionCheck(['training.read', 'training.write'], page)) {
// tampilkan fitur
}Key Points:
pageadalah Inertia page object dariusePage()framework adapter masing-masingpermissionCheckreturntruejika user adalah webmaster (fet === 'wm')
Configuration
RequestInstance dikonfigurasi melalui parameter options keempat (tipe RequestOptions):
new RequestInstance(baseURL, routes, service, {
mode: 'pkce',
ticketboothUrl: '/api/sauth/token',
accessTokenPrefix: 'tkaac_',
tokenTtl: 720,
refreshUrl: '/oauth/token',
clientId: 'my-client-id',
onAuthFailure: () => router.push('/login'),
})Available Options
| Option | Type | Default | Description |
|---|---|---|---|
mode | 'session' | 'pkce' | 'session' | Strategi token lifecycle Lihat selengkapnya |
ticketboothUrl | string | '/api/sauth/token' | Endpoint ticketbooth (session mode) |
accessTokenPrefix | string | 'tkaac_' | Prefix nama cookie access token |
tokenTtl | number | 720 | Fallback TTL dalam detik saat expires_in tidak ada di response |
refreshUrl | string | '/oauth/token' | Endpoint OAuth2 refresh grant (pkce mode) |
clientId | string | - | OAuth2 client_id untuk refresh grant (pkce mode) |
onAuthFailure | () => void | - | Dipanggil saat pkce refresh gagal Lihat selengkapnya |
mode
Menentukan strategi token lifecycle yang digunakan instance.
Values:
'session'— Tidak ada refresh token. Saat access cookie tidak ada atau 401, library POST ke ticketbooth untuk token baru. Cocok untuk aplikasi Inertia/Breeze di mana session PHP menjaga autentikasi.'pkce'— Menggunakan access + refresh token. Token di-refresh via OAuth2 refresh grant saat access cookie tidak ada atau 401. Cocok untuk SPA standalone.
Contoh:
// Session mode — default untuk Inertia apps
new RequestInstance(baseURL, routes, 'gwa', { mode: 'session' })
// PKCE mode — untuk standalone SPA
new RequestInstance(baseURL, routes, 'gwc', {
mode: 'pkce',
clientId: 'gwc-spa',
onAuthFailure: () => window.location.href = '/login',
})onAuthFailure
Callback yang dipanggil ketika refresh token gagal di pkce mode — biasanya karena refresh token expired atau tidak valid.
Signature:
onAuthFailure: () => voidContoh:
import router from './router'
new RequestInstance(baseURL, routes, 'gwc', {
mode: 'pkce',
onAuthFailure: () => router.push({ name: 'login' }),
})Use Case: Gunakan untuk redirect user ke halaman login ketika sesi berakhir. Library sudah menghapus cookies access dan refresh sebelum callback ini dipanggil.
Core Concepts
Token Lifecycle: Session vs PKCE
Library mengelola dua strategi secara otomatis via Axios interceptor:
Session mode (mode: 'session'):
- Request keluar → cek access cookie → ada? attach Bearer → kirim
- Tidak ada? → POST ticketbooth
{ target: service }→ simpan access cookie (80% TTL dariexpires_in) → attach Bearer - Dapat 401? → hapus access cookie → ulangi lewat ticketbooth → retry sekali
PKCE mode (mode: 'pkce'):
- Request keluar → cek access cookie → ada? attach Bearer → kirim
- Tidak ada? → POST refresh grant → simpan access + refresh cookie baru → attach Bearer
- Dapat 401? → POST refresh grant → retry sekali. Gagal? → hapus kedua cookie → panggil
onAuthFailure
WARNING
Interceptor hanya retry sekali — tidak ada loop. 401 kedua langsung reject.
Cookie Naming Convention
| Cookie | Nama | TTL |
|---|---|---|
| Access token | tkaac_{service} | 80% dari expires_in ticketbooth |
| Refresh token | tkarf_{service} | Session cookie (hilang saat tab ditutup) |
Prefix tkaac_ dapat diganti via accessTokenPrefix. Prefix tkarf_ tidak bisa diganti — hardcoded untuk interoperabilitas antar app dalam satu ekosistem.
Contoh: service 'gwa' → cookie tkaac_gwa (access) dan tkarf_gwa (refresh).
Subclass dapat override storeTokens() untuk mengubah TTL refresh token (contoh: ExamRequestInstance yang set refresh TTL fixed).
Wildcard Permission Matching & OR Logic
Permission check menggunakan dua mekanisme:
Wildcard (*): Pattern dibangun dari entry fet user, lalu ditest terhadap permission yang dibutuhkan. user.* di fet → /^user\..*$/ → cocok dengan user.read. Tapi user.read di fet tidak cocok dengan user.* sebagai needed — arah wildcard tidak bisa dibalik.
OR logic: permissionCheck(['perm.a', 'perm.b']) → true jika ada satu pasang (fet_entry × needed_entry) yang cocok.
Special case: fet === 'wm' (webmaster) → selalu return true, melewati semua cek.
// User dengan fet: ['training.*', 'exam.read']
permissionCheck('training.write', page) // true — wildcard match
permissionCheck('exam.read', page) // true — exact match
permissionCheck('exam.write', page) // false
permissionCheck(['exam.write', 'exam.read'], page) // true — OR: exam.read cocok
permissionCheck('training.*', page) // false — arah wildcard terbalikLogic yang sama berlaku di permissionCheckFromToken() untuk JWT-based checks.
API Reference
Classes
RequestInstance
HTTP client dengan token lifecycle otomatis. Dapat di-extend untuk behavior custom (contoh: override refreshingToken() untuk non-OAuth2 endpoint).
Constructor
new RequestInstance<TRoutes>(
baseURL: string,
routes: RouteDict,
service: string,
options?: RequestOptions
)Parameters
| Name | Type | Default | Description |
|---|---|---|---|
baseURL | string | - | Base URL resource server |
routes | RouteDict | - | Route dictionary berdasarkan HTTP method |
service | string | - | Service code — digunakan sebagai suffix nama cookie |
options | RequestOptions | {} | Konfigurasi token lifecycle |
Methods
Contains:
- Cookie Helpers
- Token Management
- Request Configuration
- Route Helpers
- HTTP Methods
- Protected (untuk Subclass)
Cookie Helpers
setCookie()
setCookie(name: string, value: string, lifetimeSeconds?: number): voidSet browser cookie dengan SameSite=None; Secure. Tanpa lifetimeSeconds → session cookie (hilang saat tab ditutup).
getCookie()
getCookie(name: string): string | nullBaca cookie berdasarkan nama. Return null jika tidak ditemukan.
eraseCookie()
eraseCookie(name: string): voidHapus cookie berdasarkan nama.
getAccessTk()
getAccessTk(): string | nullBaca access token cookie untuk service ini (tkaac_{service}).
getRefreshTk()
getRefreshTk(): string | nullBaca refresh token cookie untuk service ini (tkarf_{service}).
Token Management
storeTokens()
storeTokens(accessToken: string, refreshToken: string, expiresIn: number): voidSimpan access dan refresh token setelah PKCE exchange. Access cookie TTL = 80% dari expiresIn. Refresh disimpan sebagai session cookie — override method ini di subclass untuk set fixed TTL.
| Name | Type | Description |
|---|---|---|
accessToken | string | Access token string |
refreshToken | string | Refresh token string |
expiresIn | number | TTL access token dalam detik |
Request Configuration
Method-method ini bisa di-chain sebelum request. Konfigurasi di-reset otomatis setelah setiap HTTP call.
setBody()
setBody(data: Record<string, unknown> | FormData, asFormData?: boolean): thisSet request body. asFormData: true → Content-Type: multipart/form-data, auto-convert JSON ke FormData (mendukung File, FileList, Date, nested objects, dan arrays).
setHeader()
setHeader(key: string, value: string): thisSet satu request header. Content-Type via setBody() tidak bisa di-override.
setHeaders()
setHeaders(objHeader: Record<string, string>): thisSet beberapa request header sekaligus.
setUrlParam()
setUrlParam(key: string, value: string | number | boolean): thisSet satu URL query parameter.
setUrlParams()
setUrlParams(paramsObj: Record<string, string | number | boolean>): thisSet beberapa URL query parameter sekaligus.
setResponseType()
setResponseType(type?: ResponseType): thisSet response type Axios. Default: 'json'. Nilai lain: 'blob', 'arraybuffer', 'document', 'text', 'stream'.
getConfig()
getConfig(): AxiosRequestConfigKembalikan konfigurasi request saat ini sebagai AxiosRequestConfig.
Route Helpers
useUrl()
useUrl<TRoute extends string>(
routeName: string,
method?: HttpMethod,
...params: RouteParams<TRoute>
): stringResolve nama route ke URL penuh, mengganti path parameters. Melempar error jika parameter wajib tidak disertakan.
| Name | Type | Default | Description |
|---|---|---|---|
routeName | string | - | Nama key di route dictionary |
method | HttpMethod | 'get' | HTTP method untuk lookup |
...params | RouteParams | [] | Path params — positional, named object, atau spread |
Returns: string — URL dengan parameter yang sudah di-substitute
// Route: { get: { 'training.show': '/training/{id}' } }
api.useUrl('training.show', 'get', 123) // '/training/123'
api.useUrl('training.show', 'get', { id: 123 }) // '/training/123'setUrl()
setUrl(method: string, routeName: string, ...routeParam: RouteParams<TRoute>): stringAlias useUrl() dengan urutan argumen berbeda (method pertama). Digunakan internal oleh semua HTTP methods.
HTTP Methods
Semua method HTTP me-reset konfigurasi request setelah dipanggil. Generic T adalah tipe response data.
get() / delete()
get<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>
delete<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>post() / put() / patch()
post<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>
put<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>
patch<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>Body dikirim via setBody() sebelum memanggil method ini.
const { data } = await api.setBody({ name: 'Training Baru' }).post('training.store')
const { data: list } = await api.setUrlParam('page', 2).get('training.index')Protected (untuk Subclass)
Method berikut protected — dapat di-override di subclass untuk behavior custom.
issueToken()
protected async issueToken(): Promise<string>POST ke ticketbooth { target: service }, simpan access cookie di 80% TTL, return token string. Digunakan session mode.
refreshingToken()
protected async refreshingToken(): Promise<void>Refresh token sesuai mode aktif. Session: erase access cookie + reissue via ticketbooth. PKCE: OAuth2 refresh grant + store kedua cookie baru. Override untuk logika refresh non-standard.
Functions
auth/inertia
getUserPermissions()
Ambil permissions user dari Inertia PageProps (auth.user.fet).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
page | Page | - | Inertia page object — pass usePage() dari framework adapter |
ignoreWm | boolean | false | Jika true, return raw fet meski user adalah webmaster |
Returns: 'wm' untuk webmaster | string[] untuk user normal | null jika tidak terautentikasi
import { getUserPermissions } from '@bpmlib/sauth-frontend/auth/inertia'
import { usePage } from '@inertiajs/vue3'
const perms = getUserPermissions(usePage())
// 'wm' | string[] | nullpermissionCheck()
Cek apakah user memiliki minimal satu dari permissions yang dibutuhkan.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
needed | string | string[] | - | Permission yang dicek — OR logic jika array |
page | Page | - | Inertia page object |
Returns: boolean
auth/token
decodeTokenClaims()
Decode payload JWT tanpa verifikasi signature. Verifikasi signature dilakukan server-side oleh sauth-client.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
token | string | - | Raw JWT string |
Returns: SauthTokenClaims | null — null jika token malformed
getPermissionsFromToken()
Ekstrak klaim fet dari JWT string.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
token | string | - | Raw JWT string |
Returns: 'wm' | string[] | null
permissionCheckFromToken()
Cek permission dari JWT string. Logic identik dengan permissionCheck() — wildcard dan OR logic berlaku sama.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
needed | string | string[] | - | Permission yang dicek |
token | string | - | Raw JWT string |
Returns: boolean
Constants
ServiceCodes
Konstanta kode service dalam sauth ecosystem.
import { ServiceCodes } from '@bpmlib/sauth-frontend/constants'| Key | Value | Deskripsi |
|---|---|---|
ServiceCodes.GWA | 'gwa' | Gateway Admin |
ServiceCodes.GWC | 'gwc' | Gateway Customer |
ServiceCodes.WEBMASTER | 'wm' | Marker webmaster bypass |
ServiceCodes.ROLE_PERSONAL | 'psn' | Role personal customer |
ServiceCodes.ROLE_COMPANY | 'cpy' | Role company customer |
ServiceCodes.ROLE_MITRA | 'mit' | Role mitra customer |
ServiceCodes.CUSTOMER_ROLES | ['psn', 'cpy', 'mit'] | Array semua customer roles |
Types
RequestOptions
interface RequestOptions {
mode?: 'session' | 'pkce'
ticketboothUrl?: string
accessTokenPrefix?: string
tokenTtl?: number
refreshUrl?: string
clientId?: string
onAuthFailure?: () => void
}Konfigurasi token lifecycle untuk RequestInstance. Lihat Configuration untuk detail setiap option.
TicketboothResponse
interface TicketboothResponse {
access: string
expires_in: number
}Response dari endpoint ticketbooth (POST /api/sauth/token).
PassportTokenResponse
interface PassportTokenResponse {
access_token: string
refresh_token: string
token_type: string
expires_in: number
}Response dari Laravel Passport OAuth2 token endpoint (pkce refresh grant).
SauthTokenClaims
interface SauthTokenClaims {
iss: string
aud: string
sid: string
snm?: string
fet?: string | string[]
act?: { sub: string }
scope?: string
iat: number
exp: number
}Claims dalam JWT yang diissue oleh sauth ecosystem.
| Field | Tipe | Deskripsi |
|---|---|---|
iss | string | Issuer — service code yang menerbitkan token |
aud | string | Audience — service code target |
sid | string | Subject ID (user ID) |
snm | string? | Subject name (nama user) |
fet | string | string[]? | Permissions — 'wm' untuk webmaster, array untuk user normal |
act | { sub: string }? | Actor claims untuk token exchange / M2M |
scope | string? | OAuth2 scope |
iat | number | Issued at (Unix timestamp) |
exp | number | Expiry (Unix timestamp) |
RouteDict
type RouteDict = Partial<Record<HttpMethod, Record<string, string>>>Dictionary route yang dipartisi berdasarkan HTTP method. Biasanya diimport dari file JSON.
{
"get": {
"training.index": "/api/training",
"training.show": "/api/training/{id}"
},
"post": {
"training.store": "/api/training"
}
}RouteParams
type RouteParams<T extends string> = ExtractParams<T> extends never
? []
: [Record<ExtractParams<T>, string | number>] | (string | number)[]Tipe untuk path parameters yang di-extract dari URL template. Mendukung positional array atau named object.
HttpMethod
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'ResponseType
type ResponseType = 'json' | 'blob' | 'arraybuffer' | 'document' | 'text' | 'stream'Examples
Contains:
- 1. Setup PKCE Mode untuk SPA
- 2. Request dengan Body dan File Upload
- 3. Permission Guard di Vue Component
- 4. Permission Check via JWT (tanpa Inertia)
- 5. Extend RequestInstance untuk Custom Refresh Logic
- 6. Route dengan Path Parameters
- 7. Wildcard & OR Permission Logic
1. Setup PKCE Mode untuk SPA
Untuk SPA standalone (contoh: GWC). Token exchange dilakukan di luar library (setelah OAuth2 callback); storeTokens() dipanggil untuk menyimpan hasilnya.
// api/gwc.ts
import RequestInstance from '@bpmlib/sauth-frontend/http'
import { ServiceCodes } from '@bpmlib/sauth-frontend/constants'
import router from '@/router'
import routes from './gwc-routes.json'
const gwcApi = new RequestInstance(
'https://gwc.ppsdmmigas.id',
routes,
ServiceCodes.GWC,
{
mode: 'pkce',
clientId: 'gwc-spa',
onAuthFailure: () => router.push({ name: 'login' }),
}
)
// Setelah PKCE exchange berhasil, simpan token:
gwcApi.storeTokens(accessToken, refreshToken, expiresIn)
export default gwcApi2. Request dengan Body dan File Upload
// JSON body
const { data } = await api
.setBody({ name: 'Training Baru', capacity: 30 })
.post('training.store')
// Multipart — File dan FileList dihandle otomatis
const payload = {
title: 'Laporan Q1',
attachment: fileInput.files[0], // File
screenshots: fileInput.files, // FileList → dikonvert ke attachment[0], attachment[1], ...
meta: { year: 2025, quarter: 1 }, // Nested object
}
const { data: uploaded } = await api
.setBody(payload, true)
.post('document.store')3. Permission Guard di Vue Component
<script setup lang="ts">
import { permissionCheck } from '@bpmlib/sauth-frontend/auth/inertia'
import { usePage } from '@inertiajs/vue3'
const page = usePage()
const canCreate = permissionCheck('training.create', page)
const canManage = permissionCheck(['training.update', 'training.delete'], page)
</script>
<template>
<button v-if="canCreate">Tambah Training</button>
<div v-if="canManage">
<button>Edit</button>
<button>Hapus</button>
</div>
</template>4. Permission Check via JWT (tanpa Inertia)
Berguna di context non-Inertia: middleware, worker, atau halaman yang tidak punya PageProps.
import { decodeTokenClaims, permissionCheckFromToken } from '@bpmlib/sauth-frontend/auth/token'
import api from '@/api/gwc'
const token = api.getAccessTk()
if (token) {
const claims = decodeTokenClaims(token)
console.log('User:', claims?.snm, '| Exp:', new Date(claims!.exp * 1000))
if (permissionCheckFromToken('report.read', token)) {
// tampilkan report
}
}5. Extend RequestInstance untuk Custom Refresh Logic
protected refreshingToken() bisa di-override untuk endpoint refresh non-OAuth2.
import RequestInstance from '@bpmlib/sauth-frontend/http'
import type { RouteDict } from '@bpmlib/sauth-frontend'
import routes from './exam-routes.json'
class ExamRequestInstance extends RequestInstance<typeof routes> {
private readonly refreshTtl = 9000 // 2.5 jam
constructor() {
super('https://exam.ppsdmmigas.id', routes, 'exam', { mode: 'pkce' })
}
protected override async refreshingToken(): Promise<void> {
const refresh = this.getRefreshTk()
if (!refresh) throw new Error('No refresh token')
const res = await fetch('/api/exam/token/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refresh }),
})
if (!res.ok) {
this.eraseCookie(`tkaac_exam`)
this.eraseCookie(`tkarf_exam`)
throw new Error('Refresh failed')
}
const { access_token, refresh_token, expires_in } = await res.json()
this.setCookie('tkaac_exam', access_token, Math.floor(expires_in * 0.8))
this.setCookie('tkarf_exam', refresh_token, this.refreshTtl)
}
}
export const examApi = new ExamRequestInstance()6. Route dengan Path Parameters
// Route dict: { get: { 'training.module': '/training/{id}/module/{moduleId?}' } }
// Positional spread
api.useUrl('training.module', 'get', 1, 3) // '/training/1/module/3'
// Named object
api.useUrl('training.module', 'get', { id: 1, moduleId: 3 })
// Optional param hilang jika tidak disertakan
api.useUrl('training.module', 'get', { id: 1 }) // '/training/1/module'
// Langsung via HTTP method
const { data } = await api.get('training.module', 1, 3)7. Wildcard & OR Permission Logic
// User dengan fet: ['training.*', 'exam.read']
permissionCheck('training.create', page) // true — wildcard match
permissionCheck('training.read', page) // true — wildcard match
permissionCheck('exam.read', page) // true — exact match
permissionCheck('exam.create', page) // false — tidak ada wildcard
permissionCheck(['exam.create', 'exam.read'], page) // true — OR: exam.read cocok
// Arah wildcard tidak bisa dibalik
permissionCheck('training.*', page) // false — wildcard di needed tidak berlakuKey Takeaways:
- Wildcard hanya berfungsi dari sisi
fetuser, bukan dari sisineeded - Webmaster (
fet === 'wm') selalu returntruetanpa cek apapun - Logic yang sama berlaku di
permissionCheckFromToken()
Links
- Repository: Gitea
- Registry: Private NPM