@docyrus/docyrus 0.0.31 → 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 +2 -2
- package/main.js.map +1 -1
- package/package.json +1 -1
- 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 +36 -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/authentication.md +4 -5
- package/resources/pi-agent/skills/docyrus-app-dev-react/SKILL.md +112 -85
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/README.md +1 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/collections-and-patterns.md +1 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/component-selection-guide.md +27 -10
- package/server-loader.js +328 -87
- package/server-loader.js.map +4 -4
package/package.json
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
57
|
+
1. **Get the unified diff** directly from diffity — this handles merge-base resolution, untracked files, and all ref types automatically:
|
|
57
58
|
```
|
|
58
|
-
|
|
59
|
+
diffity agent diff
|
|
59
60
|
```
|
|
60
|
-
|
|
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,
|
|
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
|
```
|