@padosoft/laravel-iam-react-native
v1.1.0
Published
Thin, fail-closed React Native client and hooks for the Laravel IAM control plane.
Readme
@padosoft/laravel-iam-react-native
Thin, fail-closed React Native client and hooks for the Laravel IAM control plane.
Ask the IAM server "can this user do this?" from your React Native (or React) app — with the exact same wire contract and guarantees as the PHP and Node clients, plus React hooks that stay fail-closed while loading.
📚 Full documentation: doc.laravel-iam-react-native.padosoft.com — quickstart, hook lifecycle, fail-closed theory, the Hermes/Web Crypto caveat, ADRs, and the full API reference.
Why
- Fail-closed by construction. Network error, timeout, 5xx, 4xx, malformed body, or unverifiable token resolves to deny. Always. Loading resolves to deny. No fail-open switch.
- No PDP logic client-side. Every verdict comes from the server's
decisions/check. The client never interprets grants. - React Native safe. No
node:crypto, no Node built-ins. Uses the RN fetch polyfill andjose(Web Crypto /globalThis.crypto.subtle). - Zero runtime coupling to the Node SDK. Wire types are imported from
@padosoft/laravel-iam-nodewithimport type— completely erased at build time. No Node SDK runtime code runs in your app. - First-class React hooks.
usePermissionanduseCanintegrate with React context — check a permission in one line.
Install
npm install @padosoft/laravel-iam-react-nativeRequires React Native >= 0.71 (Hermes with
globalThis.crypto.subtle) forverifyToken. Forcheck()/ hooks only, any RN version withfetchworks.
Quick start
1. Configure the client and provider
import { IamClient, IamProvider } from '@padosoft/laravel-iam-react-native';
const iam = new IamClient({
baseUrl: 'https://iam.example.com/api/iam/v1',
token: process.env.IAM_SERVICE_TOKEN,
timeoutMs: 2000,
cache: { ttlMs: 5000 },
});
export default function App() {
const userId = useAuth().userId;
return (
<IamProvider client={iam} subject={{ type: 'user', id: userId }}>
<Navigation />
</IamProvider>
);
}2. Check permissions with hooks
import { usePermission } from '@padosoft/laravel-iam-react-native';
function StockAdjustButton({ warehouseId }: { warehouseId: string }) {
const { allowed, loading } = usePermission(
'stock.adjust',
{ type: 'warehouse', id: warehouseId },
);
if (loading) return <ActivityIndicator />;
if (!allowed) return null;
return <Button title="Adjust stock" onPress={handleAdjust} />;
}3. Or use the client imperatively
import { IamClient } from '@padosoft/laravel-iam-react-native';
const decision = await iam.check({
subject: { type: 'user', id: 'usr_123' },
application: 'warehouse',
permission: 'stock.adjust',
resource: { type: 'warehouse', id: 'wh_milan' },
context: { amount: 300 },
});
if (!decision.allowed) throw new Error('Forbidden');
if (decision.requiresStepUp) promptStepUp(decision.requiredAal);Fail-closed: read this
allowed === true alone is not permission. When requiresStepUp is true, the action is only permitted at a higher AAL. The hooks apply isGranted() automatically — they only set allowed: true when the PDP allowed and no step-up is pending.
Provider & Hooks API
<IamProvider client={...} subject={...}>
| Prop | Type | Description |
|------|------|-------------|
| client | IamClient | Pre-configured client instance. |
| subject | Subject | The authenticated user. Hooks use it automatically. |
| children | ReactNode | Your app tree. |
useIam()
Returns { client, subject } from the nearest IamProvider. Throws if no provider is found.
useCan(query: DecisionQuery): PermissionState
Full-control hook — accepts a complete DecisionQuery.
const { allowed, loading, requiresStepUp } = useCan({
subject: { type: 'user', id: userId },
permission: 'doc.publish',
resource: { type: 'document', id: docId },
});usePermission(permission, resource?, extra?): PermissionState
Convenience hook — reads subject from context, you supply permission and optionally resource.
const { allowed } = usePermission('orders.approve', { type: 'order', id: orderId });PermissionState
| Field | Type | Description |
|-------|------|-------------|
| allowed | boolean | true only when PDP granted AND no step-up pending. false while loading. |
| loading | boolean | true while the check is in flight. |
| requiresStepUp | boolean | true if a higher AAL is required. |
Client API
new IamClient(config)
| Option | Default | Description |
|--------|---------|-------------|
| baseUrl | required | Full API base, e.g. https://iam.example.com/api/iam/v1. |
| token | — | The user's access token, sent as Authorization: Bearer. |
| timeoutMs | 2000 | Per-request timeout in ms. |
| retries | 0 | Retries for idempotent network errors (never on 4xx/5xx). |
| cache | off | { ttlMs, maxEntries? } in-memory decision cache. |
| verify | — | { issuer?, audience?, jwksUri? } defaults for verifyToken. |
| fetch | globalThis.fetch | Inject a custom fetch (tests, proxies). |
Credential model — this is a PUBLIC client (no shared secret)
Unlike the server SDKs (laravel-iam-client, laravel-iam-node, laravel-iam-rust), a mobile app is a
public OAuth client: it must never embed a client_secret (anything shipped in an app binary is
extractable). So the client-secret rotation / self-fetch feature does not apply here — there is no
secret to rotate.
Instead, obtain the token through the Authorization Code + PKCE flow (the user logs in against IAM),
keep it short-lived, and refresh it with the refresh token — never with a static secret. Pass the
current user access token as token; when it expires, run the refresh/re-auth flow and update the client.
See Application credentials & lifecycle
(§ public clients).
check(query): Promise<Decision>
POST {baseUrl}/decisions/check. Returns a normalised Decision. Never throws.
can(query): Promise<boolean>
check() reduced to the fail-safe boolean.
listResources(subject, relation): Promise<Resource[]>
ReBAC list-resources. Returns [] on any error.
verifyToken(jwt, options?): Promise<Claims>
Verifies an ES256 token against the server JWKS. Rejects with TokenVerificationError on any failure.
Managing permissions/roles? Not here — this SDK is a consumer
This package consumes decisions (it asks "can this subject do X?"); it does not own a permission catalog and does not declare or push manifests. A mobile/public client has no server-side catalog to sync. Permissions and roles are declared by the service that owns them (a Laravel app with the bridge, or a Node/Rust service, or the console) and synced there. See Keeping IAM in sync.
Ecosystem
| Package | Runtime | Description | Docs |
|---------|---------|-------------|------|
| laravel-iam-server | PHP/Laravel | The IAM server — the PDP itself (RBAC + ABAC + ReBAC, OAuth/OIDC, audit) | docs |
| @padosoft/laravel-iam-node | Node 18+ | Core TypeScript/Node SDK — this package builds on its wire types | docs |
| @padosoft/laravel-iam-react-native | React Native / React | This package | docs |
| padosoft/laravel-iam-client | PHP 8.1+ | PHP client — the wire-contract reference | docs |
| laravel-iam-rust | Rust | Rust client SDK (crate laravel-iam), async + blocking | docs |
License
MIT (c) Padosoft
