@aiquants/authz-react-router 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI Quants / fehde-k
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @aiquants/authz-react-router
2
+
3
+ React Router (v7) adapter for [`@aiquants/authz-core`](https://www.npmjs.com/package/@aiquants/authz-core). Binds `appKey` + DI deps and provides loader/action guards, a hook, and a self-permission indicator component.
4
+
5
+ ## Install
6
+
7
+ Monorepo (already a pnpm workspace package):
8
+
9
+ ```jsonc
10
+ // consumer package.json
11
+ "dependencies": {
12
+ "@aiquants/authz-core": "workspace:*",
13
+ "@aiquants/authz-drizzle": "workspace:*",
14
+ "@aiquants/authz-react-router": "workspace:*"
15
+ }
16
+ ```
17
+
18
+ External projects:
19
+
20
+ ```bash
21
+ pnpm add @aiquants/authz-react-router react react-router # peers: react >=18, react-router ^7
22
+ ```
23
+
24
+ `react` / `react-router` are **peer dependencies**; `@aiquants/authz-core` is transitive. Pair with `@aiquants/authz-drizzle` (or another adapter) for `getEffectivePermissions`. Build / test: `pnpm run build` / `pnpm run test`.
25
+
26
+ ## Full integration (zero → guarded endpoint)
27
+
28
+ This walkthrough takes a React Router v7 app from nothing to a permission-guarded loader. It uses all three TS packages.
29
+
30
+ **Step 0 — install & create tables.** Install the three packages (above) plus your DB stack. Run the **DDL** and **bootstrap seed** from the `@aiquants/authz-drizzle` README once (this creates `dbo_authz` and an initial admin). You also need a **user table** with a numeric id and a way to resolve the current request to that id (your existing auth/session).
31
+
32
+ **Step 1 — wire it once.** Bind your `appKey`, how to resolve a user id, and how to load grants (delegated to the drizzle adapter).
33
+
34
+ ```ts
35
+ // app/services/authz/config.ts
36
+ import { defineAuthzSchema, getEffectivePermissions } from "@aiquants/authz-drizzle"
37
+ import { createAuthz } from "@aiquants/authz-react-router"
38
+ import { db } from "~/db.server" // your drizzle handle
39
+ import { Users } from "~/models/Users" // your user table (has numeric id)
40
+ import { getSessionUser } from "~/auth.server"
41
+
42
+ export const authzTables = defineAuthzSchema("dbo_authz", { userTable: Users })
43
+
44
+ const authz = createAuthz({
45
+ appKey: "myapp",
46
+ // request → your user id (or null → denied as "unresolved-user")
47
+ resolveUserId: async (request) => (await getSessionUser(request))?.id ?? null,
48
+ // load + merge this user's grants for one (app, resource)
49
+ getEffectivePermissions: (args) => getEffectivePermissions(db, authzTables, args),
50
+ })
51
+
52
+ export const { requirePermission, getMyPermissions } = authz
53
+ ```
54
+
55
+ > If your DB pool can be cold at the first request (serverless / lazy connect), have `resolveUserId` and `getEffectivePermissions` await a connection-ready guard first — otherwise a cold-start DB error can surface as a misleading 403 instead of a 500.
56
+
57
+ **Step 2 — guard a loader/action and apply scope.** On allow you get `{ userId, scope }`, so you can both authorize *and* narrow the data.
58
+
59
+ ```ts
60
+ // app/routes/report.tsx
61
+ import { applyRowScope, maskColumns } from "@aiquants/authz-drizzle"
62
+ import { requirePermission } from "~/services/authz/config"
63
+ import { db } from "~/db.server"
64
+ import { Report } from "~/models/Report"
65
+
66
+ export async function loader({ request }: { request: Request }) {
67
+ const { scope } = await requirePermission(request, { resourceKey: "report", action: "read" })
68
+ // map the scope's logical field names → real columns (per endpoint; the core layer doesn't know your table)
69
+ const columnMap = { dept: Report.dept, amount: Report.amount }
70
+ const rows = await applyRowScope(db.select().from(Report), scope.rowScope, columnMap)
71
+ return { rows: maskColumns(rows, scope.columnScope) } // deny disallowed columns before returning
72
+ }
73
+ // denied → AuthzDeniedError (throw a 403 Response in your error boundary, or pass options.failureRedirect)
74
+ ```
75
+
76
+ **Step 3 — register the resource + grant.** Insert a `TMResource('myapp','report')` and a `TDRolePermission` for the role(s) that should see it. Either run SQL, or build a tiny admin screen with the CRUD helpers (see the host app's admin guide). Example grant: "role `viewer` may `read` report, rows where dept ∈ {A}, hiding the amount column":
77
+
78
+ ```sql
79
+ -- row_scope / column_scope are the §3.2 JSON from @aiquants/authz-core
80
+ INSERT INTO dbo_authz.TDRolePermission (role_id, resource_id, action, row_scope, column_scope)
81
+ SELECT r.id, res.id, 'read',
82
+ N'{"filters":[{"field":"dept","op":"in","values":["A"]}]}',
83
+ N'{"mode":"deny","columns":["amount"]}'
84
+ FROM dbo_authz.TMRole r JOIN dbo_authz.TMResource res ON res.app_key='myapp' AND res.resource_key='report'
85
+ WHERE r.role_key='viewer';
86
+ ```
87
+
88
+ **Step 4 — show the user their permission (optional).** Feed `getMyPermissions` into a loader and render the indicator.
89
+
90
+ ```tsx
91
+ // loader: const myPermissions = await getMyPermissions(request, { resourceKeys: ["report"] })
92
+ import { MyPermissionIndicator } from "@aiquants/authz-react-router"
93
+ <MyPermissionIndicator permission={myPermissions.report} /> // → 読○ 書△ 削除×
94
+ ```
95
+
96
+ ## Scope cheat-sheet (§3.2)
97
+
98
+ The JSON you put in `row_scope` / `column_scope` (full contract in `@aiquants/authz-core`):
99
+
100
+ ```jsonc
101
+ // row_scope — which rows. NULL/empty = all rows. filters are AND'd. op ∈ in|notIn|eq|ne. null in values → IS (NOT) NULL.
102
+ { "filters": [ { "field": "dept", "op": "in", "values": ["A","B"] } ] }
103
+ // column_scope — which columns. NULL/empty = all columns.
104
+ { "mode": "deny", "columns": ["amount"] } // deny = hide these; allow = show only these
105
+ ```
106
+
107
+ Multiple roles **union** (wider wins): rows OR'd (any `NULL` ⇒ all rows); columns merged to the wider set. Anything unparseable fails **closed** (0 rows / 0 columns).
108
+
109
+ ## API
110
+
111
+ - `createAuthz({ appKey, resolveUserId, getEffectivePermissions, onDeny? })` → `{ requirePermission, getMyPermissions }`.
112
+ - `requirePermission(request, {resourceKey, action}, options?)` → `{ userId, scope }` on allow (so loaders apply row WHERE / column mask — not just 403). Denies throw `AuthzDeniedError` (403), or `throw redirect(options.failureRedirect)` when set.
113
+ - `getMyPermissions(request, {resourceKeys})` → `Record<resourceKey, PermissionView>` for the UI.
114
+ - `toPermissionView(resourceKey, perm)` → serializable view (loader → client).
115
+ - `usePermission(view)` → ○△× display states (`readState`/`writeState`/`deleteState`); pure, safe in render.
116
+ - `<MyPermissionIndicator permission={view} />` → read / write(create∪update) / delete as ○△×. `read` is △ when row/column-restricted; `write` is ○ only when both create & update are held, △ when one.
117
+
118
+ MIT
@@ -0,0 +1,474 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, CSSProperties } from 'react';
3
+ import * as _aiquants_authz_core from '@aiquants/authz-core';
4
+ import { EffectivePermission, DenyInfo, AuthzAction, GuardResult, AuthzGrant, AuthzResource, AuthzRole, AuthzAssignment, AuthzUser, AuthzAdminStore } from '@aiquants/authz-core';
5
+ import * as react_router from 'react-router';
6
+
7
+ type Awaitable<T> = T | Promise<T>;
8
+ type AuthzUserId = string | number;
9
+ type AuthzConfig = {
10
+ /** Stable app identifier (e.g. "quants"). Bound into every permission query. */
11
+ appKey: string;
12
+ /** Resolve the acting user id from the request (cookie/session/openid → TDUsers.id). */
13
+ resolveUserId: (request: Request) => Awaitable<AuthzUserId | null | undefined>;
14
+ /** Load effective permissions for (user, app, resource) — typically via @aiquants/authz-drizzle. */
15
+ getEffectivePermissions: (args: {
16
+ userId: AuthzUserId;
17
+ appKey: string;
18
+ resourceKey: string;
19
+ }) => Awaitable<EffectivePermission | null | undefined>;
20
+ /** Default deny handler. If omitted, denies throw AuthzDeniedError (403). */
21
+ onDeny?: (info: DenyInfo) => void;
22
+ };
23
+ type RequirePermissionOptions = {
24
+ /** When set, denials `throw redirect(failureRedirect)` instead of throwing 403. */
25
+ failureRedirect?: string;
26
+ };
27
+ /** Serializable per-resource permission summary for UI (loader → component). */
28
+ type PermissionView = {
29
+ resourceKey: string;
30
+ read: boolean;
31
+ create: boolean;
32
+ update: boolean;
33
+ delete: boolean;
34
+ /** create ∪ update */
35
+ write: boolean;
36
+ /** read granted but row and/or column restricted (→ △ partial). */
37
+ readPartial: boolean;
38
+ actions: AuthzAction[];
39
+ };
40
+ /** Map an EffectivePermission to a serializable {@link PermissionView}. */
41
+ declare function toPermissionView(resourceKey: string, perm: EffectivePermission | null | undefined): PermissionView;
42
+ /**
43
+ * React Router adapter. Binds `appKey` and the DI deps, exposing:
44
+ * - `requirePermission(request, {resourceKey, action}, options?)` → `{ userId, scope }` on allow
45
+ * (apply row WHERE / column mask with the returned scope); denies throw 403 or redirect.
46
+ * - `getMyPermissions(request, {resourceKeys})` → `Record<resourceKey, PermissionView>` for the UI.
47
+ */
48
+ declare function createAuthz(config: AuthzConfig): {
49
+ requirePermission: (request: Request, query: {
50
+ resourceKey: string;
51
+ action: AuthzAction;
52
+ }, options?: RequirePermissionOptions) => Promise<GuardResult<AuthzUserId>>;
53
+ getMyPermissions: (request: Request, query: {
54
+ resourceKeys: string[];
55
+ }) => Promise<Record<string, PermissionView>>;
56
+ };
57
+
58
+ /** Authz admin UI strings (i18n). Defaults are English; override per-section via `createAuthzAdminRoutes(..., { labels })`. */
59
+ type AuthzAdminLabels = {
60
+ title: string;
61
+ myPermission: string;
62
+ tabs: {
63
+ roles: string;
64
+ resources: string;
65
+ role_permissions: string;
66
+ user_roles: string;
67
+ };
68
+ common: {
69
+ add: string;
70
+ save: string;
71
+ delete: string;
72
+ remove: string;
73
+ assign: string;
74
+ grant: string;
75
+ select: string;
76
+ active: string;
77
+ inactive: string;
78
+ optional: string;
79
+ };
80
+ roles: {
81
+ heading: string;
82
+ note: string;
83
+ roleKey: string;
84
+ name: string;
85
+ description: string;
86
+ activeHeader: string;
87
+ empty: string;
88
+ newHeading: string;
89
+ hintRoleKey: string;
90
+ };
91
+ resources: {
92
+ heading: string;
93
+ note: string;
94
+ appKey: string;
95
+ resourceKey: string;
96
+ name: string;
97
+ description: string;
98
+ empty: string;
99
+ newHeading: string;
100
+ hintAppKey: string;
101
+ hintResourceKey: string;
102
+ hintDailyReport: string;
103
+ };
104
+ grants: {
105
+ heading: string;
106
+ note: string;
107
+ role: string;
108
+ resource: string;
109
+ action: string;
110
+ rowScope: string;
111
+ columnScope: string;
112
+ empty: string;
113
+ newHeading: string;
114
+ rowScopeHint: string;
115
+ columnScopeHint: string;
116
+ rowScopePlaceholder: string;
117
+ columnScopePlaceholder: string;
118
+ nullRows: string;
119
+ nullCols: string;
120
+ selectPlaceholder: string;
121
+ };
122
+ userRoles: {
123
+ heading: string;
124
+ user: string;
125
+ role: string;
126
+ empty: string;
127
+ newHeading: string;
128
+ selectPlaceholder: string;
129
+ };
130
+ error: {
131
+ forbiddenTitle: string;
132
+ forbiddenBody: string;
133
+ genericTitlePrefix: string;
134
+ genericTitle: string;
135
+ genericBody: string;
136
+ };
137
+ warn: {
138
+ selfRoleDisable: string;
139
+ selfRoleDelete: string;
140
+ selfAssignmentRemove: string;
141
+ };
142
+ };
143
+ declare const defaultAuthzAdminLabels: AuthzAdminLabels;
144
+ type PartialAuthzAdminLabels = {
145
+ title?: string;
146
+ myPermission?: string;
147
+ tabs?: Partial<AuthzAdminLabels["tabs"]>;
148
+ common?: Partial<AuthzAdminLabels["common"]>;
149
+ roles?: Partial<AuthzAdminLabels["roles"]>;
150
+ resources?: Partial<AuthzAdminLabels["resources"]>;
151
+ grants?: Partial<AuthzAdminLabels["grants"]>;
152
+ userRoles?: Partial<AuthzAdminLabels["userRoles"]>;
153
+ error?: Partial<AuthzAdminLabels["error"]>;
154
+ warn?: Partial<AuthzAdminLabels["warn"]>;
155
+ };
156
+ /** Shallow per-section merge over the English defaults. */
157
+ declare function resolveLabels(over?: PartialAuthzAdminLabels): AuthzAdminLabels;
158
+
159
+ type WithLabels = {
160
+ labels: AuthzAdminLabels;
161
+ };
162
+ type AuthzLayoutData = WithLabels & {
163
+ myPermissions: Record<string, PermissionView>;
164
+ adminResourceKey: string;
165
+ basePath?: string;
166
+ };
167
+ /** ネストルート用レイアウト(子は Outlet)。`routes.ts` でネスト構成にする場合に使う。 */
168
+ declare function AuthzLayout(): react.JSX.Element;
169
+ /**
170
+ * 認可管理 UI のエラー境界。ErrorBoundary は loader データを読めないため、ラベルは
171
+ * `makeAuthzErrorBoundary(labels)` で注入する(アプリは自分のロケールで生成する)。
172
+ * 既定の `AuthzErrorBoundary` は英語(パッケージ既定)。
173
+ */
174
+ declare function makeAuthzErrorBoundary(over?: PartialAuthzAdminLabels): () => React.ReactElement;
175
+ /** 英語(既定ラベル)のエラー境界。i18n したい場合は {@link makeAuthzErrorBoundary} を使う。 */
176
+ declare const AuthzErrorBoundary: () => React.ReactElement;
177
+ type AuthzRolesData = WithLabels & {
178
+ roles: AuthzRole[];
179
+ selfAdminRoleIds?: number[];
180
+ };
181
+ declare function AuthzRolesView(): react.JSX.Element;
182
+ type AuthzResourcesData = WithLabels & {
183
+ resources: AuthzResource[];
184
+ };
185
+ declare function AuthzResourcesView(): react.JSX.Element;
186
+ type AuthzGrantsData = WithLabels & {
187
+ grants: AuthzGrant[];
188
+ roles: Array<{
189
+ id: number;
190
+ roleKey: string;
191
+ }>;
192
+ resources: Array<{
193
+ id: number;
194
+ appKey: string;
195
+ resourceKey: string;
196
+ }>;
197
+ };
198
+ declare function AuthzGrantsView(): react.JSX.Element;
199
+ type AuthzUserRolesData = WithLabels & {
200
+ assignments: AuthzAssignment[];
201
+ roles: Array<{
202
+ id: number;
203
+ roleKey: string;
204
+ }>;
205
+ users: AuthzUser[];
206
+ actorUserId?: number;
207
+ selfAdminRoleIds?: number[];
208
+ };
209
+ declare function AuthzUserRolesView(): react.JSX.Element;
210
+ /**
211
+ * スプラット(`authz/*`)1 ルートで認可管理 UI 全体を描画するコンポーネント(store 非依存)。
212
+ * loader が返す `segment` に応じて該当タブのビューを描画する。レイアウト殻と各ビューは同一の
213
+ * loader データ(セグメントデータ + myPermissions + labels をトップレベルに統合)を読む。
214
+ */
215
+ declare function AuthzAdminAppView(): react.JSX.Element;
216
+
217
+ type Req = {
218
+ request: Request;
219
+ };
220
+ type PermissionQuery = {
221
+ resourceKey: string;
222
+ action: AuthzAction;
223
+ };
224
+ type CreateAuthzAdminServerOptions = {
225
+ store: AuthzAdminStore;
226
+ /** 管理画面のガード資源キー(既定 `authz_admin`)。 */
227
+ resourceKey?: string;
228
+ /** 未ログインをログインへ誘導する(例: throw redirect(...))。 */
229
+ requireUser: (request: Request) => Promise<unknown>;
230
+ /** 認可ガード(アプリの createAuthz 由来)。allow で { userId } を返す。 */
231
+ requirePermission: (request: Request, query: PermissionQuery) => Promise<{
232
+ userId: string | number;
233
+ }>;
234
+ /** 自己権限表示用(アプリの createAuthz 由来)。 */
235
+ getMyPermissions: (request: Request, args: {
236
+ resourceKeys: string[];
237
+ }) => Promise<Record<string, PermissionView>>;
238
+ /** 文言の部分上書き(既定は日本語)。 */
239
+ labels?: PartialAuthzAdminLabels;
240
+ };
241
+ declare function createAuthzAdminServer(opts: CreateAuthzAdminServerOptions): {
242
+ /** 露出: スプラットマウント(app.ts)がガードを 1 回だけ行うために使う。 */
243
+ labels: AuthzAdminLabels;
244
+ resourceKey: string;
245
+ layoutGuard: (request: Request) => Promise<{
246
+ userId: string | number;
247
+ }>;
248
+ loadMyPermissions: (request: Request) => Promise<Record<string, PermissionView>>;
249
+ /** /authz レイアウト: requireUser(302) → requirePermission(read, 403) → 自己権限取得。 */
250
+ layout: {
251
+ loader({ request }: Req): Promise<AuthzLayoutData>;
252
+ };
253
+ /** /authz index → roles へ。 */
254
+ index: {
255
+ loader(): never;
256
+ };
257
+ roles: {
258
+ data: (userId?: string | number) => Promise<AuthzRolesData>;
259
+ loader({ request }: Req): Promise<AuthzRolesData>;
260
+ action({ request }: Req): Promise<react_router.UNSAFE_DataWithResponseInit<{
261
+ error: string;
262
+ }> | {
263
+ ok: boolean;
264
+ }>;
265
+ };
266
+ resources: {
267
+ data: () => Promise<AuthzResourcesData>;
268
+ loader({ request }: Req): Promise<AuthzResourcesData>;
269
+ action({ request }: Req): Promise<react_router.UNSAFE_DataWithResponseInit<{
270
+ error: string;
271
+ }> | {
272
+ ok: boolean;
273
+ }>;
274
+ };
275
+ grants: {
276
+ data: () => Promise<AuthzGrantsData>;
277
+ loader({ request }: Req): Promise<AuthzGrantsData>;
278
+ action({ request }: Req): Promise<react_router.UNSAFE_DataWithResponseInit<{
279
+ error: string;
280
+ }> | {
281
+ ok: boolean;
282
+ }>;
283
+ };
284
+ userRoles: {
285
+ data: (userId?: string | number) => Promise<AuthzUserRolesData>;
286
+ loader({ request }: Req): Promise<AuthzUserRolesData>;
287
+ action({ request }: Req): Promise<react_router.UNSAFE_DataWithResponseInit<{
288
+ error: string;
289
+ }> | {
290
+ ok: boolean;
291
+ }>;
292
+ };
293
+ };
294
+
295
+ type RouteArgs = {
296
+ request: Request;
297
+ params: Record<string, string | undefined>;
298
+ };
299
+ type CreateAuthzAdminAppOptions = CreateAuthzAdminServerOptions & {
300
+ /** マウントのベースパス(タブリンク/未指定セグメントのリダイレクト先)。既定 `/authz`。 */
301
+ basePath?: string;
302
+ };
303
+ declare function createAuthzAdminApp(opts: CreateAuthzAdminAppOptions): {
304
+ loader: (args: RouteArgs) => Promise<{
305
+ labels: AuthzAdminLabels;
306
+ roles: _aiquants_authz_core.AuthzRole[];
307
+ selfAdminRoleIds?: number[];
308
+ segment: string;
309
+ basePath: string;
310
+ myPermissions: Record<string, PermissionView>;
311
+ adminResourceKey: string;
312
+ } | {
313
+ labels: AuthzAdminLabels;
314
+ resources: _aiquants_authz_core.AuthzResource[];
315
+ segment: string;
316
+ basePath: string;
317
+ myPermissions: Record<string, PermissionView>;
318
+ adminResourceKey: string;
319
+ } | {
320
+ labels: AuthzAdminLabels;
321
+ grants: _aiquants_authz_core.AuthzGrant[];
322
+ roles: Array<{
323
+ id: number;
324
+ roleKey: string;
325
+ }>;
326
+ resources: Array<{
327
+ id: number;
328
+ appKey: string;
329
+ resourceKey: string;
330
+ }>;
331
+ segment: string;
332
+ basePath: string;
333
+ myPermissions: Record<string, PermissionView>;
334
+ adminResourceKey: string;
335
+ } | {
336
+ labels: AuthzAdminLabels;
337
+ assignments: _aiquants_authz_core.AuthzAssignment[];
338
+ roles: Array<{
339
+ id: number;
340
+ roleKey: string;
341
+ }>;
342
+ users: _aiquants_authz_core.AuthzUser[];
343
+ actorUserId?: number;
344
+ selfAdminRoleIds?: number[];
345
+ segment: string;
346
+ basePath: string;
347
+ myPermissions: Record<string, PermissionView>;
348
+ adminResourceKey: string;
349
+ }>;
350
+ action: (args: RouteArgs) => Promise<react_router.UNSAFE_DataWithResponseInit<{
351
+ error: string;
352
+ }> | {
353
+ ok: boolean;
354
+ }>;
355
+ server: {
356
+ labels: AuthzAdminLabels;
357
+ resourceKey: string;
358
+ layoutGuard: (request: Request) => Promise<{
359
+ userId: string | number;
360
+ }>;
361
+ loadMyPermissions: (request: Request) => Promise<Record<string, PermissionView>>;
362
+ layout: {
363
+ loader({ request }: {
364
+ request: Request;
365
+ }): Promise<AuthzLayoutData>;
366
+ };
367
+ index: {
368
+ loader(): never;
369
+ };
370
+ roles: {
371
+ data: (userId?: string | number) => Promise<AuthzRolesData>;
372
+ loader({ request }: {
373
+ request: Request;
374
+ }): Promise<AuthzRolesData>;
375
+ action({ request }: {
376
+ request: Request;
377
+ }): Promise<react_router.UNSAFE_DataWithResponseInit<{
378
+ error: string;
379
+ }> | {
380
+ ok: boolean;
381
+ }>;
382
+ };
383
+ resources: {
384
+ data: () => Promise<AuthzResourcesData>;
385
+ loader({ request }: {
386
+ request: Request;
387
+ }): Promise<AuthzResourcesData>;
388
+ action({ request }: {
389
+ request: Request;
390
+ }): Promise<react_router.UNSAFE_DataWithResponseInit<{
391
+ error: string;
392
+ }> | {
393
+ ok: boolean;
394
+ }>;
395
+ };
396
+ grants: {
397
+ data: () => Promise<AuthzGrantsData>;
398
+ loader({ request }: {
399
+ request: Request;
400
+ }): Promise<AuthzGrantsData>;
401
+ action({ request }: {
402
+ request: Request;
403
+ }): Promise<react_router.UNSAFE_DataWithResponseInit<{
404
+ error: string;
405
+ }> | {
406
+ ok: boolean;
407
+ }>;
408
+ };
409
+ userRoles: {
410
+ data: (userId?: string | number) => Promise<AuthzUserRolesData>;
411
+ loader({ request }: {
412
+ request: Request;
413
+ }): Promise<AuthzUserRolesData>;
414
+ action({ request }: {
415
+ request: Request;
416
+ }): Promise<react_router.UNSAFE_DataWithResponseInit<{
417
+ error: string;
418
+ }> | {
419
+ ok: boolean;
420
+ }>;
421
+ };
422
+ };
423
+ };
424
+
425
+ declare const ui: Record<string, CSSProperties>;
426
+ /**
427
+ * ラベルを上、コントロールを下に縦積みするフィールド(重なり防止・一貫レイアウト)。
428
+ * コントロール側は `aria-label` を持つ前提なので `<div>` で構成する。
429
+ */
430
+ declare function Field({ label, hint, children }: {
431
+ label: string;
432
+ hint?: string;
433
+ children: ReactNode;
434
+ }): react.JSX.Element;
435
+ /** エラー表示(role="alert")。 */
436
+ declare function ErrorAlert({ message }: {
437
+ message?: string;
438
+ }): react.JSX.Element | null;
439
+
440
+ type MyPermissionIndicatorProps = {
441
+ permission: PermissionView | null | undefined;
442
+ labels?: {
443
+ read?: string;
444
+ write?: string;
445
+ delete?: string;
446
+ };
447
+ /** Show a "(部分)" hint when read is partial. Default true. */
448
+ showScopeSummary?: boolean;
449
+ className?: string;
450
+ };
451
+ /**
452
+ * Compact self-permission indicator: read / write(create∪update) / delete as ○△×.
453
+ * `write` is ○ only when both create & update are held, △ when one, × when none.
454
+ */
455
+ declare function MyPermissionIndicator({ permission, labels, showScopeSummary, className }: MyPermissionIndicatorProps): react.JSX.Element;
456
+
457
+ type PermState = "full" | "partial" | "none";
458
+ type UsePermission = {
459
+ canRead: boolean;
460
+ canWrite: boolean;
461
+ canDelete: boolean;
462
+ /** read: none(×) / partial(△, row|col restricted) / full(○). */
463
+ readState: PermState;
464
+ /** write = create ∪ update: full(○ both) / partial(△ one) / none(×). */
465
+ writeState: PermState;
466
+ deleteState: PermState;
467
+ };
468
+ /**
469
+ * Derive ○△× display states from a {@link PermissionView}. Pure (safe to call in render);
470
+ * named `usePermission` for ergonomic call-sites.
471
+ */
472
+ declare function usePermission(view: PermissionView | null | undefined): UsePermission;
473
+
474
+ export { AuthzAdminAppView, type AuthzAdminLabels, type AuthzConfig, AuthzErrorBoundary, type AuthzGrantsData, AuthzGrantsView, AuthzLayout, type AuthzLayoutData, type AuthzResourcesData, AuthzResourcesView, type AuthzRolesData, AuthzRolesView, type AuthzUserId, type AuthzUserRolesData, AuthzUserRolesView, type CreateAuthzAdminAppOptions, type CreateAuthzAdminServerOptions, ErrorAlert, Field, MyPermissionIndicator, type MyPermissionIndicatorProps, type PartialAuthzAdminLabels, type PermState, type PermissionView, type RequirePermissionOptions, type UsePermission, createAuthz, createAuthzAdminApp, createAuthzAdminServer, defaultAuthzAdminLabels, makeAuthzErrorBoundary, resolveLabels, toPermissionView, ui, usePermission };