Skip to content

Sauth FrontEnd

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

Versi: 0.3.1 Changelog
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, isFirstPartyToken, isThirdPartyToken, isM2MToken } from '@bpmlib/sauth-frontend/auth/token'
import { ServiceCodes } from '@bpmlib/sauth-frontend/constants'

TIP

NEW v0.3.0: isFirstPartyToken, isThirdPartyToken, isM2MToken tersedia di auth/token.


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, isFirstPartyToken, isM2MToken } 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: 'tkac_',
  tokenTtl: 720,
  refreshTokenTtl: 604800,
  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'tkac_'Prefix nama cookie access token
tokenTtlnumber720Fallback TTL dalam detik saat expires_in tidak ada di response
refreshTokenTtlnumber604800TTL cookie refresh token dalam detik (pkce mode) Lihat selengkapnya
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.

refreshTokenTtl

TTL cookie refresh token dalam detik (pkce mode only). Default 604800 = 7 hari, sesuai default sauth-server.token_ttl.refresh.

Contoh:

ts
new RequestInstance(baseURL, routes, 'svc', {
  mode: 'pkce',
  refreshTokenTtl: 86400,  // 1 hari — sesuaikan dengan SAUTH_REFRESH_TOKEN_TTL server
})

Tidak ada aturan 80% untuk nilai ini — TTL penuh digunakan. Server menolak refresh token expired secara eksplisit.


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 tokentkac_{service}80% dari expires_in ticketbooth
Refresh tokentkrf_{service}refreshTokenTtl option (default 604800s = 7 hari)

Prefix tkac_ dapat diganti via accessTokenPrefix. Prefix tkrf_ tidak bisa diganti — hardcoded untuk interoperabilitas antar app dalam satu ekosistem.

Contoh: service 'gwa' → cookie tkac_gwa (access) dan tkrf_gwa (refresh).

WARNING

Upgrade dari v0.1.0: Cookie prefix berubah (tkaac_tkac_, tkarf_tkrf_). Cookie lama tidak lagi dibaca setelah upgrade — user akan melakukan satu request ekstra untuk re-issue token. Tidak ada data loss.


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. HTTPS: SameSite=None; Secure. HTTP: SameSite=Lax. 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 (tkac_{service}).

getRefreshTk()
ts
getRefreshTk(): string | null

Baca refresh token cookie untuk service ini (tkrf_{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 cookie TTL = refreshTokenTtl option (default 604800s = 7 hari).

NOTE

Tidak ada aturan 80% untuk refresh token — TTL penuh digunakan. Server menolak refresh token expired secara eksplisit; onAuthFailure menangani kasus itu.

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


isFirstPartyToken()

TIP

NEW v0.3.0

Cek apakah token diterbitkan untuk first-party client. Menggunakan klaim fp jika ada (sauth-server v0.3+); fallback ke inferensi scope-absence untuk token pre-v0.3.

Parameters

NameTypeDefaultDescription
claimsSauthTokenClaims-Decoded token claims — gunakan decodeTokenClaims() terlebih dahulu

Returns: boolean

ts
const claims = decodeTokenClaims(token)
if (claims && isFirstPartyToken(claims)) {
  // token dari GWA/GWC SPA atau session gateway
}
KondisiResult
fp === '1p'true
fp === '3p'false
fp absent, scope absenttrue (fallback)
fp absent, scope presentfalse (fallback)

isThirdPartyToken()

TIP

NEW v0.3.0

Cek apakah token diterbitkan untuk third-party client (external developer app atau partner). Kebalikan dari isFirstPartyToken().

Parameters

NameTypeDefaultDescription
claimsSauthTokenClaims-Decoded token claims

Returns: boolean


isM2MToken()

TIP

NEW v0.3.0

Cek apakah token adalah machine-to-machine token (client_credentials atau API key JWT). Ditentukan dari absennya klaim snm — tidak terpengaruh oleh fp.

Parameters

NameTypeDefaultDescription
claimsSauthTokenClaims-Decoded token claims

Returns: boolean

ts
const claims = decodeTokenClaims(token)
if (claims) {
  if (isM2MToken(claims)) {
    console.log('M2M token — sid adalah client/app name:', claims.sid)
  } else {
    console.log('User token — snm:', claims.snm)
  }
}

TIP

Gunakan isM2MToken() (bukan isFirstPartyToken()) untuk membedakan token user vs mesin. isFirstPartyToken() mengembalikan true untuk 1p user token dan 1p M2M token.


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
  refreshTokenTtl?: number   // default: 604800 (7 hari)
  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          // short code ('gwa'/'gwc') untuk 1p; APP_URL untuk 3p (v0.3+)
  aud: string
  sid: string
  fp?: '1p' | '3p'    // explicit party marker — absent pada token pre-v0.3
  snm?: string
  fet?: string | string[]
  act?: { sub: string }
  scope?: string
  jti?: string         // hadir di API key JWT; absent di OAuth token
  iat: number
  exp?: number         // absent di API key JWT; hadir di semua OAuth token
}

Claims dalam JWT yang diissue oleh sauth ecosystem.

FieldTipeDeskripsi
issstringIssuer — short code (gwa/gwc) untuk 1p token; APP_URL untuk 3p token (v0.3+)
audstringAudience — service code target
sidstringSubject ID (user ID, client ID, atau app name)
fp'1p' | '3p'?Party marker eksplisit — absent pada token yang diterbitkan sebelum v0.3
snmstring?Subject name (nama user) — absent di semua M2M token
fetstring | string[]?Permissions — 'wm' untuk webmaster, array untuk user normal
act{ sub: string }?Actor claims untuk token exchange / M2M
scopestring?OAuth2 scope — hadir di 3p token dan API key JWT
jtistring?JWT ID — hadir di API key JWT untuk revocation via blacklist
iatnumberIssued at (Unix timestamp)
expnumber?Expiry (Unix timestamp) — absent di API key JWT

NOTE

Gunakan isFirstPartyToken(), isThirdPartyToken(), dan isM2MToken() untuk mengidentifikasi jenis token — jangan baca fp, scope, atau snm secara langsung. Helper ini menangani fallback inference untuk token pre-v0.3 yang tidak punya fp.


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, isM2MToken } from '@bpmlib/sauth-frontend/auth/token'
import api from '@/api/gwc'

const token = api.getAccessTk()

if (token) {
  const claims = decodeTokenClaims(token)
  if (claims && !isM2MToken(claims)) {
    console.log('User:', claims.snm, '| Exp:', claims.exp ? new Date(claims.exp * 1000) : 'no expiry')
  }

  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(`tkac_exam`)
      this.eraseCookie(`tkrf_exam`)
      throw new Error('Refresh failed')
    }

    const { access_token, refresh_token, expires_in } = await res.json()
    this.setCookie('tkac_exam', access_token, Math.floor(expires_in * 0.8))
    this.setCookie('tkrf_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()

8. Identifikasi Jenis Token (v0.3+)

Helper untuk membedakan 1p vs 3p vs M2M token — berguna di context yang menerima berbagai jenis token.

ts
import { decodeTokenClaims, isFirstPartyToken, isThirdPartyToken, isM2MToken } from '@bpmlib/sauth-frontend/auth/token'

const claims = decodeTokenClaims(token)
if (!claims) return

// Cek party
if (isFirstPartyToken(claims)) {
  // Token dari GWA/GWC SPA, atau session gateway
  // 1p user token: ada snm + fet, tidak ada scope
  // 1p M2M token: tidak ada snm, tidak ada scope
}

if (isThirdPartyToken(claims)) {
  // Token dari external developer app atau partner
  // Selalu punya scope
}

// Cek user vs mesin — terlepas dari party
if (isM2MToken(claims)) {
  // client_credentials atau API key JWT — snm absent
  console.log('App/service:', claims.sid)
} else {
  // User token — snm present
  console.log('User:', claims.snm)
}

Fallback pre-v0.3: Jika token tidak memiliki fp (diterbitkan sebelum sauth-server v0.3), helper menggunakan inferensi scope-presence — 1p token tidak punya scope, 3p token punya scope. Fallback ini transparan — tidak perlu cek versi token secara manual.