@rebasepro/app 0.21.2-canary.g1ea48be → 0.23.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.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Empty every module-level cache that holds data read as the signed-in user:
3
+ * fetched records, each table's scroll position and the rows kept with it,
4
+ * edits parked for a layout handoff, and resolved display values (the titles
5
+ * on relation chips and breadcrumbs).
6
+ *
7
+ * All of them are keyed by path and id only, with no user in the key, and the
8
+ * views seed their first render from them — so without this the next user to
9
+ * sign in to the tab is shown what the last one could read. Called by both auth
10
+ * controllers on `SIGNED_OUT`.
11
+ *
12
+ * The local-changes backup is not cleared here: a sign-out is also how a
13
+ * rejected refresh token ends a session, and the backup exists for that user
14
+ * coming back. {@link bindSessionCachesToUser} drops it when somebody else
15
+ * signs in instead.
16
+ */
17
+ export declare function clearSessionCaches(): void;
18
+ /**
19
+ * Record that the tab now acts as `userId`. When that is not who the caches
20
+ * were filled for, everything goes — the session caches, and the drafts the
21
+ * other user left in the local-changes backup.
22
+ *
23
+ * Call it before the new user is rendered: the views read these caches in
24
+ * their first render, before any effect of the auth controller would run.
25
+ */
26
+ export declare function bindSessionCachesToUser(userId: string): void;
@@ -36,6 +36,8 @@ export type RebaseAuthController = AuthController & {
36
36
  forgotPassword: (email: string) => Promise<void>;
37
37
  /** Reset password using token from email */
38
38
  resetPassword: (token: string, password: string) => Promise<void>;
39
+ /** Confirm an email address using the token from a verification email */
40
+ verifyEmail: (token: string) => Promise<void>;
39
41
  /** Change password for authenticated user */
40
42
  changePassword: (oldPassword: string, newPassword: string) => Promise<void>;
41
43
  /** Update user profile */
@@ -52,6 +54,16 @@ export type RebaseAuthController = AuthController & {
52
54
  clearError: () => void;
53
55
  /** Set or clear the auth provider error */
54
56
  setAuthProviderError: (error: Error | null) => void;
57
+ /**
58
+ * Open a challenge to finish a sign-in refused with `MFA_REQUIRED`. See
59
+ * `AuthControllerExtended`. Absent when the client has no MFA support.
60
+ */
61
+ startMfaChallenge?: (mfaToken: string, factorId: string) => Promise<string>;
62
+ /**
63
+ * Answer that challenge; the user is signed in on success. Absent when
64
+ * the client has no MFA support.
65
+ */
66
+ verifyMfaChallenge?: (mfaToken: string, challengeId: string, code: string) => Promise<void>;
55
67
  };
56
68
  /**
57
69
  * Structural type for the subset of `client.auth` (from `@rebasepro/client`)
@@ -78,6 +90,8 @@ export interface ClientAuth {
78
90
  signInWithOAuth(providerId: string, payload: Record<string, unknown>): Promise<unknown>;
79
91
  resetPasswordForEmail(email: string): Promise<unknown>;
80
92
  resetPassword(token: string, password: string): Promise<unknown>;
93
+ /** Optional so a hand-built auth client need not implement it. */
94
+ verifyEmail?(token: string): Promise<unknown>;
81
95
  changePassword(oldPassword: string, newPassword: string): Promise<unknown>;
82
96
  updateUser(updates: {
83
97
  displayName?: string;
@@ -86,6 +100,22 @@ export interface ClientAuth {
86
100
  getSessions(): Promise<DeviceSession[]>;
87
101
  revokeSession(sessionId: string): Promise<unknown>;
88
102
  revokeAllSessions(): Promise<unknown>;
103
+ /**
104
+ * The challenge that finishes a sign-in refused with `MFA_REQUIRED`: both
105
+ * requests carry that refusal's `mfaToken`, and a verified one signs the
106
+ * user in. Optional so a hand-built auth client need not implement it;
107
+ * without it an account with a second factor cannot sign in here.
108
+ */
109
+ mfa?: {
110
+ challenge(factorId: string, options: {
111
+ mfaToken: string;
112
+ }): Promise<{
113
+ challengeId: string;
114
+ }>;
115
+ verifyChallenge(challengeId: string, code: string, options: {
116
+ mfaToken: string;
117
+ }): Promise<unknown>;
118
+ };
89
119
  }
90
120
  /**
91
121
  * Props for useRebaseAuthController hook
@@ -16,6 +16,7 @@ import type { EntityDisplayRole } from "@rebasepro/cms-types";
16
16
  export type EntityDisplayKey = string;
17
17
  /** The identity of one role of one record, as a cache key. */
18
18
  export declare function entityDisplayKey(path: string, entityId: string | number | undefined, role: EntityDisplayRole): EntityDisplayKey;
19
+ export declare function getSharedEntityDisplayCache(): EntityDisplayCache;
19
20
  export declare class EntityDisplayCache {
20
21
  private readonly entries;
21
22
  private readonly listeners;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The links the backend puts in its auth emails, as the login view reads them.
3
+ *
4
+ * The server builds them from the configured frontend URL:
5
+ * `<frontend>/reset-password?token=…` for a forgotten password, an admin's
6
+ * "send reset email" and every invitation, and `<frontend>/verify-email?token=…`
7
+ * to confirm an address. The frontend URL may carry a base path (`/admin`), so
8
+ * the action is the *last* path segment, not the whole path.
9
+ */
10
+ export type EmailLinkAction = {
11
+ kind: "reset-password";
12
+ token: string;
13
+ } | {
14
+ kind: "verify-email";
15
+ token: string;
16
+ };
17
+ export declare function readEmailLinkAction(location: {
18
+ pathname: string;
19
+ search: string;
20
+ }): EmailLinkAction | null;
21
+ /**
22
+ * Where the app lives: the link's own address without the action segment and
23
+ * its token. `/admin/reset-password?token=…` becomes `/admin/`.
24
+ */
25
+ export declare function appAddressOfEmailLink(location: {
26
+ origin: string;
27
+ pathname: string;
28
+ }): string;
@@ -0,0 +1,33 @@
1
+ import type { AuthControllerExtended, MfaFactorSummary } from "@rebasepro/cms-types";
2
+ /**
3
+ * A sign-in that is waiting for its second factor.
4
+ *
5
+ * The server refuses the first factor of an account with MFA enrolled with
6
+ * `401 MFA_REQUIRED`, and puts in `details` what the second step needs: a
7
+ * pending token, good for five minutes and for the two challenge routes only,
8
+ * and the factors the account can answer with.
9
+ */
10
+ export interface PendingMfaSignIn {
11
+ mfaToken: string;
12
+ factors: MfaFactorSummary[];
13
+ }
14
+ /**
15
+ * The pending sign-in an `MFA_REQUIRED` refusal describes, or `null` for any
16
+ * other error — including an `MFA_REQUIRED` without the token or a factor to
17
+ * answer with, which no code step could finish.
18
+ */
19
+ export declare function readMfaRequired(error: unknown): PendingMfaSignIn | null;
20
+ /** Whether this controller can take a sign-in through its second step. */
21
+ export declare function canAnswerMfa(authController: AuthControllerExtended): boolean;
22
+ /**
23
+ * What a refused challenge means for the code step.
24
+ *
25
+ * - `invalid-code`: the code was wrong; the same challenge takes another.
26
+ * - `exhausted`: the challenge has had its five attempts; the next code needs
27
+ * a new one.
28
+ * - `expired`: the sign-in itself is over. The pending token lasts five
29
+ * minutes from the password, and once it has lapsed both routes refuse the
30
+ * caller as unauthenticated; a challenge that lapsed, or a sign-in revoked
31
+ * meanwhile, ends the same way. Only a new sign-in gets a new token.
32
+ */
33
+ export declare function classifyMfaRefusal(error: unknown): "invalid-code" | "exhausted" | "expired" | "other";
@@ -42,4 +42,4 @@ export type DataTableControllerProps<M extends Record<string, any> = any> = {
42
42
  * @param fixedFilterFromProps
43
43
  * @param updateUrl
44
44
  */
45
- export declare function useDataTableController<M extends Record<string, any> = any, USER extends User = User>({ path, collection, scrollRestoration, entitiesDisplayedFirst, lastDeleteTimestamp: _lastDeleteTimestamp, fixedFilter: fixedFilterFromProps, updateUrl }: DataTableControllerProps<M>): EntityTableController<M>;
45
+ export declare function useDataTableController<M extends Record<string, any> = any, USER extends User = User>({ path, collection, scrollRestoration, entitiesDisplayedFirst, lastDeleteTimestamp, fixedFilter: fixedFilterFromProps, updateUrl }: DataTableControllerProps<M>): EntityTableController<M>;
@@ -1,4 +1,11 @@
1
1
  import { Entity, FilterValues } from "@rebasepro/types";
2
+ /**
3
+ * Forget every collection's scroll position and the rows kept with it.
4
+ *
5
+ * The rows are what the signed-in user was allowed to read, and a table seeds
6
+ * its first render from them, so they must not outlive that user's session.
7
+ */
8
+ export declare function clearCollectionScrollCache(): void;
2
9
  export type ScrollRestorationController = {
3
10
  getCollectionScroll: (path: string, filters?: FilterValues<any>) => {
4
11
  scrollOffset: number;
@@ -2,16 +2,24 @@ import type { RebasePlugin, RebaseContext } from "@rebasepro/cms-types";
2
2
  /**
3
3
  * Render-less component that manages plugin lifecycle hooks.
4
4
  *
5
- * - Calls `lifecycle.onMount(context)` when plugins mount.
6
- * - Calls `lifecycle.onUnmount()` when plugins unmount.
7
- * - Subscribes to auth state changes and calls `lifecycle.onAuthStateChange`.
5
+ * - Calls `lifecycle.onMount(context)` once, the first time auth is ready.
6
+ * - Calls `lifecycle.onAuthStateChange(user)` on every change of user after
7
+ * that — `null` on sign-out, the new user on the next sign-in.
8
+ * - Calls `lifecycle.onUnmount()` when the Rebase tree unmounts.
9
+ *
10
+ * Mounted for as long as there are plugins, signed in or not. It used to be
11
+ * mounted only while a user was signed in, so a sign-out unmounted it and the
12
+ * next sign-in mounted a fresh one — `onAuthStateChange` could never see the
13
+ * user change, and never fired.
8
14
  *
9
15
  * Mount this component inside the Rebase tree, below PluginProviderStack,
10
16
  * so that the RebaseContext is fully available.
11
17
  *
12
18
  * @internal
13
19
  */
14
- export declare function PluginLifecycleManager({ plugins, context }: {
20
+ export declare function PluginLifecycleManager({ plugins, context, authReady }: {
15
21
  plugins: RebasePlugin[];
16
22
  context: RebaseContext;
23
+ /** Signed in, or sign-in skipped: what `onMount` waits for. */
24
+ authReady: boolean;
17
25
  }): null;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The same filter object for as long as its contents stay the same.
3
+ *
4
+ * Callers write filters inline — the hooks guide does — so every render hands
5
+ * a data hook a new object. Used as an effect dependency by identity, that
6
+ * re-subscribed on every render, and every delivery rendered again: a
7
+ * subscribe loop against the server for as long as the component was mounted.
8
+ */
9
+ export declare function useStableFilterValues<T>(filterValues: T): T;