@tangle-network/agent-app 0.60.184 → 0.60.185
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/dist/agent-profiles/d1.js +3 -3
- package/dist/agent-profiles/index.js +3 -3
- package/dist/assistant/index.js +9 -9
- package/dist/channels/index.js +1 -1
- package/dist/chat-react/index.js +4 -4
- package/dist/chat-routes/index.js +4 -4
- package/dist/{chunk-YXLN4PXK.js → chunk-2RCPPOQ4.js} +2 -2
- package/dist/{chunk-YESFGM56.js → chunk-5IS3GCMR.js} +3 -3
- package/dist/{chunk-3RHAF2P5.js → chunk-A3CUVYRZ.js} +3 -3
- package/dist/{chunk-LN5RBVGP.js → chunk-AVP5SNIG.js} +4 -4
- package/dist/{chunk-LN5RBVGP.js.map → chunk-AVP5SNIG.js.map} +1 -1
- package/dist/{chunk-7BYY7QKC.js → chunk-E65YV7VV.js} +5 -5
- package/dist/{chunk-7BYY7QKC.js.map → chunk-E65YV7VV.js.map} +1 -1
- package/dist/chunk-EZFBGW23.js +179 -0
- package/dist/chunk-EZFBGW23.js.map +1 -0
- package/dist/{chunk-UDLA3NGY.js → chunk-GNBBY3KN.js} +3 -3
- package/dist/{chunk-KOTNMTHV.js → chunk-HCFVEJQL.js} +3 -3
- package/dist/{chunk-YGBEGBJY.js → chunk-HEPEGY6Q.js} +2 -2
- package/dist/{chunk-AJZ3EJB5.js → chunk-IJAXOQJU.js} +2 -2
- package/dist/{chunk-EQRVOMMS.js → chunk-KMIDRUUH.js} +2 -2
- package/dist/{chunk-BBHVV37T.js → chunk-MSAGTFN4.js} +4 -4
- package/dist/chunk-OBKVNZPH.js +70 -0
- package/dist/chunk-OBKVNZPH.js.map +1 -0
- package/dist/{chunk-75ZIWV4U.js → chunk-QAF7ZTO3.js} +4 -4
- package/dist/{chunk-TJ4DYKF2.js → chunk-XG6FODYO.js} +3 -2
- package/dist/chunk-XG6FODYO.js.map +1 -0
- package/dist/{chunk-6O2NQ7WD.js → chunk-Y2SOQOJE.js} +3 -3
- package/dist/integrations-react/index.js +1 -1
- package/dist/{mention-editor-ZJAOUDEE.js → mention-editor-NOS5R3AU.js} +2 -2
- package/dist/preset-cloudflare/index.js +2 -2
- package/dist/sandbox/index.js +4 -4
- package/dist/settings-react/confirm-action.d.ts +51 -0
- package/dist/settings-react/delete-workspace-section.d.ts +31 -0
- package/dist/settings-react/export-section.d.ts +26 -0
- package/dist/settings-react/index.d.ts +20 -0
- package/dist/settings-react/index.js +1438 -0
- package/dist/settings-react/index.js.map +1 -0
- package/dist/settings-react/members-section.d.ts +66 -0
- package/dist/settings-react/save-status.d.ts +24 -0
- package/dist/settings-react/setting-outcome.d.ts +33 -0
- package/dist/settings-react/settings-layout.d.ts +71 -0
- package/dist/settings-react/settings-save-bar.d.ts +26 -0
- package/dist/settings-react/timezone-picker.d.ts +17 -0
- package/dist/settings-react/use-setting-field.d.ts +70 -0
- package/dist/stories/settings/settings-backend.d.ts +66 -0
- package/dist/stories/settings/settings-demo.d.ts +16 -0
- package/dist/stories/settings/team-panels-demo.d.ts +21 -0
- package/dist/studio-react/index.js +2 -2
- package/dist/teams-react/components/InviteForm.d.ts +2 -2
- package/dist/teams-react/components/MembersPanel.d.ts +9 -5
- package/dist/teams-react/components/RoleSelect.d.ts +10 -8
- package/dist/teams-react/components/invitation-detail.d.ts +8 -0
- package/dist/teams-react/components/member-access.d.ts +17 -0
- package/dist/teams-react/contracts.d.ts +2 -0
- package/dist/teams-react/index.js +119 -242
- package/dist/teams-react/index.js.map +1 -1
- package/dist/theme/tokens.css +43 -2
- package/dist/tools/index.js +4 -4
- package/dist/web-react/agent-profile-editor.js +4 -4
- package/dist/web-react/async/index.js +1 -1
- package/dist/web-react/controls.d.ts +4 -5
- package/dist/web-react/index.js +12 -12
- package/dist/work-product/index.js +2 -2
- package/dist/workspace-export/react.d.ts +26 -0
- package/dist/workspace-export/react.js +6 -169
- package/dist/workspace-export/react.js.map +1 -1
- package/dist/workspace-react/index.js +3 -3
- package/package.json +9 -4
- package/dist/chunk-TJ4DYKF2.js.map +0 -1
- /package/dist/{chunk-YXLN4PXK.js.map → chunk-2RCPPOQ4.js.map} +0 -0
- /package/dist/{chunk-YESFGM56.js.map → chunk-5IS3GCMR.js.map} +0 -0
- /package/dist/{chunk-3RHAF2P5.js.map → chunk-A3CUVYRZ.js.map} +0 -0
- /package/dist/{chunk-UDLA3NGY.js.map → chunk-GNBBY3KN.js.map} +0 -0
- /package/dist/{chunk-KOTNMTHV.js.map → chunk-HCFVEJQL.js.map} +0 -0
- /package/dist/{chunk-YGBEGBJY.js.map → chunk-HEPEGY6Q.js.map} +0 -0
- /package/dist/{chunk-AJZ3EJB5.js.map → chunk-IJAXOQJU.js.map} +0 -0
- /package/dist/{chunk-EQRVOMMS.js.map → chunk-KMIDRUUH.js.map} +0 -0
- /package/dist/{chunk-BBHVV37T.js.map → chunk-MSAGTFN4.js.map} +0 -0
- /package/dist/{chunk-75ZIWV4U.js.map → chunk-QAF7ZTO3.js.map} +0 -0
- /package/dist/{chunk-6O2NQ7WD.js.map → chunk-Y2SOQOJE.js.map} +0 -0
- /package/dist/{mention-editor-ZJAOUDEE.js.map → mention-editor-NOS5R3AU.js.map} +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One setting's save behaviour: the control shows the person's value at once,
|
|
3
|
+
* the write goes through `useConfirmedMutation`, and the row says what
|
|
4
|
+
* happened — "Saving…" only after the pending delay, "Saved" only on a
|
|
5
|
+
* confirmed write and only for two seconds, the server's own sentence and a
|
|
6
|
+
* Retry when it refused. The control keeps the person's value on a failure.
|
|
7
|
+
* Writes go one at a time: a change made while one is in flight waits for it,
|
|
8
|
+
* and only the newest waiting value is sent, so the server ends on the
|
|
9
|
+
* person's last choice. A confirmed write the person has not edited since
|
|
10
|
+
* shows the value the server kept, so a name it trims reads as saved.
|
|
11
|
+
*/
|
|
12
|
+
import { type ReactNode } from 'react';
|
|
13
|
+
import { type AsyncLoadContext, type MutationOutcome } from '../web-react/async';
|
|
14
|
+
/**
|
|
15
|
+
* - `idle`: the control shows the saved value.
|
|
16
|
+
* - `unsaved`: an edit is waiting for blur or Enter (`commitOn: 'blur'`).
|
|
17
|
+
* - `invalid`: the edit fails `validate`; nothing is sent.
|
|
18
|
+
* - `saving`: the write is in flight.
|
|
19
|
+
* - `saved`: the write was confirmed in the last two seconds.
|
|
20
|
+
* - `failed`: the write was refused; the edit stays for a retry.
|
|
21
|
+
*/
|
|
22
|
+
export type SettingFieldState = 'idle' | 'unsaved' | 'invalid' | 'saving' | 'saved' | 'failed';
|
|
23
|
+
export interface UseSettingFieldOptions<T> {
|
|
24
|
+
/** The value the server holds. A new value replaces the draft unless the person is editing it. */
|
|
25
|
+
saved: T;
|
|
26
|
+
/**
|
|
27
|
+
* Writes the value and resolves with the value the server kept, which the control then shows:
|
|
28
|
+
* `confirmSettingResponse(fetch(...), () => next)` for a route that answers `{ success: true }`
|
|
29
|
+
* or 204, a `parse` that reads the kept value from a route that answers with the record, or
|
|
30
|
+
* `confirmWrite`/`rejectWrite` from `web-react/async`. A refusal's message is what the row shows.
|
|
31
|
+
*/
|
|
32
|
+
save: (next: T, context: AsyncLoadContext) => Promise<MutationOutcome<T>>;
|
|
33
|
+
/** `change` (default) saves on every change: selects, switches, segmented choices. `blur` saves on `commit()`: text. */
|
|
34
|
+
commitOn?: 'change' | 'blur';
|
|
35
|
+
/**
|
|
36
|
+
* With `commitOn: 'change'`, waits this long after the last change before saving. A change
|
|
37
|
+
* still waiting when the field unmounts, as on a section switch, is sent then.
|
|
38
|
+
*/
|
|
39
|
+
debounceMs?: number;
|
|
40
|
+
/** Returns the sentence that says what to fix, or null. Checked on every change; an invalid value is never sent. */
|
|
41
|
+
validate?: (draft: T) => string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Defaults to structural equality over primitives, arrays and plain objects, so a `saved`
|
|
44
|
+
* record built inline on each render is not a new server value. Other objects compare by
|
|
45
|
+
* identity: pass one for them, such as `(a, b) => a.getTime() === b.getTime()` for a Date.
|
|
46
|
+
*/
|
|
47
|
+
isEqual?: (a: T, b: T) => boolean;
|
|
48
|
+
/** Fires with the confirmed value. */
|
|
49
|
+
onSaved?: (value: T) => void;
|
|
50
|
+
}
|
|
51
|
+
/** Spread into `SettingsRow`: the status after the label, the error that replaces the description, and Retry. */
|
|
52
|
+
export interface SettingFieldRowProps {
|
|
53
|
+
status?: ReactNode;
|
|
54
|
+
error?: ReactNode;
|
|
55
|
+
onRetry?: () => void;
|
|
56
|
+
}
|
|
57
|
+
export interface SettingField<T> {
|
|
58
|
+
draft: T;
|
|
59
|
+
/** Shows the value at once; with `commitOn: 'change'` it also saves it. */
|
|
60
|
+
setDraft: (next: T) => void;
|
|
61
|
+
/** Saves the draft (or `next`) unless it is invalid or already saved. Use it on blur and Enter. */
|
|
62
|
+
commit: (next?: T) => void;
|
|
63
|
+
/** Puts the saved value back and clears any error. */
|
|
64
|
+
revert: () => void;
|
|
65
|
+
state: SettingFieldState;
|
|
66
|
+
/** The validation or failure sentence, or null. */
|
|
67
|
+
error: string | null;
|
|
68
|
+
rowProps: SettingFieldRowProps;
|
|
69
|
+
}
|
|
70
|
+
export declare function useSettingField<T>({ saved, save, commitOn, debounceMs, validate, isEqual, onSaved, }: UseSettingFieldOptions<T>): SettingField<T>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-browser settings API for the settings page story and its browser
|
|
3
|
+
* proof. It answers real `Request`s with real `Response`s in the shape a
|
|
4
|
+
* product's settings routes use — `{ error: { code, message, field?,
|
|
5
|
+
* retryAfterSeconds? } }` on a refusal — so the page's typed-outcome path is
|
|
6
|
+
* the one a product runs. `control` holds or refuses the next write, which is
|
|
7
|
+
* how the proof photographs "Saving…" and a failed save.
|
|
8
|
+
*/
|
|
9
|
+
export type ViewerRole = 'owner' | 'admin';
|
|
10
|
+
export interface DemoWorkspace {
|
|
11
|
+
name: string;
|
|
12
|
+
timezone: string;
|
|
13
|
+
charts: boolean;
|
|
14
|
+
instructions: string;
|
|
15
|
+
dailyRun: boolean;
|
|
16
|
+
digest: boolean;
|
|
17
|
+
approvalEmail: 'each' | 'daily' | 'never';
|
|
18
|
+
approvalText: 'each' | 'daily' | 'never';
|
|
19
|
+
caps: {
|
|
20
|
+
perDay: number | null;
|
|
21
|
+
total: number | null;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export interface DemoMember {
|
|
25
|
+
id: string;
|
|
26
|
+
userId: string | null;
|
|
27
|
+
role: 'viewer' | 'editor' | 'admin' | 'owner';
|
|
28
|
+
name: string | null;
|
|
29
|
+
email: string | null;
|
|
30
|
+
acceptedAt: number | null;
|
|
31
|
+
}
|
|
32
|
+
export interface DemoInvitation {
|
|
33
|
+
id: string;
|
|
34
|
+
email: string;
|
|
35
|
+
permissions: 'viewer' | 'editor' | 'admin';
|
|
36
|
+
status: 'pending' | 'accepted' | 'expired' | 'revoked';
|
|
37
|
+
emailStatus: 'not_sent' | 'sent' | 'failed';
|
|
38
|
+
expiresAt: string;
|
|
39
|
+
acceptedAt?: string | null;
|
|
40
|
+
inviteUrl: string;
|
|
41
|
+
}
|
|
42
|
+
export interface DemoBackend {
|
|
43
|
+
fetch: typeof fetch;
|
|
44
|
+
control: {
|
|
45
|
+
/** The next write waits until `release()`. */
|
|
46
|
+
holdNext(): void;
|
|
47
|
+
release(): void;
|
|
48
|
+
/** The next write is refused with this status and typed error. */
|
|
49
|
+
failNext(status: number, error: {
|
|
50
|
+
code: string;
|
|
51
|
+
message: string;
|
|
52
|
+
field?: string;
|
|
53
|
+
retryAfterSeconds?: number;
|
|
54
|
+
}): void;
|
|
55
|
+
};
|
|
56
|
+
/** Every write the backend accepted, for the proof's checks. */
|
|
57
|
+
writes: Array<{
|
|
58
|
+
method: string;
|
|
59
|
+
path: string;
|
|
60
|
+
body: unknown;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
export declare function createSettingsBackend({ viewer, latencyMs }?: {
|
|
64
|
+
viewer?: ViewerRole;
|
|
65
|
+
latencyMs?: number;
|
|
66
|
+
}): DemoBackend;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A project settings page built only from `settings-react` and the ui
|
|
3
|
+
* settings atoms, over the in-browser backend: every row autosaves, confirms
|
|
4
|
+
* or groups its save the way the settings contract says. The story renders it
|
|
5
|
+
* and `scripts/prove-settings-pages.mjs` drives it in a real browser.
|
|
6
|
+
*/
|
|
7
|
+
import type { DemoBackend, ViewerRole } from './settings-backend';
|
|
8
|
+
export type DemoSectionId = 'general' | 'agent' | 'notifications' | 'limits' | 'team';
|
|
9
|
+
export interface SettingsDemoProps {
|
|
10
|
+
backend: DemoBackend;
|
|
11
|
+
viewer?: ViewerRole;
|
|
12
|
+
initialSection?: DemoSectionId;
|
|
13
|
+
/** How long a section's "loader" takes, so the optimistic nav and progress line show. */
|
|
14
|
+
routeDelayMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function SettingsDemo({ backend: api, viewer, initialSection, routeDelayMs }: SettingsDemoProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The teams panels that Tax, Legal and GTM render today, each in a card the
|
|
3
|
+
* way those pages draw them, so the story and the browser proof can check
|
|
4
|
+
* that the role picker, Remove and Invite stay inside the card at phone width.
|
|
5
|
+
* As `admin` the viewer is Amir, an admin whose own row offers no role change
|
|
6
|
+
* or removal.
|
|
7
|
+
*/
|
|
8
|
+
declare const VIEWERS: {
|
|
9
|
+
readonly owner: {
|
|
10
|
+
readonly currentRole: 'owner';
|
|
11
|
+
readonly currentUserId: 'usr-dana';
|
|
12
|
+
};
|
|
13
|
+
readonly admin: {
|
|
14
|
+
readonly currentRole: 'admin';
|
|
15
|
+
readonly currentUserId: 'usr-amir';
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function TeamPanelsDemo({ viewer }: {
|
|
19
|
+
viewer?: keyof typeof VIEWERS;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
export {};
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
} from "../chunk-6PRQ45QE.js";
|
|
40
40
|
import {
|
|
41
41
|
useInfiniteScroll
|
|
42
|
-
} from "../chunk-
|
|
42
|
+
} from "../chunk-KMIDRUUH.js";
|
|
43
43
|
import "../chunk-A2GG4D4A.js";
|
|
44
44
|
import "../chunk-SJWIZT7B.js";
|
|
45
45
|
import {
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
POPOVER_SURFACE_ATTR,
|
|
52
52
|
PopoverSurface,
|
|
53
53
|
usePopover
|
|
54
|
-
} from "../chunk-
|
|
54
|
+
} from "../chunk-XG6FODYO.js";
|
|
55
55
|
import {
|
|
56
56
|
ProviderLogo
|
|
57
57
|
} from "../chunk-RASGKEAL.js";
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* address is checked with the same rule the invitations API enforces, so
|
|
4
4
|
* Invite stays disabled until the address could be delivered to, and the
|
|
5
5
|
* reason appears under the field once the person leaves it or tries to send.
|
|
6
|
-
* On a narrow card the role and Invite
|
|
7
|
-
*
|
|
6
|
+
* On a narrow card the field, the role and Invite stack, the role at full
|
|
7
|
+
* width so its longest name fits, and nothing passes the card's edge.
|
|
8
8
|
*/
|
|
9
9
|
import type { WorkspaceRole } from '../../teams/roles';
|
|
10
10
|
interface InviteFormProps {
|
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
* `./teams/members-api`), so this imports no app router, fetch client, or toast.
|
|
6
6
|
* Styled with the shipped Tangle Quiet tokens (`var(--*)`).
|
|
7
7
|
*
|
|
8
|
-
* Role gating mirrors the API
|
|
9
|
-
* remove control; inherited org owners
|
|
10
|
-
*
|
|
11
|
-
* admins/owners.
|
|
8
|
+
* Role gating mirrors the API and `MembersSection` (`canManageMember`): only
|
|
9
|
+
* admins/owners see role selects and the remove control; inherited org owners,
|
|
10
|
+
* explicit owners and the viewer's own row are not editable here. The invite
|
|
11
|
+
* role select offers `admin` only to admins/owners.
|
|
12
|
+
*
|
|
13
|
+
* Below 36rem of panel a member's role picker takes its own full-width line
|
|
14
|
+
* under the name, and Remove stays beside the name, so the longest capability
|
|
15
|
+
* name fits and nothing passes the card's edge.
|
|
12
16
|
*/
|
|
13
17
|
import type { MembersPanelProps } from '../contracts';
|
|
14
|
-
export declare function MembersPanel({ members, currentRole, onInvite, onChangeRole, onRemove, onNotice, showInviteForm, }: MembersPanelProps): import("react").JSX.Element;
|
|
18
|
+
export declare function MembersPanel({ members, currentRole, currentUserId, onInvite, onChangeRole, onRemove, onNotice, showInviteForm, }: MembersPanelProps): import("react").JSX.Element;
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* options live here too.
|
|
2
|
+
* The role picker both team panels and the settings sections share: the ui
|
|
3
|
+
* `Select`, with each role named by what it lets a person do and, in the open
|
|
4
|
+
* list, one line on what it adds, so choosing one never needs a help page.
|
|
5
|
+
* The trigger shows the name alone and fills its container; give the
|
|
6
|
+
* container the width.
|
|
8
7
|
*/
|
|
9
|
-
import type
|
|
8
|
+
import { type WorkspaceRole } from '../../teams/roles';
|
|
9
|
+
/** Each workspace role in the words a person decides by. */
|
|
10
|
+
export declare const ROLE_CAPABILITY: Record<WorkspaceRole, string>;
|
|
10
11
|
export interface RoleSelectProps {
|
|
11
12
|
value: WorkspaceRole;
|
|
12
13
|
onChange(role: WorkspaceRole): void;
|
|
13
14
|
/** Accessible name (no visible label — the panels label the row). */
|
|
14
15
|
ariaLabel: string;
|
|
16
|
+
id?: string;
|
|
15
17
|
disabled?: boolean;
|
|
16
18
|
}
|
|
17
|
-
export declare function RoleSelect({ value, onChange, ariaLabel, disabled }: RoleSelectProps): import("react").JSX.Element;
|
|
19
|
+
export declare function RoleSelect({ value, onChange, ariaLabel, id, disabled }: RoleSelectProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The line under an invitation's address, shared by `InvitationsPanel` and the settings `InvitationsSection`. */
|
|
2
|
+
import type { InvitationView } from '../contracts';
|
|
3
|
+
/**
|
|
4
|
+
* The line under the address: what the invitee can do, then only what is true
|
|
5
|
+
* of this invitation now. Every status says itself in words, so a past
|
|
6
|
+
* invitation reads as joined, expired or revoked without a badge.
|
|
7
|
+
*/
|
|
8
|
+
export declare function invitationDetail(invitation: InvitationView): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may change a member's access, and the words for a role shown as text.
|
|
3
|
+
* `MembersPanel` and the settings `MembersSection` both read these, so the two
|
|
4
|
+
* member lists apply one rule.
|
|
5
|
+
*/
|
|
6
|
+
import { type WorkspaceRole } from '../../teams/roles';
|
|
7
|
+
import type { MemberView } from '../contracts';
|
|
8
|
+
/**
|
|
9
|
+
* Whether the viewer can change this member's role or remove them: an admin
|
|
10
|
+
* can, except for an owner, an inherited organization admin, or their own row,
|
|
11
|
+
* so no one demotes or removes themselves from a list.
|
|
12
|
+
*/
|
|
13
|
+
export declare function canManageMember(member: MemberView, currentRole: WorkspaceRole, currentUserId?: string | null): boolean;
|
|
14
|
+
/** True on the viewer's own row. */
|
|
15
|
+
export declare function isCurrentMember(member: MemberView, currentUserId?: string | null): boolean;
|
|
16
|
+
/** A role shown as text rather than a picker. */
|
|
17
|
+
export declare function memberRoleLabel(member: MemberView): string;
|
|
@@ -24,6 +24,8 @@ export interface MembersPanelProps {
|
|
|
24
24
|
members: MemberView[];
|
|
25
25
|
/** The viewer's effective role — gates which controls are interactive. */
|
|
26
26
|
currentRole: WorkspaceRole;
|
|
27
|
+
/** The viewer's user id, so their own row offers no role change or removal, as in `MembersSection`. */
|
|
28
|
+
currentUserId?: string | null;
|
|
27
29
|
/** Invite a member; resolve with the shareable invite URL (or void). */
|
|
28
30
|
onInvite(input: {
|
|
29
31
|
email: string;
|