@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,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` |