@docyrus/docyrus 0.0.31 → 0.0.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docyrus/docyrus",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "private": false,
5
5
  "description": "Docyrus API CLI",
6
6
  "main": "./main.js",
@@ -0,0 +1,31 @@
1
+ import { isToolCallEventType, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { buildDocyrusWebBrowserProtocolInstructions } from "../shared/docyrusWebBrowserProtocol";
3
+
4
+ const DOCYRUS_CHROME_COMMAND_PATTERN = /\bdocyrus\b(?:\s+-g|\s+--global)?\s+chrome\b/;
5
+ const DOCYRUS_CHROME_BLOCK_REASON =
6
+ "Docyrus Server sessions must use the docyrus-web-browser preview tools instead of `docyrus chrome`.";
7
+
8
+ export default function docyrusWebBrowserExtension(pi: ExtensionAPI) {
9
+ pi.on("before_agent_start", async(event) => {
10
+ const protocolInstructions = buildDocyrusWebBrowserProtocolInstructions();
11
+ return {
12
+ systemPrompt: [event.systemPrompt, protocolInstructions].filter(Boolean).join("\n\n"),
13
+ };
14
+ });
15
+
16
+ pi.on("tool_call", async(event) => {
17
+ if (!isToolCallEventType("bash", event)) {
18
+ return;
19
+ }
20
+
21
+ const command = typeof event.input.command === "string" ? event.input.command.trim() : "";
22
+ if (!command || !DOCYRUS_CHROME_COMMAND_PATTERN.test(command)) {
23
+ return;
24
+ }
25
+
26
+ return {
27
+ block: true,
28
+ reason: DOCYRUS_CHROME_BLOCK_REASON,
29
+ };
30
+ });
31
+ }
@@ -0,0 +1,169 @@
1
+ export const DOCYRUS_WEB_BROWSER_EXTENSION_NAME = "docyrus-web-browser";
2
+ export const DOCYRUS_WEB_BROWSER_TAG = "docyrus_web_browser";
3
+ export const DOCYRUS_WEB_BROWSER_OPEN = `<${DOCYRUS_WEB_BROWSER_TAG}>`;
4
+ export const DOCYRUS_WEB_BROWSER_CLOSE = `</${DOCYRUS_WEB_BROWSER_TAG}>`;
5
+ export const DOCYRUS_WEB_BROWSER_RESULT_TAG = "docyrus_web_browser_result";
6
+ export const DOCYRUS_WEB_BROWSER_RESULT_OPEN = `<${DOCYRUS_WEB_BROWSER_RESULT_TAG}>`;
7
+ export const DOCYRUS_WEB_BROWSER_RESULT_CLOSE = `</${DOCYRUS_WEB_BROWSER_RESULT_TAG}>`;
8
+ export const WEB_PREVIEW_CONTEXT_TOOL = "web_preview_context";
9
+ export const WEB_PREVIEW_PLAYWRIGHT_TOOL = "web_preview_playwright";
10
+
11
+ export const DOCYRUS_WEB_BROWSER_TOOL_NAMES = [
12
+ WEB_PREVIEW_CONTEXT_TOOL,
13
+ WEB_PREVIEW_PLAYWRIGHT_TOOL,
14
+ ] as const;
15
+
16
+ export type IDocyrusWebBrowserToolName = typeof DOCYRUS_WEB_BROWSER_TOOL_NAMES[number];
17
+
18
+ export interface IDocyrusWebBrowserToolRequest {
19
+ tool: IDocyrusWebBrowserToolName;
20
+ input?: Record<string, unknown>;
21
+ }
22
+
23
+ export interface IDocyrusWebBrowserToolResponsePrompt {
24
+ tool: IDocyrusWebBrowserToolName;
25
+ request?: Record<string, unknown>;
26
+ status: "success" | "error";
27
+ output?: unknown;
28
+ errorText?: string;
29
+ }
30
+
31
+ export interface IDocyrusWebBrowserClientToolInfo {
32
+ name: IDocyrusWebBrowserToolName;
33
+ description: string;
34
+ inputSchema?: Record<string, unknown>;
35
+ }
36
+
37
+ export const DOCYRUS_WEB_BROWSER_CLIENT_TOOLS: IDocyrusWebBrowserClientToolInfo[] = [
38
+ {
39
+ name: WEB_PREVIEW_CONTEXT_TOOL,
40
+ description:
41
+ "Inspect the current Docyrus web preview state before automation. Prefer this first when preview availability or bridge state is unknown.",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ includeSnapshot: { type: "boolean" },
46
+ },
47
+ additionalProperties: false,
48
+ },
49
+ },
50
+ {
51
+ name: WEB_PREVIEW_PLAYWRIGHT_TOOL,
52
+ description:
53
+ "Run Playwright-style actions against the Docyrus web preview. Prefer structured steps over raw scripts.",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {
57
+ script: { type: "string" },
58
+ steps: {
59
+ type: "array",
60
+ items: {
61
+ type: "object",
62
+ properties: {
63
+ action: { type: "string" },
64
+ selector: { type: "string" },
65
+ url: { type: "string" },
66
+ value: { type: "string" },
67
+ values: {
68
+ type: "array",
69
+ items: { type: "string" },
70
+ },
71
+ key: { type: "string" },
72
+ attribute: { type: "string" },
73
+ timeoutMs: { type: "number" },
74
+ state: { type: "string" },
75
+ },
76
+ additionalProperties: true,
77
+ },
78
+ },
79
+ timeoutMs: { type: "number" },
80
+ stopOnError: { type: "boolean" },
81
+ },
82
+ additionalProperties: true,
83
+ },
84
+ },
85
+ ];
86
+
87
+ function hashString(value: string): string {
88
+ let hash = 0;
89
+ for (let index = 0; index < value.length; index += 1) {
90
+ hash = ((hash << 5) - hash) + value.charCodeAt(index);
91
+ hash |= 0;
92
+ }
93
+ return Math.abs(hash).toString(36);
94
+ }
95
+
96
+ function isRecord(value: unknown): value is Record<string, unknown> {
97
+ return typeof value === "object" && value !== null && !Array.isArray(value);
98
+ }
99
+
100
+ export function isDocyrusWebBrowserToolName(value: unknown): value is IDocyrusWebBrowserToolName {
101
+ return DOCYRUS_WEB_BROWSER_TOOL_NAMES.some((toolName) => toolName === value);
102
+ }
103
+
104
+ export function normalizeDocyrusWebBrowserToolRequest(value: unknown): IDocyrusWebBrowserToolRequest | undefined {
105
+ if (!isRecord(value) || !isDocyrusWebBrowserToolName(value.tool)) {
106
+ return undefined;
107
+ }
108
+
109
+ return {
110
+ tool: value.tool,
111
+ input: isRecord(value.input) ? value.input : undefined,
112
+ };
113
+ }
114
+
115
+ export function serializeDocyrusWebBrowserToolRequest(request: IDocyrusWebBrowserToolRequest): string {
116
+ return `${DOCYRUS_WEB_BROWSER_OPEN}\n${JSON.stringify(request, null, 2)}\n${DOCYRUS_WEB_BROWSER_CLOSE}`;
117
+ }
118
+
119
+ export function createDocyrusWebBrowserToolCallId(request: IDocyrusWebBrowserToolRequest): string {
120
+ return `docyrus_web_browser_${hashString(JSON.stringify(request))}`;
121
+ }
122
+
123
+ export function parseDocyrusWebBrowserRequestFromText(text: string): IDocyrusWebBrowserToolRequest | undefined {
124
+ const trimmed = text.trim();
125
+ if (!trimmed.startsWith(DOCYRUS_WEB_BROWSER_OPEN) || !trimmed.endsWith(DOCYRUS_WEB_BROWSER_CLOSE)) {
126
+ return undefined;
127
+ }
128
+
129
+ const body = trimmed.slice(DOCYRUS_WEB_BROWSER_OPEN.length, trimmed.length - DOCYRUS_WEB_BROWSER_CLOSE.length).trim();
130
+ if (!body) {
131
+ return undefined;
132
+ }
133
+
134
+ try {
135
+ return normalizeDocyrusWebBrowserToolRequest(JSON.parse(body));
136
+ } catch {
137
+ return undefined;
138
+ }
139
+ }
140
+
141
+ export function buildDocyrusWebBrowserProtocolInstructions(): string {
142
+ return [
143
+ "When you need to inspect or automate the Docyrus web preview in server-backed sessions, use the docyrus-web-browser client tools.",
144
+ "Do not use `docyrus chrome` or any visible Chrome DevTools workflow in this environment.",
145
+ `Instead, output only a single ${DOCYRUS_WEB_BROWSER_OPEN}...${DOCYRUS_WEB_BROWSER_CLOSE} block and nothing else in the assistant message.`,
146
+ "Inside that block, emit strict JSON with this shape:",
147
+ "{\"tool\":\"web_preview_context\",\"input\":{\"includeSnapshot\":true}}",
148
+ "or",
149
+ "{\"tool\":\"web_preview_playwright\",\"input\":{\"steps\":[{\"action\":\"goto\",\"url\":\"/login\"},{\"action\":\"fill\",\"selector\":\"input[name='email']\",\"value\":\"demo@example.com\"}],\"timeoutMs\":10000,\"stopOnError\":true}}",
150
+ "Call `web_preview_context` first when preview state is unknown.",
151
+ "Prefer `steps` over `script` for `web_preview_playwright`.",
152
+ "Supported playwright step actions are: goto, click, dblclick, hover, fill, press, select, check, uncheck, waitForSelector, waitForTimeout, textContent, getAttribute, title, url, snapshot.",
153
+ "If the tool reports that the preview is unavailable or blocked by bridge/cross-origin constraints, stop and explain the exact blocker to the user.",
154
+ ].join("\n");
155
+ }
156
+
157
+ export function formatDocyrusWebBrowserToolResponsePrompt(
158
+ response: IDocyrusWebBrowserToolResponsePrompt,
159
+ ): string {
160
+ return [
161
+ "The docyrus-web-browser client tool returned a result.",
162
+ "",
163
+ `${DOCYRUS_WEB_BROWSER_RESULT_OPEN}`,
164
+ JSON.stringify(response, null, 2),
165
+ `${DOCYRUS_WEB_BROWSER_RESULT_CLOSE}`,
166
+ "",
167
+ "Continue the task using this result. If the tool reported an availability or bridge blocker, explain that blocker exactly and do not claim success.",
168
+ ].join("\n");
169
+ }
@@ -14,7 +14,7 @@ You are opening the diffity diff viewer so the user can see their changes in the
14
14
 
15
15
  ## Instructions
16
16
 
17
- 1. Check that `diffity` is available: run `which diffity`. If not found, tell the user to restart `docyrus coder` so the launcher can install the managed `diffity` CLI.
17
+ 1. Check that `diffity` is available: run `which diffity`. If not found, install it with `npm install -g diffity`.
18
18
  2. Run `diffity <ref>` (or just `diffity` if no ref) using the Bash tool with `run_in_background: true`:
19
19
  - The CLI handles everything: if an instance is already running for this repo it reuses it and opens the browser, otherwise it starts a new server and opens the browser.
20
20
  - Do NOT use `&` or `--quiet` — let the Bash tool handle backgrounding.
@@ -15,6 +15,7 @@ You are reading open review comments and resolving them by making the requested
15
15
  ## CLI Reference
16
16
 
17
17
  ```
18
+ diffity agent diff
18
19
  diffity agent list [--status open|resolved|dismissed] [--json]
19
20
  diffity agent comment --file <path> --line <n> [--end-line <n>] [--side new|old] --body "<text>"
20
21
  diffity agent general-comment --body "<text>"
@@ -31,7 +32,7 @@ diffity agent reply <id> --body "<text>"
31
32
 
32
33
  ## Prerequisites
33
34
 
34
- 1. Check that `diffity` is available: run `which diffity`. If not found, tell the user to restart `docyrus coder` so the launcher can install the managed `diffity` CLI.
35
+ 1. Check that `diffity` is available: run `which diffity`. If not found, install it with `npm install -g diffity`.
35
36
  2. Check that a review session exists: run `diffity agent list`. If this fails with "No active review session", tell the user to start diffity first (e.g. `diffity` or **/diffity-diff**).
36
37
 
37
38
  ## Instructions
@@ -46,11 +47,10 @@ diffity agent reply <id> --body "<text>"
46
47
  a. **Skip** general comments (filePath `__general__`) — these are summaries, not actionable code changes.
47
48
  b. **Skip** threads where the last comment is an agent reply that asks the user a question (e.g. "Could you clarify...?") and the user hasn't responded yet — the agent is waiting for user input. Still process threads where the agent left the original comment (code suggestion, review feedback, etc.) — those are actionable.
48
49
  c. **`[nit]` comments** — these are minor suggestions but still actionable. Resolve them like any other comment.
49
- d. **`[question]` comments** (from the user) — read the question, examine the relevant code, and reply with an answer:
50
+ d. **`[question]` comments** (from the user) — read the question, examine the relevant code, and resolve the thread with your answer as the summary:
50
51
  ```
51
- diffity agent reply <thread-id> --body "Your answer here"
52
+ diffity agent resolve <thread-id> --summary "Your answer here"
52
53
  ```
53
- Then resolve the thread with a summary of your answer.
54
54
  e. Comments phrased as questions without an explicit `[question]` tag (e.g. "should we add X?" or "can we rename this?") are suggestions — treat them as actionable requests and make the change.
55
55
  f. Read the comment body from the JSON output and understand what change is requested. Interpret the intent:
56
56
  - If the comment suggests a code change, make the change.
@@ -16,6 +16,7 @@ You are reviewing a diff and leaving inline comments using the `diffity agent` C
16
16
  ## CLI Reference
17
17
 
18
18
  ```
19
+ diffity agent diff
19
20
  diffity agent list [--status open|resolved|dismissed] [--json]
20
21
  diffity agent comment --file <path> --line <n> [--end-line <n>] [--side new|old] --body "<text>"
21
22
  diffity agent general-comment --body "<text>"
@@ -32,7 +33,7 @@ diffity agent reply <id> --body "<text>"
32
33
 
33
34
  ## Prerequisites
34
35
 
35
- 1. Check that `diffity` is available: run `which diffity`. If not found, tell the user to restart `docyrus coder` so the launcher can install the managed `diffity` CLI.
36
+ 1. Check that `diffity` is available: run `which diffity`. If not found, install it with `npm install -g diffity`.
36
37
 
37
38
  ## Instructions
38
39
 
@@ -53,11 +54,11 @@ The review needs a running session whose ref matches the requested ref. A ref mi
53
54
 
54
55
  ### Step 2: Review the diff
55
56
 
56
- 1. **Get the resolved diff args from diffity's API**, then run `git diff` yourself do NOT construct the diff ref manually, as diffity uses merge-base resolution:
57
+ 1. **Get the unified diff** directly from diffity — this handles merge-base resolution, untracked files, and all ref types automatically:
57
58
  ```
58
- curl -s 'http://localhost:<port>/api/diff/ref?ref=<ref>'
59
+ diffity agent diff
59
60
  ```
60
- If no ref was provided, omit the `ref` query parameter. The response is JSON with an `args` field (e.g. `"abc123def"`). Run `git diff <args>` to get the unified diff. Line numbers are in the `@@` hunk headers.
61
+ This outputs the full unified diff for the current session. Line numbers are in the `@@` hunk headers.
61
62
  2. Find and read all relevant CLAUDE.md files — the root CLAUDE.md and any CLAUDE.md files in directories containing modified files. These define project-specific rules that the diff must follow.
62
63
 
63
64
  #### Understand the change before reviewing it
@@ -89,6 +89,36 @@ GET /v1/users/me — Current user profile
89
89
  PATCH /v1/users/me — Update current user
90
90
  ```
91
91
 
92
+ ### ACL / Role Management Endpoints
93
+ ```
94
+ GET /v1/users/acl?dataSourceId={uuid}&recordId={uuid} — Read record ACL rows
95
+ POST /v1/users/acl/share — Upsert record shares
96
+ DELETE /v1/users/acl/share — Revoke record shares
97
+ PUT /v1/users/acl/owner — Transfer record ownership
98
+
99
+ GET /v1/users/acl/roles — List roles
100
+ GET /v1/users/acl/roles/{roleId} — Get one role
101
+ POST /v1/users/acl/roles — Create role
102
+ PATCH /v1/users/acl/roles/{roleId} — Update role
103
+ DELETE /v1/users/acl/roles/{roleId} — Delete role
104
+
105
+ GET /v1/users/acl/user-roles — List user-role assignments
106
+ GET /v1/users/acl/users/{userId}/roles — List one user's roles
107
+ POST /v1/users/acl/users/{userId}/roles — Add roles to a user
108
+ PUT /v1/users/acl/users/{userId}/roles — Replace a user's full role set
109
+ DELETE /v1/users/acl/users/{userId}/roles/{roleId} — Remove one role assignment
110
+
111
+ GET /v1/users/acl/role-queries — List role queries
112
+ GET /v1/users/acl/role-queries/{roleQueryId} — Get one role query
113
+ POST /v1/users/acl/role-queries — Create role query
114
+ PATCH /v1/users/acl/role-queries/{roleQueryId} — Update role query
115
+ DELETE /v1/users/acl/role-queries/{roleQueryId} — Delete role query
116
+ ```
117
+
118
+ ACL routes require the normal authenticated API session, but they may not appear in generated Swagger/OpenAPI output because the backend currently excludes them from public docs. Integrate them with direct `RestApiClient` calls when you need record sharing, role CRUD, user-role assignment management, or role-query management.
119
+
120
+ For all ACL role operations, prefer using role `uid` values returned by the API. Nested role objects expose both `id` and `uid`, and both map to the role UID value.
121
+
92
122
  ### Making API Calls
93
123
 
94
124
  ```typescript
@@ -150,6 +180,11 @@ The GET items endpoint accepts a powerful query payload:
150
180
  4. **Child query keys must appear in `columns`** — if childQuery key is `orders`, include `orders` in columns.
151
181
  5. **Formula keys must appear in `columns`** — if formula key is `total`, include `total` in columns.
152
182
  6. **Filter by related field** using `rel_{{relation_field}}/{{field}}` syntax.
183
+ 7. **ACL routes may be hidden from generated OpenAPI** — call them directly via `RestApiClient` instead of expecting generated collection support.
184
+ 8. **Prefer role `uid` values** for ACL role writes, user-role `roleIds`, and role-query `roleIds`.
185
+ 9. **Treat `PUT /v1/users/acl/users/:userId/roles` as full replacement** and `POST /v1/users/acl/users/:userId/roles` as additive.
186
+ 10. **Send role-query `query` as raw JSON** and let backend derive `tenantAppId` from `dataSourceId` when applicable.
187
+ 11. **After deleting a role, refresh dependent ACL state** — role lists, user-role lists, role-query lists, and any UI showing primary-role labels.
153
188
 
154
189
  ## References
155
190
 
@@ -159,3 +194,4 @@ Read these files when you need detailed information:
159
194
  - **`references/authentication.md`** — @docyrus/signin React provider, useDocyrusAuth/useDocyrusClient hooks, hasRole/hasPermission authorization helpers, SignInButton, standalone vs iframe auth modes, env vars, API client access pattern
160
195
  - **`references/data-source-query-guide.md`** — Up-to-date query payload guide: columns, filters, orderBy, pagination, calculations, formulas, child queries, pivots, and operator reference
161
196
  - **`references/formula-design-guide-llm.md`** — Up-to-date formula design guide for building and validating `formulas` payloads
197
+ - **`references/acl-endpoints-frontend.md`** — Hidden ACL endpoint reference covering record sharing, roles, user-role assignment flows, role queries, identifier rules, and expected frontend integration behavior
@@ -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
+ ```
@@ -255,19 +255,16 @@ createRoot(document.getElementById('root')!).render(
255
255
  import { useDocyrusAuth, useDocyrusClient, SignInButton } from '@docyrus/signin'
256
256
 
257
257
  function App() {
258
- const { status, user, hasRole, hasPermission, signOut } = useDocyrusAuth()
258
+ const { status, signOut } = useDocyrusAuth()
259
259
  const client = useDocyrusClient()
260
260
 
261
261
  if (status === 'loading') return <div>Loading...</div>
262
262
  if (status === 'unauthenticated') return <SignInButton />
263
- if (!user) return <div>Loading profile...</div>
264
263
 
265
264
  // client is guaranteed non-null when authenticated
266
265
  return (
267
266
  <div>
268
267
  <p>Authenticated!</p>
269
- {hasRole('super_admin') && <p>Admin mode enabled</p>}
270
- {hasPermission('view', 'projects-data-source-id') && <p>Can view projects</p>}
271
268
  <button onClick={() => client!.get('/v1/users/me').then(console.log)}>My Profile</button>
272
269
  <button onClick={signOut}>Sign Out</button>
273
270
  </div>
@@ -292,8 +289,10 @@ const data = await client!.get('/v1/custom-endpoint')
292
289
 
293
290
  ## Advanced Usage
294
291
 
295
- Core classes exported for advanced scenarios:
292
+ Core classes and permission functions exported for advanced scenarios:
296
293
 
297
294
  ```typescript
298
295
  import { AuthManager, StandaloneOAuth2Auth, IframeAuth, detectAuthMode } from '@docyrus/signin'
296
+ import { hasRole, hasPermission, getAllRoles } from '@docyrus/signin/core'
297
+ import type { DocyrusUser, DocyrusRole, DocyrusAclRule, AclOperation, PermissionConfig } from '@docyrus/signin/core'
299
298
  ```