@docyrus/docyrus 0.0.30 → 0.0.32

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 (30) hide show
  1. package/agent-loader.js +36 -25
  2. package/agent-loader.js.map +4 -4
  3. package/main.js +4 -4
  4. package/main.js.map +1 -1
  5. package/package.json +3 -3
  6. package/resources/pi-agent/extensions/docyrus-web-browser.ts +31 -0
  7. package/resources/pi-agent/shared/docyrusWebBrowserProtocol.ts +169 -0
  8. package/resources/pi-agent/skills/diffity-diff/SKILL.md +1 -1
  9. package/resources/pi-agent/skills/diffity-resolve/SKILL.md +4 -4
  10. package/resources/pi-agent/skills/diffity-review/SKILL.md +5 -4
  11. package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +197 -0
  12. package/resources/pi-agent/skills/docyrus-api-dev/references/acl-endpoints-frontend.md +295 -0
  13. package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +349 -0
  14. package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +298 -0
  15. package/resources/pi-agent/skills/docyrus-api-dev/references/data-source-query-guide.md +2063 -0
  16. package/resources/pi-agent/skills/docyrus-api-dev/references/formula-design-guide-llm.md +312 -0
  17. package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +592 -0
  18. package/resources/pi-agent/skills/docyrus-app-dev-react/SKILL.md +361 -0
  19. package/resources/pi-agent/skills/docyrus-app-dev-react/references/README.md +29 -0
  20. package/resources/pi-agent/skills/docyrus-app-dev-react/references/api-client-and-auth.md +326 -0
  21. package/resources/pi-agent/skills/docyrus-app-dev-react/references/collections-and-patterns.md +353 -0
  22. package/resources/pi-agent/skills/docyrus-app-dev-react/references/component-selection-guide.md +619 -0
  23. package/resources/pi-agent/skills/docyrus-app-dev-react/references/icon-usage-guide.md +463 -0
  24. package/resources/pi-agent/skills/docyrus-app-dev-react/references/preferred-components-catalog.md +242 -0
  25. package/resources/pi-agent/skills/docyrus-platform/SKILL.md +2 -2
  26. package/resources/pi-agent/skills/docyrus-platform/references/auth-and-multi-tenancy.md +9 -1
  27. package/resources/pi-agent/skills/docyrus-platform/references/developer-tools.md +3 -2
  28. package/server-loader.js +328 -87
  29. package/server-loader.js.map +4 -4
  30. package/resources/pi-agent/extensions/multi-edit.ts +0 -835
@@ -0,0 +1,295 @@
1
+ # ACL Endpoints for Frontend Developers
2
+
3
+ Base path: `/api/v1/users/acl`
4
+
5
+ All ACL endpoints require the normal authenticated API session.
6
+
7
+ These endpoints may be hidden from generated Swagger/OpenAPI output because the backend currently marks them with `@ApiExcludeEndpoint()`. Treat this document as the frontend integration source of truth and call these routes directly with `RestApiClient` or `useDocyrusClient()`.
8
+
9
+ ## Important identifier rules
10
+
11
+ - Role assignments and ACL role relations are stored using `tenant_role.uid`.
12
+ - Returned nested role objects expose both `id` and `uid`, and both values map to the role UID.
13
+ - For role operations, backend can resolve incoming `roleId` values against both `tenant_role.uid` and `tenant_role.id`, but frontend apps should prefer role `uid` values from API responses.
14
+ - For user-role writes and role-query `roleIds`, send role UUIDs and prefer UID values.
15
+ - `tenant_role_query.query` is a JSON object matching the app's filter-query structure. Send raw JSON, not stringified JSON.
16
+
17
+ ## Enum values
18
+
19
+ ### Role ownership
20
+
21
+ - `APP`
22
+ - `CUSTOM`
23
+ - `PRODUCT`
24
+ - `SYSTEM`
25
+ - `USER`
26
+
27
+ ### Role query restriction level
28
+
29
+ - `hidden`
30
+ - `read-only`
31
+ - `not-deletable`
32
+
33
+ ## Endpoint groups
34
+
35
+ ### 1) Record ACL endpoints
36
+
37
+ These manage record-level shares, not role CRUD.
38
+
39
+ | Method | Path | Purpose |
40
+ | :----- | :--- | :------ |
41
+ | `GET` | `/v1/users/acl?dataSourceId={uuid}&recordId={uuid}` | Fetch direct and effective ACL rows for a record |
42
+ | `POST` | `/v1/users/acl/share` | Upsert record share rows |
43
+ | `DELETE` | `/v1/users/acl/share` | Revoke matching share rows |
44
+ | `PUT` | `/v1/users/acl/owner` | Transfer record ownership |
45
+
46
+ #### Share payload notes
47
+
48
+ - `principalType` must be one of: `user`, `team`, `role`, `tenant`, `public`.
49
+ - `permissions` is the backend ACL bitmask value.
50
+ - `expiresAt` is optional and must be a valid ISO date if provided.
51
+
52
+ #### Record ACL response shapes
53
+
54
+ `GET /v1/users/acl` returns:
55
+
56
+ ```json
57
+ {
58
+ "direct": [
59
+ {
60
+ "id": "uuid",
61
+ "principal_type": "user",
62
+ "principal_id": "uuid",
63
+ "permissions": 7,
64
+ "expires_at": null,
65
+ "created_by": "uuid-or-null",
66
+ "created_on": "2026-03-29T20:10:00.000Z"
67
+ }
68
+ ],
69
+ "effective": [
70
+ {
71
+ "id": "uuid",
72
+ "user_id": "uuid",
73
+ "permissions": 7,
74
+ "source_principal_type": "role",
75
+ "source_principal_id": "uuid"
76
+ }
77
+ ]
78
+ }
79
+ ```
80
+
81
+ ### 2) Role endpoints
82
+
83
+ | Method | Path | Purpose |
84
+ | :----- | :--- | :------ |
85
+ | `GET` | `/v1/users/acl/roles` | List tenant roles |
86
+ | `GET` | `/v1/users/acl/roles/:roleId` | Get one role |
87
+ | `POST` | `/v1/users/acl/roles` | Create role |
88
+ | `PATCH` | `/v1/users/acl/roles/:roleId` | Partial update role |
89
+ | `DELETE` | `/v1/users/acl/roles/:roleId` | Hard delete role |
90
+
91
+ #### Role create/update rules
92
+
93
+ - `slug` is required on create and must be unique within the tenant.
94
+ - Create defaults:
95
+ - `ownership = "CUSTOM"`
96
+ - `privileges = ""`
97
+ - `status = 1`
98
+ - Role responses include both `id` and `uid`, both pointing to the role UID.
99
+
100
+ #### Role delete side effects
101
+
102
+ Deleting a role also cleans up dependent ACL state:
103
+
104
+ - removes `tenant_user_role` assignments for that role
105
+ - sets `tenant_user.primary_role` to `null` where applicable
106
+ - removes linked `tenant_acl_rule` rows
107
+ - removes linked `tenant_acl_field_rule` rows
108
+ - removes the role from `tenant_role_query` role arrays
109
+ - hard deletes role-query rows that become empty after role removal
110
+
111
+ Frontend implication: after role deletion, refresh role lists, user-role lists, role-query lists, and any UI showing primary-role labels.
112
+
113
+ ### 3) User-role endpoints
114
+
115
+ | Method | Path | Purpose |
116
+ | :----- | :--- | :------ |
117
+ | `GET` | `/v1/users/acl/user-roles` | List assignments across the tenant |
118
+ | `GET` | `/v1/users/acl/users/:userId/roles` | List assignments for one user |
119
+ | `POST` | `/v1/users/acl/users/:userId/roles` | Add roles to a user |
120
+ | `PUT` | `/v1/users/acl/users/:userId/roles` | Replace the full role set for a user |
121
+ | `DELETE` | `/v1/users/acl/users/:userId/roles/:roleId` | Remove one role assignment |
122
+
123
+ #### User-role behavior
124
+
125
+ - `GET /v1/users/acl/user-roles` accepts optional `userId` and `roleId` filters.
126
+ - If `roleId` is supplied, backend resolves it to the canonical role UID first.
127
+ - `POST /users/:userId/roles` is additive.
128
+ - `POST /users/:userId/roles` safely ignores duplicate assignments.
129
+ - `PUT /users/:userId/roles` is a full replacement operation.
130
+ - Sending `roleIds: []` to `PUT /users/:userId/roles` clears all additional roles for that user.
131
+
132
+ ### 4) Role-query endpoints
133
+
134
+ Role queries are role-based filtering rules attached to roles and optionally scoped to a specific data source.
135
+
136
+ | Method | Path | Purpose |
137
+ | :----- | :--- | :------ |
138
+ | `GET` | `/v1/users/acl/role-queries` | List role queries |
139
+ | `GET` | `/v1/users/acl/role-queries/:roleQueryId` | Get one role query |
140
+ | `POST` | `/v1/users/acl/role-queries` | Create role query |
141
+ | `PATCH` | `/v1/users/acl/role-queries/:roleQueryId` | Partial update role query |
142
+ | `DELETE` | `/v1/users/acl/role-queries/:roleQueryId` | Hard delete role query |
143
+
144
+ #### Role-query rules
145
+
146
+ - `roleIds` is required on create and must contain at least one UUID.
147
+ - Stored role IDs are normalized to role UIDs.
148
+ - `query` is required on create and must be a JSON object.
149
+ - `filterChildRelations` defaults to `false`.
150
+ - `restrictionLevel` defaults to `hidden`.
151
+ - If `dataSourceId` is provided, backend derives `tenantAppId` automatically.
152
+ - If `dataSourceId` changes during update, backend recalculates `tenantAppId`.
153
+
154
+ ## Common request examples
155
+
156
+ ### Sync a user's complete role set
157
+
158
+ ```json
159
+ {
160
+ "roleIds": ["role-uid-uuid-1", "role-uid-uuid-2"]
161
+ }
162
+ ```
163
+
164
+ ### Create a role query
165
+
166
+ ```json
167
+ {
168
+ "name": "Hide archived records",
169
+ "dataSourceId": "data-source-uuid",
170
+ "roleIds": ["role-uid-uuid"],
171
+ "query": {
172
+ "condition": "and",
173
+ "filters": []
174
+ },
175
+ "filterChildRelations": false,
176
+ "restrictionLevel": "hidden"
177
+ }
178
+ ```
179
+
180
+ ### Share a record
181
+
182
+ ```json
183
+ {
184
+ "dataSourceId": "uuid",
185
+ "recordId": "uuid",
186
+ "items": [
187
+ {
188
+ "principalType": "user",
189
+ "principalId": "uuid",
190
+ "permissions": 7,
191
+ "expiresAt": "2026-12-31T00:00:00.000Z"
192
+ }
193
+ ]
194
+ }
195
+ ```
196
+
197
+ ## Error expectations
198
+
199
+ Common backend error patterns:
200
+
201
+ - `400 Bad Request`
202
+ - malformed UUID in path, query, or body
203
+ - missing required DTO fields
204
+ - invalid enum values
205
+ - invalid boolean or date format in share payloads
206
+ - `404 Not Found`
207
+ - role not found
208
+ - role query not found
209
+ - tenant user not found
210
+ - tenant data source not found
211
+ - one or more submitted role IDs could not be resolved
212
+ - `409 Conflict`
213
+ - role slug already exists in the tenant
214
+ - `500 Internal Server Error`
215
+ - unexpected persistence failure
216
+
217
+ ## Frontend integration recommendations
218
+
219
+ - Use direct `RestApiClient` calls or `useDocyrusClient()` for ACL work; these routes may not be present in generated OpenAPI or collection layers.
220
+ - Prefer role `uid` values from API responses for future writes and filters.
221
+ - Treat `PUT /users/:userId/roles` as the canonical full-sync endpoint.
222
+ - Treat `POST /users/:userId/roles` as an additive convenience endpoint.
223
+ - Send role-query `query` values as raw JSON objects.
224
+ - Omit `tenantAppId` when sending a role query scoped by `dataSourceId`; backend derives it.
225
+ - After deleting a role, invalidate and refetch role lists, user-role lists, role-query lists, and any dependent role-label UI.
226
+
227
+ ## Suggested TypeScript interfaces
228
+
229
+ ```ts
230
+ export interface IAclRole {
231
+ activitySummaryReportQueryId: string | null;
232
+ createdBy: string | null;
233
+ createdOn: string | null;
234
+ databaseId: string | null;
235
+ disableLogin: number | null;
236
+ id: string;
237
+ lastModifiedBy: string | null;
238
+ lastModifiedOn: string | null;
239
+ name: string;
240
+ ownership: "APP" | "CUSTOM" | "PRODUCT" | "SYSTEM" | "USER";
241
+ privileges: string;
242
+ slug: string;
243
+ status: number | null;
244
+ tenantAppId: string | null;
245
+ uid: string;
246
+ }
247
+
248
+ export interface IAclUserRoleAssignment {
249
+ createdOn: string | null;
250
+ id: string;
251
+ role: {
252
+ databaseId: string | null;
253
+ id: string;
254
+ name: string;
255
+ slug: string;
256
+ uid: string;
257
+ };
258
+ roleId: string;
259
+ status: number | null;
260
+ userId: string;
261
+ }
262
+
263
+ export interface IAclRoleQuery {
264
+ createdBy: string | null;
265
+ createdOn: string | null;
266
+ dataSourceId: string | null;
267
+ filterChildRelations: boolean;
268
+ id: string;
269
+ lastModifiedBy: string | null;
270
+ lastModifiedOn: string | null;
271
+ name: string | null;
272
+ query: Record<string, unknown> | null;
273
+ restrictionLevel: "hidden" | "read-only" | "not-deletable";
274
+ roleIds: string[];
275
+ tenantAppId: string | null;
276
+ }
277
+
278
+ export interface IAclRecordShare {
279
+ id: string;
280
+ principal_type: "user" | "team" | "role" | "tenant" | "public";
281
+ principal_id: string;
282
+ permissions: number;
283
+ expires_at: string | null;
284
+ created_by: string | null;
285
+ created_on: string | null;
286
+ }
287
+
288
+ export interface IAclEffectiveUserAccess {
289
+ id: string;
290
+ user_id: string;
291
+ permissions: number;
292
+ source_principal_type: "user" | "team" | "role" | "tenant" | "public";
293
+ source_principal_id: string;
294
+ }
295
+ ```
@@ -0,0 +1,349 @@
1
+ # @docyrus/api-client Reference
2
+
3
+ ## Table of Contents
4
+
5
+ 1. [RestApiClient](#restapiclient)
6
+ 2. [HTTP Methods](#http-methods)
7
+ 3. [Configuration](#configuration)
8
+ 4. [Token Management](#token-management)
9
+ 5. [OAuth2Client](#oauth2client)
10
+ 6. [Interceptors](#interceptors)
11
+ 7. [Error Handling](#error-handling)
12
+ 8. [Streaming](#streaming)
13
+ 9. [File Operations](#file-operations)
14
+ 10. [Utilities](#utilities)
15
+
16
+ ---
17
+
18
+ ## RestApiClient
19
+
20
+ ```typescript
21
+ import { RestApiClient, MemoryTokenManager } from '@docyrus/api-client'
22
+
23
+ const client = new RestApiClient({
24
+ baseURL: 'https://api.docyrus.com',
25
+ tokenManager: new MemoryTokenManager(),
26
+ timeout: 5000,
27
+ headers: { 'X-API-Version': '1.0' },
28
+ })
29
+ ```
30
+
31
+ ---
32
+
33
+ ## HTTP Methods
34
+
35
+ ```typescript
36
+ // GET with query params
37
+ const users = await client.get<User[]>('/v1/users', { params: { page: 1, limit: 10 } })
38
+
39
+ // POST with body
40
+ const newUser = await client.post<User>('/v1/users', { name: 'John', email: 'john@example.com' })
41
+
42
+ // PATCH (partial update)
43
+ const updated = await client.patch<User>('/v1/users/123', { name: 'Jane' })
44
+
45
+ // PUT (full replace)
46
+ await client.put('/v1/users/123', { name: 'Jane', email: 'jane@example.com' })
47
+
48
+ // DELETE
49
+ await client.delete('/v1/users/123')
50
+
51
+ // DELETE with body
52
+ await client.delete('/v1/items', { recordIds: ['id1', 'id2'] })
53
+ ```
54
+
55
+ ### Typed Responses
56
+
57
+ ```typescript
58
+ interface ApiResponse<T> { data: T; meta: { page: number; total: number } }
59
+ const response = await client.get<ApiResponse<User[]>>('/v1/users')
60
+ const users: User[] = response.data.data
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Configuration
66
+
67
+ ```typescript
68
+ interface ApiClientConfig {
69
+ baseURL?: string // Base URL for all requests
70
+ tokenManager?: TokenManager // Token manager instance
71
+ headers?: Record<string, string> // Default headers
72
+ timeout?: number // Request timeout in ms
73
+ fetch?: typeof fetch // Custom fetch implementation
74
+ FormData?: typeof FormData // Custom FormData
75
+ AbortController?: typeof AbortController
76
+ storage?: Storage // Browser storage for persistence
77
+ }
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Token Management
83
+
84
+ ### MemoryTokenManager (default)
85
+ ```typescript
86
+ import { MemoryTokenManager } from '@docyrus/api-client'
87
+ const tokenManager = new MemoryTokenManager()
88
+ ```
89
+
90
+ ### StorageTokenManager (persistent)
91
+ ```typescript
92
+ import { StorageTokenManager } from '@docyrus/api-client'
93
+ const tokenManager = new StorageTokenManager(localStorage, 'auth_token')
94
+ ```
95
+
96
+ ### AsyncTokenManager (custom)
97
+ ```typescript
98
+ import { AsyncTokenManager } from '@docyrus/api-client'
99
+ const tokenManager = new AsyncTokenManager({
100
+ async getToken() { return await secureStorage.get('token') },
101
+ async setToken(token) { await secureStorage.set('token', token) },
102
+ async clearToken() { await secureStorage.remove('token') },
103
+ })
104
+ ```
105
+
106
+ ### Set Token Directly
107
+ ```typescript
108
+ await client.setAccessToken('your-auth-token')
109
+ ```
110
+
111
+ ---
112
+
113
+ ## OAuth2Client
114
+
115
+ Full OAuth2 support with PKCE, Device Code, and Client Credentials flows.
116
+
117
+ ### Setup
118
+ ```typescript
119
+ import { OAuth2Client, BrowserOAuth2TokenStorage } from '@docyrus/api-client'
120
+
121
+ const oauth2 = new OAuth2Client({
122
+ baseURL: 'https://api.docyrus.com',
123
+ clientId: 'your-client-id',
124
+ clientSecret: 'your-client-secret', // optional for public clients
125
+ redirectUri: 'http://localhost:3000/callback',
126
+ defaultScopes: ['openid', 'offline_access'],
127
+ usePKCE: true, // default: true
128
+ tokenStorage: new BrowserOAuth2TokenStorage(localStorage),
129
+ })
130
+ ```
131
+
132
+ ### Authorization Code Flow (PKCE)
133
+ ```typescript
134
+ // Step 1: Generate auth URL
135
+ const { url, state, codeVerifier } = await oauth2.getAuthorizationUrl({
136
+ scope: 'openid offline_access Users.Read',
137
+ })
138
+
139
+ // Step 2: Redirect user
140
+ window.location.href = url
141
+
142
+ // Step 3: Handle callback
143
+ const tokens = await oauth2.handleCallback(window.location.href)
144
+ // tokens: { accessToken, refreshToken, ... }
145
+ ```
146
+
147
+ ### Client Credentials Flow (server-to-server)
148
+ ```typescript
149
+ const tokens = await oauth2.getClientCredentialsToken({
150
+ scope: 'Read.All',
151
+ delegatedUserId: 'user-id-to-impersonate',
152
+ })
153
+ ```
154
+
155
+ ### Device Code Flow (CLI/headless)
156
+ ```typescript
157
+ const deviceAuth = await oauth2.startDeviceAuthorization('openid offline_access')
158
+ console.log(`Go to: ${deviceAuth.verification_uri}`)
159
+ console.log(`Enter code: ${deviceAuth.user_code}`)
160
+
161
+ const tokens = await oauth2.pollDeviceAuthorization(
162
+ deviceAuth.device_code, deviceAuth.interval, deviceAuth.expires_in,
163
+ { onExpired: () => console.log('Code expired'), signal: abortController.signal },
164
+ )
165
+ ```
166
+
167
+ ### Token Operations
168
+ ```typescript
169
+ const tokens = await oauth2.getTokens()
170
+ const isExpired = await oauth2.isTokenExpired()
171
+ const accessToken = await oauth2.getValidAccessToken() // auto-refreshes
172
+ const newTokens = await oauth2.refreshAccessToken()
173
+ await oauth2.revokeToken(tokens.refreshToken)
174
+ const tokenInfo = await oauth2.introspectToken(tokens.accessToken)
175
+ await oauth2.logout()
176
+ ```
177
+
178
+ ### Integrate OAuth2 with RestApiClient
179
+ ```typescript
180
+ import { RestApiClient, OAuth2Client, OAuth2TokenManagerAdapter, BrowserOAuth2TokenStorage } from '@docyrus/api-client'
181
+
182
+ const tokenStorage = new BrowserOAuth2TokenStorage(localStorage)
183
+ const oauth2 = new OAuth2Client({ baseURL: 'https://api.docyrus.com', clientId: 'id', tokenStorage })
184
+
185
+ const tokenManager = new OAuth2TokenManagerAdapter(tokenStorage, async () => {
186
+ const tokens = await oauth2.refreshAccessToken()
187
+ return tokens.accessToken
188
+ })
189
+
190
+ const apiClient = new RestApiClient({ baseURL: 'https://api.docyrus.com', tokenManager })
191
+ ```
192
+
193
+ ### Rate Limit Check
194
+ ```typescript
195
+ const rateLimit = await oauth2.checkRateLimit()
196
+ // { remaining, limit, reset }
197
+ ```
198
+
199
+ ### PKCE Utilities
200
+ ```typescript
201
+ import { generatePKCEChallenge, generateCodeVerifier, generateCodeChallenge, generateState, generateNonce } from '@docyrus/api-client'
202
+
203
+ const pkce = await generatePKCEChallenge()
204
+ // { codeVerifier, codeChallenge, codeChallengeMethod: 'S256' }
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Interceptors
210
+
211
+ ```typescript
212
+ client.use({
213
+ // Transform outgoing requests
214
+ async request(config) {
215
+ config.headers = { ...config.headers, 'X-Request-Time': new Date().toISOString() }
216
+ return config
217
+ },
218
+ // Transform incoming responses
219
+ async response(response, request) {
220
+ console.log(`${request.url} took ${Date.now() - request.timestamp}ms`)
221
+ return response
222
+ },
223
+ // Handle errors globally
224
+ async error(error, request, response) {
225
+ if (error.status === 401) { await refreshToken() }
226
+ return { error, request, response }
227
+ },
228
+ })
229
+ ```
230
+
231
+ ### Common Interceptor: Unwrap Response Data
232
+ ```typescript
233
+ client.use({
234
+ response: (response) => {
235
+ if (response.data?.data && typeof response.data === 'object' && !Array.isArray(response.data)) {
236
+ response.data = response.data.data
237
+ }
238
+ return response
239
+ },
240
+ })
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Error Handling
246
+
247
+ ```typescript
248
+ import {
249
+ ApiError, NetworkError, TimeoutError,
250
+ AuthenticationError, // 401
251
+ AuthorizationError, // 403
252
+ NotFoundError, // 404
253
+ RateLimitError, // 429 — has error.retryAfter
254
+ ValidationError,
255
+ // OAuth2-specific
256
+ OAuth2Error, InvalidGrantError, InvalidClientError,
257
+ AccessDeniedError, ExpiredTokenError, AuthorizationPendingError,
258
+ } from '@docyrus/api-client'
259
+
260
+ try {
261
+ await client.get('/resource')
262
+ } catch (error) {
263
+ if (error instanceof AuthenticationError) { /* re-login */ }
264
+ else if (error instanceof AuthorizationError) { /* forbidden */ }
265
+ else if (error instanceof NotFoundError) { /* 404 */ }
266
+ else if (error instanceof RateLimitError) { /* retry after error.retryAfter */ }
267
+ else if (error instanceof NetworkError) { /* offline */ }
268
+ else if (error instanceof TimeoutError) { /* timed out */ }
269
+ }
270
+ ```
271
+
272
+ ---
273
+
274
+ ## Streaming
275
+
276
+ ### Server-Sent Events (SSE)
277
+ ```typescript
278
+ const eventSource = client.sse('/events', {
279
+ onMessage(data) { console.log('Received:', data) },
280
+ onError(error) { console.error(error) },
281
+ onComplete() { console.log('Stream completed') },
282
+ })
283
+ eventSource.close()
284
+ ```
285
+
286
+ ### Chunked Streaming
287
+ ```typescript
288
+ for await (const chunk of client.stream('/stream', {
289
+ method: 'POST',
290
+ body: { query: 'stream data' },
291
+ })) {
292
+ console.log('Chunk:', chunk)
293
+ }
294
+ ```
295
+
296
+ ---
297
+
298
+ ## File Operations
299
+
300
+ ### Upload
301
+ ```typescript
302
+ const formData = new FormData()
303
+ formData.append('file', fileInput.files[0])
304
+ formData.append('description', 'My file')
305
+ await client.post('/upload', formData)
306
+ ```
307
+
308
+ ### Download
309
+ ```typescript
310
+ const response = await client.get('/download/file.pdf', { responseType: 'blob' })
311
+ const url = URL.createObjectURL(response.data)
312
+ const link = document.createElement('a')
313
+ link.href = url
314
+ link.download = 'file.pdf'
315
+ link.click()
316
+ ```
317
+
318
+ ### HTML to PDF
319
+ ```typescript
320
+ await client.html2pdf({
321
+ html: '<html><body>Content</body></html>',
322
+ // or: url: 'https://example.com',
323
+ options: { format: 'A4', margin: { top: 10, bottom: 10, left: 10, right: 10 }, landscape: false },
324
+ })
325
+ ```
326
+
327
+ ### Custom Query/Report
328
+ ```typescript
329
+ const results = await client.runCustomQuery(customQueryId, options)
330
+ // PUT reports/runCustomQuery/:customQueryId
331
+ ```
332
+
333
+ ---
334
+
335
+ ## Utilities
336
+
337
+ ```typescript
338
+ import { buildUrl, isAbortError, parseContentDisposition, createAbortSignal, jsonToQueryString, withRetry } from '@docyrus/api-client'
339
+
340
+ const url = buildUrl('/api/users', { page: 1, limit: 10 })
341
+ // '/api/users?page=1&limit=10'
342
+
343
+ const signal = createAbortSignal(5000) // 5s timeout
344
+
345
+ const response = await withRetry(() => client.get('/flaky'), {
346
+ retries: 3, retryDelay: 1000,
347
+ retryCondition: (error) => error.status >= 500,
348
+ })
349
+ ```