@segmentstream-cloud/app 0.17.1 → 0.18.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.
@@ -1,10 +1,14 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ChannelSignInState } from "./use-channel-sign-in.js";
1
3
  import type { SignInOptions } from "./use-sign-in.js";
2
4
  export interface SignInProps extends SignInOptions {
3
5
  /** The heading. Defaults to "Sign in". */
4
6
  readonly title?: string;
7
+ /** App-owned rendering for channel status/error codes; no SDK error copy is supplied. */
8
+ readonly renderTelegram?: (state: ChannelSignInState) => ReactNode;
5
9
  }
6
10
  /**
7
11
  * Email sign-in with Neon Auth in the SDK's neutral styles: `useSignIn()` with markup. Apps
8
12
  * with their own design system render `useSignIn()` themselves.
9
13
  */
10
- export declare function SignIn({ title, ...options }: SignInProps): import("react").JSX.Element;
14
+ export declare function SignIn({ renderTelegram, ...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;
@@ -1,12 +1,26 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useId, useRef } from "react";
4
+ import { telegramInitData, useTelegramSignIn } from "./telegram-sign-in.js";
4
5
  import { useSignIn } from "./use-sign-in.js";
5
6
  /**
6
7
  * Email sign-in with Neon Auth in the SDK's neutral styles: `useSignIn()` with markup. Apps
7
8
  * with their own design system render `useSignIn()` themselves.
8
9
  */
9
- export function SignIn({ title = "Sign in", ...options }) {
10
+ export function SignIn({ renderTelegram, ...options }) {
11
+ const destination = typeof window === "undefined"
12
+ ? "/"
13
+ : (new URLSearchParams(window.location.search).get("returnTo") ?? "/");
14
+ const telegram = useTelegramSignIn({ redirectTo: options.returnTo ?? destination });
15
+ const active = telegram.status !== "loading" &&
16
+ typeof window !== "undefined" &&
17
+ telegramInitData() !== undefined;
18
+ if (active && telegram.status !== "disabled") {
19
+ return renderTelegram?.(telegram) ?? null;
20
+ }
21
+ return _jsx(EmailSignIn, { ...options });
22
+ }
23
+ function EmailSignIn({ title = "Sign in", ...options }) {
10
24
  const ids = useId();
11
25
  const { step, segmentstreamSignInUrl, email, setEmail, code, setCode, pending, error, notice, resendIn, codeLength, destination, signedIn, notInvited, linkInvalid, sessionEmail, submitEmail, submitCode, resend, changeEmail, switchAccount, } = useSignIn(options);
12
26
  const codeInput = useRef(null);
@@ -16,12 +16,16 @@ export type SessionState = {
16
16
  readonly status: "loading";
17
17
  } | {
18
18
  readonly status: "signed-out";
19
+ readonly source?: "oidc";
19
20
  } | {
20
21
  readonly status: "signed-in";
21
22
  readonly sessionId: string;
22
23
  readonly user: SessionUser;
24
+ readonly source?: "channel" | "oidc";
25
+ readonly expiresAt?: string;
23
26
  } | {
24
27
  readonly status: "unavailable";
28
+ readonly source?: "oidc";
25
29
  };
26
30
  /** `useUser()`: the `/api/ontology/me` answer for the session, loaded once per session. */
27
31
  export type UserState = {
@@ -48,6 +52,7 @@ interface Snapshot {
48
52
  }
49
53
  export declare const LOADING_USER: UserState;
50
54
  export declare const SIGNED_OUT_USER: UserState;
55
+ export declare function sessionWasEnded(): boolean;
51
56
  /** The Neon Auth browser client, created on first use. It talks to the app's `/api/auth`. */
52
57
  export declare function authClient(): ReturnType<typeof createAuthClient>;
53
58
  export declare function subscribe(listener: () => void): () => void;
@@ -57,5 +62,14 @@ export declare function getServerSnapshot(): Snapshot;
57
62
  export declare function refreshSession(): Promise<void>;
58
63
  /** Forgets the session and every `/me` answer, after signing out. */
59
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
+ }
72
+ /** Public, non-secret app opt-ins from the existing SDK handlers. No provider is enabled by default. */
73
+ export declare function appSignInConfiguration(): Promise<AppSignInConfiguration | null>;
60
74
  /** Starts loading `/me` for a session unless it is loaded or loading. */
61
- export declare function ensureUser(sessionId: string): void;
75
+ export declare function ensureUser(sessionId: string, source?: "channel" | "oidc"): void;
package/dist/ui/store.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { createAuthClient } from "@neondatabase/auth/next";
3
+ import { CHANNEL_LOGOUT_PATH, CHANNEL_SESSION_PATH } from "../channel-protocol.js";
3
4
  import { NOT_INVITED, SegmentStreamApiError, apiErrorFrom, readJson } from "../errors.js";
4
5
  import { ME_PATH, parseMe } from "../me.js";
5
6
  /** The Neon Auth API the app serves from `app/src/app/api/auth/[...path]/route.ts`. */
@@ -12,9 +13,14 @@ export const SIGNED_OUT_USER = { status: "signed-out", user: null, error: null }
12
13
  const SERVER_SNAPSHOT = { session: { status: "loading" }, users: new Map() };
13
14
  let snapshot = SERVER_SNAPSHOT;
14
15
  let sessionLoad = null;
16
+ let sessionGeneration = 0;
15
17
  const loadingUsers = new Set();
16
18
  const listeners = new Set();
17
19
  let client;
20
+ let explicitlySignedOut = false;
21
+ export function sessionWasEnded() {
22
+ return explicitlySignedOut;
23
+ }
18
24
  /** The Neon Auth browser client, created on first use. It talks to the app's `/api/auth`. */
19
25
  export function authClient() {
20
26
  client ??= createAuthClient();
@@ -41,30 +47,133 @@ export function getServerSnapshot() {
41
47
  }
42
48
  /** Reads the session again, after signing in or out. */
43
49
  export function refreshSession() {
50
+ if (snapshot.session.status === "unavailable") {
51
+ configurationLoad = undefined;
52
+ }
44
53
  sessionLoad = loadSession();
45
54
  return sessionLoad;
46
55
  }
47
56
  /** Forgets the session and every `/me` answer, after signing out. */
48
57
  export function clearSession() {
58
+ sessionGeneration++;
59
+ explicitlySignedOut = true;
49
60
  loadingUsers.clear();
50
61
  update({ session: { status: "signed-out" }, users: new Map() });
51
62
  }
52
- async function loadSession() {
53
- let session;
63
+ /** Ends the selected authentication session before any browser state is cleared. */
64
+ export async function endBrowserSession() {
65
+ if (snapshot.session.status !== "signed-in" || snapshot.session.source !== "channel") {
66
+ if (snapshot.session.status === "signed-in" && snapshot.session.source === "oidc") {
67
+ const response = await fetch("/api/segmentstream/sign-out", {
68
+ method: "POST",
69
+ credentials: "same-origin",
70
+ });
71
+ if (!response.ok) {
72
+ throw new Error("logout_failed");
73
+ }
74
+ }
75
+ else {
76
+ await authClient().signOut();
77
+ }
78
+ return;
79
+ }
80
+ const response = await fetch(CHANNEL_LOGOUT_PATH, {
81
+ method: "POST",
82
+ credentials: "same-origin",
83
+ });
84
+ if (!response.ok) {
85
+ throw new Error("logout_failed");
86
+ }
87
+ }
88
+ let configurationLoad;
89
+ /** Public, non-secret app opt-ins from the existing SDK handlers. No provider is enabled by default. */
90
+ export function appSignInConfiguration() {
91
+ configurationLoad ??= (async () => {
92
+ try {
93
+ const response = await fetch("/api/segmentstream/configuration", {
94
+ credentials: "same-origin",
95
+ cache: "no-store",
96
+ });
97
+ const body = response.ok ? await readJson(response) : null;
98
+ if (!body || typeof body !== "object" || !("enabled" in body)) {
99
+ return null;
100
+ }
101
+ const { enabled } = body;
102
+ // Older SDK handlers omit this field; keep their existing session discovery precedence.
103
+ const neon = "neon" in body ? body.neon : true;
104
+ const channels = "channels" in body ? body.channels : {};
105
+ if (typeof enabled !== "boolean" ||
106
+ typeof neon !== "boolean" ||
107
+ !channels ||
108
+ typeof channels !== "object" ||
109
+ Array.isArray(channels) ||
110
+ Object.values(channels).some((value) => typeof value !== "boolean")) {
111
+ return null;
112
+ }
113
+ return { enabled, neon, channels: channels };
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ })();
119
+ return configurationLoad;
120
+ }
121
+ async function discover(path, source) {
54
122
  try {
55
- const response = await fetch(`${AUTH_API_PATH}/get-session`, {
123
+ const response = await fetch(path, {
56
124
  credentials: "same-origin",
57
125
  cache: "no-store",
58
126
  headers: { accept: "application/json" },
59
127
  });
60
- session = response.ok
61
- ? sessionFrom(await readJson(response))
128
+ const payload = await readJson(response);
129
+ const session = response.ok
130
+ ? sessionFrom(payload)
62
131
  : response.status === 401
63
132
  ? { status: "signed-out" }
64
133
  : { status: "unavailable" };
134
+ // Channel/OIDC endpoints confirm absence with 401. A malformed success is an outage,
135
+ // and must not select a different user from a lower-priority provider.
136
+ if (source && response.ok && session.status === "signed-out") {
137
+ return { status: "unavailable" };
138
+ }
139
+ if (source === "oidc" &&
140
+ (session.status === "signed-out" || session.status === "unavailable") &&
141
+ payload &&
142
+ typeof payload === "object" &&
143
+ payload.source === "oidc") {
144
+ return { ...session, source: "oidc" };
145
+ }
146
+ return session.status === "signed-in" && source
147
+ ? {
148
+ ...session,
149
+ sessionId: `${source}:${session.sessionId}`,
150
+ source,
151
+ }
152
+ : session;
65
153
  }
66
154
  catch {
67
- session = { status: "unavailable" };
155
+ return { status: "unavailable" };
156
+ }
157
+ }
158
+ async function loadSession() {
159
+ const generation = ++sessionGeneration;
160
+ const config = await appSignInConfiguration();
161
+ let session = { status: config ? "signed-out" : "unavailable" };
162
+ // Same precedence as ontology cookie authentication and app server helpers: channel, OIDC, Neon.
163
+ if (config && Object.values(config.channels).some((enabled) => enabled === true)) {
164
+ session = await discover(CHANNEL_SESSION_PATH, "channel");
165
+ }
166
+ if (session.status === "signed-out" && config?.enabled) {
167
+ session = await discover("/api/segmentstream/session", "oidc");
168
+ }
169
+ if (session.status === "signed-out" && session.source !== "oidc" && config?.neon) {
170
+ session = await discover(`${AUTH_API_PATH}/get-session`);
171
+ }
172
+ if (generation !== sessionGeneration) {
173
+ return;
174
+ }
175
+ if (session.status === "signed-in") {
176
+ explicitlySignedOut = false;
68
177
  }
69
178
  update({ session });
70
179
  }
@@ -79,7 +188,7 @@ function sessionFrom(payload) {
79
188
  user === null) {
80
189
  return { status: "signed-out" };
81
190
  }
82
- const { id: sessionId } = session;
191
+ const { id: sessionId, expires_at } = session;
83
192
  const { id, email, name } = user;
84
193
  if (typeof sessionId !== "string" || typeof id !== "string") {
85
194
  return { status: "signed-out" };
@@ -87,6 +196,9 @@ function sessionFrom(payload) {
87
196
  return {
88
197
  status: "signed-in",
89
198
  sessionId,
199
+ ...(typeof expires_at === "string" && Number.isFinite(Date.parse(expires_at))
200
+ ? { expiresAt: expires_at }
201
+ : {}),
90
202
  user: {
91
203
  id,
92
204
  email: typeof email === "string" ? email : null,
@@ -95,12 +207,12 @@ function sessionFrom(payload) {
95
207
  };
96
208
  }
97
209
  /** Starts loading `/me` for a session unless it is loaded or loading. */
98
- export function ensureUser(sessionId) {
210
+ export function ensureUser(sessionId, source) {
99
211
  if (snapshot.users.has(sessionId) || loadingUsers.has(sessionId)) {
100
212
  return;
101
213
  }
102
214
  loadingUsers.add(sessionId);
103
- void loadUser().then((state) => {
215
+ void loadUser(source).then((state) => {
104
216
  if (!loadingUsers.delete(sessionId)) {
105
217
  return; // Signed out meanwhile.
106
218
  }
@@ -108,26 +220,34 @@ export function ensureUser(sessionId) {
108
220
  });
109
221
  }
110
222
  /** The session's JWT from Neon Auth, then the ontology's account of the user. */
111
- async function loadUser() {
223
+ async function loadUser(source) {
112
224
  try {
113
- const tokenResponse = await fetch(`${AUTH_API_PATH}/token`, {
114
- credentials: "same-origin",
115
- cache: "no-store",
116
- headers: { accept: "application/json" },
117
- });
118
- if (tokenResponse.status === 401) {
119
- return SIGNED_OUT_USER;
120
- }
121
- const tokenPayload = await readJson(tokenResponse);
122
- const token = typeof tokenPayload === "object" && tokenPayload !== null
123
- ? tokenPayload.token
124
- : undefined;
125
- if (!tokenResponse.ok || typeof token !== "string" || token === "") {
126
- return failed("auth_unavailable", 503, "Neon Auth did not issue a token. Try again.");
225
+ let token;
226
+ if (source === undefined) {
227
+ const tokenResponse = await fetch(`${AUTH_API_PATH}/token`, {
228
+ credentials: "same-origin",
229
+ cache: "no-store",
230
+ headers: { accept: "application/json" },
231
+ });
232
+ if (tokenResponse.status === 401) {
233
+ return SIGNED_OUT_USER;
234
+ }
235
+ const tokenPayload = await readJson(tokenResponse);
236
+ token =
237
+ typeof tokenPayload === "object" && tokenPayload !== null
238
+ ? tokenPayload.token
239
+ : undefined;
240
+ if (!tokenResponse.ok || typeof token !== "string" || token === "") {
241
+ return failed("auth_unavailable", 503, "Neon Auth did not issue a token. Try again.");
242
+ }
127
243
  }
128
244
  const response = await fetch(ME_PATH, {
129
245
  cache: "no-store",
130
- headers: { accept: "application/json", authorization: `Bearer ${token}` },
246
+ credentials: "same-origin",
247
+ headers: {
248
+ accept: "application/json",
249
+ ...(typeof token === "string" ? { authorization: `Bearer ${token}` } : {}),
250
+ },
131
251
  });
132
252
  const payload = await readJson(response);
133
253
  if (!response.ok) {
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from "react";
2
+ import { type ChannelSignInState } from "./use-channel-sign-in.js";
3
+ export interface TelegramSignInOptions {
4
+ readonly redirectTo?: string;
5
+ /** Read fresh signed initData from the app's Telegram integration. */
6
+ readonly initData?: () => string | undefined;
7
+ }
8
+ /** Telegram carries launch data in a URL fragment, which browsers inherit through HTTP redirects. */
9
+ export declare function telegramInitData(): string | undefined;
10
+ /** Works on any app page; the public SDK configuration must explicitly enable Telegram. */
11
+ export declare function useTelegramSignIn(options?: TelegramSignInOptions): ChannelSignInState;
12
+ export interface TelegramSignInProps extends TelegramSignInOptions {
13
+ readonly children?: (state: ChannelSignInState) => ReactNode;
14
+ }
15
+ /** Headless component: the app owns all labels and error messages. */
16
+ export declare function TelegramSignIn({ children, ...options }: TelegramSignInProps): string | number | bigint | boolean | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { useChannelSignIn } from "./use-channel-sign-in.js";
3
+ /** Telegram carries launch data in a URL fragment, which browsers inherit through HTTP redirects. */
4
+ export function telegramInitData() {
5
+ const telegram = window.Telegram;
6
+ return (telegram?.WebApp?.initData ||
7
+ new URLSearchParams(window.location.hash.slice(1)).get("tgWebAppData") ||
8
+ undefined);
9
+ }
10
+ /** Works on any app page; the public SDK configuration must explicitly enable Telegram. */
11
+ export function useTelegramSignIn(options = {}) {
12
+ return useChannelSignIn("telegram", {
13
+ ...options,
14
+ assertion: options.initData ?? telegramInitData,
15
+ });
16
+ }
17
+ /** Headless component: the app owns all labels and error messages. */
18
+ export function TelegramSignIn({ children, ...options }) {
19
+ const state = useTelegramSignIn(options);
20
+ return children?.(state) ?? null;
21
+ }
@@ -0,0 +1,10 @@
1
+ export type ChannelSignInState = {
2
+ readonly status: "loading" | "disabled" | "idle" | "pending" | "signed_in" | "error";
3
+ readonly error: string | null;
4
+ };
5
+ export interface ChannelSignInOptions {
6
+ readonly assertion: () => string | undefined;
7
+ readonly redirectTo?: string;
8
+ }
9
+ /** One exchange per assertion, with an absolute expiry timer. Errors are app-rendered codes. */
10
+ export declare function useChannelSignIn(provider: string, options: ChannelSignInOptions): ChannelSignInState;
@@ -0,0 +1,118 @@
1
+ "use client";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { channelSignInPath } from "../channel-protocol.js";
4
+ import { safeReturnTo } from "../return-to.js";
5
+ import { useSession } from "./hooks.js";
6
+ import { navigate } from "./navigation.js";
7
+ import { appSignInConfiguration, getSnapshot, refreshSession, sessionWasEnded } from "./store.js";
8
+ // A layout and the SDK sign-in page may mount together. Retain only the latest attempt per provider,
9
+ // in memory, so both share one exchange/discovery and never retry the same launch assertion.
10
+ const attempts = new Map();
11
+ function exchange(provider, assertion) {
12
+ const previous = attempts.get(provider);
13
+ if (previous?.assertion === assertion) {
14
+ return previous.result;
15
+ }
16
+ const result = (async () => {
17
+ try {
18
+ const response = await fetch(channelSignInPath(provider), {
19
+ method: "POST",
20
+ credentials: "same-origin",
21
+ headers: { "content-type": "application/json" },
22
+ body: JSON.stringify({ assertion }),
23
+ });
24
+ if (!response.ok) {
25
+ const body = (await response.json());
26
+ return typeof body.error === "string" ? body.error : "sign_in_failed";
27
+ }
28
+ await refreshSession();
29
+ return getSnapshot().session.status === "signed-in" ? null : "session_unavailable";
30
+ }
31
+ catch {
32
+ return "sign_in_unavailable";
33
+ }
34
+ })();
35
+ attempts.set(provider, { assertion, result });
36
+ return result;
37
+ }
38
+ /** One exchange per assertion, with an absolute expiry timer. Errors are app-rendered codes. */
39
+ export function useChannelSignIn(provider, options) {
40
+ const session = useSession();
41
+ const [enabled, setEnabled] = useState(undefined);
42
+ const [settled, setSettled] = useState();
43
+ const [expired, setExpired] = useState();
44
+ const redirected = useRef(false);
45
+ const raw = enabled && typeof window !== "undefined" ? options.assertion() : undefined;
46
+ const redirectTo = options.redirectTo;
47
+ useEffect(() => {
48
+ let mounted = true;
49
+ void appSignInConfiguration().then((config) => {
50
+ if (mounted) {
51
+ setEnabled(config === null ? null : config.channels[provider] === true);
52
+ }
53
+ });
54
+ return () => {
55
+ mounted = false;
56
+ };
57
+ }, [provider, session]);
58
+ useEffect(() => {
59
+ if (!enabled ||
60
+ session.status !== "signed-in" ||
61
+ session.source !== "channel" ||
62
+ !session.expiresAt ||
63
+ expired === session.sessionId) {
64
+ return;
65
+ }
66
+ const timer = window.setTimeout(() => {
67
+ setExpired(session.sessionId);
68
+ void refreshSession();
69
+ }, Math.max(0, Date.parse(session.expiresAt) - Date.now()));
70
+ return () => window.clearTimeout(timer);
71
+ }, [session, enabled, expired]);
72
+ useEffect(() => {
73
+ if (!enabled || session.status === "loading" || session.status === "unavailable") {
74
+ return;
75
+ }
76
+ if (session.status === "signed-in" && expired !== session.sessionId) {
77
+ if (raw && redirectTo !== undefined && !redirected.current) {
78
+ redirected.current = true;
79
+ navigate(safeReturnTo(redirectTo));
80
+ }
81
+ return;
82
+ }
83
+ if (sessionWasEnded() || !raw) {
84
+ return;
85
+ }
86
+ let mounted = true;
87
+ void exchange(provider, raw).then((error) => {
88
+ if (mounted) {
89
+ setSettled({ assertion: raw, error });
90
+ }
91
+ });
92
+ return () => {
93
+ mounted = false;
94
+ };
95
+ }, [session, enabled, provider, raw, redirectTo, expired]);
96
+ if (enabled === undefined || session.status === "loading") {
97
+ return { status: "loading", error: null };
98
+ }
99
+ if (enabled === null) {
100
+ return { status: "error", error: "configuration_unavailable" };
101
+ }
102
+ if (!enabled) {
103
+ return { status: "disabled", error: null };
104
+ }
105
+ if (session.status === "unavailable") {
106
+ return { status: "error", error: "session_unavailable" };
107
+ }
108
+ if (session.status === "signed-in" && expired !== session.sessionId) {
109
+ return { status: "signed_in", error: null };
110
+ }
111
+ if (sessionWasEnded() || !raw) {
112
+ return { status: "idle", error: null };
113
+ }
114
+ if (settled?.assertion === raw) {
115
+ return { status: "error", error: settled.error ?? "fresh_assertion_required" };
116
+ }
117
+ return { status: "pending", error: null };
118
+ }
@@ -5,7 +5,7 @@ import { safeReturnTo } from "../return-to.js";
5
5
  import { useSession } from "./hooks.js";
6
6
  import { navigate } from "./navigation.js";
7
7
  import { checkSignIn } from "./sign-in-check.js";
8
- import { authClient, clearSession, refreshSession } from "./store.js";
8
+ import { authClient, clearSession, endBrowserSession, refreshSession } from "./store.js";
9
9
  /** Seconds before another code or link may be requested. */
10
10
  const RESEND_AFTER_SECONDS = 30;
11
11
  const CODE_LENGTH = 6;
@@ -175,7 +175,15 @@ export function useSignIn({ returnTo, error: reason } = {}) {
175
175
  async function switchAccount() {
176
176
  setPending("sign-out");
177
177
  setErrorCode(null);
178
- await attempt(() => authClient().signOut());
178
+ const result = await attempt(async () => {
179
+ await endBrowserSession();
180
+ return { error: null };
181
+ });
182
+ if (result !== null) {
183
+ setErrorCode(signInErrorCode(result));
184
+ setPending(null);
185
+ return;
186
+ }
179
187
  clearSession();
180
188
  await refreshSession();
181
189
  setSwitchingAccount(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream-cloud/app",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Sign-in, users, permissions, the ontology client and warehouse data access for SegmentStream workspace apps built with Next.js.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SegmentStream",
@@ -46,10 +46,10 @@
46
46
  "node": ">=20"
47
47
  },
48
48
  "scripts": {
49
- "build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/ui/styles.css dist/ui/styles.css",
50
- "typecheck": "tsc -p tsconfig.json --noEmit",
51
- "lint": "eslint src tests --config ../../eslint.config.mjs",
52
- "test": "vitest run",
49
+ "build": "node scripts/prepare-channel-protocol.mjs && rm -rf dist && tsc -p tsconfig.build.json && cp src/ui/styles.css dist/ui/styles.css",
50
+ "typecheck": "node scripts/prepare-channel-protocol.mjs && tsc -p tsconfig.json --noEmit",
51
+ "lint": "node scripts/prepare-channel-protocol.mjs && eslint src tests --config ../../eslint.config.mjs",
52
+ "test": "node scripts/prepare-channel-protocol.mjs && vitest run",
53
53
  "prepack": "npm run build"
54
54
  },
55
55
  "dependencies": {