Permissions
@ailuracode/alpine-permissions
Framework-agnostic browser permission registry with a typed adapter contract.
Install
pnpm add @ailuracode/alpine-permissions @ailuracode/alpine-core alpinejsRegister only the adapters you need from feature packages such as @ailuracode/alpine-notify, @ailuracode/alpine-geo, and @ailuracode/alpine-attention.
Quick start
import Alpine from "alpinejs";import { permissionsPlugin } from "@ailuracode/alpine-permissions";import { createNotificationPermissionAdapter } from "@ailuracode/alpine-notify";import { createGeoPermissionAdapter } from "@ailuracode/alpine-geo";import { createIdlePermissionAdapter } from "@ailuracode/alpine-attention";
Alpine.plugin( permissionsPlugin({ adapters: [ createNotificationPermissionAdapter(), createGeoPermissionAdapter(), createIdlePermissionAdapter(), ], }));
Alpine.start();Avoiding name collisions
If your application already owns a $store.permissions or another toolkit plugin registers on that name, rename the integration surface without touching the controller:
Alpine.plugin( permissionsPlugin({ storeKey: "caps", // → $store.caps // magicKey follows storeKey by default → $caps magicKey: "capsState", // explicit override → $capsState adapters: [/* ... */], }));storeKey is the only argument most hosts need. magicKey moves independently only when both names must be freed. The exposed constants DEFAULT_PERMISSIONS_STORE_KEY and DEFAULT_PERMISSIONS_MAGIC_KEY keep the rename discoverable from TypeScript.
<button x-show="$permissions.registry.notifications?.canRequest" @click="await $permissions.request('notifications')"> Enable notifications</button><p x-text="$permissions.registry.notifications?.permission"></p>On plugin registration the registry queries current browser permission for each adapter and starts watching revocations. Native prompts are never shown automatically.
Store / magic API
permissionsPlugin() registers $store.permissions and $permissions (same reactive object).
| Member | Description |
|---|---|
registry |
Readonly map of capability name → PermissionSnapshot |
get(name) |
Same as registry[name] |
query(name) |
Refresh permission from the browser (no prompt) |
request(name, options?) |
Request permission when canRequest is true |
refresh(name) |
Alias for query(name) |
watch(name) |
Subscribe to PermissionStatus.change when supported |
register(adapter) |
Register an adapter at runtime |
Bind templates to $permissions.registry.<name> so Alpine re-renders when snapshots change.
Controller API
import { createPermissions } from "@ailuracode/alpine-permissions";import { createGeoPermissionAdapter } from "@ailuracode/alpine-geo";
const permissions = createPermissions();const dispose = permissions.register(createGeoPermissionAdapter());
await permissions.query("geolocation");await permissions.request("geolocation");permissions.get("geolocation");
dispose();permissions.destroy();Feature adapters
| Package | Factory | Permission name |
|---|---|---|
@ailuracode/alpine-notify |
createNotificationPermissionAdapter(options?) |
notifications |
@ailuracode/alpine-geo |
createGeoPermissionAdapter() |
geolocation |
@ailuracode/alpine-attention |
createIdlePermissionAdapter() |
idle-detection |
UX policy
- Never request permission during plugin registration or controller mount.
query()andwatch()run on plugin init — they do not prompt.- Trigger
request()only from explicit user actions. - Do not re-request permissions already reported as denied.
- Distinguish unsupported APIs, insecure contexts, policy blocks, and platform restrictions.
Quick start
import Alpine from "alpinejs";import { permissionsPlugin } from "@ailuracode/alpine-permissions";import { createNotificationPermissionAdapter } from "@ailuracode/alpine-notify";import { createGeoPermissionAdapter } from "@ailuracode/alpine-geo";import { createIdlePermissionAdapter } from "@ailuracode/alpine-attention";
Alpine.plugin( permissionsPlugin({ adapters: [ createNotificationPermissionAdapter(), createGeoPermissionAdapter(), createIdlePermissionAdapter(), ], }));
Alpine.start();Avoiding name collisions
If your application already owns a $store.permissions or another toolkit plugin registers on that name, rename the integration surface without touching the controller:
Alpine.plugin( permissionsPlugin({ storeKey: "caps", // → $store.caps // magicKey follows storeKey by default → $caps magicKey: "capsState", // explicit override → $capsState adapters: [/* ... */], }));storeKey is the only argument most hosts need. magicKey moves independently only when both names must be freed. The exposed constants DEFAULT_PERMISSIONS_STORE_KEY and DEFAULT_PERMISSIONS_MAGIC_KEY keep the rename discoverable from TypeScript.
When the plugin registers, it queries each adapter for the current permission and starts watching for revocations. This never opens a native prompt.
Headless controller
import { createPermissions } from "@ailuracode/alpine-permissions";import { createGeoPermissionAdapter } from "@ailuracode/alpine-geo";
const permissions = createPermissions();const dispose = permissions.register(createGeoPermissionAdapter());
await permissions.query("geolocation");await permissions.request("geolocation");
dispose();permissions.destroy();PermissionsController is framework-agnostic. Alpine integration is optional.
Relationship to feature plugins
$notify, $store.geo, and $idle keep their existing APIs. Use $permissions when you need a single registry across capabilities or normalized availability metadata.
- Notify —
createNotificationPermissionAdapter()wrapsNotification.permissionandrequestPermission(). - Geo —
createGeoPermissionAdapter()uses the Permissions API and geolocation prompt semantics. - Attention —
createIdlePermissionAdapter()covers idle-detection permission;IdleControlleruses the same adapter internally.
License
MIT
