Skip to content

@bpmlib/sauth-frontend

HTTP client dengan auto-token lifecycle dan permission utilities untuk sauth ecosystem.

Versi: 0.1.0
Kategori: Pure Utils

npm versionTypeScript


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:

ts
// 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

DependencyVersiStatusDeskripsi
axios^1.13.0RequiredHTTP client
@inertiajs/core^2.0.0OptionalDiperlukan untuk auth/inertia utilities
bash
npm install axios

# Jika menggunakan auth/inertia:
npm install @inertiajs/core

Package Installation

Library dipublish ke private registry. Konfigurasi .npmrc terlebih dahulu:

ini
# .npmrc
@bpmlib:registry=https://js.pkg.ppsdmmigas.id/
bash
npm install @bpmlib/sauth-frontend
bash
yarn add @bpmlib/sauth-frontend
bash
pnpm add @bpmlib/sauth-frontend
bash
bun add @bpmlib/sauth-frontend

Import

Library menyediakan beberapa entry points — gunakan sub-path import untuk bundle lebih ringan:

ts
// 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.

ts
// 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)

ts
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:

  • page adalah Inertia page object dari usePage() framework adapter masing-masing
  • permissionCheck return true jika user adalah webmaster (fet === 'wm')

Configuration

RequestInstance dikonfigurasi melalui parameter options keempat (tipe RequestOptions):

ts
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

OptionTypeDefaultDescription
mode'session' | 'pkce''session'Strategi token lifecycle Lihat selengkapnya
ticketboothUrlstring'/api/sauth/token'Endpoint ticketbooth (session mode)
accessTokenPrefixstring'tkaac_'Prefix nama cookie access token
tokenTtlnumber720Fallback TTL dalam detik saat expires_in tidak ada di response
refreshUrlstring'/oauth/token'Endpoint OAuth2 refresh grant (pkce mode)
clientIdstring-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:

ts
// 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:

ts
onAuthFailure: () => void

Contoh:

ts
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 dari expires_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.


CookieNamaTTL
Access tokentkaac_{service}80% dari expires_in ticketbooth
Refresh tokentkarf_{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.

ts
// 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 terbalik

Logic 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

ts
new RequestInstance<TRoutes>(
  baseURL: string,
  routes: RouteDict,
  service: string,
  options?: RequestOptions
)

Parameters

NameTypeDefaultDescription
baseURLstring-Base URL resource server
routesRouteDict-Route dictionary berdasarkan HTTP method
servicestring-Service code — digunakan sebagai suffix nama cookie
optionsRequestOptions{}Konfigurasi token lifecycle

Methods

Contains:

setCookie()
ts
setCookie(name: string, value: string, lifetimeSeconds?: number): void

Set browser cookie dengan SameSite=None; Secure. Tanpa lifetimeSeconds → session cookie (hilang saat tab ditutup).

getCookie()
ts
getCookie(name: string): string | null

Baca cookie berdasarkan nama. Return null jika tidak ditemukan.

eraseCookie()
ts
eraseCookie(name: string): void

Hapus cookie berdasarkan nama.

getAccessTk()
ts
getAccessTk(): string | null

Baca access token cookie untuk service ini (tkaac_{service}).

getRefreshTk()
ts
getRefreshTk(): string | null

Baca refresh token cookie untuk service ini (tkarf_{service}).

Token Management
storeTokens()
ts
storeTokens(accessToken: string, refreshToken: string, expiresIn: number): void

Simpan 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.

NameTypeDescription
accessTokenstringAccess token string
refreshTokenstringRefresh token string
expiresInnumberTTL access token dalam detik
Request Configuration

Method-method ini bisa di-chain sebelum request. Konfigurasi di-reset otomatis setelah setiap HTTP call.

setBody()
ts
setBody(data: Record<string, unknown> | FormData, asFormData?: boolean): this

Set request body. asFormData: trueContent-Type: multipart/form-data, auto-convert JSON ke FormData (mendukung File, FileList, Date, nested objects, dan arrays).

setHeader()
ts
setHeader(key: string, value: string): this

Set satu request header. Content-Type via setBody() tidak bisa di-override.

setHeaders()
ts
setHeaders(objHeader: Record<string, string>): this

Set beberapa request header sekaligus.

setUrlParam()
ts
setUrlParam(key: string, value: string | number | boolean): this

Set satu URL query parameter.

setUrlParams()
ts
setUrlParams(paramsObj: Record<string, string | number | boolean>): this

Set beberapa URL query parameter sekaligus.

setResponseType()
ts
setResponseType(type?: ResponseType): this

Set response type Axios. Default: 'json'. Nilai lain: 'blob', 'arraybuffer', 'document', 'text', 'stream'.

getConfig()
ts
getConfig(): AxiosRequestConfig

Kembalikan konfigurasi request saat ini sebagai AxiosRequestConfig.

Route Helpers
useUrl()
ts
useUrl<TRoute extends string>(
  routeName: string,
  method?: HttpMethod,
  ...params: RouteParams<TRoute>
): string

Resolve nama route ke URL penuh, mengganti path parameters. Melempar error jika parameter wajib tidak disertakan.

NameTypeDefaultDescription
routeNamestring-Nama key di route dictionary
methodHttpMethod'get'HTTP method untuk lookup
...paramsRouteParams[]Path params — positional, named object, atau spread

Returns: string — URL dengan parameter yang sudah di-substitute

ts
// Route: { get: { 'training.show': '/training/{id}' } }
api.useUrl('training.show', 'get', 123)           // '/training/123'
api.useUrl('training.show', 'get', { id: 123 })  // '/training/123'
setUrl()
ts
setUrl(method: string, routeName: string, ...routeParam: RouteParams<TRoute>): string

Alias 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()
ts
get<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>
delete<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>
post() / put() / patch()
ts
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.

ts
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()
ts
protected async issueToken(): Promise<string>

POST ke ticketbooth { target: service }, simpan access cookie di 80% TTL, return token string. Digunakan session mode.

refreshingToken()
ts
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

NameTypeDefaultDescription
pagePage-Inertia page object — pass usePage() dari framework adapter
ignoreWmbooleanfalseJika true, return raw fet meski user adalah webmaster

Returns: 'wm' untuk webmaster | string[] untuk user normal | null jika tidak terautentikasi

ts
import { getUserPermissions } from '@bpmlib/sauth-frontend/auth/inertia'
import { usePage } from '@inertiajs/vue3'

const perms = getUserPermissions(usePage())
// 'wm' | string[] | null

permissionCheck()

Cek apakah user memiliki minimal satu dari permissions yang dibutuhkan.

Parameters

NameTypeDefaultDescription
neededstring | string[]-Permission yang dicek — OR logic jika array
pagePage-Inertia page object

Returns: boolean


auth/token

decodeTokenClaims()

Decode payload JWT tanpa verifikasi signature. Verifikasi signature dilakukan server-side oleh sauth-client.

Parameters

NameTypeDefaultDescription
tokenstring-Raw JWT string

Returns: SauthTokenClaims | nullnull jika token malformed


getPermissionsFromToken()

Ekstrak klaim fet dari JWT string.

Parameters

NameTypeDefaultDescription
tokenstring-Raw JWT string

Returns: 'wm' | string[] | null


permissionCheckFromToken()

Cek permission dari JWT string. Logic identik dengan permissionCheck() — wildcard dan OR logic berlaku sama.

Parameters

NameTypeDefaultDescription
neededstring | string[]-Permission yang dicek
tokenstring-Raw JWT string

Returns: boolean


Constants

ServiceCodes

Konstanta kode service dalam sauth ecosystem.

ts
import { ServiceCodes } from '@bpmlib/sauth-frontend/constants'
KeyValueDeskripsi
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

ts
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

ts
interface TicketboothResponse {
  access: string
  expires_in: number
}

Response dari endpoint ticketbooth (POST /api/sauth/token).


PassportTokenResponse

ts
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

ts
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.

FieldTipeDeskripsi
issstringIssuer — service code yang menerbitkan token
audstringAudience — service code target
sidstringSubject ID (user ID)
snmstring?Subject name (nama user)
fetstring | string[]?Permissions — 'wm' untuk webmaster, array untuk user normal
act{ sub: string }?Actor claims untuk token exchange / M2M
scopestring?OAuth2 scope
iatnumberIssued at (Unix timestamp)
expnumberExpiry (Unix timestamp)

RouteDict

ts
type RouteDict = Partial<Record<HttpMethod, Record<string, string>>>

Dictionary route yang dipartisi berdasarkan HTTP method. Biasanya diimport dari file JSON.

json
{
  "get": {
    "training.index": "/api/training",
    "training.show": "/api/training/{id}"
  },
  "post": {
    "training.store": "/api/training"
  }
}

RouteParams

ts
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

ts
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'

ResponseType

ts
type ResponseType = 'json' | 'blob' | 'arraybuffer' | 'document' | 'text' | 'stream'

Examples

Contains:


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.

ts
// 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 gwcApi

2. Request dengan Body dan File Upload

ts
// 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

vue
<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.

ts
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.

ts
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

ts
// 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

ts
// 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 berlaku

Key Takeaways:

  • Wildcard hanya berfungsi dari sisi fet user, bukan dari sisi needed
  • Webmaster (fet === 'wm') selalu return true tanpa cek apapun
  • Logic yang sama berlaku di permissionCheckFromToken()