@aiquants/auth-react-router 0.15.1 → 0.17.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/README.md CHANGED
@@ -81,15 +81,88 @@ export const userAdminApp = createUserAdminApp({
81
81
  import { AuthUserAdminAppView } from "@aiquants/auth-react-router/admin"
82
82
  export const loader = userAdminApp.loader
83
83
  export const action = userAdminApp.action
84
- export default () => <AuthUserAdminAppView />
84
+ // mounted as the splat route "/users/*"
85
+ export default () => <AuthUserAdminAppView basePath="/users" />
85
86
  ```
86
87
 
88
+ - **`basePath` is required** (`AuthUserAdminAppView` / `UserAdminShell`): the absolute path the console is mounted at,
89
+ without a trailing slash (`"/users"` for a `/users/*` route). The five tabs and the per-row "members" links of the
90
+ groups tab are built from it. There is no default: the host decides where the console lives, and a built-in default
91
+ would keep drawing links to that default after the mount moves — the routes still resolve and the build still
92
+ passes, only the links point at a place that is not served.
93
+
87
94
  - **`guards` is mandatory** — this surface lists every user and mutates group membership, so an unguarded mount is an account-enumeration and privilege-escalation path. Allow by returning, deny by throwing (`Response` / `redirect` / `Error`); the return value is `void` by contract, so a predicate-style guard that returns `false` cannot silently fail open.
88
95
  - **Per-operation authorization**: the guard receives the CRUD verb each intent actually performs (`read` / `create` / `update` / `delete`), so a principal holding only `update` cannot delete. Membership add/remove map to `create`/`delete` because they add and remove rows.
89
96
  - Unknown intents reach neither the guard nor the store (the intent table is a `Map`, so prototype keys such as `constructor` do not resolve).
90
97
  - Store errors (e.g. "cannot remove the last administrator") are returned as `{ ok: false, error }` and rendered by the views; guard rejections propagate untouched.
91
98
  - Labels default to **English** (`defaultUserAdminLabels`); inject `jaUserAdminLabels` or a partial override via `resolveUserAdminLabels`.
92
99
 
100
+ ### Localization (`labels.locale` vs `labels.formatLocale`)
101
+
102
+ Every string this package draws lives in `UserAdminLabels`, and a host picks the language by injecting a whole catalog
103
+ (`defaultUserAdminLabels` for English, the package default, or `jaUserAdminLabels`) or a partial override. Two keys
104
+ carry language, and they are **different axes**:
105
+
106
+ | Key | Type | `defaultUserAdminLabels` | `jaUserAdminLabels` | Drives |
107
+ | --- | --- | --- | --- | --- |
108
+ | `locale` | `UserAdminLocale` (`"en"` \| `"ja"`) | `"en"` | `"ja"` | UI chrome language of both upstream-group pickers (`SelectBox` `locale`) |
109
+ | `formatLocale` | BCP 47 `string` | `"en-US"` | `"ja-JP"` | `Intl.DateTimeFormat` (with `timeZone`) and the user-list `Intl.Collator` |
110
+
111
+ - **`locale`** is forwarded to both `@aiquants/select-box` pickers (the create-group dialog and the directory link
112
+ form). The picker strings this package owns (`directoryView.pickerNoOptions` / `pickerClear` / `pickerToggle` /
113
+ `pickerSelected` / `pickerAvailable`) are passed as the picker's `labels`, the placeholder is the per-instance
114
+ `placeholder` prop, and every other key comes from the select-box catalog of `locale` (the values below are those of
115
+ `defaultUserAdminLabels` / `jaUserAdminLabels` and of select-box 0.11.0):
116
+
117
+ | select-box key | Source | `"en"` | `"ja"` |
118
+ | --- | --- | --- | --- |
119
+ | `noOptions` | `directoryView.pickerNoOptions` | No matching group | 一致するグループがありません |
120
+ | `clear` | `directoryView.pickerClear` | Clear the selection | 選択を解除 |
121
+ | `toggle` | `directoryView.pickerToggle` | Show the group list | グループ一覧を開く |
122
+ | `selection` | `directoryView.pickerSelected` (`{value}` = the picked option's label) | Selected: {value} | 選択中: {value} |
123
+ | `status` | `directoryView.pickerAvailable` (`{count}` = options shown) | {count} groups available | 候補 {count} 件 |
124
+ | `placeholder` | `placeholder` prop: `groupsView.pickUpstreamPlaceholder` (dialog) / `directoryView.pickUpstreamPlaceholder` (link form) | Search by name or address... | 名前かアドレスで検索... |
125
+ | `scrollUp` | select-box catalog | Scroll up | 上へスクロール |
126
+ | `scrollDown` | select-box catalog | Scroll down | 下へスクロール |
127
+ | `scrollLeft` | select-box catalog | Scroll left | 左へスクロール |
128
+ | `scrollRight` | select-box catalog | Scroll right | 右へスクロール |
129
+ | `scrollToTop` | select-box catalog | Top | 先頭へ |
130
+ | `scrollToBottom` | select-box catalog | Bottom | 末尾へ |
131
+ | `noItems` | select-box catalog | No items | 項目がありません |
132
+ | `searching` | select-box catalog (message of the open list while nothing matches yet and more rows may still come) | Searching... | 検索中... |
133
+ | `guess` | select-box catalog (accessible description of a suggested row, one the picker lists as a guess rather than a match) | suggestion | 推測 |
134
+ | `removeTag` | select-box catalog (multi-select chips; both pickers are single-select) | Remove {label} | 「{label}」を削除 |
135
+ | `resizeHandle` | select-box catalog (tooltip of the option list's resize handle) | Resize handle | サイズ変更ハンドル |
136
+
137
+ To change a catalog-sourced string, there is no per-key override here: pick the `locale` whose catalog you want.
138
+
139
+ - `resolveUserAdminLabels` (run once when `createUserAdminApp` is built) **validates `locale`**: an omitted value keeps
140
+ the English default `"en"`; `"en"` / `"ja"` pass; anything else (`"ja-JP"`, `"EN"`, `""`, `null`, ...) throws
141
+ `RangeError` naming `labels.formatLocale`, so a mistake stops the app at startup instead of crashing the picker on
142
+ first render. There is no language negotiation — map `navigator.language` or a user setting to `"en"` / `"ja"`
143
+ yourself.
144
+ - **`formatLocale` is not validated**: an unreadable tag degrades (dates fall back to the raw ISO string, sorting to the
145
+ runtime collator) instead of throwing, because formatting must never take the screen down.
146
+
147
+ #### Migrating from 0.15.x (breaking changes in 0.16.0)
148
+
149
+ 1. **`basePath` is now required** on `AuthUserAdminAppView` / `UserAdminShell` (see above). Pass the absolute mount
150
+ path, without a trailing slash: `<AuthUserAdminAppView basePath="/users" />`. Omitting it fails type-checking
151
+ (TS2741); an untyped caller would draw links such as `undefined/groups`.
152
+ 2. **`labels.locale` changed meaning**: the old `labels.locale` (a BCP 47 tag for dates and sorting) is now
153
+ `labels.formatLocale` — no alias. The new `labels.locale` (type `UserAdminLocale` = `"en"` | `"ja"`) is the UI
154
+ language of the pickers, validated at startup.
155
+ - Hosts that inject `jaUserAdminLabels` whole, or spread it (`{ ...jaUserAdminLabels, heading: "" }`), need no
156
+ change: it carries `locale: "ja"` and `formatLocale: "ja-JP"`.
157
+ - A host that overrode `locale: "ja-JP"` must write `formatLocale: "ja-JP"`, and add `locale: "ja"` if it relies
158
+ on Japanese picker chrome (without it the pickers' catalog-sourced strings, such as the scroll-arrow names, are
159
+ English).
160
+ - A host that builds a full `UserAdminLabels` literal must add both keys.
161
+ 3. **`@aiquants/select-box` `texts` is gone**: the pickers take select-box 0.10.0 `locale` / `labels`
162
+ (`SelectBoxLabelOverrides`) instead of `texts` / `SelectBoxTexts`. This is internal to the views — hosts that only
163
+ render `AuthUserAdminAppView` do nothing — but a host must install `@aiquants/select-box` >= 0.10.0 and
164
+ `@aiquants/virtualscroll` >= 3.7.0 (the new peer floors).
165
+
93
166
  ### Upstream group catalog (optional port)
94
167
 
95
168
  The groups and directory tabs let the operator **pick** an upstream group instead of spelling its address. Wire the optional
@@ -180,11 +253,32 @@ the host, or the group picker's dropdown renders unstyled:
180
253
  @import "@aiquants/select-box/styles/select-box.css";
181
254
  ```
182
255
 
183
- Its transitive peers (`@aiquants/fuzzy-search`, `@aiquants/virtualscroll`) are declared here as well, so a host that
184
- installs this package is told what the picker needs rather than discovering it as a runtime resolution failure.
185
-
186
- All three are **required** peers, not optional. `src/admin/views.tsx` imports `@aiquants/select-box` at the top level and
187
- `./admin` re-exports it, so the specifier survives into `dist/admin.mjs`: a host without it does not get a degraded
256
+ That is the components-only build. Follow select-box's own README ("CSS Setup") for the rest of your build: a Tailwind v4
257
+ host imports it (and the `@aiquants/virtualscroll` peer build) in `layer(components)` and points `@source` at
258
+ `node_modules/@aiquants/select-box/dist` (the published package ships no `src`); a host without Tailwind imports
259
+ `@aiquants/select-box/styles/select-box.standalone.css` instead.
260
+
261
+ The option rows of both pickers are drawn by this package: each row shows the group name and address, a note (the local
262
+ group already linked to it, which makes the row unpickable, or else its member count when known), and the part of the
263
+ label the typed query matched. The match paint is
264
+ the picker's own (`RenderOptionContext.highlight`: the query compiled once under the picker's normalization and bound to
265
+ the row's match category), so a row the picker lists as a guess is never painted as an exact match; the runs carry
266
+ `data-aqar-match="exact"` or `"fuzzy"` and are paint-only (background and text colour, no bold), so a label keeps its
267
+ width while you type.
268
+
269
+ `@aiquants/fuzzy-search` is a transitive peer (the picker's search index, reached only through select-box) and is
270
+ declared here as well, so a host that installs this package is told what the picker needs rather than discovering it
271
+ as a runtime resolution failure. `@aiquants/virtualscroll` is both: select-box renders its option list with it, and
272
+ this package imports it **directly** — `src/admin/labels.ts` resolves the UI language with
273
+ `resolveVirtualScrollLocale`, so `dist/admin.mjs` carries its specifier. The pickers use the `locale` / `labels` props
274
+ of `@aiquants/select-box` and its 0.11 render and typing contract (`RenderOptionContext.highlight`, and `value` /
275
+ `onChange` typed by the option type), plus the locale resolution of `@aiquants/virtualscroll` 3.7.0, so select-box 0.11.0
276
+ and virtualscroll 3.7.0 are the peer floors (the published manifest carries `^0.11.0`, which a 0.10 install does not
277
+ satisfy).
278
+
279
+ All three are **required** peers, not optional. `src/admin/views.tsx` imports `@aiquants/select-box` and
280
+ `src/admin/labels.ts` imports `@aiquants/virtualscroll` at the top level, and `./admin` re-exports both modules, so the
281
+ specifiers survive into `dist/admin.mjs`: a host without them does not get a degraded
188
282
  picker, it gets `ERR_MODULE_NOT_FOUND` for the whole admin surface — including the groups and allowlist tabs, which have
189
283
  nothing to do with the picker. Marking them optional would suppress the install warning that is the only thing standing
190
284
  between a consumer and that failure. Note also that `@aiquants/select-box` pins React 19, which is stricter than this
package/dist/admin.d.mts CHANGED
@@ -1,9 +1,12 @@
1
+ import { VirtualScrollLocale } from "@aiquants/virtualscroll";
1
2
  import * as react_router from "react-router";
2
3
  import { AuthUser, AuthGroup, AuthGroupMember, AuthAllowlistEntry, AuthDirectoryMembershipMode, AuthDirectoryStatus, AuthDirectoryCatalogSnapshot, AuthDirectoryCatalogGroup } from "@aiquants/auth-core";
3
4
  import React__default from "react";
5
+ type UserAdminLocale = VirtualScrollLocale;
4
6
  type UserAdminLabels = {
5
7
  title: string;
6
- locale: string;
8
+ locale: UserAdminLocale;
9
+ formatLocale: string;
7
10
  timeZone: string;
8
11
  common: {
9
12
  create: string;
@@ -211,7 +214,8 @@ declare const defaultUserAdminLabels: UserAdminLabels;
211
214
  declare const jaUserAdminLabels: UserAdminLabels;
212
215
  type PartialUserAdminLabels = {
213
216
  title?: string;
214
- locale?: string;
217
+ locale?: UserAdminLocale;
218
+ formatLocale?: string;
215
219
  timeZone?: string;
216
220
  common?: Partial<UserAdminLabels["common"]>;
217
221
  errors?: Partial<UserAdminLabels["errors"]>;
@@ -382,9 +386,10 @@ type UserAdminShellProps = {
382
386
  title: string;
383
387
  annotation: React__default.ReactNode;
384
388
  }) => React__default.ReactNode;
389
+ basePath: string;
385
390
  pendingSyncPolling?: PendingSyncPolling;
386
391
  };
387
- declare function UserAdminShell({ children, renderHeader, pendingSyncPolling }: UserAdminShellProps): React__default.JSX.Element;
392
+ declare function UserAdminShell({ children, renderHeader, pendingSyncPolling, basePath }: UserAdminShellProps): React__default.JSX.Element;
388
393
  type UserAdminLoaderData = {
389
394
  segment: string;
390
395
  users: AuthUser[];
@@ -401,5 +406,5 @@ declare function GroupMembersView(): React__default.JSX.Element;
401
406
  declare function AllowlistView(): React__default.JSX.Element;
402
407
  declare function DirectoryView(): React__default.JSX.Element;
403
408
  type AuthUserAdminAppViewProps = UserAdminShellProps;
404
- declare function AuthUserAdminAppView({ renderHeader, pendingSyncPolling }?: AuthUserAdminAppViewProps): React__default.JSX.Element;
405
- export { AllowlistView, AuthUserAdminAppView, type AuthUserAdminAppViewProps, DEFAULT_PENDING_SYNC_POLLING, DirectoryView, GroupMembersView, GroupsView, type PartialUserAdminLabels, type PendingSyncPolling, type UserAdminAccess, type UserAdminAppConfig, type UserAdminCatalog, type UserAdminCatalogFailure, type UserAdminCatalogGroup, type UserAdminCatalogSnapshot, type UserAdminGuards, UserAdminInputError, type UserAdminLabels, type UserAdminLinkSyncOutcome, type UserAdminLoaderData, UserAdminOperatorError, type UserAdminPermission, UserAdminShell, type UserAdminShellProps, type UserAdminStore, type UserAdminSyncRequestOutcome, UsersView, createInMemoryUserAdminStore, createUserAdminApp, defaultUserAdminLabels, jaUserAdminLabels, normalizeUpstreamKey, resolveUserAdminLabels, userAdminAccessFor, userAdminIntents };
409
+ declare function AuthUserAdminAppView({ renderHeader, pendingSyncPolling, basePath }: AuthUserAdminAppViewProps): React__default.JSX.Element;
410
+ export { AllowlistView, AuthUserAdminAppView, type AuthUserAdminAppViewProps, DEFAULT_PENDING_SYNC_POLLING, DirectoryView, GroupMembersView, GroupsView, type PartialUserAdminLabels, type PendingSyncPolling, type UserAdminAccess, type UserAdminAppConfig, type UserAdminCatalog, type UserAdminCatalogFailure, type UserAdminCatalogGroup, type UserAdminCatalogSnapshot, type UserAdminGuards, UserAdminInputError, type UserAdminLabels, type UserAdminLinkSyncOutcome, type UserAdminLoaderData, type UserAdminLocale, UserAdminOperatorError, type UserAdminPermission, UserAdminShell, type UserAdminShellProps, type UserAdminStore, type UserAdminSyncRequestOutcome, UsersView, createInMemoryUserAdminStore, createUserAdminApp, defaultUserAdminLabels, jaUserAdminLabels, normalizeUpstreamKey, resolveUserAdminLabels, userAdminAccessFor, userAdminIntents };
package/dist/admin.d.ts CHANGED
@@ -1,9 +1,12 @@
1
+ import { VirtualScrollLocale } from "@aiquants/virtualscroll";
1
2
  import * as react_router from "react-router";
2
3
  import { AuthUser, AuthGroup, AuthGroupMember, AuthAllowlistEntry, AuthDirectoryMembershipMode, AuthDirectoryStatus, AuthDirectoryCatalogSnapshot, AuthDirectoryCatalogGroup } from "@aiquants/auth-core";
3
4
  import React__default from "react";
5
+ type UserAdminLocale = VirtualScrollLocale;
4
6
  type UserAdminLabels = {
5
7
  title: string;
6
- locale: string;
8
+ locale: UserAdminLocale;
9
+ formatLocale: string;
7
10
  timeZone: string;
8
11
  common: {
9
12
  create: string;
@@ -211,7 +214,8 @@ declare const defaultUserAdminLabels: UserAdminLabels;
211
214
  declare const jaUserAdminLabels: UserAdminLabels;
212
215
  type PartialUserAdminLabels = {
213
216
  title?: string;
214
- locale?: string;
217
+ locale?: UserAdminLocale;
218
+ formatLocale?: string;
215
219
  timeZone?: string;
216
220
  common?: Partial<UserAdminLabels["common"]>;
217
221
  errors?: Partial<UserAdminLabels["errors"]>;
@@ -382,9 +386,10 @@ type UserAdminShellProps = {
382
386
  title: string;
383
387
  annotation: React__default.ReactNode;
384
388
  }) => React__default.ReactNode;
389
+ basePath: string;
385
390
  pendingSyncPolling?: PendingSyncPolling;
386
391
  };
387
- declare function UserAdminShell({ children, renderHeader, pendingSyncPolling }: UserAdminShellProps): React__default.JSX.Element;
392
+ declare function UserAdminShell({ children, renderHeader, pendingSyncPolling, basePath }: UserAdminShellProps): React__default.JSX.Element;
388
393
  type UserAdminLoaderData = {
389
394
  segment: string;
390
395
  users: AuthUser[];
@@ -401,5 +406,5 @@ declare function GroupMembersView(): React__default.JSX.Element;
401
406
  declare function AllowlistView(): React__default.JSX.Element;
402
407
  declare function DirectoryView(): React__default.JSX.Element;
403
408
  type AuthUserAdminAppViewProps = UserAdminShellProps;
404
- declare function AuthUserAdminAppView({ renderHeader, pendingSyncPolling }?: AuthUserAdminAppViewProps): React__default.JSX.Element;
405
- export { AllowlistView, AuthUserAdminAppView, type AuthUserAdminAppViewProps, DEFAULT_PENDING_SYNC_POLLING, DirectoryView, GroupMembersView, GroupsView, type PartialUserAdminLabels, type PendingSyncPolling, type UserAdminAccess, type UserAdminAppConfig, type UserAdminCatalog, type UserAdminCatalogFailure, type UserAdminCatalogGroup, type UserAdminCatalogSnapshot, type UserAdminGuards, UserAdminInputError, type UserAdminLabels, type UserAdminLinkSyncOutcome, type UserAdminLoaderData, UserAdminOperatorError, type UserAdminPermission, UserAdminShell, type UserAdminShellProps, type UserAdminStore, type UserAdminSyncRequestOutcome, UsersView, createInMemoryUserAdminStore, createUserAdminApp, defaultUserAdminLabels, jaUserAdminLabels, normalizeUpstreamKey, resolveUserAdminLabels, userAdminAccessFor, userAdminIntents };
409
+ declare function AuthUserAdminAppView({ renderHeader, pendingSyncPolling, basePath }: AuthUserAdminAppViewProps): React__default.JSX.Element;
410
+ export { AllowlistView, AuthUserAdminAppView, type AuthUserAdminAppViewProps, DEFAULT_PENDING_SYNC_POLLING, DirectoryView, GroupMembersView, GroupsView, type PartialUserAdminLabels, type PendingSyncPolling, type UserAdminAccess, type UserAdminAppConfig, type UserAdminCatalog, type UserAdminCatalogFailure, type UserAdminCatalogGroup, type UserAdminCatalogSnapshot, type UserAdminGuards, UserAdminInputError, type UserAdminLabels, type UserAdminLinkSyncOutcome, type UserAdminLoaderData, type UserAdminLocale, UserAdminOperatorError, type UserAdminPermission, UserAdminShell, type UserAdminShellProps, type UserAdminStore, type UserAdminSyncRequestOutcome, UsersView, createInMemoryUserAdminStore, createUserAdminApp, defaultUserAdminLabels, jaUserAdminLabels, normalizeUpstreamKey, resolveUserAdminLabels, userAdminAccessFor, userAdminIntents };