@iqauth/sdk 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +287 -0
- package/dist/browser-session.d.mts +12 -0
- package/dist/browser-session.d.ts +12 -0
- package/dist/browser-session.js +1812 -0
- package/dist/browser-session.mjs +28 -0
- package/dist/browser.d.mts +46 -0
- package/dist/browser.d.ts +46 -0
- package/dist/browser.js +768 -0
- package/dist/browser.mjs +47 -0
- package/dist/chunk-5HF3OBNO.mjs +189 -0
- package/dist/chunk-5WFR6Y33.mjs +59 -0
- package/dist/chunk-6I6RM4MN.mjs +51 -0
- package/dist/chunk-73R6BEGO.mjs +176 -0
- package/dist/chunk-E46DKOVI.mjs +632 -0
- package/dist/chunk-JQWYIIIS.mjs +1740 -0
- package/dist/chunk-X3K3WOBR.mjs +64 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +581 -0
- package/dist/cli/index.mjs +57 -0
- package/dist/client-C1DXfB8Z.d.mts +911 -0
- package/dist/client-CggvJmmm.d.ts +911 -0
- package/dist/dev-FUTJZSWN.mjs +56 -0
- package/dist/doctor-OHJRZBBT.mjs +89 -0
- package/dist/errors-CDdl24MP.d.mts +52 -0
- package/dist/errors-CDdl24MP.d.ts +52 -0
- package/dist/express-BKAXB5Nl.d.ts +61 -0
- package/dist/express-CpfyYTmw.d.mts +61 -0
- package/dist/express.d.mts +45 -0
- package/dist/express.d.ts +45 -0
- package/dist/express.js +2252 -0
- package/dist/express.mjs +122 -0
- package/dist/fastify.d.mts +23 -0
- package/dist/fastify.d.ts +23 -0
- package/dist/fastify.js +2062 -0
- package/dist/fastify.mjs +118 -0
- package/dist/hono.d.mts +22 -0
- package/dist/hono.d.ts +22 -0
- package/dist/hono.js +2051 -0
- package/dist/hono.mjs +107 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2070 -0
- package/dist/index.mjs +83 -0
- package/dist/init-LLCSQGNL.mjs +198 -0
- package/dist/keys-NLWFAOEM.mjs +63 -0
- package/dist/mobile.d.mts +11 -0
- package/dist/mobile.d.ts +11 -0
- package/dist/mobile.js +1809 -0
- package/dist/mobile.mjs +25 -0
- package/dist/next.d.mts +37 -0
- package/dist/next.d.ts +37 -0
- package/dist/next.js +2078 -0
- package/dist/next.mjs +130 -0
- package/dist/publishableKey-B5DIK81A.d.mts +24 -0
- package/dist/publishableKey-B5DIK81A.d.ts +24 -0
- package/dist/react.d.mts +196 -0
- package/dist/react.d.ts +196 -0
- package/dist/react.js +1457 -0
- package/dist/react.mjs +787 -0
- package/dist/server/handlers.d.mts +96 -0
- package/dist/server/handlers.d.ts +96 -0
- package/dist/server/handlers.js +243 -0
- package/dist/server/handlers.mjs +14 -0
- package/dist/server.d.mts +14 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +2195 -0
- package/dist/server.mjs +47 -0
- package/dist/service.d.mts +11 -0
- package/dist/service.d.ts +11 -0
- package/dist/service.js +1809 -0
- package/dist/service.mjs +25 -0
- package/dist/signIn-C8f6qVjD.d.mts +238 -0
- package/dist/signIn-Cy2lbEXb.d.ts +238 -0
- package/dist/types-Cxl3bQHt.d.mts +900 -0
- package/dist/types-Cxl3bQHt.d.ts +900 -0
- package/docs/APP_INTEGRATION_MATRIX.md +59 -0
- package/docs/BROWSER_SESSION_MIGRATION.md +69 -0
- package/docs/FRESH_IMPLEMENTATION_GUIDE.md +188 -0
- package/docs/TARBALL_RELEASE_WORKFLOW.md +98 -0
- package/docs/V1_TO_V2_UPGRADE_GUIDE.md +318 -0
- package/docs/guides/api-keys.md +130 -0
- package/docs/guides/app-registration.md +149 -0
- package/docs/guides/auth-flows.md +168 -0
- package/docs/guides/branding.md +160 -0
- package/docs/guides/entitlements.md +115 -0
- package/docs/guides/entity-hierarchy.md +200 -0
- package/docs/guides/error-handling.md +251 -0
- package/docs/guides/gdpr-compliance.md +123 -0
- package/docs/guides/invitations.md +143 -0
- package/docs/guides/mfa-enrollment.md +170 -0
- package/docs/guides/middleware-reference.md +205 -0
- package/docs/guides/mobile-native.md +110 -0
- package/docs/guides/roles-and-permissions.md +220 -0
- package/docs/guides/scoped-authorization.md +247 -0
- package/docs/guides/server-platform-integration.md +52 -0
- package/docs/guides/service-automation-integration.md +36 -0
- package/docs/guides/session-management.md +97 -0
- package/docs/guides/tenant-management.md +216 -0
- package/docs/guides/token-verification.md +178 -0
- package/docs/guides/user-management.md +184 -0
- package/docs/guides/webhooks.md +136 -0
- package/docs/integration-prompts/README.md +20 -0
- package/docs/integration-prompts/first-party-browser-app.md +29 -0
- package/docs/integration-prompts/install-from-tarball.md +41 -0
- package/docs/integration-prompts/migrate-from-local-packages-source.md +57 -0
- package/docs/integration-prompts/native-mobile-app.md +24 -0
- package/docs/integration-prompts/server-platform-app.md +20 -0
- package/docs/integration-prompts/service-automation-app.md +20 -0
- package/package.json +115 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Auth Flows
|
|
2
|
+
|
|
3
|
+
Authentication flow guidance depends on the environment.
|
|
4
|
+
|
|
5
|
+
## Environment Rules
|
|
6
|
+
|
|
7
|
+
### First-party browser apps
|
|
8
|
+
|
|
9
|
+
Use a backend-managed session:
|
|
10
|
+
|
|
11
|
+
- backend receives credentials
|
|
12
|
+
- backend calls IQAuth
|
|
13
|
+
- backend stores tokens in `httpOnly` cookies
|
|
14
|
+
- browser restores auth from `/auth/me`
|
|
15
|
+
|
|
16
|
+
Do not use the browser as the durable owner of the refresh token.
|
|
17
|
+
|
|
18
|
+
### Native mobile apps
|
|
19
|
+
|
|
20
|
+
Use authorization code + PKCE, then secure storage.
|
|
21
|
+
|
|
22
|
+
### Server-side apps
|
|
23
|
+
|
|
24
|
+
Direct token handling is acceptable.
|
|
25
|
+
|
|
26
|
+
### Service clients
|
|
27
|
+
|
|
28
|
+
Prefer API keys.
|
|
29
|
+
|
|
30
|
+
## Shared Login State Machine
|
|
31
|
+
|
|
32
|
+
The underlying auth flow still has three outcomes:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
type LoginResult =
|
|
36
|
+
| { status: "authenticated"; authMode: "token"; tokens: TokenPair; user: UserProfile }
|
|
37
|
+
| { status: "authenticated"; authMode: "session"; user: SessionUser }
|
|
38
|
+
| { status: "mfa_required"; mfaChallengeToken: string; availableMethods: string[] }
|
|
39
|
+
| { status: "tenant_selection"; tenantSelectionToken: string; tenants: Tenant[] };
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This state machine is directly useful in server, mobile, and backend-proxy layers.
|
|
43
|
+
|
|
44
|
+
## Browser Flow
|
|
45
|
+
|
|
46
|
+
Recommended first-party browser sequence:
|
|
47
|
+
|
|
48
|
+
1. `POST /auth/login` on your backend
|
|
49
|
+
2. if MFA is required, complete MFA through your backend
|
|
50
|
+
3. if tenant selection is required, complete tenant selection through your backend
|
|
51
|
+
4. backend sets auth cookies
|
|
52
|
+
5. frontend calls `/auth/me`
|
|
53
|
+
6. frontend calls your protected backend APIs with `credentials: "include"`
|
|
54
|
+
|
|
55
|
+
The browser should receive user/session state, not raw refresh tokens.
|
|
56
|
+
|
|
57
|
+
## Server / Trusted Client Flow
|
|
58
|
+
|
|
59
|
+
If your runtime is trusted and can safely hold credentials:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const result = await client.auth.login(email, password);
|
|
63
|
+
|
|
64
|
+
if (result.status === "authenticated") {
|
|
65
|
+
if (result.authMode !== "token") throw new Error("Expected token-authenticated result");
|
|
66
|
+
client.setTokens(result.tokens);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### MFA
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const mfaResult = await client.auth.completeMfa(mfaChallengeToken, code);
|
|
74
|
+
client.setTokens(mfaResult.tokens);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Backup codes
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const mfaResult = await client.auth.completeMfaWithBackup(
|
|
81
|
+
mfaChallengeToken,
|
|
82
|
+
backupCode,
|
|
83
|
+
);
|
|
84
|
+
client.setTokens(mfaResult.tokens);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Tenant selection
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const result = await client.auth.selectTenant(tenantSelectionToken, tenantId);
|
|
91
|
+
|
|
92
|
+
if (result.status === "authenticated") {
|
|
93
|
+
if (result.authMode !== "token") throw new Error("Expected token-authenticated result");
|
|
94
|
+
client.setTokens(result.tokens);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### OAuth code exchange
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const result = await client.auth.exchangeOAuthCode(code);
|
|
102
|
+
|
|
103
|
+
if (result.status === "authenticated") {
|
|
104
|
+
if (result.authMode !== "token") throw new Error("Expected token-authenticated result");
|
|
105
|
+
client.setTokens(result.tokens);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Mobile Flow
|
|
110
|
+
|
|
111
|
+
For native mobile:
|
|
112
|
+
|
|
113
|
+
1. create PKCE verifier/challenge
|
|
114
|
+
2. redirect to authorization endpoint in system browser
|
|
115
|
+
3. receive authorization code via deep link / universal link
|
|
116
|
+
4. exchange code
|
|
117
|
+
5. store tokens in secure storage
|
|
118
|
+
6. refresh from secure storage when needed
|
|
119
|
+
|
|
120
|
+
If you use the SDK as a token-owning client on mobile, pair it with secure storage and a PKCE-based login only.
|
|
121
|
+
|
|
122
|
+
## Refresh Behavior
|
|
123
|
+
|
|
124
|
+
### Browser
|
|
125
|
+
|
|
126
|
+
- backend should own refresh
|
|
127
|
+
- browser should not send a JS-managed refresh token as its default first-party pattern
|
|
128
|
+
|
|
129
|
+
### Server and mobile
|
|
130
|
+
|
|
131
|
+
`client.auth.refreshTokens(refreshToken)` is appropriate when the refresh token is held by a trusted server or secure mobile storage.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const tokens = await client.auth.refreshTokens(currentRefreshToken);
|
|
135
|
+
client.setTokens(tokens);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Logout Behavior
|
|
139
|
+
|
|
140
|
+
### Browser
|
|
141
|
+
|
|
142
|
+
- call your backend logout endpoint
|
|
143
|
+
- backend clears cookies and terminates the session
|
|
144
|
+
|
|
145
|
+
### Server and mobile
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
await client.auth.logout();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Error Handling
|
|
152
|
+
|
|
153
|
+
| Error Code | Meaning |
|
|
154
|
+
|--------|--------|
|
|
155
|
+
| `INVALID_CREDENTIALS` | Wrong email/password |
|
|
156
|
+
| `ACCOUNT_LOCKED` | Too many failed attempts |
|
|
157
|
+
| `ACCOUNT_INACTIVE` | Account disabled |
|
|
158
|
+
| `MFA_INVALID_CODE` | Wrong MFA code |
|
|
159
|
+
| `MFA_RATE_LIMITED` | Too many MFA attempts |
|
|
160
|
+
| `TOKEN_EXPIRED` | Access token expired |
|
|
161
|
+
| `PASSWORD_EXPIRED` | Password change required |
|
|
162
|
+
|
|
163
|
+
## Recommendation Summary
|
|
164
|
+
|
|
165
|
+
- browser first-party: backend cookie session
|
|
166
|
+
- mobile: PKCE + secure storage
|
|
167
|
+
- server: direct token handling allowed
|
|
168
|
+
- service: API key
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Branding
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Manage per-vendor branding configuration for login pages and UI customization. Upload assets, map custom domains, and control publish state.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- `vendor_admin` or `platform_admin` role for branding management
|
|
11
|
+
- No auth required for public branding endpoints
|
|
12
|
+
|
|
13
|
+
## Environment Note
|
|
14
|
+
|
|
15
|
+
These management examples assume a trusted runtime. First-party browser apps should route branding administration through a backend-managed session layer.
|
|
16
|
+
|
|
17
|
+
## SDK Methods
|
|
18
|
+
|
|
19
|
+
| Module | Method | Description |
|
|
20
|
+
|--------|--------|-------------|
|
|
21
|
+
| `branding` | `get(vendorId)` | Get branding config → `BrandingConfig` |
|
|
22
|
+
| `branding` | `updateBranding(vendorId, data)` | Update branding config |
|
|
23
|
+
| `branding` | `publishBranding(vendorId)` | Publish branding |
|
|
24
|
+
| `branding` | `unpublishBranding(vendorId)` | Unpublish branding |
|
|
25
|
+
| `branding` | `resetBranding(vendorId)` | Reset to defaults |
|
|
26
|
+
| `branding` | `uploadAsset(vendorId, data)` | Upload asset (base64) |
|
|
27
|
+
| `branding` | `listAssets(vendorId)` | List assets |
|
|
28
|
+
| `branding` | `deleteAsset(vendorId, assetId)` | Delete asset |
|
|
29
|
+
| `branding` | `listDomains(vendorId)` | List domain mappings |
|
|
30
|
+
| `branding` | `addDomain(vendorId, domain)` | Add domain mapping |
|
|
31
|
+
| `branding` | `removeDomain(vendorId, domainId)` | Remove domain mapping |
|
|
32
|
+
| `tenants` | `getPublicBranding(params?)` | Public branding (no auth) |
|
|
33
|
+
| `tenants` | `getPublicBrandingBySlug(slug)` | Public branding by slug (no auth) |
|
|
34
|
+
|
|
35
|
+
## Step-by-Step
|
|
36
|
+
|
|
37
|
+
### 1. Update Branding
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
await client.branding.updateBranding(vendorId, {
|
|
41
|
+
primaryColor: "#1E40AF",
|
|
42
|
+
secondaryColor: "#3B82F6",
|
|
43
|
+
companyName: "Acme Disposal",
|
|
44
|
+
headline: "Welcome to Acme Portal",
|
|
45
|
+
subheadline: "Sign in to manage your operations",
|
|
46
|
+
supportEmail: "support@acme.com",
|
|
47
|
+
termsUrl: "https://acme.com/terms",
|
|
48
|
+
privacyUrl: "https://acme.com/privacy",
|
|
49
|
+
customCss: ".login-form { border-radius: 12px; }",
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Publish
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
await client.branding.publishBranding(vendorId);
|
|
57
|
+
await client.branding.unpublishBranding(vendorId);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3. Upload Assets
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const asset = await client.branding.uploadAsset(vendorId, {
|
|
64
|
+
filename: "logo.png",
|
|
65
|
+
mimeType: "image/png",
|
|
66
|
+
data: base64EncodedData,
|
|
67
|
+
purpose: "logo",
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 4. Domain Mapping
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
await client.branding.addDomain(vendorId, "login.acme.com");
|
|
75
|
+
const domains = await client.branding.listDomains(vendorId);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 5. Public Branding (No Auth)
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const vendorBranding = await client.tenants.getPublicBranding({ vendor: "acme" });
|
|
82
|
+
const slugBranding = await client.tenants.getPublicBrandingBySlug("acme-disposal");
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Error Handling
|
|
86
|
+
|
|
87
|
+
| Error Code | Meaning | Recovery |
|
|
88
|
+
|------------|---------|----------|
|
|
89
|
+
| `NOT_FOUND` | Vendor or asset not found | Check IDs |
|
|
90
|
+
| `UPLOAD_ERROR` | Asset upload failed | Retry or check data format |
|
|
91
|
+
| `INSUFFICIENT_PERMISSIONS` | Caller lacks admin role | Requires `vendor_admin`+ |
|
|
92
|
+
| `VALIDATION_ERROR` | Invalid branding data | Check field formats |
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await client.branding.uploadAsset(vendorId, assetData);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.UPLOAD_ERROR) {
|
|
101
|
+
console.error("Failed to upload asset:", err.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Complete Example
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { IQAuthClient } from "@iqauth/sdk";
|
|
110
|
+
|
|
111
|
+
const client = new IQAuthClient({
|
|
112
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
113
|
+
// Trusted runtime example
|
|
114
|
+
accessToken: vendorAdminToken,
|
|
115
|
+
refreshToken: vendorAdminRefreshToken,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
async function setupBranding(vendorId: string) {
|
|
119
|
+
await client.branding.updateBranding(vendorId, {
|
|
120
|
+
primaryColor: "#1E40AF",
|
|
121
|
+
companyName: "Acme Disposal",
|
|
122
|
+
headline: "Welcome to Acme",
|
|
123
|
+
supportEmail: "support@acme.com",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const logoAsset = await client.branding.uploadAsset(vendorId, {
|
|
127
|
+
filename: "logo.png",
|
|
128
|
+
mimeType: "image/png",
|
|
129
|
+
data: logoBase64,
|
|
130
|
+
purpose: "logo",
|
|
131
|
+
});
|
|
132
|
+
console.log("Logo uploaded:", logoAsset.url);
|
|
133
|
+
|
|
134
|
+
await client.branding.addDomain(vendorId, "login.acme.com");
|
|
135
|
+
await client.branding.publishBranding(vendorId);
|
|
136
|
+
|
|
137
|
+
const config = await client.branding.get(vendorId);
|
|
138
|
+
console.log("Published:", config.isPublished);
|
|
139
|
+
|
|
140
|
+
// Public access (login page):
|
|
141
|
+
const publicBranding = await client.tenants.getPublicBranding({ vendor: "acme" });
|
|
142
|
+
console.log("Public branding:", publicBranding?.companyName);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## API Reference
|
|
147
|
+
|
|
148
|
+
| Method | HTTP | Path |
|
|
149
|
+
|--------|------|------|
|
|
150
|
+
| `get(vendorId)` | GET | `/api/v1/branding/:vendorId` |
|
|
151
|
+
| `updateBranding(vendorId, data)` | PUT | `/api/v1/branding/:vendorId` |
|
|
152
|
+
| `publishBranding(vendorId)` | POST | `/api/v1/branding/:vendorId/publish` |
|
|
153
|
+
| `unpublishBranding(vendorId)` | POST | `/api/v1/branding/:vendorId/unpublish` |
|
|
154
|
+
| `resetBranding(vendorId)` | POST | `/api/v1/branding/:vendorId/reset` |
|
|
155
|
+
| `uploadAsset(vendorId, data)` | POST | `/api/v1/branding/:vendorId/upload` |
|
|
156
|
+
| `listAssets(vendorId)` | GET | `/api/v1/branding/:vendorId/assets` |
|
|
157
|
+
| `deleteAsset(vendorId, assetId)` | DELETE | `/api/v1/branding/:vendorId/assets/:assetId` |
|
|
158
|
+
| `listDomains(vendorId)` | GET | `/api/v1/branding/:vendorId/domains` |
|
|
159
|
+
| `addDomain(vendorId, domain)` | POST | `/api/v1/branding/:vendorId/domains` |
|
|
160
|
+
| `removeDomain(vendorId, domainId)` | DELETE | `/api/v1/branding/:vendorId/domains/:domainId` |
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Entitlements
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Control which products a tenant has access to (e.g., `iqcapture`, `iqreuse`, `iqvalidate`). Entitlements are included in the JWT `entitlements[]` claim.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- `platform_admin` role for granting/revoking entitlements
|
|
11
|
+
- Any authenticated user for checking entitlements client-side
|
|
12
|
+
|
|
13
|
+
## Environment Note
|
|
14
|
+
|
|
15
|
+
Administrative entitlement examples are intended for trusted server-side or administrative runtimes.
|
|
16
|
+
|
|
17
|
+
## SDK Methods
|
|
18
|
+
|
|
19
|
+
| Module | Method | Description |
|
|
20
|
+
|--------|--------|-------------|
|
|
21
|
+
| `entitlements` | `list(tenantId)` | List entitlements → `Entitlement[]` |
|
|
22
|
+
| `entitlements` | `grant(tenantId, data)` | Grant entitlement → `Entitlement` |
|
|
23
|
+
| `entitlements` | `revoke(tenantId, product)` | Revoke entitlement |
|
|
24
|
+
| `permissions` | `hasEntitlement(ent)` | Client-side check → `boolean` |
|
|
25
|
+
| `permissions` | `hasAllEntitlements(ents)` | Client-side AND check → `boolean` |
|
|
26
|
+
| `permissions` | `hasAnyEntitlement(ents)` | Client-side OR check → `boolean` |
|
|
27
|
+
|
|
28
|
+
## Step-by-Step
|
|
29
|
+
|
|
30
|
+
### 1. Grant Entitlement
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const entitlement = await client.entitlements.grant(tenantId, {
|
|
34
|
+
product: "iqcapture",
|
|
35
|
+
expiresAt: "2027-12-31T23:59:59Z", // optional
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. List Entitlements
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
const entitlements = await client.entitlements.list(tenantId);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Revoke Entitlement
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
await client.entitlements.revoke(tenantId, "iqcapture");
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 4. Check Client-Side
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
client.permissions.hasEntitlement("iqcapture");
|
|
55
|
+
client.permissions.hasAllEntitlements(["iqcapture", "iqreuse"]);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 5. Check in Middleware
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
app.use("/api/capture", iqAuthMiddleware(client, {
|
|
62
|
+
requiredEntitlements: ["iqcapture"], // AND logic
|
|
63
|
+
}));
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Error Handling
|
|
67
|
+
|
|
68
|
+
| Error Code | Meaning | Recovery |
|
|
69
|
+
|------------|---------|----------|
|
|
70
|
+
| `NOT_FOUND` | Tenant or entitlement not found | Check IDs |
|
|
71
|
+
| `ALREADY_EXISTS` | Product already entitled | No action needed |
|
|
72
|
+
| `INSUFFICIENT_PERMISSIONS` | Caller lacks admin role | Requires `platform_admin` |
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await client.entitlements.grant(tenantId, { product: "iqcapture" });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.ALREADY_EXISTS) {
|
|
81
|
+
console.log("Product already entitled for this tenant");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Complete Example
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { IQAuthClient, iqAuthMiddleware } from "@iqauth/sdk";
|
|
90
|
+
|
|
91
|
+
const client = new IQAuthClient({
|
|
92
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
93
|
+
accessToken: platformAdminToken,
|
|
94
|
+
refreshToken: platformAdminRefreshToken,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
async function setupTenantEntitlements(tenantId: string) {
|
|
98
|
+
await client.entitlements.grant(tenantId, { product: "iqcapture" });
|
|
99
|
+
await client.entitlements.grant(tenantId, { product: "iqreuse" });
|
|
100
|
+
|
|
101
|
+
const all = await client.entitlements.list(tenantId);
|
|
102
|
+
console.log("Active entitlements:", all.map(e => e.product));
|
|
103
|
+
|
|
104
|
+
// After user logs in, check client-side:
|
|
105
|
+
// client.permissions.hasEntitlement("iqcapture") → true
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
| Method | HTTP | Path |
|
|
112
|
+
|--------|------|------|
|
|
113
|
+
| `list(tenantId)` | GET | `/api/v1/tenants/:tenantId/entitlements` |
|
|
114
|
+
| `grant(tenantId, data)` | POST | `/api/v1/tenants/:tenantId/entitlements` |
|
|
115
|
+
| `revoke(tenantId, product)` | DELETE | `/api/v1/tenants/:tenantId/entitlements/:product` |
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Entity Hierarchy
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Manage the three-level organizational hierarchy: **Vendor → Source → Client**. Create entities, link them into a graph, and query the full hierarchy.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- `vendor_admin` or `platform_admin` role for entity management
|
|
11
|
+
- Tenant with vendor hierarchy set up
|
|
12
|
+
|
|
13
|
+
## Environment Note
|
|
14
|
+
|
|
15
|
+
These examples assume a trusted runtime with direct token access. They are not first-party browser storage guidance.
|
|
16
|
+
|
|
17
|
+
## SDK Methods
|
|
18
|
+
|
|
19
|
+
### VendorsModule
|
|
20
|
+
|
|
21
|
+
| Method | Description |
|
|
22
|
+
|--------|-------------|
|
|
23
|
+
| `list()` | List all vendors → `Vendor[]` |
|
|
24
|
+
| `get(vendorId)` | Get vendor → `Vendor` |
|
|
25
|
+
| `create(data)` | Create vendor → `Vendor` |
|
|
26
|
+
| `update(vendorId, data)` | Update vendor → `Vendor` |
|
|
27
|
+
| `delete(vendorId)` | Delete vendor |
|
|
28
|
+
|
|
29
|
+
### SourcesModule
|
|
30
|
+
|
|
31
|
+
| Method | Description |
|
|
32
|
+
|--------|-------------|
|
|
33
|
+
| `create(vendorId, data)` | Create source under vendor → `Source` |
|
|
34
|
+
| `listForVendor(vendorId)` | List sources for vendor → `Source[]` |
|
|
35
|
+
| `get(sourceId)` | Get source → `Source` |
|
|
36
|
+
| `update(sourceId, data)` | Update source → `Source` |
|
|
37
|
+
| `delete(sourceId)` | Delete source |
|
|
38
|
+
| `createClient(sourceId, data)` | Create client under source → `Client` |
|
|
39
|
+
| `listClients(sourceId)` | List clients for source → `Client[]` |
|
|
40
|
+
|
|
41
|
+
### ClientsModule
|
|
42
|
+
|
|
43
|
+
| Method | Description |
|
|
44
|
+
|--------|-------------|
|
|
45
|
+
| `get(clientId)` | Get client → `Client` |
|
|
46
|
+
| `update(clientId, data)` | Update client → `Client` |
|
|
47
|
+
| `delete(clientId)` | Delete client |
|
|
48
|
+
|
|
49
|
+
### HierarchyModule
|
|
50
|
+
|
|
51
|
+
| Method | Description |
|
|
52
|
+
|--------|-------------|
|
|
53
|
+
| `getGraph()` | Full hierarchy graph → `HierarchyVendor[]` |
|
|
54
|
+
| `linkVendorSource(vendorId, sourceId)` | Link vendor to source |
|
|
55
|
+
| `unlinkVendorSource(vendorId, sourceId)` | Unlink vendor from source |
|
|
56
|
+
| `linkSourceClient(sourceId, clientId)` | Link source to client |
|
|
57
|
+
| `unlinkSourceClient(sourceId, clientId)` | Unlink source from client |
|
|
58
|
+
|
|
59
|
+
## Step-by-Step
|
|
60
|
+
|
|
61
|
+
### 1. Create Entities
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const vendor = await client.vendors.create({
|
|
65
|
+
name: "Acme Disposal",
|
|
66
|
+
slug: "acme-disposal",
|
|
67
|
+
status: "active",
|
|
68
|
+
metadata: { region: "northeast" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const source = await client.sources.create(vendor.id, {
|
|
72
|
+
name: "North MRF",
|
|
73
|
+
slug: "north-mrf",
|
|
74
|
+
status: "active",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const clientEntity = await client.sources.createClient(source.id, {
|
|
78
|
+
name: "City of Springfield",
|
|
79
|
+
slug: "springfield",
|
|
80
|
+
clientType: "municipal",
|
|
81
|
+
status: "active",
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2. Link Hierarchy
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
await client.hierarchy.linkVendorSource(vendor.id, source.id);
|
|
89
|
+
await client.hierarchy.linkSourceClient(source.id, clientEntity.id);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Note: Unlink operations send entity IDs in the request body (unconventional but matches server). See [DECISION-012](../../DECISIONS.md).
|
|
93
|
+
|
|
94
|
+
### 3. Query Full Graph
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const graph = await client.hierarchy.getGraph();
|
|
98
|
+
// HierarchyVendor[] — each with nested sources and clients
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 4. Update / Delete
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
await client.vendors.update(vendor.id, { name: "Acme Waste Solutions" });
|
|
105
|
+
await client.clients.update(clientEntity.id, { status: "inactive" });
|
|
106
|
+
await client.hierarchy.unlinkSourceClient(source.id, clientEntity.id);
|
|
107
|
+
await client.clients.delete(clientEntity.id);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Error Handling
|
|
111
|
+
|
|
112
|
+
| Error Code | Meaning | Recovery |
|
|
113
|
+
|------------|---------|----------|
|
|
114
|
+
| `NOT_FOUND` | Entity does not exist | Check entity ID |
|
|
115
|
+
| `ALREADY_EXISTS` | Slug already taken | Use different slug |
|
|
116
|
+
| `INSUFFICIENT_PERMISSIONS` | Caller lacks admin role | Requires `vendor_admin`+ |
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await client.vendors.create({ name: "Test", slug: "test" });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.ALREADY_EXISTS) {
|
|
125
|
+
console.log("Vendor slug already exists");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Complete Example
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { IQAuthClient } from "@iqauth/sdk";
|
|
134
|
+
|
|
135
|
+
const client = new IQAuthClient({
|
|
136
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
137
|
+
accessToken: adminToken,
|
|
138
|
+
refreshToken: adminRefreshToken,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
async function buildHierarchy() {
|
|
142
|
+
const vendor = await client.vendors.create({
|
|
143
|
+
name: "Regional Waste Co",
|
|
144
|
+
slug: "regional-waste",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const mrfNorth = await client.sources.create(vendor.id, {
|
|
148
|
+
name: "MRF North",
|
|
149
|
+
slug: "mrf-north",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const mrfSouth = await client.sources.create(vendor.id, {
|
|
153
|
+
name: "MRF South",
|
|
154
|
+
slug: "mrf-south",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const springfield = await client.sources.createClient(mrfNorth.id, {
|
|
158
|
+
name: "Springfield",
|
|
159
|
+
slug: "springfield",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const shelbyville = await client.sources.createClient(mrfSouth.id, {
|
|
163
|
+
name: "Shelbyville",
|
|
164
|
+
slug: "shelbyville",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await client.hierarchy.linkVendorSource(vendor.id, mrfNorth.id);
|
|
168
|
+
await client.hierarchy.linkVendorSource(vendor.id, mrfSouth.id);
|
|
169
|
+
await client.hierarchy.linkSourceClient(mrfNorth.id, springfield.id);
|
|
170
|
+
await client.hierarchy.linkSourceClient(mrfSouth.id, shelbyville.id);
|
|
171
|
+
|
|
172
|
+
const graph = await client.hierarchy.getGraph();
|
|
173
|
+
console.log("Hierarchy:", JSON.stringify(graph, null, 2));
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## API Reference
|
|
178
|
+
|
|
179
|
+
| Method | HTTP | Path |
|
|
180
|
+
|--------|------|------|
|
|
181
|
+
| `vendors.list()` | GET | `/api/v1/vendors` |
|
|
182
|
+
| `vendors.get(id)` | GET | `/api/v1/vendors/:id` |
|
|
183
|
+
| `vendors.create(data)` | POST | `/api/v1/vendors` |
|
|
184
|
+
| `vendors.update(id, data)` | PATCH | `/api/v1/vendors/:id` |
|
|
185
|
+
| `vendors.delete(id)` | DELETE | `/api/v1/vendors/:id` |
|
|
186
|
+
| `sources.create(vendorId, data)` | POST | `/api/v1/vendors/:vendorId/sources` |
|
|
187
|
+
| `sources.listForVendor(vendorId)` | GET | `/api/v1/vendors/:vendorId/sources` |
|
|
188
|
+
| `sources.get(id)` | GET | `/api/v1/sources/:id` |
|
|
189
|
+
| `sources.update(id, data)` | PATCH | `/api/v1/sources/:id` |
|
|
190
|
+
| `sources.delete(id)` | DELETE | `/api/v1/sources/:id` |
|
|
191
|
+
| `sources.createClient(sourceId, data)` | POST | `/api/v1/sources/:sourceId/clients` |
|
|
192
|
+
| `sources.listClients(sourceId)` | GET | `/api/v1/sources/:sourceId/clients` |
|
|
193
|
+
| `clients.get(id)` | GET | `/api/v1/clients/:id` |
|
|
194
|
+
| `clients.update(id, data)` | PATCH | `/api/v1/clients/:id` |
|
|
195
|
+
| `clients.delete(id)` | DELETE | `/api/v1/clients/:id` |
|
|
196
|
+
| `hierarchy.getGraph()` | GET | `/api/v1/hierarchy` |
|
|
197
|
+
| `hierarchy.linkVendorSource(v, s)` | POST | `/api/v1/hierarchy/link/vendor-source` |
|
|
198
|
+
| `hierarchy.unlinkVendorSource(v, s)` | DELETE | `/api/v1/hierarchy/link/vendor-source` |
|
|
199
|
+
| `hierarchy.linkSourceClient(s, c)` | POST | `/api/v1/hierarchy/link/source-client` |
|
|
200
|
+
| `hierarchy.unlinkSourceClient(s, c)` | DELETE | `/api/v1/hierarchy/link/source-client` |
|