@plantagoai/auth
Ring-based permission system inspired by Linux protection rings. Handles authentication flows, role hierarchies, multi-tenant context, and server-side middleware.
Consumers: Foundation, MarketHub, Soho, HerbPulse
Installation
"@plantagoai/auth": "file:../../shared/packages/auth"
Exports
| Entry Point | Description |
|---|---|
@plantagoai/auth |
Client-side: auth flows, role definitions, session, tenant context |
@plantagoai/auth/middleware |
Server-side: Cloud Function auth middleware |
Ring-Based Permission Model
Access control uses concentric rings, where lower numbers have higher privilege:
| Ring | Name | Description | Example Roles |
|---|---|---|---|
| 0 | PLATFORM_OWNER |
Full system access | PlantagoAI super admin |
| 1 | TENANT_ADMIN |
Manage tenant resources | Foundation admin, MarketHub store owner |
| 2 | PRIVILEGED |
Enhanced permissions | Vendor, moderator |
| 3 | USER |
Standard user | Registered voter, shopper |
| 4 | RESTRICTED |
Limited access | Demo user, suspended account |
import { Ring } from "@plantagoai/auth";
Ring.PLATFORM_OWNER // 0
Ring.TENANT_ADMIN // 1
Ring.PRIVILEGED // 2
Ring.USER // 3
Ring.RESTRICTED // 4
Defining Project Roles
Each project maps its own role names to ring levels:
import { defineRoles, Ring } from "@plantagoai/auth";
// Foundation roles
const roles = defineRoles({
super_admin: Ring.PLATFORM_OWNER,
admin: Ring.TENANT_ADMIN,
user: Ring.USER,
demo_user: Ring.RESTRICTED,
});
// MarketHub roles (adds vendor)
const roles = defineRoles({
super_admin: Ring.PLATFORM_OWNER,
admin: Ring.TENANT_ADMIN,
vendor: Ring.PRIVILEGED,
user: Ring.USER,
demo_user: Ring.RESTRICTED,
});
// Usage
roles.ringOf("admin"); // 1
roles.canAccess("vendor", Ring.USER); // true (ring 2 >= ring 3)
roles.canAccess("user", Ring.PRIVILEGED); // false (ring 3 < ring 2)
roles.rolesAtOrAbove(Ring.PRIVILEGED); // ["super_admin", "admin", "vendor"]
defineRoles() Return Value
| Method | Returns | Description |
|---|---|---|
ringOf(role?) |
RingLevel |
Get ring level for a role name |
canAccess(userRole?, requiredRing) |
boolean |
Check if role meets required ring |
isRing(userRole?, ring) |
boolean |
Check if role is exactly at ring |
rolesAtOrAbove(ring) |
string[] |
List all roles at or above a ring |
Client-Side Auth Flows
Sign In
import { signInWithEmail, signInWithGoogle, signInWithApple } from "@plantagoai/auth";
const user = await signInWithEmail("user@example.com", "password");
const user = await signInWithGoogle();
const user = await signInWithApple();
Sign Up & Sign Out
import { signUp, signOutUser } from "@plantagoai/auth";
const user = await signUp("new@example.com", "password");
await signOutUser();
Auth State
import { onAuthChange, getCurrentUser, getIdToken } from "@plantagoai/auth";
// Listen for changes
const unsubscribe = onAuthChange((user) => {
if (user) console.log("Signed in:", user.email);
else console.log("Signed out");
});
// Synchronous check
const user = getCurrentUser();
// Get token for API calls
const token = await getIdToken(true); // forceRefresh
AuthUser
interface AuthUser {
uid: string;
email: string | null;
displayName: string | null;
photoURL: string | null;
emailVerified: boolean;
firebaseUser: User; // raw Firebase User object
}
Session & Claims
import { getUserRing, getUserClaims } from "@plantagoai/auth";
const ring = await getUserRing(); // e.g. 3 (Ring.USER)
const claims = await getUserClaims();
// {
// uid: "...",
// email: "user@example.com",
// ring: 3,
// role: "user",
// claims: { ring: 3, role: "user", tenantId: "org-1", ... },
// tenantId: "org-1",
// tenantRings: { "org-1": 3, "org-2": 1 }
// }
SessionInfo
interface SessionInfo {
uid: string;
email: string | null;
ring: RingLevel;
role: string;
claims: RingClaims;
tenantId: string;
tenantRings: Record<string, RingLevel>;
}
Multi-Tenant Context
import { createTenantContext, getTenantFromClaims, effectiveRing } from "@plantagoai/auth";
// From custom claims
const ctx = createTenantContext(claims, "org-123");
// { tenantId: "org-123", ring: 1, role: "admin", tenantRings: { ... } }
// From decoded token
const ctx = getTenantFromClaims(decodedToken);
// Get effective ring for a specific tenant
const ring = effectiveRing(claims, "org-456");
Server-Side Middleware
For Firebase Cloud Functions. All middleware functions validate the auth token and return an AuthResult or throw an HttpsError.
requireAuth
Verify the caller is authenticated:
import { requireAuth } from "@plantagoai/auth/middleware";
export const myFunction = onCall(async (request) => {
const auth = await requireAuth(request);
// auth.uid, auth.email, auth.ring, auth.role, auth.tenantId
});
requireRing
Verify minimum ring level:
import { requireRing, Ring } from "@plantagoai/auth/middleware";
export const adminOnly = onCall(async (request) => {
const auth = await requireRing(request, Ring.TENANT_ADMIN);
// Caller must be Ring 0 or Ring 1
});
requirePlatformOwner
Verify Ring 0 access:
import { requirePlatformOwner } from "@plantagoai/auth/middleware";
export const superAdminFunc = onCall(async (request) => {
const auth = await requirePlatformOwner(request);
});
requireTenantAccess
Verify access to a specific tenant:
import { requireTenantAccess, Ring } from "@plantagoai/auth/middleware";
export const tenantFunc = onCall(async (request) => {
const auth = await requireTenantAccess(request, "org-123", Ring.PRIVILEGED);
// Caller must be Ring 2+ in tenant "org-123"
});
setUserClaims
Set custom claims on a user (admin operation):
import { setUserClaims, Ring } from "@plantagoai/auth/middleware";
await setUserClaims("user-uid-123", {
ring: Ring.TENANT_ADMIN,
role: "admin",
tenantId: "org-1",
tenantRings: { "org-1": Ring.TENANT_ADMIN },
permissions: ["manage_users", "manage_proposals"],
});
bootstrapPlatformOwner
Bootstrap the first platform owner (one-time setup):
import { bootstrapPlatformOwner } from "@plantagoai/auth/middleware";
export const bootstrap = onCall(async (request) => {
return await bootstrapPlatformOwner(request, "admin@plantagoai.com");
// { success: true, message: "Platform owner bootstrapped" }
});
AuthResult
interface AuthResult {
uid: string;
email: string | undefined;
ring: RingLevel;
role: string;
tenantId: string;
tenantRings: Record<string, RingLevel>;
token: admin.auth.DecodedIdToken;
}
Types Reference
RingClaims
interface RingClaims {
ring: RingLevel;
role: string;
tenantId?: string;
tenantRings?: Record<string, RingLevel>;
permissions?: string[];
}
TenantContext
interface TenantContext {
tenantId: string;
ring: RingLevel;
role: string;
tenantRings?: Record<string, RingLevel>;
}
RoleMap
type RoleMap = { [roleName: string]: RingLevel };