@aiquants/auth-react-router 0.15.1 → 0.16.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,86 @@ 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.10.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
+ | `removeTag` | select-box catalog (multi-select chips; both pickers are single-select) | Remove {label} | 「{label}」を削除 |
133
+ | `resizeHandle` | select-box catalog (tooltip of the option list's resize handle) | Resize handle | サイズ変更ハンドル |
134
+
135
+ To change a catalog-sourced string, there is no per-key override here: pick the `locale` whose catalog you want.
136
+
137
+ - `resolveUserAdminLabels` (run once when `createUserAdminApp` is built) **validates `locale`**: an omitted value keeps
138
+ the English default `"en"`; `"en"` / `"ja"` pass; anything else (`"ja-JP"`, `"EN"`, `""`, `null`, ...) throws
139
+ `RangeError` naming `labels.formatLocale`, so a mistake stops the app at startup instead of crashing the picker on
140
+ first render. There is no language negotiation — map `navigator.language` or a user setting to `"en"` / `"ja"`
141
+ yourself.
142
+ - **`formatLocale` is not validated**: an unreadable tag degrades (dates fall back to the raw ISO string, sorting to the
143
+ runtime collator) instead of throwing, because formatting must never take the screen down.
144
+
145
+ #### Migrating from 0.15.x (breaking changes in 0.16.0)
146
+
147
+ 1. **`basePath` is now required** on `AuthUserAdminAppView` / `UserAdminShell` (see above). Pass the absolute mount
148
+ path, without a trailing slash: `<AuthUserAdminAppView basePath="/users" />`. Omitting it fails type-checking
149
+ (TS2741); an untyped caller would draw links such as `undefined/groups`.
150
+ 2. **`labels.locale` changed meaning**: the old `labels.locale` (a BCP 47 tag for dates and sorting) is now
151
+ `labels.formatLocale` — no alias. The new `labels.locale` (type `UserAdminLocale` = `"en"` | `"ja"`) is the UI
152
+ language of the pickers, validated at startup.
153
+ - Hosts that inject `jaUserAdminLabels` whole, or spread it (`{ ...jaUserAdminLabels, heading: "" }`), need no
154
+ change: it carries `locale: "ja"` and `formatLocale: "ja-JP"`.
155
+ - A host that overrode `locale: "ja-JP"` must write `formatLocale: "ja-JP"`, and add `locale: "ja"` if it relies
156
+ on Japanese picker chrome (without it the pickers' catalog-sourced strings, such as the scroll-arrow names, are
157
+ English).
158
+ - A host that builds a full `UserAdminLabels` literal must add both keys.
159
+ 3. **`@aiquants/select-box` `texts` is gone**: the pickers take select-box 0.10.0 `locale` / `labels`
160
+ (`SelectBoxLabelOverrides`) instead of `texts` / `SelectBoxTexts`. This is internal to the views — hosts that only
161
+ render `AuthUserAdminAppView` do nothing — but a host must install `@aiquants/select-box` >= 0.10.0 and
162
+ `@aiquants/virtualscroll` >= 3.7.0 (the new peer floors).
163
+
93
164
  ### Upstream group catalog (optional port)
94
165
 
95
166
  The groups and directory tabs let the operator **pick** an upstream group instead of spelling its address. Wire the optional
@@ -180,11 +251,17 @@ the host, or the group picker's dropdown renders unstyled:
180
251
  @import "@aiquants/select-box/styles/select-box.css";
181
252
  ```
182
253
 
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
254
+ `@aiquants/fuzzy-search` is a transitive peer (the picker's search index, reached only through select-box) and is
255
+ declared here as well, so a host that installs this package is told what the picker needs rather than discovering it
256
+ as a runtime resolution failure. `@aiquants/virtualscroll` is both: select-box renders its option list with it, and
257
+ this package imports it **directly** — `src/admin/labels.ts` resolves the UI language with
258
+ `resolveVirtualScrollLocale`, so `dist/admin.mjs` carries its specifier. The pickers use the `locale` / `labels` props
259
+ of `@aiquants/select-box` 0.10.0 and the locale resolution of `@aiquants/virtualscroll` 3.7.0, so those are the peer
260
+ floors.
261
+
262
+ All three are **required** peers, not optional. `src/admin/views.tsx` imports `@aiquants/select-box` and
263
+ `src/admin/labels.ts` imports `@aiquants/virtualscroll` at the top level, and `./admin` re-exports both modules, so the
264
+ specifiers survive into `dist/admin.mjs`: a host without them does not get a degraded
188
265
  picker, it gets `ERR_MODULE_NOT_FOUND` for the whole admin surface — including the groups and allowlist tabs, which have
189
266
  nothing to do with the picker. Marking them optional would suppress the install warning that is the only thing standing
190
267
  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 };