@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,318 @@
|
|
|
1
|
+
# V1 to V2 Authorization Upgrade Guide
|
|
2
|
+
|
|
3
|
+
This guide covers migrating from V1 (flat JWT roles) to V2 (scoped memberships + permission groups).
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
| Aspect | V1 (Flat Roles) | V2 (Scoped Auth) |
|
|
8
|
+
|--------|-----------------|-------------------|
|
|
9
|
+
| Role storage | `roles[]` in JWT | Memberships table + JWT `scopeContext` |
|
|
10
|
+
| Scope | Tenant-wide | Vendor / Source / Client scoped |
|
|
11
|
+
| Permission model | Implicit (role name checks) | Explicit permission groups with inheritance |
|
|
12
|
+
| SDK modules | `client.permissions` | `client.memberships` + `client.scope` + `client.permissionGroups` |
|
|
13
|
+
| Tenant flag | — | `enableScopedAuth: true` |
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- SDK version with `memberships`, `scope`, and `permissionGroups` modules
|
|
18
|
+
- IQAuth server supporting the V2 scoped auth endpoints
|
|
19
|
+
|
|
20
|
+
## Environment Note
|
|
21
|
+
|
|
22
|
+
Any examples in this guide that manipulate bearer tokens directly assume a trusted runtime or secure mobile storage.
|
|
23
|
+
|
|
24
|
+
For first-party browser apps, keep the token lifecycle in your backend session layer.
|
|
25
|
+
|
|
26
|
+
## Step 1: Enable Scoped Auth on Your Tenant
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
await client.tenants.update(tenantId, {
|
|
30
|
+
enableScopedAuth: true,
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This flag enables the scoped authorization system for that tenant. V1 role checks (`roles[]` in JWT) continue to work — V2 is additive.
|
|
35
|
+
|
|
36
|
+
## Step 2: Create Your Entity Hierarchy
|
|
37
|
+
|
|
38
|
+
V2 authorization scopes down through a three-level hierarchy: **Vendor → Source → Client**.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// Create a vendor
|
|
42
|
+
const vendor = await client.vendors.create({
|
|
43
|
+
name: "Acme Disposal",
|
|
44
|
+
slug: "acme-disposal",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Create a source under the vendor
|
|
48
|
+
const source = await client.sources.create(vendor.id, {
|
|
49
|
+
name: "Acme MRF North",
|
|
50
|
+
slug: "acme-mrf-north",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Create a client under the source
|
|
54
|
+
const clientEntity = await client.sources.createClient(source.id, {
|
|
55
|
+
name: "City of Springfield",
|
|
56
|
+
slug: "springfield",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Link them in the hierarchy
|
|
60
|
+
await client.hierarchy.linkVendorSource(vendor.id, source.id);
|
|
61
|
+
await client.hierarchy.linkSourceClient(source.id, clientEntity.id);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Step 3: Define Roles
|
|
65
|
+
|
|
66
|
+
Create tenant-scoped roles if you need custom ones beyond the system roles:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
await client.roles.create(tenantId, {
|
|
70
|
+
name: "site_manager",
|
|
71
|
+
description: "Manages operations at a specific source",
|
|
72
|
+
hierarchyLevel: 30,
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
System roles (`platform_admin`, `tenant_admin`, `vendor_admin`) are already available.
|
|
77
|
+
|
|
78
|
+
## Step 4: Grant Scoped Memberships
|
|
79
|
+
|
|
80
|
+
Replace flat role assignments with scoped memberships:
|
|
81
|
+
|
|
82
|
+
**Before (V1):**
|
|
83
|
+
```typescript
|
|
84
|
+
// V1: flat role — user is "vendor_admin" for the entire tenant
|
|
85
|
+
await client.roles.assignRole(tenantId, userId, { roleName: "vendor_admin" });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**After (V2):**
|
|
89
|
+
```typescript
|
|
90
|
+
// V2: scoped membership — user is "vendor_admin" for a specific vendor
|
|
91
|
+
await client.memberships.grant({
|
|
92
|
+
userId,
|
|
93
|
+
roleName: "vendor_admin",
|
|
94
|
+
scopeType: "vendor",
|
|
95
|
+
scopeId: vendorId,
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Valid `scopeType` values: `"tenant"`, `"vendor"`, `"source"`, `"client"`.
|
|
100
|
+
|
|
101
|
+
## Step 5: Set Up Permission Groups
|
|
102
|
+
|
|
103
|
+
Permission groups give fine-grained control beyond role-based access:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Create a group
|
|
107
|
+
const group = await client.permissionGroups.create(
|
|
108
|
+
tenantId,
|
|
109
|
+
"Capture Operators",
|
|
110
|
+
"Can operate IQCapture at assigned sources",
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Add permissions to the group
|
|
114
|
+
await client.permissionGroups.addPermission(tenantId, group.id, {
|
|
115
|
+
appKey: "iqcapture",
|
|
116
|
+
nodeKey: "capture.operate",
|
|
117
|
+
effect: "allow",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await client.permissionGroups.addPermission(tenantId, group.id, {
|
|
121
|
+
appKey: "iqcapture",
|
|
122
|
+
nodeKey: "capture.reports.view",
|
|
123
|
+
effect: "allow",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Assign user to the group
|
|
127
|
+
await client.permissionGroups.assignUserToGroup(tenantId, userId, group.id);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Permission Group Inheritance
|
|
131
|
+
|
|
132
|
+
Groups can inherit permissions from other groups:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const baseGroup = await client.permissionGroups.create(tenantId, "Base Viewer");
|
|
136
|
+
const adminGroup = await client.permissionGroups.create(tenantId, "Admin");
|
|
137
|
+
|
|
138
|
+
// Admin inherits all permissions from Base Viewer
|
|
139
|
+
await client.permissionGroups.addInheritance(tenantId, adminGroup.id, baseGroup.id);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### User Permission Overrides
|
|
143
|
+
|
|
144
|
+
Override group permissions for specific users:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Deny a specific permission for one user (overrides group "allow")
|
|
148
|
+
await client.permissionGroups.addUserOverride(tenantId, userId, {
|
|
149
|
+
appKey: "iqcapture",
|
|
150
|
+
nodeKey: "capture.delete",
|
|
151
|
+
effect: "deny",
|
|
152
|
+
weight: 100, // Higher weight wins
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Step 6: Check Effective Permissions
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Get all effective permissions for a user in an app
|
|
160
|
+
const perms = await client.permissionGroups.getEffectivePermissions(
|
|
161
|
+
tenantId,
|
|
162
|
+
userId,
|
|
163
|
+
{ appKey: "iqcapture" },
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Check a specific permission
|
|
167
|
+
const check = await client.permissionGroups.checkPermission(
|
|
168
|
+
tenantId,
|
|
169
|
+
userId,
|
|
170
|
+
"iqcapture",
|
|
171
|
+
"capture.operate",
|
|
172
|
+
);
|
|
173
|
+
if (check.allowed) {
|
|
174
|
+
// User can operate
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Step 7: Scope Switching
|
|
179
|
+
|
|
180
|
+
In V2, users can switch their active scope context to work within a specific vendor, source, or client:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// See available scopes (hierarchical tree)
|
|
184
|
+
const tree = await client.scope.getAvailable();
|
|
185
|
+
// tree.vendors[0].sources[0].clients[0]
|
|
186
|
+
|
|
187
|
+
// Switch to a specific vendor scope
|
|
188
|
+
const { accessToken, scopeContext } = await client.scope.switchScope(
|
|
189
|
+
"vendor",
|
|
190
|
+
vendorId,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// The new token has scopeContext embedded
|
|
194
|
+
// Update the client with the new token
|
|
195
|
+
client.setTokens({ accessToken, refreshToken: client.getRefreshToken()! });
|
|
196
|
+
|
|
197
|
+
// Now all API calls are scoped to that vendor
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The `scopeContext` in the JWT contains:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
interface ScopeContext {
|
|
204
|
+
type: string; // "vendor" | "source" | "client"
|
|
205
|
+
id: string; // Entity ID
|
|
206
|
+
role: string; // User's role in that scope
|
|
207
|
+
membershipId: string;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Step 8: Update Your Middleware
|
|
212
|
+
|
|
213
|
+
V1 middleware only checked `roles[]`. For V2, also check scoped permissions:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { iqAuthMiddleware } from "@iqauth/sdk";
|
|
217
|
+
|
|
218
|
+
// V1 style (still works)
|
|
219
|
+
app.use(iqAuthMiddleware(client, {
|
|
220
|
+
requiredRoles: ["vendor_admin"],
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
// V2 style — check entitlements + scoped role in route handler
|
|
224
|
+
app.get("/api/sources/:sourceId/data", async (req, res) => {
|
|
225
|
+
const { auth } = req;
|
|
226
|
+
if (!auth) return res.status(401).json({ error: "Not authenticated" });
|
|
227
|
+
|
|
228
|
+
// Check the scope context in the JWT
|
|
229
|
+
if (auth.scopeContext?.type === "source" && auth.scopeContext.id === req.params.sourceId) {
|
|
230
|
+
// User has switched to this source scope — proceed
|
|
231
|
+
} else {
|
|
232
|
+
// Check membership server-side
|
|
233
|
+
const { memberships } = await client.memberships.listForScope("source", req.params.sourceId);
|
|
234
|
+
const userMembership = memberships.find(m => m.userId === auth.sub && m.isActive);
|
|
235
|
+
if (!userMembership) {
|
|
236
|
+
return res.status(403).json({ error: "No access to this source" });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Migration Checklist
|
|
243
|
+
|
|
244
|
+
- [ ] Enable `enableScopedAuth` on target tenants
|
|
245
|
+
- [ ] Create entity hierarchy (vendors, sources, clients)
|
|
246
|
+
- [ ] Link hierarchy entities
|
|
247
|
+
- [ ] Create any custom roles needed
|
|
248
|
+
- [ ] Grant scoped memberships for existing users
|
|
249
|
+
- [ ] Set up permission groups for fine-grained control
|
|
250
|
+
- [ ] Register your app manifest with permission nodes
|
|
251
|
+
- [ ] Update API route handlers to check `scopeContext` or memberships
|
|
252
|
+
- [ ] Update frontend to handle scope switching UI
|
|
253
|
+
- [ ] Test effective permissions with `checkPermission`
|
|
254
|
+
|
|
255
|
+
## Dual-Write Period
|
|
256
|
+
|
|
257
|
+
During migration, V1 and V2 coexist. Follow these rules:
|
|
258
|
+
|
|
259
|
+
**Coexistence rules:**
|
|
260
|
+
1. Keep V1 role assignments active while granting V2 memberships — don't remove V1 roles until V2 is fully verified
|
|
261
|
+
2. Any new user provisioning must create both V1 role assignment AND V2 scoped membership
|
|
262
|
+
3. Any role change must be applied in both systems during the transition
|
|
263
|
+
4. Keep V1 middleware (`requiredRoles`) on existing routes while adding V2 checks to new routes
|
|
264
|
+
|
|
265
|
+
**Rollback guardrails:**
|
|
266
|
+
- Setting `enableScopedAuth: false` reverts the tenant to V1-only mode; V2 membership data is preserved but inactive
|
|
267
|
+
- `client.permissions.hasRole()` continues to work even with V2 enabled, so V1 route guards are always safe
|
|
268
|
+
- If a V2 permission check returns unexpected results, fall back to V1 role check while investigating
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// Dual-write: grant both V1 role and V2 membership
|
|
272
|
+
await client.roles.assignRole(tenantId, userId, { roleName: "vendor_admin" });
|
|
273
|
+
await client.memberships.grant({
|
|
274
|
+
userId, roleName: "vendor_admin", scopeType: "vendor", scopeId: vendorId,
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Verify Migration Complete
|
|
279
|
+
|
|
280
|
+
Run these checks before removing V1 role logic:
|
|
281
|
+
|
|
282
|
+
- [ ] **No V1-only route guards remain**: Search codebase for `requiredRoles` without a parallel V2 `scopeContext` check
|
|
283
|
+
- [ ] **All users have memberships**: `client.memberships.listForTenant()` returns at least one membership per active user
|
|
284
|
+
- [ ] **scopeContext coverage**: Decode a sample of JWTs and confirm `scopeContext` is present after login + scope selection
|
|
285
|
+
- [ ] **Permission group parity**: For each V1 role-gated feature, verify `client.permissionGroups.checkPermission()` returns the expected result
|
|
286
|
+
- [ ] **No dual-write code**: Remove all V1 role assignment calls (`client.roles.assignRole`) that shadow V2 membership grants
|
|
287
|
+
- [ ] **Frontend scope switching works**: Confirm users can switch scope and the UI reflects the correct entity context
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// Automated migration verification
|
|
291
|
+
async function verifyMigrationComplete(tenantId: string) {
|
|
292
|
+
const users = await client.tenants.getUsers(tenantId);
|
|
293
|
+
const { memberships } = await client.memberships.listForTenant();
|
|
294
|
+
|
|
295
|
+
const usersWithMemberships = new Set(memberships.map(m => m.userId));
|
|
296
|
+
const missing = users.filter(u => u.isActive && !usersWithMemberships.has(u.id));
|
|
297
|
+
|
|
298
|
+
if (missing.length > 0) {
|
|
299
|
+
console.error("Users missing V2 memberships:", missing.map(u => u.userId));
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const groups = await client.permissionGroups.list(tenantId);
|
|
304
|
+
if (groups.length === 0) {
|
|
305
|
+
console.warn("No permission groups defined — V2 fine-grained permissions not configured");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log("Migration verification passed");
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Backward Compatibility
|
|
314
|
+
|
|
315
|
+
- V1 `roles[]` continues to appear in JWTs even with V2 enabled
|
|
316
|
+
- `client.permissions.hasRole()` still works for tenant-wide role checks
|
|
317
|
+
- V2 is additive — you don't need to migrate all routes at once
|
|
318
|
+
- The `entitlements[]` claim is unchanged between V1 and V2
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# API Keys
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Create, list, revoke, and introspect API keys for service-to-service authentication. API keys are sent as `X-API-Key` headers instead of Bearer tokens.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- `tenant_admin` or `platform_admin` role for API key management
|
|
11
|
+
|
|
12
|
+
## Environment Note
|
|
13
|
+
|
|
14
|
+
The admin client examples in this guide assume a trusted runtime, such as a backend service, CLI, or secure mobile context.
|
|
15
|
+
|
|
16
|
+
For first-party browser apps, manage the admin session through your backend cookie session instead of exposing raw refresh tokens to browser JavaScript.
|
|
17
|
+
|
|
18
|
+
## SDK Methods
|
|
19
|
+
|
|
20
|
+
| Module | Method | Description |
|
|
21
|
+
|--------|--------|-------------|
|
|
22
|
+
| `apiKeys` | `create(data)` | Create API key → `{ key, rawKey }` |
|
|
23
|
+
| `apiKeys` | `list(params?)` | List API keys → `ApiKeyInfo[]` |
|
|
24
|
+
| `apiKeys` | `revoke(id)` | Revoke API key |
|
|
25
|
+
| `apiKeys` | `introspect(apiKey)` | Introspect key metadata |
|
|
26
|
+
|
|
27
|
+
## Step-by-Step
|
|
28
|
+
|
|
29
|
+
### 1. Create an API Key
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const { key, rawKey } = await client.apiKeys.create({
|
|
33
|
+
name: "CI Pipeline Key",
|
|
34
|
+
scopes: ["users:read", "tenants:read"],
|
|
35
|
+
expiresAt: "2027-01-01T00:00:00Z",
|
|
36
|
+
tenantId: "tenant-uuid",
|
|
37
|
+
});
|
|
38
|
+
// IMPORTANT: rawKey is only shown once — store it securely
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Use an API Key
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const serviceClient = new IQAuthClient({
|
|
45
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
46
|
+
apiKey: rawKey,
|
|
47
|
+
});
|
|
48
|
+
// All requests now use X-API-Key header
|
|
49
|
+
const users = await serviceClient.users.list();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
When both `apiKey` and `accessToken` are set, the SDK prefers `apiKey`.
|
|
53
|
+
|
|
54
|
+
### 3. List / Revoke
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const allKeys = await client.apiKeys.list();
|
|
58
|
+
const tenantKeys = await client.apiKeys.list({ tenantId: "tenant-uuid" });
|
|
59
|
+
await client.apiKeys.revoke(keyId);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 4. Introspect
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const info = await client.apiKeys.introspect("iqk_live_abc123...");
|
|
66
|
+
// { tenantId, scopes, name, ... }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Error Handling
|
|
70
|
+
|
|
71
|
+
| Error Code | Meaning | Recovery |
|
|
72
|
+
|------------|---------|----------|
|
|
73
|
+
| `API_KEY_INVALID` | Key is invalid or expired | Use valid key |
|
|
74
|
+
| `API_KEY_REQUIRED` | Endpoint requires API key | Set `apiKey` on client |
|
|
75
|
+
| `INSUFFICIENT_PERMISSIONS` | Caller lacks admin role | Requires `tenant_admin`+ |
|
|
76
|
+
| `NOT_FOUND` | Key ID not found | Check key ID |
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await client.apiKeys.introspect(apiKey);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.API_KEY_INVALID) {
|
|
85
|
+
console.error("Invalid or expired API key");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Complete Example
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { IQAuthClient, IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
94
|
+
|
|
95
|
+
const adminClient = new IQAuthClient({
|
|
96
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
97
|
+
// Trusted runtime example
|
|
98
|
+
accessToken: adminToken,
|
|
99
|
+
refreshToken: adminRefreshToken,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
async function setupServiceKey(name: string, scopes: string[]) {
|
|
103
|
+
const { key, rawKey } = await adminClient.apiKeys.create({
|
|
104
|
+
name,
|
|
105
|
+
scopes,
|
|
106
|
+
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
|
|
107
|
+
});
|
|
108
|
+
console.log("Created key:", key.id, "prefix:", key.prefix);
|
|
109
|
+
// Store rawKey in your secrets manager
|
|
110
|
+
|
|
111
|
+
const serviceClient = new IQAuthClient({
|
|
112
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
113
|
+
apiKey: rawKey,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const users = await serviceClient.users.list();
|
|
117
|
+
console.log("Service key works — found", users.length, "users");
|
|
118
|
+
|
|
119
|
+
return { keyId: key.id, rawKey };
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## API Reference
|
|
124
|
+
|
|
125
|
+
| Method | HTTP | Path |
|
|
126
|
+
|--------|------|------|
|
|
127
|
+
| `create(data)` | POST | `/api/v1/api-keys` |
|
|
128
|
+
| `list(params?)` | GET | `/api/v1/api-keys` |
|
|
129
|
+
| `revoke(id)` | DELETE | `/api/v1/api-keys/:id` |
|
|
130
|
+
| `introspect(apiKey)` | POST | `/api/v1/api-keys/introspect` |
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# App Registration
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Register applications and their permission node trees with IQAuth. Used by the V2 permission system to define what permissions exist for each application.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@iqauth/sdk` installed
|
|
10
|
+
- `platform_admin` role
|
|
11
|
+
- `apps.manage` permission on the `iqauth-admin` app
|
|
12
|
+
|
|
13
|
+
## Environment Note
|
|
14
|
+
|
|
15
|
+
These examples assume a trusted administrative runtime. Do not treat them as guidance to persist admin refresh tokens in first-party browser JavaScript.
|
|
16
|
+
|
|
17
|
+
## SDK Methods
|
|
18
|
+
|
|
19
|
+
| Module | Method | Description |
|
|
20
|
+
|--------|--------|-------------|
|
|
21
|
+
| `apps` | `list()` | List registered apps → `AppInfo[]` |
|
|
22
|
+
| `apps` | `get(appKey)` | Get app with permission nodes |
|
|
23
|
+
| `apps` | `register(manifest)` | Register/sync app manifest (idempotent) |
|
|
24
|
+
| `apps` | `isRegistered(appKey)` | Check if app exists → `boolean` |
|
|
25
|
+
|
|
26
|
+
## Step-by-Step
|
|
27
|
+
|
|
28
|
+
### 1. Define Your Manifest
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const manifest = {
|
|
32
|
+
key: "iqcapture",
|
|
33
|
+
name: "IQ Capture",
|
|
34
|
+
description: "Image capture and classification tool",
|
|
35
|
+
version: "2.1.0",
|
|
36
|
+
permissions: [
|
|
37
|
+
{
|
|
38
|
+
key: "capture",
|
|
39
|
+
label: "Capture",
|
|
40
|
+
children: [
|
|
41
|
+
{ key: "capture.operate", label: "Operate Capture Station" },
|
|
42
|
+
{ key: "capture.configure", label: "Configure Capture Settings" },
|
|
43
|
+
{
|
|
44
|
+
key: "capture.reports",
|
|
45
|
+
label: "Reports",
|
|
46
|
+
children: [
|
|
47
|
+
{ key: "capture.reports.view", label: "View Reports" },
|
|
48
|
+
{ key: "capture.reports.export", label: "Export Reports" },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Register
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const result = await client.apps.register(manifest);
|
|
61
|
+
// { appId: "uuid", nodesUpserted: 5 }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is idempotent — calling again updates the existing app and upserts permission nodes. Maps to `POST /api/v1/apps/sync`. See [DECISION-010](../../DECISIONS.md).
|
|
65
|
+
|
|
66
|
+
### 3. List / Get
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const apps = await client.apps.list();
|
|
70
|
+
const app = await client.apps.get("iqcapture");
|
|
71
|
+
// app.permissionNodes contains the full tree
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 4. Check Registration
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const exists = await client.apps.isRegistered("iqcapture"); // catches 404
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Error Handling
|
|
81
|
+
|
|
82
|
+
| Error Code | Meaning | Recovery |
|
|
83
|
+
|------------|---------|----------|
|
|
84
|
+
| `INSUFFICIENT_PERMISSIONS` | Not `platform_admin` | Requires elevated role |
|
|
85
|
+
| `VALIDATION_ERROR` | Invalid manifest | Check required fields |
|
|
86
|
+
| `NOT_FOUND` | App key doesn't exist (for `get`) | Register first |
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { IQAuthError, ErrorCodes } from "@iqauth/sdk";
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await client.apps.register(manifest);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err instanceof IQAuthError && err.code === ErrorCodes.INSUFFICIENT_PERMISSIONS) {
|
|
95
|
+
console.error("Requires platform_admin role");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Complete Example
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { IQAuthClient } from "@iqauth/sdk";
|
|
104
|
+
|
|
105
|
+
const client = new IQAuthClient({
|
|
106
|
+
baseUrl: "https://auth.dispositioniq.com",
|
|
107
|
+
// Trusted runtime example
|
|
108
|
+
accessToken: platformAdminToken,
|
|
109
|
+
refreshToken: platformAdminRefreshToken,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
async function registerMyApp() {
|
|
113
|
+
const isAlreadyRegistered = await client.apps.isRegistered("myapp");
|
|
114
|
+
|
|
115
|
+
const result = await client.apps.register({
|
|
116
|
+
key: "myapp",
|
|
117
|
+
name: "My Application",
|
|
118
|
+
version: "1.0.0",
|
|
119
|
+
permissions: [
|
|
120
|
+
{
|
|
121
|
+
key: "data",
|
|
122
|
+
label: "Data Access",
|
|
123
|
+
children: [
|
|
124
|
+
{ key: "data.read", label: "Read Data" },
|
|
125
|
+
{ key: "data.write", label: "Write Data" },
|
|
126
|
+
{ key: "data.delete", label: "Delete Data" },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log(
|
|
133
|
+
isAlreadyRegistered ? "Updated" : "Created",
|
|
134
|
+
`app ${result.appId} with ${result.nodesUpserted} permission nodes`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const app = await client.apps.get("myapp");
|
|
138
|
+
console.log("Permission nodes:", app.permissionNodes.map(n => n.key));
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## API Reference
|
|
143
|
+
|
|
144
|
+
| Method | HTTP | Path |
|
|
145
|
+
|--------|------|------|
|
|
146
|
+
| `list()` | GET | `/api/v1/apps` |
|
|
147
|
+
| `get(appKey)` | GET | `/api/v1/apps/:appKey` |
|
|
148
|
+
| `register(manifest)` | POST | `/api/v1/apps/sync` |
|
|
149
|
+
| `isRegistered(appKey)` | GET | `/api/v1/apps/:appKey` (catches 404) |
|