Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Installation](#installation)
- [Prerequisite](#prerequisite)
- [Usage](#usage)
- [Examples](#examples)
- [Lookup Operations](#lookup-operations)
- [Monitoring](#monitoring)

Expand Down Expand Up @@ -171,11 +172,66 @@ If `at` not provided, it defaults to the current UTC time.

> **Note:** The `at` parameter is also supported in [Lookup Operations](#lookup-operations) with the same format and behavior.

## Examples

Runnable demos live in [`examples/`](./examples/). They use a **separate `package.json`** and are **not** installed when you run `yarn` at the repo root.

```bash
cd examples
yarn install # pulls @frontegg/e10s-client@latest from npm
```

See [examples/Readme.md](./examples/Readme.md) for Docker/SpiceDB setup and demo scripts.

## Lookup Operations

The client provides lookup operations that query the ReBAC authorization model to discover access relationships between entities.

All lookup operations support the optional `at` parameter for time-based access control (see [Time-Based Access](#query-for-fga-with-time-based-access-active_at-caveat)).
FGA lookup operations support the optional `at` parameter for time-based access control (see [Time-Based Access](#query-for-fga-with-time-based-access-active_at-caveat)).

### Lookup Entitlements

Find all entitlement feature keys granted to a tenant or user.

This lookup returns **effective feature entitlements** for the requested subject:

- When only `tenantId` is provided, the response contains feature grants available to that tenant.
- When both `tenantId` and `userId` are provided, the response contains the user's effective feature grants: tenant-inherited grants plus user-direct grants.
- If `userId` is provided but the user is not a member of the tenant, the response is empty.
- If the same feature key is granted through both the tenant and the user, it is returned once in the page response.

Use a tenant-only subject when you need tenant-level entitlements. Use a subject with `userId` when you need the feature set that should apply to a specific user.

> **Pagination note:** For user lookups, tenant and user grants are looked up and paginated independently. A feature key that is reachable through both streams may appear on different pages, so callers that aggregate multiple pages should deduplicate feature keys across pages if needed.

```typescript
const response = await e10sClient.lookupEntitlements({
subject: {
tenantId: 'tenant-123',
userId: 'user-456', // Optional: include for effective user entitlements
attributes: { plan: 'pro' } // Optional: evaluated by targeting rules
},
criteria: {
type: RequestContextType.Feature
},
limit: 100, // Optional: default 50
cursor: undefined // Optional: pagination cursor
});

console.log(`Found ${response.totalReturned} entitlement features`);

response.entitlements.forEach((entitlement) => {
console.log(`${entitlement.type}:${entitlement.key}`);
// entitlement.permissionship: 'HAS_PERMISSION' | 'CONDITIONAL_PERMISSION' | 'NO_PERMISSION'
});

if (response.cursor) {
const nextPage = await e10sClient.lookupEntitlements({
// ... same params
cursor: response.cursor
});
}
```

### Lookup Target Entities

Expand Down
12 changes: 12 additions & 0 deletions examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
**/node_modules
**/dist
**/build
**/coverage
**/logs
**/tmp
**/temp
**/test-results
**/test-reports
**/test-coverage
**/test-reports
yarn.lock
106 changes: 106 additions & 0 deletions examples/1. is-entitled-demo-alice-inheritance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { SpiceDBEntitlementsClient, SimpleLoggingClient, RequestContextType } from '@frontegg/e10s-client';

const config = {
engineEndpoint: 'localhost:50051',
engineToken: 'spicedb'
};

// Example subject context - adjust these values based on your SpiceDB schema
const subjectContext = {
entityType: 'user',
key: 'Alice'
};

// Different types of entitlement checks to test
const testCases = [
{
name: "Alice can read Tim's salary",
requestContext: {
type: RequestContextType.Entity as const,
entityType: 'document',
key: "Tim's_salary_Jan",
action: 'read_doc',
at: '2026-02-01T00:00:00.000Z'
}
}
];

async function main() {
console.log('\n🔐 Is Entitled Demo\n');
console.log('━'.repeat(60));
console.log('Configuration:');
console.log(` SpiceDB Endpoint: ${config.engineEndpoint}`);
console.log(` SpiceDB Token: ${config.engineToken.substring(0, 8)}...`);
console.log('━'.repeat(60));
console.log('Subject Context:');
console.log(` Entity Type: ${subjectContext.entityType}`);
console.log(` Key: ${subjectContext.key}`);
console.log('━'.repeat(60));
console.log('\n');

// Create the logging client
const loggingClient = new SimpleLoggingClient();

// Create the SpiceDB entitlements client
const client = new SpiceDBEntitlementsClient(
config,
loggingClient,
true, // logResults
{ defaultFallback: false }
);

console.log('📡 Running entitlement checks...\n');

const results: Array<{ name: string; entitled: boolean; context: object }> = [];

for (const testCase of testCases) {
try {
console.log(`\n🔍 Testing: ${testCase.name}`);
console.log(` Context: ${JSON.stringify(testCase.requestContext, null, 2).replace(/\n/g, '\n ')}`);

const result = await client.isEntitledTo(subjectContext, testCase.requestContext);

const entitled = result.result ?? false;
results.push({
name: testCase.name,
entitled,
context: testCase.requestContext
});

if (entitled) {
console.log(` ✅ Result: ENTITLED`);
} else {
console.log(` ❌ Result: NOT ENTITLED`);
}

if (result.monitoring) {
console.log(` ℹ️ Monitoring mode: true`);
}
} catch (error) {
console.error(` ⚠️ Error: ${error instanceof Error ? error.message : String(error)}`);
results.push({
name: testCase.name,
entitled: false,
context: testCase.requestContext
});
}
}

// Summary
console.log('\n');
console.log('━'.repeat(60));
console.log('Summary:');
console.log('━'.repeat(60));

for (const result of results) {
const checkName = result.name.padEnd(30);
const status = result.entitled ? '✅ Entitled' : '❌ Denied ';
console.log(` ${checkName} │ ${status} `);
}

const entitledCount = results.filter((r) => r.entitled).length;
console.log(`\nTotal: ${entitledCount}/${results.length} checks passed`);
console.log('\n');
}

main();
17 changes: 17 additions & 0 deletions examples/1. is-entitled-demo-alice-inheritance/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": ".",
"resolveJsonModule": true
},
"include": ["./*.ts"],
"exclude": ["node_modules"]
}

37 changes: 37 additions & 0 deletions examples/2. is-entitled-demo-tim-direct/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { SpiceDBEntitlementsClient, SimpleLoggingClient } from "@frontegg/e10s-client";

export const config = {
engineEndpoint: "localhost:50051",
engineToken: "spicedb",
};

export function createClient() {
const loggingClient = new SimpleLoggingClient();
return new SpiceDBEntitlementsClient(config, loggingClient, false, {
defaultFallback: false,
});
}

export function printHeader(title: string) {
console.log(`\n${title}\n`);
console.log("━".repeat(60));
console.log(` SpiceDB: ${config.engineEndpoint}`);
console.log("━".repeat(60));
}

export function printSummary(
title: string,
rows: Array<{ label: string; ok: boolean; detail: string }>,
) {
console.log("\n" + "━".repeat(60));
console.log(title);
console.log("━".repeat(60));

for (const row of rows) {
const icon = row.ok ? "✅" : "❌";
console.log(` ${icon} ${row.label.padEnd(42)} ${row.detail}`);
}

const passed = rows.filter((row) => row.ok).length;
console.log(`\n${passed}/${rows.length} passed`);
}
Loading
Loading