@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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +287 -0
  3. package/dist/browser-session.d.mts +12 -0
  4. package/dist/browser-session.d.ts +12 -0
  5. package/dist/browser-session.js +1812 -0
  6. package/dist/browser-session.mjs +28 -0
  7. package/dist/browser.d.mts +46 -0
  8. package/dist/browser.d.ts +46 -0
  9. package/dist/browser.js +768 -0
  10. package/dist/browser.mjs +47 -0
  11. package/dist/chunk-5HF3OBNO.mjs +189 -0
  12. package/dist/chunk-5WFR6Y33.mjs +59 -0
  13. package/dist/chunk-6I6RM4MN.mjs +51 -0
  14. package/dist/chunk-73R6BEGO.mjs +176 -0
  15. package/dist/chunk-E46DKOVI.mjs +632 -0
  16. package/dist/chunk-JQWYIIIS.mjs +1740 -0
  17. package/dist/chunk-X3K3WOBR.mjs +64 -0
  18. package/dist/chunk-Y6FXYEAI.mjs +10 -0
  19. package/dist/cli/index.d.mts +1 -0
  20. package/dist/cli/index.d.ts +1 -0
  21. package/dist/cli/index.js +581 -0
  22. package/dist/cli/index.mjs +57 -0
  23. package/dist/client-C1DXfB8Z.d.mts +911 -0
  24. package/dist/client-CggvJmmm.d.ts +911 -0
  25. package/dist/dev-FUTJZSWN.mjs +56 -0
  26. package/dist/doctor-OHJRZBBT.mjs +89 -0
  27. package/dist/errors-CDdl24MP.d.mts +52 -0
  28. package/dist/errors-CDdl24MP.d.ts +52 -0
  29. package/dist/express-BKAXB5Nl.d.ts +61 -0
  30. package/dist/express-CpfyYTmw.d.mts +61 -0
  31. package/dist/express.d.mts +45 -0
  32. package/dist/express.d.ts +45 -0
  33. package/dist/express.js +2252 -0
  34. package/dist/express.mjs +122 -0
  35. package/dist/fastify.d.mts +23 -0
  36. package/dist/fastify.d.ts +23 -0
  37. package/dist/fastify.js +2062 -0
  38. package/dist/fastify.mjs +118 -0
  39. package/dist/hono.d.mts +22 -0
  40. package/dist/hono.d.ts +22 -0
  41. package/dist/hono.js +2051 -0
  42. package/dist/hono.mjs +107 -0
  43. package/dist/index.d.mts +6 -0
  44. package/dist/index.d.ts +6 -0
  45. package/dist/index.js +2070 -0
  46. package/dist/index.mjs +83 -0
  47. package/dist/init-LLCSQGNL.mjs +198 -0
  48. package/dist/keys-NLWFAOEM.mjs +63 -0
  49. package/dist/mobile.d.mts +11 -0
  50. package/dist/mobile.d.ts +11 -0
  51. package/dist/mobile.js +1809 -0
  52. package/dist/mobile.mjs +25 -0
  53. package/dist/next.d.mts +37 -0
  54. package/dist/next.d.ts +37 -0
  55. package/dist/next.js +2078 -0
  56. package/dist/next.mjs +130 -0
  57. package/dist/publishableKey-B5DIK81A.d.mts +24 -0
  58. package/dist/publishableKey-B5DIK81A.d.ts +24 -0
  59. package/dist/react.d.mts +196 -0
  60. package/dist/react.d.ts +196 -0
  61. package/dist/react.js +1457 -0
  62. package/dist/react.mjs +787 -0
  63. package/dist/server/handlers.d.mts +96 -0
  64. package/dist/server/handlers.d.ts +96 -0
  65. package/dist/server/handlers.js +243 -0
  66. package/dist/server/handlers.mjs +14 -0
  67. package/dist/server.d.mts +14 -0
  68. package/dist/server.d.ts +14 -0
  69. package/dist/server.js +2195 -0
  70. package/dist/server.mjs +47 -0
  71. package/dist/service.d.mts +11 -0
  72. package/dist/service.d.ts +11 -0
  73. package/dist/service.js +1809 -0
  74. package/dist/service.mjs +25 -0
  75. package/dist/signIn-C8f6qVjD.d.mts +238 -0
  76. package/dist/signIn-Cy2lbEXb.d.ts +238 -0
  77. package/dist/types-Cxl3bQHt.d.mts +900 -0
  78. package/dist/types-Cxl3bQHt.d.ts +900 -0
  79. package/docs/APP_INTEGRATION_MATRIX.md +59 -0
  80. package/docs/BROWSER_SESSION_MIGRATION.md +69 -0
  81. package/docs/FRESH_IMPLEMENTATION_GUIDE.md +188 -0
  82. package/docs/TARBALL_RELEASE_WORKFLOW.md +98 -0
  83. package/docs/V1_TO_V2_UPGRADE_GUIDE.md +318 -0
  84. package/docs/guides/api-keys.md +130 -0
  85. package/docs/guides/app-registration.md +149 -0
  86. package/docs/guides/auth-flows.md +168 -0
  87. package/docs/guides/branding.md +160 -0
  88. package/docs/guides/entitlements.md +115 -0
  89. package/docs/guides/entity-hierarchy.md +200 -0
  90. package/docs/guides/error-handling.md +251 -0
  91. package/docs/guides/gdpr-compliance.md +123 -0
  92. package/docs/guides/invitations.md +143 -0
  93. package/docs/guides/mfa-enrollment.md +170 -0
  94. package/docs/guides/middleware-reference.md +205 -0
  95. package/docs/guides/mobile-native.md +110 -0
  96. package/docs/guides/roles-and-permissions.md +220 -0
  97. package/docs/guides/scoped-authorization.md +247 -0
  98. package/docs/guides/server-platform-integration.md +52 -0
  99. package/docs/guides/service-automation-integration.md +36 -0
  100. package/docs/guides/session-management.md +97 -0
  101. package/docs/guides/tenant-management.md +216 -0
  102. package/docs/guides/token-verification.md +178 -0
  103. package/docs/guides/user-management.md +184 -0
  104. package/docs/guides/webhooks.md +136 -0
  105. package/docs/integration-prompts/README.md +20 -0
  106. package/docs/integration-prompts/first-party-browser-app.md +29 -0
  107. package/docs/integration-prompts/install-from-tarball.md +41 -0
  108. package/docs/integration-prompts/migrate-from-local-packages-source.md +57 -0
  109. package/docs/integration-prompts/native-mobile-app.md +24 -0
  110. package/docs/integration-prompts/server-platform-app.md +20 -0
  111. package/docs/integration-prompts/service-automation-app.md +20 -0
  112. 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) |