@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.
- package/agent-loader.js +36 -25
- package/agent-loader.js.map +4 -4
- package/main.js +4 -4
- package/main.js.map +1 -1
- package/package.json +3 -3
- package/resources/pi-agent/extensions/docyrus-web-browser.ts +31 -0
- package/resources/pi-agent/shared/docyrusWebBrowserProtocol.ts +169 -0
- package/resources/pi-agent/skills/diffity-diff/SKILL.md +1 -1
- package/resources/pi-agent/skills/diffity-resolve/SKILL.md +4 -4
- package/resources/pi-agent/skills/diffity-review/SKILL.md +5 -4
- package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +197 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/acl-endpoints-frontend.md +295 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +349 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +298 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/data-source-query-guide.md +2063 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/formula-design-guide-llm.md +312 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +592 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/SKILL.md +361 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/README.md +29 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/api-client-and-auth.md +326 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/collections-and-patterns.md +353 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/component-selection-guide.md +619 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/icon-usage-guide.md +463 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/preferred-components-catalog.md +242 -0
- package/resources/pi-agent/skills/docyrus-platform/SKILL.md +2 -2
- package/resources/pi-agent/skills/docyrus-platform/references/auth-and-multi-tenancy.md +9 -1
- package/resources/pi-agent/skills/docyrus-platform/references/developer-tools.md +3 -2
- package/server-loader.js +328 -87
- package/server-loader.js.map +4 -4
- 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
|
+
```
|