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: true → Content-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 | null — null 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.