@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,251 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Handle all SDK errors using `IQAuthError` and `ErrorCodes`. Understand error codes, HTTP status mapping, and implement recovery patterns.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
|
|
11
|
+
## Environment Note
|
|
12
|
+
|
|
13
|
+
Refresh-and-retry examples in this guide apply to token-owning runtimes such as servers and native mobile apps with secure storage.
|
|
14
|
+
|
|
15
|
+
For first-party browser apps, the backend should own refresh and session recovery.
|
|
16
|
+
|
|
17
|
+
## SDK Methods
|
|
18
|
+
|
|
19
|
+
| Export | Description |
|
|
20
|
+
|--------|-------------|
|
|
21
|
+
| `IQAuthError` | Error class thrown by all SDK API calls |
|
|
22
|
+
| `ErrorCodes` | Constant object with all error code strings |
|
|
23
|
+
| `ErrorCode` | TypeScript type for error code values |
|
|
24
|
+
|
|
25
|
+
### IQAuthError Properties
|
|
26
|
+
|
|
27
|
+
| Property | Type | Description |
|
|
28
|
+
|----------|------|-------------|
|
|
29
|
+
| `code` | `string` | Machine-readable error code |
|
|
30
|
+
| `message` | `string` | Human-readable description |
|
|
31
|
+
| `status` | `number \| undefined` | HTTP status code |
|
|
32
|
+
| `raw` | `unknown` | Full raw response body |
|
|
33
|
+
| `name` | `string` | Always `"IQAuthError"` |
|
|
34
|
+
|
|
35
|
+
## Step-by-Step
|
|
36
|
+
|
|
37
|
+
### 1. Basic Error Handling
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await client.auth.login(email, password);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err instanceof IQAuthError) {
|
|
46
|
+
console.error(`[${err.code}] ${err.message} (HTTP ${err.status})`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Switch on Error Codes
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
try {
|
|
55
|
+
await client.auth.login(email, password);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err instanceof IQAuthError) {
|
|
58
|
+
switch (err.code) {
|
|
59
|
+
case ErrorCodes.INVALID_CREDENTIALS:
|
|
60
|
+
showError("Wrong email or password");
|
|
61
|
+
break;
|
|
62
|
+
case ErrorCodes.ACCOUNT_LOCKED:
|
|
63
|
+
showError("Account locked — try again later");
|
|
64
|
+
break;
|
|
65
|
+
case ErrorCodes.ACCOUNT_INACTIVE:
|
|
66
|
+
showError("Account deactivated");
|
|
67
|
+
break;
|
|
68
|
+
case ErrorCodes.PASSWORD_EXPIRED:
|
|
69
|
+
redirectToPasswordChange();
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
showError("An error occurred");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Token Error Recovery
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
try {
|
|
82
|
+
await client.users.getCurrent();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err instanceof IQAuthError) {
|
|
85
|
+
if (err.code === ErrorCodes.TOKEN_EXPIRED) {
|
|
86
|
+
const newTokens = await client.auth.refreshTokens(client.getRefreshToken()!);
|
|
87
|
+
client.setTokens(newTokens);
|
|
88
|
+
return client.users.getCurrent(); // Retry
|
|
89
|
+
}
|
|
90
|
+
if (err.code === ErrorCodes.REFRESH_TOKEN_REUSED) {
|
|
91
|
+
// Security: possible token theft — force re-login
|
|
92
|
+
forceReLogin();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Error Handling
|
|
99
|
+
|
|
100
|
+
All SDK API calls throw `IQAuthError` on failure. The error object carries a machine-readable `code`, human-readable `message`, HTTP `status`, and the `raw` response body. Use `ErrorCodes` constants for reliable comparisons.
|
|
101
|
+
|
|
102
|
+
## Error Codes Reference
|
|
103
|
+
|
|
104
|
+
### Authentication
|
|
105
|
+
|
|
106
|
+
| Code | HTTP | Description |
|
|
107
|
+
|------|------|-------------|
|
|
108
|
+
| `INVALID_CREDENTIALS` | 401 | Wrong email or password |
|
|
109
|
+
| `ACCOUNT_INACTIVE` | 403 | User account is deactivated |
|
|
110
|
+
| `ACCOUNT_LOCKED` | 403 | Account locked (too many failed attempts) |
|
|
111
|
+
| `AUTH_REQUIRED` | 401 | No authentication provided |
|
|
112
|
+
| `PASSWORD_EXPIRED` | 403 | Password must be changed |
|
|
113
|
+
| `PASSWORD_POLICY_VIOLATION` | 400 | New password doesn't meet policy |
|
|
114
|
+
|
|
115
|
+
### Tokens
|
|
116
|
+
|
|
117
|
+
| Code | HTTP | Description |
|
|
118
|
+
|------|------|-------------|
|
|
119
|
+
| `TOKEN_INVALID` | 401 | Malformed or fails verification |
|
|
120
|
+
| `TOKEN_EXPIRED` | 401 | `exp` claim is in the past |
|
|
121
|
+
| `TOKEN_REVOKED` | 401 | Token revoked server-side |
|
|
122
|
+
| `REFRESH_TOKEN_REUSED` | 401 | Refresh token reuse (possible theft) |
|
|
123
|
+
|
|
124
|
+
### Sessions
|
|
125
|
+
|
|
126
|
+
| Code | HTTP | Description |
|
|
127
|
+
|------|------|-------------|
|
|
128
|
+
| `SESSION_INVALID` | 401 | Session does not exist |
|
|
129
|
+
| `SESSION_EXPIRED` | 401 | Session has expired |
|
|
130
|
+
|
|
131
|
+
### MFA
|
|
132
|
+
|
|
133
|
+
| Code | HTTP | Description |
|
|
134
|
+
|------|------|-------------|
|
|
135
|
+
| `MFA_INVALID_CODE` | 401 | Wrong MFA code |
|
|
136
|
+
| `MFA_METHOD_UNAVAILABLE` | 400 | Method not available |
|
|
137
|
+
| `MFA_RATE_LIMITED` | 429 | Too many attempts |
|
|
138
|
+
| `MFA_ENROLLMENT_REQUIRED` | 403 | MFA must be enrolled |
|
|
139
|
+
|
|
140
|
+
### Authorization
|
|
141
|
+
|
|
142
|
+
| Code | HTTP | Description |
|
|
143
|
+
|------|------|-------------|
|
|
144
|
+
| `INSUFFICIENT_PERMISSIONS` | 403 | Lacks required role/entitlement |
|
|
145
|
+
| `FORBIDDEN` | 403 | Action not allowed |
|
|
146
|
+
| `USER_INACTIVE` | 403 | User inactive in tenant |
|
|
147
|
+
|
|
148
|
+
### API Keys
|
|
149
|
+
|
|
150
|
+
| Code | HTTP | Description |
|
|
151
|
+
|------|------|-------------|
|
|
152
|
+
| `API_KEY_REQUIRED` | 401 | Endpoint requires API key |
|
|
153
|
+
| `API_KEY_INVALID` | 401 | Invalid or expired API key |
|
|
154
|
+
|
|
155
|
+
### Resources
|
|
156
|
+
|
|
157
|
+
| Code | HTTP | Description |
|
|
158
|
+
|------|------|-------------|
|
|
159
|
+
| `NOT_FOUND` | 404 | Resource doesn't exist |
|
|
160
|
+
| `ALREADY_EXISTS` | 409 | Duplicate identifier |
|
|
161
|
+
| `VALIDATION_ERROR` | 400 | Failed request validation |
|
|
162
|
+
|
|
163
|
+
### PIN
|
|
164
|
+
|
|
165
|
+
| Code | HTTP | Description |
|
|
166
|
+
|------|------|-------------|
|
|
167
|
+
| `PIN_EXPIRED` | 401 | PIN has expired |
|
|
168
|
+
|
|
169
|
+
### Invitations
|
|
170
|
+
|
|
171
|
+
| Code | HTTP | Description |
|
|
172
|
+
|------|------|-------------|
|
|
173
|
+
| `INVALID_CODE` | 400 | Invalid invite code |
|
|
174
|
+
| `CODE_ALREADY_USED` | 400 | Code already accepted |
|
|
175
|
+
| `CODE_EXPIRED` | 400 | Code has expired |
|
|
176
|
+
| `CODE_IP_MISMATCH` | 403 | IP mismatch |
|
|
177
|
+
|
|
178
|
+
### External Services
|
|
179
|
+
|
|
180
|
+
| Code | HTTP | Description |
|
|
181
|
+
|------|------|-------------|
|
|
182
|
+
| `OAUTH_NOT_CONFIGURED` | 500 | OAuth not configured |
|
|
183
|
+
| `EMAIL_SERVICE_UNAVAILABLE` | 503 | Email service down |
|
|
184
|
+
| `UPLOAD_ERROR` | 500 | Upload failed |
|
|
185
|
+
|
|
186
|
+
### Other
|
|
187
|
+
|
|
188
|
+
| Code | HTTP | Description |
|
|
189
|
+
|------|------|-------------|
|
|
190
|
+
| `INTERNAL_ERROR` | 500 | Server-side error |
|
|
191
|
+
| `UNKNOWN_PAYLOAD` | 500 | Unexpected response |
|
|
192
|
+
|
|
193
|
+
## Complete Example
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { IQAuthClient, IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
197
|
+
|
|
198
|
+
const client = new IQAuthClient({
|
|
199
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
200
|
+
// Trusted runtime example
|
|
201
|
+
accessToken: storedToken,
|
|
202
|
+
refreshToken: storedRefreshToken,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
async function safeApiCall<T>(fn: () => Promise<T>): Promise<T> {
|
|
206
|
+
try {
|
|
207
|
+
return await fn();
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (!(err instanceof IQAuthError)) throw err;
|
|
210
|
+
|
|
211
|
+
switch (err.code) {
|
|
212
|
+
case ErrorCodes.TOKEN_EXPIRED: {
|
|
213
|
+
const refreshToken = client.getRefreshToken();
|
|
214
|
+
if (!refreshToken) throw new Error("Session expired — please log in again");
|
|
215
|
+
const newTokens = await client.auth.refreshTokens(refreshToken);
|
|
216
|
+
client.setTokens(newTokens);
|
|
217
|
+
return fn(); // Retry
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
case ErrorCodes.REFRESH_TOKEN_REUSED:
|
|
221
|
+
throw new Error("Session compromised — log in again");
|
|
222
|
+
|
|
223
|
+
case ErrorCodes.INSUFFICIENT_PERMISSIONS:
|
|
224
|
+
throw new Error("You don't have permission for this action");
|
|
225
|
+
|
|
226
|
+
case ErrorCodes.NOT_FOUND:
|
|
227
|
+
throw new Error("The requested resource was not found");
|
|
228
|
+
|
|
229
|
+
default:
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Usage
|
|
236
|
+
const user = await safeApiCall(() => client.users.getCurrent());
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## HTTP Response Envelope
|
|
240
|
+
|
|
241
|
+
The server wraps all responses:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
// Success
|
|
245
|
+
{ "success": true, "data": { ... } }
|
|
246
|
+
|
|
247
|
+
// Error
|
|
248
|
+
{ "success": false, "error": { "code": "...", "message": "..." } }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The SDK automatically unwraps success responses and throws `IQAuthError` for errors.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# GDPR Compliance
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Implement GDPR data subject rights: personal data export (right of access) and account deletion (right to erasure).
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- Authenticated user (access token set on client)
|
|
11
|
+
|
|
12
|
+
## Environment Note
|
|
13
|
+
|
|
14
|
+
This guide's token examples assume a trusted runtime or secure mobile storage, not browser-local token ownership for first-party web apps.
|
|
15
|
+
|
|
16
|
+
## SDK Methods
|
|
17
|
+
|
|
18
|
+
| Module | Method | Description |
|
|
19
|
+
|--------|--------|-------------|
|
|
20
|
+
| `gdpr` | `exportData()` | Export all personal data → `GdprExportData` |
|
|
21
|
+
| `gdpr` | `deleteAccount(confirmEmail)` | Permanently delete account |
|
|
22
|
+
|
|
23
|
+
## Step-by-Step
|
|
24
|
+
|
|
25
|
+
### 1. Export Personal Data
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
const exportData = await client.gdpr.exportData();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Returns `GdprExportData`:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
interface GdprExportData {
|
|
35
|
+
exportedAt: string;
|
|
36
|
+
user: Record<string, unknown>;
|
|
37
|
+
memberships: Record<string, unknown>[];
|
|
38
|
+
sessions: Record<string, unknown>[];
|
|
39
|
+
linkedAccounts: Record<string, unknown>[];
|
|
40
|
+
mfaEnrollments: Record<string, unknown>[];
|
|
41
|
+
auditLog: Record<string, unknown>[];
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The export includes: user profile, tenant memberships, session history, linked OAuth accounts, MFA enrollments, and audit log entries.
|
|
46
|
+
|
|
47
|
+
### 2. Delete Account
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
await client.gdpr.deleteAccount("user@example.com");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The `confirmEmail` must match the authenticated user's email as a confirmation step. This action is **irreversible** — all user data is permanently deleted.
|
|
54
|
+
|
|
55
|
+
## Error Handling
|
|
56
|
+
|
|
57
|
+
| Error Code | Meaning | Recovery |
|
|
58
|
+
|------------|---------|----------|
|
|
59
|
+
| `TOKEN_INVALID` | Not authenticated | Re-authenticate |
|
|
60
|
+
| `VALIDATION_ERROR` | Email doesn't match | Pass correct email |
|
|
61
|
+
| `INTERNAL_ERROR` | Server error during export/delete | Retry |
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await client.gdpr.deleteAccount(email);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.VALIDATION_ERROR) {
|
|
70
|
+
console.error("Email confirmation doesn't match your account");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Complete Example
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { IQAuthClient, IQAuthError } from "@iqauth/sdk";
|
|
79
|
+
|
|
80
|
+
const client = new IQAuthClient({
|
|
81
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
82
|
+
accessToken: userToken,
|
|
83
|
+
refreshToken: userRefreshToken,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
async function handleDataExportRequest() {
|
|
87
|
+
const data = await client.gdpr.exportData();
|
|
88
|
+
console.log("Export generated at:", data.exportedAt);
|
|
89
|
+
console.log("Sessions:", data.sessions.length);
|
|
90
|
+
console.log("Memberships:", data.memberships.length);
|
|
91
|
+
console.log("MFA enrollments:", data.mfaEnrollments.length);
|
|
92
|
+
|
|
93
|
+
// Provide as downloadable JSON
|
|
94
|
+
return JSON.stringify(data, null, 2);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function handleAccountDeletionRequest(userEmail: string) {
|
|
98
|
+
// Confirm with user first (this is irreversible)
|
|
99
|
+
// promptUserConfirmation() is your app's UI function — show a confirmation dialog
|
|
100
|
+
const confirmed = await promptUserConfirmation(
|
|
101
|
+
"This will permanently delete your account and all data. Are you sure?",
|
|
102
|
+
);
|
|
103
|
+
if (!confirmed) return;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await client.gdpr.deleteAccount(userEmail);
|
|
107
|
+
console.log("Account deleted successfully");
|
|
108
|
+
// Redirect to goodbye page
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err instanceof IQAuthError) {
|
|
111
|
+
console.error("Deletion failed:", err.message);
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## API Reference
|
|
119
|
+
|
|
120
|
+
| Method | HTTP | Path |
|
|
121
|
+
|--------|------|------|
|
|
122
|
+
| `exportData()` | GET | `/api/v1/gdpr/export` |
|
|
123
|
+
| `deleteAccount(confirmEmail)` | POST | `/api/v1/gdpr/delete` |
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Invitations
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Invite users to join a tenant. Supports both new users (who create an account) and existing users (who join the tenant directly).
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- `tenant_admin` role for creating invitations
|
|
11
|
+
- No auth required for `validate()` and `accept()`
|
|
12
|
+
|
|
13
|
+
## Environment Note
|
|
14
|
+
|
|
15
|
+
These administrative examples assume a trusted runtime. First-party browser apps should execute invitation management through a backend cookie session.
|
|
16
|
+
|
|
17
|
+
## SDK Methods
|
|
18
|
+
|
|
19
|
+
| Module | Method | Description |
|
|
20
|
+
|--------|--------|-------------|
|
|
21
|
+
| `invites` | `create(data)` | Create invitation → `{ invitation, inviteToken?, warning? }` |
|
|
22
|
+
| `invites` | `validate(token)` | Validate invite token → `InviteValidation` |
|
|
23
|
+
| `invites` | `accept(token, data)` | Accept invitation |
|
|
24
|
+
|
|
25
|
+
## Step-by-Step
|
|
26
|
+
|
|
27
|
+
### 1. Create an Invitation
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
const result = await client.invites.create({
|
|
31
|
+
email: "newuser@example.com",
|
|
32
|
+
tenantId: "tenant-uuid",
|
|
33
|
+
role: "user",
|
|
34
|
+
vendorId: "vendor-uuid", // optional
|
|
35
|
+
products: ["iqcapture"], // optional product access
|
|
36
|
+
});
|
|
37
|
+
// result.inviteToken — for programmatic flows
|
|
38
|
+
// In production, user receives email with invite link
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Validate Token
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const validation = await client.invites.validate(inviteToken);
|
|
45
|
+
// { valid: true, email: "user@example.com", role: "user" }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. Accept Invitation
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// New user
|
|
52
|
+
await client.invites.accept(inviteToken, {
|
|
53
|
+
name: "New User",
|
|
54
|
+
password: "securePassword123",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Existing Google user
|
|
58
|
+
await client.invites.accept(inviteToken, {
|
|
59
|
+
googleId: "google-uid-123",
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Alternative: Tenant-Level Invite
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
await client.tenants.inviteUser(tenantId, {
|
|
67
|
+
email: "user@example.com",
|
|
68
|
+
role: "user",
|
|
69
|
+
products: ["iqcapture"],
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Error Handling
|
|
74
|
+
|
|
75
|
+
| Error Code | Meaning | Recovery |
|
|
76
|
+
|------------|---------|----------|
|
|
77
|
+
| `INVALID_CODE` | Invite code is invalid | Check token |
|
|
78
|
+
| `CODE_ALREADY_USED` | Invite already accepted | Create new invitation |
|
|
79
|
+
| `CODE_EXPIRED` | Invite has expired | Create new invitation |
|
|
80
|
+
| `CODE_IP_MISMATCH` | IP doesn't match invite | Security restriction |
|
|
81
|
+
| `ALREADY_EXISTS` | User already in tenant | No action needed |
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await client.invites.accept(token, { name: "User", password: "pass" });
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err instanceof IQAuthError) {
|
|
90
|
+
if (err.code === ErrorCodes.CODE_EXPIRED) {
|
|
91
|
+
console.error("Invitation has expired — request a new one");
|
|
92
|
+
}
|
|
93
|
+
if (err.code === ErrorCodes.CODE_ALREADY_USED) {
|
|
94
|
+
console.error("This invitation was already used");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Complete Example
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { IQAuthClient, IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
104
|
+
|
|
105
|
+
const adminClient = new IQAuthClient({
|
|
106
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
107
|
+
// Trusted runtime example
|
|
108
|
+
accessToken: adminToken,
|
|
109
|
+
refreshToken: adminRefreshToken,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
async function inviteAndAccept(tenantId: string, email: string) {
|
|
113
|
+
const { invitation, inviteToken } = await adminClient.invites.create({
|
|
114
|
+
email,
|
|
115
|
+
tenantId,
|
|
116
|
+
role: "user",
|
|
117
|
+
products: ["iqcapture"],
|
|
118
|
+
});
|
|
119
|
+
console.log("Invitation created:", invitation.id);
|
|
120
|
+
|
|
121
|
+
// In a real app, the user clicks an email link containing inviteToken
|
|
122
|
+
// For programmatic flows:
|
|
123
|
+
|
|
124
|
+
const validation = await adminClient.invites.validate(inviteToken!);
|
|
125
|
+
if (!validation.valid) {
|
|
126
|
+
throw new Error("Invalid invitation");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await adminClient.invites.accept(inviteToken!, {
|
|
130
|
+
name: "New User",
|
|
131
|
+
password: "SecurePass123!",
|
|
132
|
+
});
|
|
133
|
+
console.log("Invitation accepted");
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## API Reference
|
|
138
|
+
|
|
139
|
+
| Method | HTTP | Path |
|
|
140
|
+
|--------|------|------|
|
|
141
|
+
| `create(data)` | POST | `/api/v1/invites` |
|
|
142
|
+
| `validate(token)` | GET | `/api/v1/invites/:token/validate` |
|
|
143
|
+
| `accept(token, data)` | POST | `/api/v1/invites/:token/accept` |
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# MFA Enrollment
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Manage multi-factor authentication enrollment for the current user. Supports TOTP (authenticator app), SMS, and email methods, with backup code management.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- Authenticated user (access token set on client)
|
|
11
|
+
- For SMS enrollment: a valid phone number
|
|
12
|
+
|
|
13
|
+
## Environment Note
|
|
14
|
+
|
|
15
|
+
The token-owning examples in this guide fit trusted runtimes or native mobile secure storage. First-party browser apps should drive MFA through backend-managed session endpoints.
|
|
16
|
+
|
|
17
|
+
## SDK Methods
|
|
18
|
+
|
|
19
|
+
| Module | Method | Description |
|
|
20
|
+
|--------|--------|-------------|
|
|
21
|
+
| `mfa` | `getAvailableMethods()` | Available MFA methods |
|
|
22
|
+
| `mfa` | `enrollTotp()` | Start TOTP enrollment → QR code + secret |
|
|
23
|
+
| `mfa` | `verifyTotpEnrollment(code, secret)` | Verify TOTP → enrollment + backup codes |
|
|
24
|
+
| `mfa` | `enrollSms(phone)` | Start SMS enrollment |
|
|
25
|
+
| `mfa` | `verifySmsEnrollment(code, phone)` | Verify SMS enrollment |
|
|
26
|
+
| `mfa` | `startEmailEnrollment()` | Start email enrollment |
|
|
27
|
+
| `mfa` | `verifyEmailEnrollment(code)` | Verify email enrollment |
|
|
28
|
+
| `mfa` | `listEnrollments()` | List enrollments → `MfaEnrollment[]` |
|
|
29
|
+
| `mfa` | `setPrimaryEnrollment(id)` | Set primary method |
|
|
30
|
+
| `mfa` | `deactivateEnrollment(id)` | Remove enrollment |
|
|
31
|
+
| `mfa` | `regenerateBackupCodes()` | Regenerate backup codes |
|
|
32
|
+
| `mfa` | `getBackupCodeCount()` | Get remaining count |
|
|
33
|
+
|
|
34
|
+
## Step-by-Step
|
|
35
|
+
|
|
36
|
+
### 1. Check Available Methods
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const { available } = await client.mfa.getAvailableMethods();
|
|
40
|
+
// ["totp", "sms", "email"]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. TOTP Enrollment
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const { secret, qrCodeUri, qrCodeDataUrl } = await client.mfa.enrollTotp();
|
|
47
|
+
// Display qrCodeDataUrl as <img src="..."> for the user to scan
|
|
48
|
+
|
|
49
|
+
const { enrollment, backupCodes, backupCodesWarning } = await client.mfa.verifyTotpEnrollment(
|
|
50
|
+
userEnteredCode,
|
|
51
|
+
secret,
|
|
52
|
+
);
|
|
53
|
+
// Show backupCodes to user — they won't be shown again
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. SMS Enrollment
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const { sent } = await client.mfa.enrollSms("+15551234567");
|
|
60
|
+
const { enrollment } = await client.mfa.verifySmsEnrollment(smsCode, "+15551234567");
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 4. Email Enrollment
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const { sent } = await client.mfa.startEmailEnrollment();
|
|
67
|
+
const { enrollment } = await client.mfa.verifyEmailEnrollment(emailCode);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 5. Manage Enrollments
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const enrollments = await client.mfa.listEnrollments();
|
|
74
|
+
await client.mfa.setPrimaryEnrollment(enrollmentId);
|
|
75
|
+
await client.mfa.deactivateEnrollment(enrollmentId);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 6. Backup Codes
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const { backupCodes, warning } = await client.mfa.regenerateBackupCodes();
|
|
82
|
+
const { remainingBackupCodes } = await client.mfa.getBackupCodeCount();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Error Handling
|
|
86
|
+
|
|
87
|
+
| Error Code | Meaning | Recovery |
|
|
88
|
+
|------------|---------|----------|
|
|
89
|
+
| `MFA_INVALID_CODE` | Wrong verification code | Let user retry |
|
|
90
|
+
| `MFA_METHOD_UNAVAILABLE` | Method not supported | Check available methods |
|
|
91
|
+
| `MFA_RATE_LIMITED` | Too many attempts | Wait and retry |
|
|
92
|
+
| `MFA_ENROLLMENT_REQUIRED` | MFA must be enrolled | Redirect to enrollment |
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await client.mfa.verifyTotpEnrollment(code, secret);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.MFA_INVALID_CODE) {
|
|
101
|
+
console.error("Wrong code — please try again");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Complete Example
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { IQAuthClient, IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
110
|
+
|
|
111
|
+
const client = new IQAuthClient({
|
|
112
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
113
|
+
// Trusted runtime example
|
|
114
|
+
accessToken: userToken,
|
|
115
|
+
refreshToken: userRefreshToken,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
async function setupTotp() {
|
|
119
|
+
const { available } = await client.mfa.getAvailableMethods();
|
|
120
|
+
if (!available.includes("totp")) {
|
|
121
|
+
throw new Error("TOTP not available");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { secret, qrCodeDataUrl } = await client.mfa.enrollTotp();
|
|
125
|
+
console.log("Show this QR code to user:", qrCodeDataUrl);
|
|
126
|
+
|
|
127
|
+
// User scans QR and enters the 6-digit code from their authenticator app
|
|
128
|
+
// promptUserForCode() is your app's UI function — show an input field and return the value
|
|
129
|
+
const code = await promptUserForCode();
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const { enrollment, backupCodes } = await client.mfa.verifyTotpEnrollment(code, secret);
|
|
133
|
+
console.log("TOTP enrolled:", enrollment.id);
|
|
134
|
+
console.log("Backup codes (save these!):", backupCodes);
|
|
135
|
+
|
|
136
|
+
await client.mfa.setPrimaryEnrollment(enrollment.id);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.MFA_INVALID_CODE) {
|
|
139
|
+
console.error("Invalid code — scan QR again and retry");
|
|
140
|
+
}
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function checkBackupCodes() {
|
|
146
|
+
const { remainingBackupCodes } = await client.mfa.getBackupCodeCount();
|
|
147
|
+
if (remainingBackupCodes < 3) {
|
|
148
|
+
console.warn("Low backup codes — consider regenerating");
|
|
149
|
+
const { backupCodes } = await client.mfa.regenerateBackupCodes();
|
|
150
|
+
console.log("New backup codes:", backupCodes);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## API Reference
|
|
156
|
+
|
|
157
|
+
| Method | HTTP | Path |
|
|
158
|
+
|--------|------|------|
|
|
159
|
+
| `getAvailableMethods()` | GET | `/api/v1/mfa/methods` |
|
|
160
|
+
| `enrollTotp()` | POST | `/api/v1/mfa/enroll/totp` |
|
|
161
|
+
| `verifyTotpEnrollment(code, secret)` | POST | `/api/v1/mfa/enroll/totp/verify` |
|
|
162
|
+
| `enrollSms(phone)` | POST | `/api/v1/mfa/enroll/sms` |
|
|
163
|
+
| `verifySmsEnrollment(code, phone)` | POST | `/api/v1/mfa/enroll/sms/verify` |
|
|
164
|
+
| `startEmailEnrollment()` | POST | `/api/v1/mfa/enroll/email/start` |
|
|
165
|
+
| `verifyEmailEnrollment(code)` | POST | `/api/v1/mfa/enroll/email/verify` |
|
|
166
|
+
| `listEnrollments()` | GET | `/api/v1/mfa/enrollments` |
|
|
167
|
+
| `setPrimaryEnrollment(id)` | PATCH | `/api/v1/mfa/enrollments/:id/primary` |
|
|
168
|
+
| `deactivateEnrollment(id)` | DELETE | `/api/v1/mfa/enrollments/:id` |
|
|
169
|
+
| `regenerateBackupCodes()` | POST | `/api/v1/mfa/backup-codes/regenerate` |
|
|
170
|
+
| `getBackupCodeCount()` | GET | `/api/v1/mfa/backup-codes/count` |
|