@segmentstream/app 0.0.0-stage → 0.38.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.
Files changed (125) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +2 -0
  3. package/README.md +1236 -2
  4. package/dist/agent.d.ts +56 -0
  5. package/dist/agent.js +203 -0
  6. package/dist/app.d.ts +82 -0
  7. package/dist/app.js +434 -0
  8. package/dist/browser-auth-capability.d.ts +2 -0
  9. package/dist/browser-auth-capability.js +2 -0
  10. package/dist/browser-revision.d.ts +19 -0
  11. package/dist/browser-revision.js +140 -0
  12. package/dist/channel-protocol.d.ts +16 -0
  13. package/dist/channel-protocol.js +17 -0
  14. package/dist/channel-session.d.ts +22 -0
  15. package/dist/channel-session.js +89 -0
  16. package/dist/config.d.ts +53 -0
  17. package/dist/config.js +112 -0
  18. package/dist/context.d.ts +10 -0
  19. package/dist/context.js +1 -0
  20. package/dist/cookies.d.ts +56 -0
  21. package/dist/cookies.js +131 -0
  22. package/dist/data.d.ts +233 -0
  23. package/dist/data.js +206 -0
  24. package/dist/decision-pages.d.ts +14 -0
  25. package/dist/decision-pages.js +87 -0
  26. package/dist/deployment.d.ts +30 -0
  27. package/dist/deployment.js +72 -0
  28. package/dist/errors.d.ts +60 -0
  29. package/dist/errors.js +123 -0
  30. package/dist/federation.d.ts +24 -0
  31. package/dist/federation.js +613 -0
  32. package/dist/groups.d.ts +95 -0
  33. package/dist/groups.js +280 -0
  34. package/dist/handlers.d.ts +10 -0
  35. package/dist/handlers.js +161 -0
  36. package/dist/index.d.ts +46 -0
  37. package/dist/index.js +29 -0
  38. package/dist/jobs.d.ts +49 -0
  39. package/dist/jobs.js +40 -0
  40. package/dist/legacy.d.ts +77 -0
  41. package/dist/legacy.js +57 -0
  42. package/dist/logo.d.ts +1 -0
  43. package/dist/logo.js +25 -0
  44. package/dist/me.d.ts +42 -0
  45. package/dist/me.js +54 -0
  46. package/dist/navigation/build.d.ts +68 -0
  47. package/dist/navigation/build.js +290 -0
  48. package/dist/navigation/cli.d.ts +19 -0
  49. package/dist/navigation/cli.js +37 -0
  50. package/dist/navigation/generate.d.ts +43 -0
  51. package/dist/navigation/generate.js +138 -0
  52. package/dist/navigation/next.d.ts +10 -0
  53. package/dist/navigation/next.js +18 -0
  54. package/dist/navigation-protocol.d.ts +55 -0
  55. package/dist/navigation-protocol.js +298 -0
  56. package/dist/neon.d.ts +101 -0
  57. package/dist/neon.js +539 -0
  58. package/dist/oauth-completion.d.ts +6 -0
  59. package/dist/oauth-completion.js +106 -0
  60. package/dist/oauth.d.ts +59 -0
  61. package/dist/oauth.js +129 -0
  62. package/dist/ontology-names.d.ts +10 -0
  63. package/dist/ontology-names.js +38 -0
  64. package/dist/ontology-transport.d.ts +34 -0
  65. package/dist/ontology-transport.js +130 -0
  66. package/dist/ontology-wire.d.ts +66 -0
  67. package/dist/ontology-wire.js +355 -0
  68. package/dist/ontology.d.ts +381 -0
  69. package/dist/ontology.js +367 -0
  70. package/dist/pages.d.ts +6 -0
  71. package/dist/pages.js +6 -0
  72. package/dist/parameters.d.ts +55 -0
  73. package/dist/parameters.js +118 -0
  74. package/dist/permissions.d.ts +19 -0
  75. package/dist/permissions.js +55 -0
  76. package/dist/pkce.d.ts +8 -0
  77. package/dist/pkce.js +40 -0
  78. package/dist/proxy.d.ts +25 -0
  79. package/dist/proxy.js +135 -0
  80. package/dist/request-timing.d.ts +6 -0
  81. package/dist/request-timing.js +53 -0
  82. package/dist/return-to.d.ts +2 -0
  83. package/dist/return-to.js +24 -0
  84. package/dist/server.d.ts +11 -0
  85. package/dist/server.js +80 -0
  86. package/dist/sessions.d.ts +27 -0
  87. package/dist/sessions.js +72 -0
  88. package/dist/sign-in.d.ts +37 -0
  89. package/dist/sign-in.js +95 -0
  90. package/dist/storage.d.ts +79 -0
  91. package/dist/storage.js +180 -0
  92. package/dist/telegram-launch.d.ts +7 -0
  93. package/dist/telegram-launch.js +134 -0
  94. package/dist/telegram.d.ts +2 -0
  95. package/dist/telegram.js +6 -0
  96. package/dist/tokens.d.ts +51 -0
  97. package/dist/tokens.js +70 -0
  98. package/dist/ui/agent-connect.d.ts +18 -0
  99. package/dist/ui/agent-connect.js +23 -0
  100. package/dist/ui/components.d.ts +27 -0
  101. package/dist/ui/components.js +71 -0
  102. package/dist/ui/hooks.d.ts +19 -0
  103. package/dist/ui/hooks.js +56 -0
  104. package/dist/ui/index.d.ts +22 -0
  105. package/dist/ui/index.js +15 -0
  106. package/dist/ui/navigation.d.ts +5 -0
  107. package/dist/ui/navigation.js +8 -0
  108. package/dist/ui/segmentstream-logo.d.ts +2 -0
  109. package/dist/ui/segmentstream-logo.js +7 -0
  110. package/dist/ui/sign-in-check.d.ts +14 -0
  111. package/dist/ui/sign-in-check.js +41 -0
  112. package/dist/ui/sign-in.d.ts +17 -0
  113. package/dist/ui/sign-in.js +65 -0
  114. package/dist/ui/store.d.ts +80 -0
  115. package/dist/ui/store.js +280 -0
  116. package/dist/ui/styles.css +249 -0
  117. package/dist/ui/telegram-sign-in.d.ts +16 -0
  118. package/dist/ui/telegram-sign-in.js +21 -0
  119. package/dist/ui/use-browser-sign-in.d.ts +52 -0
  120. package/dist/ui/use-browser-sign-in.js +169 -0
  121. package/dist/ui/use-channel-sign-in.d.ts +10 -0
  122. package/dist/ui/use-channel-sign-in.js +118 -0
  123. package/dist/ui/use-sign-in.d.ts +109 -0
  124. package/dist/ui/use-sign-in.js +304 -0
  125. package/package.json +95 -4
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { safeReturnTo } from "../return-to.js";
5
+ import { usePermissions, useSession, useUser } from "./hooks.js";
6
+ import { navigate } from "./navigation.js";
7
+ import { SIGN_IN_PAGE, endBrowserSession, clearSession, getSnapshot } from "./store.js";
8
+ /** Renders its children when a Neon Auth session exists in this browser. */
9
+ export function SignedIn({ children }) {
10
+ return useSession().status === "signed-in" ? children : null;
11
+ }
12
+ /** Renders its children when nobody is signed in. */
13
+ export function SignedOut({ children }) {
14
+ return useSession().status === "signed-out" ? children : null;
15
+ }
16
+ /**
17
+ * Renders its children when the signed-in user holds the permission. It only hides controls: the
18
+ * ontology service checks every read and action itself.
19
+ */
20
+ export function Can({ permission, children, fallback = null }) {
21
+ const { isLoading, can } = usePermissions();
22
+ if (isLoading) {
23
+ return null;
24
+ }
25
+ return can(permission) ? children : fallback;
26
+ }
27
+ /** Ends the selected session in this browser, then opens `redirectTo`. */
28
+ export async function signOut(redirectTo = SIGN_IN_PAGE) {
29
+ const session = getSnapshot().session;
30
+ if (session.status === "signed-in" &&
31
+ (session.source === "channel" || session.source === "oidc")) {
32
+ await endBrowserSession();
33
+ clearSession();
34
+ navigate(safeReturnTo(redirectTo));
35
+ return;
36
+ }
37
+ try {
38
+ await endBrowserSession();
39
+ }
40
+ finally {
41
+ clearSession();
42
+ navigate(safeReturnTo(redirectTo));
43
+ }
44
+ }
45
+ /** The signed-in user's name, email and roles, and a sign-out button. */
46
+ export function UserMenu({ signedOutPath = SIGN_IN_PAGE }) {
47
+ const session = useSession();
48
+ const state = useUser();
49
+ const [signingOut, setSigningOut] = useState(false);
50
+ if (session.status !== "signed-in") {
51
+ return null;
52
+ }
53
+ const user = state.status === "signed-in" ? state.user : null;
54
+ const name = user?.name ?? session.user.name;
55
+ const email = user?.email ?? session.user.email;
56
+ const label = name ?? email ?? "Account";
57
+ const roles = user?.roles ?? [];
58
+ return (_jsxs("details", { className: "ss-user-menu", children: [_jsxs("summary", { className: "ss-user-menu-trigger", children: [_jsx("span", { className: "ss-avatar", "aria-hidden": "true", children: initials(label) }), _jsx("span", { className: "ss-user-menu-label", children: label })] }), _jsxs("div", { className: "ss-user-menu-panel", children: [name !== null && _jsx("p", { className: "ss-user-menu-name", children: name }), email !== null && _jsx("p", { className: "ss-user-menu-email", children: email }), roles.length > 0 && (_jsx("ul", { className: "ss-badges", "aria-label": "Roles", children: roles.map((role) => (_jsx("li", { className: "ss-badge", children: role }, role))) })), state.status === "error" && (_jsx("p", { className: "ss-error", role: "alert", children: state.error.detail })), _jsx("button", { type: "button", className: "ss-button ss-button--secondary", disabled: signingOut, "aria-busy": signingOut, onClick: () => {
59
+ setSigningOut(true);
60
+ void signOut(signedOutPath).catch(() => setSigningOut(false));
61
+ }, children: signingOut ? "Signing out…" : "Sign out" })] })] }));
62
+ }
63
+ function initials(label) {
64
+ const letters = label
65
+ .split(/[\s@._-]+/)
66
+ .filter((part) => part !== "")
67
+ .slice(0, 2)
68
+ .map((part) => part.charAt(0).toUpperCase())
69
+ .join("");
70
+ return letters === "" ? "?" : letters;
71
+ }
@@ -0,0 +1,19 @@
1
+ import type { SessionState, UserState } from "./store.js";
2
+ /** The Neon Auth session in this browser: `loading`, `signed-out`, `signed-in` or `unavailable`. */
3
+ export declare function useSession(): SessionState;
4
+ /**
5
+ * The signed-in user as the ontology knows them, from `GET /api/ontology/me`, loaded once per
6
+ * session. `status` is `loading`, `signed-out`, `signed-in` (with `user`) or `error` (with `error`;
7
+ * `error.code` is `not_invited` for people the app has not invited).
8
+ */
9
+ export declare function useUser(): UserState;
10
+ export interface Permissions {
11
+ /** True until the user's permissions are known. */
12
+ readonly isLoading: boolean;
13
+ readonly roles: ReadonlyArray<string>;
14
+ readonly permissions: ReadonlyArray<string>;
15
+ /** Whether the user's permissions cover `permission`, wildcards included. */
16
+ readonly can: (permission: string) => boolean;
17
+ }
18
+ /** The signed-in user's roles and permissions, from the same per-session answer as `useUser()`. */
19
+ export declare function usePermissions(): Permissions;
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { useEffect, useMemo, useSyncExternalStore } from "react";
3
+ import { SegmentStreamApiError } from "../errors.js";
4
+ import { isAllowed } from "../permissions.js";
5
+ import { LOADING_USER, SIGNED_OUT_USER, ensureUser, getServerSnapshot, getSnapshot, subscribe, } from "./store.js";
6
+ const UNAVAILABLE_USER = {
7
+ status: "error",
8
+ user: null,
9
+ error: new SegmentStreamApiError({
10
+ code: "auth_unavailable",
11
+ status: 503,
12
+ detail: "Neon Auth could not be reached. Try again in a moment.",
13
+ retryable: true,
14
+ }),
15
+ };
16
+ /** The Neon Auth session in this browser: `loading`, `signed-out`, `signed-in` or `unavailable`. */
17
+ export function useSession() {
18
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot).session;
19
+ }
20
+ /**
21
+ * The signed-in user as the ontology knows them, from `GET /api/ontology/me`, loaded once per
22
+ * session. `status` is `loading`, `signed-out`, `signed-in` (with `user`) or `error` (with `error`;
23
+ * `error.code` is `not_invited` for people the app has not invited).
24
+ */
25
+ export function useUser() {
26
+ const { session, users } = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
27
+ const sessionId = session.status === "signed-in" ? session.sessionId : null;
28
+ useEffect(() => {
29
+ if (sessionId !== null) {
30
+ ensureUser(sessionId, session.status === "signed-in" ? session.source : undefined);
31
+ }
32
+ }, [sessionId, session]);
33
+ switch (session.status) {
34
+ case "loading":
35
+ return LOADING_USER;
36
+ case "signed-out":
37
+ return SIGNED_OUT_USER;
38
+ case "unavailable":
39
+ return UNAVAILABLE_USER;
40
+ case "signed-in":
41
+ return users.get(session.sessionId) ?? LOADING_USER;
42
+ }
43
+ }
44
+ /** The signed-in user's roles and permissions, from the same per-session answer as `useUser()`. */
45
+ export function usePermissions() {
46
+ const state = useUser();
47
+ return useMemo(() => {
48
+ const user = state.status === "signed-in" ? state.user : null;
49
+ return {
50
+ isLoading: state.status === "loading",
51
+ roles: user?.roles ?? [],
52
+ permissions: user?.permissions ?? [],
53
+ can: (permission) => user !== null && isAllowed(permission, user),
54
+ };
55
+ }, [state]);
56
+ }
@@ -0,0 +1,22 @@
1
+ export { Can, SignedIn, SignedOut, UserMenu, signOut } from "./components.js";
2
+ export type { CanProps, ChildrenProps, UserMenuProps } from "./components.js";
3
+ export { AgentConnect } from "./agent-connect.js";
4
+ export type { AgentConnectProps } from "./agent-connect.js";
5
+ export { usePermissions, useSession, useUser } from "./hooks.js";
6
+ export type { Permissions } from "./hooks.js";
7
+ export { SignIn } from "./sign-in.js";
8
+ export { TelegramSignIn, useTelegramSignIn } from "./telegram-sign-in.js";
9
+ export type { TelegramSignInProps, TelegramSignInOptions } from "./telegram-sign-in.js";
10
+ export type { SignInProps } from "./sign-in.js";
11
+ export { SIGN_IN_ERROR_MESSAGES, signInErrorCode, signInCodeFromLink, signInErrorMessage, useSignIn, } from "./use-sign-in.js";
12
+ export type { SignInController, SignInErrorCode, SignInNoticeCode, SignInOptions, } from "./use-sign-in.js";
13
+ export type { SessionState, SessionUser, UserState } from "./store.js";
14
+ export { isAllowed } from "../permissions.js";
15
+ export type { CurrentUser, ScopedPermission } from "../me.js";
16
+ export type { AgentPairing } from "../agent.js";
17
+ export { SegmentStreamApiError, NOT_INVITED, SIGN_IN_LINK_INVALID } from "../errors.js";
18
+ export { SegmentStreamLogo } from "./segmentstream-logo.js";
19
+ export { useChannelSignIn } from "./use-channel-sign-in.js";
20
+ export type { ChannelSignInState, ChannelSignInOptions } from "./use-channel-sign-in.js";
21
+ export { useBrowserSignIn, useIdentityLink, useEmailIdentityLink } from "./use-browser-sign-in.js";
22
+ export type { BrowserSignInMethod, BrowserSignInOptions, IdentityLinkState, LinkAccountIdentity, } from "./use-browser-sign-in.js";
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ // Client components for apps that sign in with Neon Auth. Import the stylesheet once, in the root
3
+ // layout, when rendering the components: `import "@segmentstream/app/ui/styles.css";`. The
4
+ // hooks, `useSignIn()` among them, need no stylesheet.
5
+ export { Can, SignedIn, SignedOut, UserMenu, signOut } from "./components.js";
6
+ export { AgentConnect } from "./agent-connect.js";
7
+ export { usePermissions, useSession, useUser } from "./hooks.js";
8
+ export { SignIn } from "./sign-in.js";
9
+ export { TelegramSignIn, useTelegramSignIn } from "./telegram-sign-in.js";
10
+ export { SIGN_IN_ERROR_MESSAGES, signInErrorCode, signInCodeFromLink, signInErrorMessage, useSignIn, } from "./use-sign-in.js";
11
+ export { isAllowed } from "../permissions.js";
12
+ export { SegmentStreamApiError, NOT_INVITED, SIGN_IN_LINK_INVALID } from "../errors.js";
13
+ export { SegmentStreamLogo } from "./segmentstream-logo.js";
14
+ export { useChannelSignIn } from "./use-channel-sign-in.js";
15
+ export { useBrowserSignIn, useIdentityLink, useEmailIdentityLink } from "./use-browser-sign-in.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * A full page load, so that the server renders the next page with the session cookies Neon Auth
3
+ * has just set or cleared.
4
+ */
5
+ export declare function navigate(path: string): void;
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ /**
3
+ * A full page load, so that the server renders the next page with the session cookies Neon Auth
4
+ * has just set or cleared.
5
+ */
6
+ export function navigate(path) {
7
+ window.location.assign(path);
8
+ }
@@ -0,0 +1,2 @@
1
+ /** The official SegmentStream wordmark, also used on segmentstream.com. */
2
+ export declare function SegmentStreamLogo(): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import Image from "next/image";
3
+ import { SEGMENTSTREAM_LOGO_URI } from "../logo.js";
4
+ /** The official SegmentStream wordmark, also used on segmentstream.com. */
5
+ export function SegmentStreamLogo() {
6
+ return (_jsx(Image, { unoptimized: true, src: SEGMENTSTREAM_LOGO_URI, alt: "SegmentStream", width: 210, height: 45 }));
7
+ }
@@ -0,0 +1,14 @@
1
+ /** The ontology's sign-in check, which the deployment serves beside the app. */
2
+ export declare const SIGN_IN_CHECK_PATH = "/api/ontology/sign-in/check";
3
+ /**
4
+ * How an address signs in, or why it may not: `code` for a member with a Neon Auth account, `link`
5
+ * for a first sign-in, which the link's account creation needs; `not_invited`, `email_invalid` or
6
+ * `rate_limited` when the ontology refuses before any email is sent.
7
+ */
8
+ export type SignInCheckOutcome = "code" | "link" | "not_invited" | "email_invalid" | "rate_limited";
9
+ /**
10
+ * Asks the ontology how this address signs in, before any email is sent. An app without an
11
+ * ontology, an ontology deployed with an SDK before 0.4, an answer the SDK cannot read and a failed
12
+ * request all get `code`: the sign-in works as it did before the check existed.
13
+ */
14
+ export declare function checkSignIn(email: string): Promise<SignInCheckOutcome>;
@@ -0,0 +1,41 @@
1
+ import { readJson } from "../errors.js";
2
+ /** The ontology's sign-in check, which the deployment serves beside the app. */
3
+ export const SIGN_IN_CHECK_PATH = "/api/ontology/sign-in/check";
4
+ /**
5
+ * Asks the ontology how this address signs in, before any email is sent. An app without an
6
+ * ontology, an ontology deployed with an SDK before 0.4, an answer the SDK cannot read and a failed
7
+ * request all get `code`: the sign-in works as it did before the check existed.
8
+ */
9
+ export async function checkSignIn(email) {
10
+ let response;
11
+ try {
12
+ response = await fetch(SIGN_IN_CHECK_PATH, {
13
+ method: "POST",
14
+ headers: { "content-type": "application/json", accept: "application/json" },
15
+ body: JSON.stringify({ email }),
16
+ credentials: "same-origin",
17
+ cache: "no-store",
18
+ });
19
+ }
20
+ catch {
21
+ return "code";
22
+ }
23
+ if (response.status === 429) {
24
+ return "rate_limited";
25
+ }
26
+ if (response.status === 400) {
27
+ return "email_invalid";
28
+ }
29
+ if (!response.ok) {
30
+ return "code";
31
+ }
32
+ const body = await readJson(response);
33
+ if (typeof body !== "object" || body === null) {
34
+ return "code";
35
+ }
36
+ const { allowed, method } = body;
37
+ if (allowed === false) {
38
+ return "not_invited";
39
+ }
40
+ return allowed === true && method === "link" ? "link" : "code";
41
+ }
@@ -0,0 +1,17 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ChannelSignInState } from "./use-channel-sign-in.js";
3
+ import { useBrowserSignIn } from "./use-browser-sign-in.js";
4
+ import type { SignInOptions } from "./use-sign-in.js";
5
+ export interface SignInProps extends SignInOptions {
6
+ /** The heading. Defaults to "Sign in". */
7
+ readonly title?: string;
8
+ /** Workspace-owned browser method and recovery UI for declared SegmentStream sign-in. */
9
+ readonly renderBrowser?: (state: ReturnType<typeof useBrowserSignIn>) => ReactNode;
10
+ /** App-owned rendering for channel status/error codes; no SDK error copy is supplied. */
11
+ readonly renderTelegram?: (state: ChannelSignInState) => ReactNode;
12
+ }
13
+ /**
14
+ * Email sign-in with Neon Auth in the SDK's neutral styles: `useSignIn()` with markup. Apps
15
+ * with their own design system render `useSignIn()` themselves.
16
+ */
17
+ export declare function SignIn({ renderTelegram, renderBrowser, ...options }: SignInProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null;
@@ -0,0 +1,65 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef } from "react";
4
+ import { telegramInitData, useTelegramSignIn } from "./telegram-sign-in.js";
5
+ import { useBrowserSignIn } from "./use-browser-sign-in.js";
6
+ import { useSignIn } from "./use-sign-in.js";
7
+ /**
8
+ * Email sign-in with Neon Auth in the SDK's neutral styles: `useSignIn()` with markup. Apps
9
+ * with their own design system render `useSignIn()` themselves.
10
+ */
11
+ export function SignIn({ renderTelegram, renderBrowser, ...options }) {
12
+ const browser = useBrowserSignIn(options);
13
+ const destination = typeof window === "undefined"
14
+ ? "/"
15
+ : (new URLSearchParams(window.location.search).get("returnTo") ?? "/");
16
+ const telegram = useTelegramSignIn({ redirectTo: options.returnTo ?? destination });
17
+ const active = telegram.status !== "loading" &&
18
+ typeof window !== "undefined" &&
19
+ telegramInitData() !== undefined;
20
+ if (active && telegram.status !== "disabled") {
21
+ return renderTelegram?.(telegram) ?? null;
22
+ }
23
+ if (browser.status === "ready" && !browser.methods.includes("email"))
24
+ return renderBrowser?.(browser) ?? null;
25
+ return _jsx(EmailSignIn, { ...options });
26
+ }
27
+ function EmailSignIn({ title = "Sign in", ...options }) {
28
+ const ids = useId();
29
+ const { step, segmentstreamSignInUrl, email, setEmail, code, setCode, pending, error, notice, resendIn, codeLength, destination, signedIn, notInvited, linkInvalid, sessionEmail, submitEmail, submitCode, resend, changeEmail, switchAccount, } = useSignIn(options);
30
+ const codeInput = useRef(null);
31
+ // The code field takes focus when it appears and again after a wrong code.
32
+ useEffect(() => {
33
+ if (step === "code" && pending === null) {
34
+ codeInput.current?.focus();
35
+ }
36
+ }, [step, pending]);
37
+ const errorId = `${ids}-error`;
38
+ if (notInvited) {
39
+ return (_jsxs("section", { className: "ss-card ss-sign-in", "aria-labelledby": `${ids}-title`, children: [_jsx("h1", { id: `${ids}-title`, className: "ss-title", children: "Ask for an invitation" }), _jsxs("p", { className: "ss-text", children: [sessionEmail === null ? "You are signed in" : `You are signed in as ${sessionEmail}`, ", but this app has not invited you. Ask your administrator for an invitation, or sign in with another email address."] }), error !== null && (_jsx("p", { id: errorId, className: "ss-error", role: "alert", children: error })), _jsx("button", { type: "button", className: "ss-button", onClick: () => void switchAccount(), disabled: pending !== null, "aria-busy": pending === "sign-out", children: pending === "sign-out" ? "Signing out…" : "Use another email" })] }));
40
+ }
41
+ if (signedIn && step === "email" && pending === null) {
42
+ return (_jsxs("section", { className: "ss-card ss-sign-in", "aria-labelledby": `${ids}-title`, children: [_jsx("h1", { id: `${ids}-title`, className: "ss-title", children: "You are signed in" }), _jsx("p", { className: "ss-text", children: sessionEmail === null ? "You already have a session." : `Signed in as ${sessionEmail}.` }), _jsxs("div", { className: "ss-actions", children: [_jsx("a", { className: "ss-button", href: destination, children: "Continue" }), _jsx("button", { type: "button", className: "ss-button ss-button--secondary", onClick: () => void switchAccount(), children: "Use another email" })] })] }));
43
+ }
44
+ if (step === "email") {
45
+ return (_jsxs("section", { className: "ss-card ss-sign-in", "aria-labelledby": `${ids}-title`, children: [_jsx("h1", { id: `${ids}-title`, className: "ss-title", children: title }), _jsx("p", { className: "ss-text", children: "We will email you a code or a link to sign in." }), segmentstreamSignInUrl && (_jsx("a", { className: "ss-button ss-button--secondary", href: segmentstreamSignInUrl, children: "Continue with SegmentStream" })), _jsxs("form", { className: "ss-form", onSubmit: (event) => {
46
+ event.preventDefault();
47
+ void submitEmail();
48
+ }, noValidate: true, children: [linkInvalid && (_jsx("p", { className: "ss-notice", role: "status", children: "This sign-in link is not valid anymore. Enter your email to get a new code." })), _jsxs("div", { className: "ss-field", children: [_jsx("label", { className: "ss-label", htmlFor: `${ids}-email`, children: "Email" }), _jsx("input", { id: `${ids}-email`, className: "ss-input", type: "email", name: "email", autoComplete: "email", inputMode: "email", required: true, value: email, onChange: (event) => setEmail(event.target.value), disabled: pending !== null, "aria-invalid": error !== null, "aria-describedby": error === null ? undefined : errorId })] }), error !== null && (_jsx("p", { id: errorId, className: "ss-error", role: "alert", children: error })), _jsx("button", { type: "submit", className: "ss-button", disabled: pending !== null, "aria-busy": pending === "send", children: pending === "send" ? "Sending…" : "Continue" })] })] }));
49
+ }
50
+ const resendButton = (label) => (_jsx("button", { type: "button", className: "ss-link", onClick: () => void resend(), disabled: pending !== null || resendIn > 0, "aria-busy": pending === "resend", children: pending === "resend" ? "Sending…" : resendIn > 0 ? `${label} in ${resendIn} s` : label }));
51
+ const anotherEmail = (_jsx("button", { type: "button", className: "ss-link", onClick: changeEmail, disabled: pending !== null, children: "Use another email" }));
52
+ if (step === "link") {
53
+ return (_jsxs("section", { className: "ss-card ss-sign-in", "aria-labelledby": `${ids}-title`, children: [_jsx("h1", { id: `${ids}-title`, className: "ss-title", children: "Check your email" }), _jsxs("p", { className: "ss-text", children: ["We sent a sign-in link to ", _jsx("strong", { children: email }), ". Open it on this device to sign in."] }), error !== null && (_jsx("p", { className: "ss-error", role: "alert", children: error })), notice !== null && (_jsx("p", { className: "ss-notice", role: "status", children: notice })), _jsxs("div", { className: "ss-actions", children: [resendButton("Resend link"), anotherEmail] })] }));
54
+ }
55
+ return (_jsxs("section", { className: "ss-card ss-sign-in", "aria-labelledby": `${ids}-title`, children: [_jsx("h1", { id: `${ids}-title`, className: "ss-title", children: "Check your email" }), _jsxs("p", { className: "ss-text", children: ["Enter the ", codeLength, "-digit code we sent to ", _jsx("strong", { children: email }), "."] }), _jsxs("form", { className: "ss-form", onSubmit: (event) => {
56
+ event.preventDefault();
57
+ void submitCode();
58
+ }, noValidate: true, children: [_jsxs("div", { className: "ss-field", children: [_jsx("label", { className: "ss-label", htmlFor: `${ids}-code`, children: "Code" }), _jsx("input", { ref: codeInput, id: `${ids}-code`, className: "ss-input ss-input--code", type: "text", name: "code", inputMode: "numeric", autoComplete: "one-time-code", pattern: "[0-9]*", maxLength: codeLength, required: true, value: code, onChange: (event) => {
59
+ const digits = event.target.value.replace(/\D/g, "").slice(0, codeLength);
60
+ setCode(digits);
61
+ if (digits.length === codeLength) {
62
+ void submitCode(digits);
63
+ }
64
+ }, disabled: pending === "verify", "aria-invalid": error !== null, "aria-describedby": error === null ? undefined : errorId })] }), error !== null && (_jsx("p", { id: errorId, className: "ss-error", role: "alert", children: error })), notice !== null && (_jsx("p", { className: "ss-notice", role: "status", children: notice })), _jsx("button", { type: "submit", className: "ss-button", disabled: pending !== null, "aria-busy": pending === "verify", children: pending === "verify" ? "Signing in…" : "Sign in" })] }), _jsxs("div", { className: "ss-actions", children: [resendButton("Resend code"), anotherEmail] })] }));
65
+ }
@@ -0,0 +1,80 @@
1
+ import { createAuthClient } from "@neondatabase/auth/next";
2
+ import { NOT_INVITED, SegmentStreamApiError } from "../errors.js";
3
+ import type { CurrentUser } from "../me.js";
4
+ /** The Neon Auth API the app serves from `app/src/app/api/auth/[...path]/route.ts`. */
5
+ export declare const AUTH_API_PATH = "/api/auth";
6
+ /** The app's sign-in page. */
7
+ export declare const SIGN_IN_PAGE = "/sign-in";
8
+ export { NOT_INVITED };
9
+ /** The person Neon Auth has signed in, before the ontology is asked about them. */
10
+ export interface SessionUser {
11
+ readonly id: string;
12
+ readonly email: string | null;
13
+ readonly name: string | null;
14
+ }
15
+ export type SessionState = {
16
+ readonly status: "loading";
17
+ } | {
18
+ readonly status: "signed-out";
19
+ readonly source?: "oidc";
20
+ } | {
21
+ readonly status: "signed-in";
22
+ readonly sessionId: string;
23
+ readonly user: SessionUser;
24
+ readonly source?: "channel" | "oidc";
25
+ readonly expiresAt?: string;
26
+ } | {
27
+ readonly status: "unavailable";
28
+ readonly source?: "oidc";
29
+ };
30
+ /** `useUser()`: the `/api/ontology/me` answer for the session, loaded once per session. */
31
+ export type UserState = {
32
+ readonly status: "loading";
33
+ readonly user: null;
34
+ readonly error: null;
35
+ } | {
36
+ readonly status: "signed-out";
37
+ readonly user: null;
38
+ readonly error: null;
39
+ } | {
40
+ readonly status: "signed-in";
41
+ readonly user: CurrentUser;
42
+ readonly error: null;
43
+ } | {
44
+ readonly status: "error";
45
+ readonly user: null;
46
+ readonly error: SegmentStreamApiError;
47
+ };
48
+ interface Snapshot {
49
+ readonly session: SessionState;
50
+ /** `/me` answers by Neon Auth session id. */
51
+ readonly users: ReadonlyMap<string, UserState>;
52
+ }
53
+ export declare const LOADING_USER: UserState;
54
+ export declare const SIGNED_OUT_USER: UserState;
55
+ export declare function sessionWasEnded(): boolean;
56
+ /** The Neon Auth browser client, created on first use. It talks to the app's `/api/auth`. */
57
+ export declare function authClient(): ReturnType<typeof createAuthClient>;
58
+ export declare function subscribe(listener: () => void): () => void;
59
+ export declare function getSnapshot(): Snapshot;
60
+ export declare function getServerSnapshot(): Snapshot;
61
+ /** Reads the session again, after signing in or out. */
62
+ export declare function refreshSession(): Promise<void>;
63
+ /** Forgets the session and every `/me` answer, after signing out. */
64
+ export declare function clearSession(): void;
65
+ /** Ends the selected authentication session before any browser state is cleared. */
66
+ export declare function endBrowserSession(): Promise<void>;
67
+ export interface AppSignInConfiguration {
68
+ readonly enabled: boolean;
69
+ readonly neon: boolean;
70
+ readonly channels: Readonly<Record<string, boolean>>;
71
+ readonly browser?: {
72
+ readonly methods: readonly ("email" | "segmentstream")[];
73
+ readonly primary: "email" | "segmentstream";
74
+ readonly auto_redirect: boolean;
75
+ } | null;
76
+ }
77
+ /** Public, non-secret app opt-ins from the existing SDK handlers. No provider is enabled by default. */
78
+ export declare function appSignInConfiguration(): Promise<AppSignInConfiguration | null>;
79
+ /** Starts loading `/me` for a session unless it is loaded or loading. */
80
+ export declare function ensureUser(sessionId: string, source?: "channel" | "oidc"): void;