@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.
- package/LICENSE +201 -0
- package/NOTICE +2 -0
- package/README.md +1236 -2
- package/dist/agent.d.ts +56 -0
- package/dist/agent.js +203 -0
- package/dist/app.d.ts +82 -0
- package/dist/app.js +434 -0
- package/dist/browser-auth-capability.d.ts +2 -0
- package/dist/browser-auth-capability.js +2 -0
- package/dist/browser-revision.d.ts +19 -0
- package/dist/browser-revision.js +140 -0
- package/dist/channel-protocol.d.ts +16 -0
- package/dist/channel-protocol.js +17 -0
- package/dist/channel-session.d.ts +22 -0
- package/dist/channel-session.js +89 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.js +112 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.js +1 -0
- package/dist/cookies.d.ts +56 -0
- package/dist/cookies.js +131 -0
- package/dist/data.d.ts +233 -0
- package/dist/data.js +206 -0
- package/dist/decision-pages.d.ts +14 -0
- package/dist/decision-pages.js +87 -0
- package/dist/deployment.d.ts +30 -0
- package/dist/deployment.js +72 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.js +123 -0
- package/dist/federation.d.ts +24 -0
- package/dist/federation.js +613 -0
- package/dist/groups.d.ts +95 -0
- package/dist/groups.js +280 -0
- package/dist/handlers.d.ts +10 -0
- package/dist/handlers.js +161 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +29 -0
- package/dist/jobs.d.ts +49 -0
- package/dist/jobs.js +40 -0
- package/dist/legacy.d.ts +77 -0
- package/dist/legacy.js +57 -0
- package/dist/logo.d.ts +1 -0
- package/dist/logo.js +25 -0
- package/dist/me.d.ts +42 -0
- package/dist/me.js +54 -0
- package/dist/navigation/build.d.ts +68 -0
- package/dist/navigation/build.js +290 -0
- package/dist/navigation/cli.d.ts +19 -0
- package/dist/navigation/cli.js +37 -0
- package/dist/navigation/generate.d.ts +43 -0
- package/dist/navigation/generate.js +138 -0
- package/dist/navigation/next.d.ts +10 -0
- package/dist/navigation/next.js +18 -0
- package/dist/navigation-protocol.d.ts +55 -0
- package/dist/navigation-protocol.js +298 -0
- package/dist/neon.d.ts +101 -0
- package/dist/neon.js +539 -0
- package/dist/oauth-completion.d.ts +6 -0
- package/dist/oauth-completion.js +106 -0
- package/dist/oauth.d.ts +59 -0
- package/dist/oauth.js +129 -0
- package/dist/ontology-names.d.ts +10 -0
- package/dist/ontology-names.js +38 -0
- package/dist/ontology-transport.d.ts +34 -0
- package/dist/ontology-transport.js +130 -0
- package/dist/ontology-wire.d.ts +66 -0
- package/dist/ontology-wire.js +355 -0
- package/dist/ontology.d.ts +381 -0
- package/dist/ontology.js +367 -0
- package/dist/pages.d.ts +6 -0
- package/dist/pages.js +6 -0
- package/dist/parameters.d.ts +55 -0
- package/dist/parameters.js +118 -0
- package/dist/permissions.d.ts +19 -0
- package/dist/permissions.js +55 -0
- package/dist/pkce.d.ts +8 -0
- package/dist/pkce.js +40 -0
- package/dist/proxy.d.ts +25 -0
- package/dist/proxy.js +135 -0
- package/dist/request-timing.d.ts +6 -0
- package/dist/request-timing.js +53 -0
- package/dist/return-to.d.ts +2 -0
- package/dist/return-to.js +24 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +80 -0
- package/dist/sessions.d.ts +27 -0
- package/dist/sessions.js +72 -0
- package/dist/sign-in.d.ts +37 -0
- package/dist/sign-in.js +95 -0
- package/dist/storage.d.ts +79 -0
- package/dist/storage.js +180 -0
- package/dist/telegram-launch.d.ts +7 -0
- package/dist/telegram-launch.js +134 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +6 -0
- package/dist/tokens.d.ts +51 -0
- package/dist/tokens.js +70 -0
- package/dist/ui/agent-connect.d.ts +18 -0
- package/dist/ui/agent-connect.js +23 -0
- package/dist/ui/components.d.ts +27 -0
- package/dist/ui/components.js +71 -0
- package/dist/ui/hooks.d.ts +19 -0
- package/dist/ui/hooks.js +56 -0
- package/dist/ui/index.d.ts +22 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/navigation.d.ts +5 -0
- package/dist/ui/navigation.js +8 -0
- package/dist/ui/segmentstream-logo.d.ts +2 -0
- package/dist/ui/segmentstream-logo.js +7 -0
- package/dist/ui/sign-in-check.d.ts +14 -0
- package/dist/ui/sign-in-check.js +41 -0
- package/dist/ui/sign-in.d.ts +17 -0
- package/dist/ui/sign-in.js +65 -0
- package/dist/ui/store.d.ts +80 -0
- package/dist/ui/store.js +280 -0
- package/dist/ui/styles.css +249 -0
- package/dist/ui/telegram-sign-in.d.ts +16 -0
- package/dist/ui/telegram-sign-in.js +21 -0
- package/dist/ui/use-browser-sign-in.d.ts +52 -0
- package/dist/ui/use-browser-sign-in.js +169 -0
- package/dist/ui/use-channel-sign-in.d.ts +10 -0
- package/dist/ui/use-channel-sign-in.js +118 -0
- package/dist/ui/use-sign-in.d.ts +109 -0
- package/dist/ui/use-sign-in.js +304 -0
- package/package.json +95 -4
package/dist/ui/store.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createAuthClient } from "@neondatabase/auth/next";
|
|
3
|
+
import { CHANNEL_LOGOUT_PATH, CHANNEL_SESSION_PATH } from "../channel-protocol.js";
|
|
4
|
+
import { NOT_INVITED, SegmentStreamApiError, apiErrorFrom, readJson } from "../errors.js";
|
|
5
|
+
import { ME_PATH, parseMe } from "../me.js";
|
|
6
|
+
/** The Neon Auth API the app serves from `app/src/app/api/auth/[...path]/route.ts`. */
|
|
7
|
+
export const AUTH_API_PATH = "/api/auth";
|
|
8
|
+
/** The app's sign-in page. */
|
|
9
|
+
export const SIGN_IN_PAGE = "/sign-in";
|
|
10
|
+
export { NOT_INVITED };
|
|
11
|
+
export const LOADING_USER = { status: "loading", user: null, error: null };
|
|
12
|
+
export const SIGNED_OUT_USER = { status: "signed-out", user: null, error: null };
|
|
13
|
+
const SERVER_SNAPSHOT = { session: { status: "loading" }, users: new Map() };
|
|
14
|
+
let snapshot = SERVER_SNAPSHOT;
|
|
15
|
+
let sessionLoad = null;
|
|
16
|
+
let sessionGeneration = 0;
|
|
17
|
+
const loadingUsers = new Set();
|
|
18
|
+
const listeners = new Set();
|
|
19
|
+
let client;
|
|
20
|
+
let explicitlySignedOut = false;
|
|
21
|
+
export function sessionWasEnded() {
|
|
22
|
+
return explicitlySignedOut;
|
|
23
|
+
}
|
|
24
|
+
/** The Neon Auth browser client, created on first use. It talks to the app's `/api/auth`. */
|
|
25
|
+
export function authClient() {
|
|
26
|
+
client ??= createAuthClient();
|
|
27
|
+
return client;
|
|
28
|
+
}
|
|
29
|
+
function update(change) {
|
|
30
|
+
snapshot = { ...snapshot, ...change };
|
|
31
|
+
for (const listener of listeners) {
|
|
32
|
+
listener();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function subscribe(listener) {
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
sessionLoad ??= loadSession();
|
|
38
|
+
return () => {
|
|
39
|
+
listeners.delete(listener);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function getSnapshot() {
|
|
43
|
+
return snapshot;
|
|
44
|
+
}
|
|
45
|
+
export function getServerSnapshot() {
|
|
46
|
+
return SERVER_SNAPSHOT;
|
|
47
|
+
}
|
|
48
|
+
/** Reads the session again, after signing in or out. */
|
|
49
|
+
export function refreshSession() {
|
|
50
|
+
if (snapshot.session.status === "unavailable") {
|
|
51
|
+
configurationLoad = undefined;
|
|
52
|
+
}
|
|
53
|
+
sessionLoad = loadSession();
|
|
54
|
+
return sessionLoad;
|
|
55
|
+
}
|
|
56
|
+
/** Forgets the session and every `/me` answer, after signing out. */
|
|
57
|
+
export function clearSession() {
|
|
58
|
+
sessionGeneration++;
|
|
59
|
+
explicitlySignedOut = true;
|
|
60
|
+
loadingUsers.clear();
|
|
61
|
+
update({ session: { status: "signed-out" }, users: new Map() });
|
|
62
|
+
}
|
|
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 {
|
|
114
|
+
enabled,
|
|
115
|
+
neon,
|
|
116
|
+
channels: channels,
|
|
117
|
+
browser: body.browser,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
})();
|
|
124
|
+
return configurationLoad;
|
|
125
|
+
}
|
|
126
|
+
async function discover(path, source) {
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(path, {
|
|
129
|
+
credentials: "same-origin",
|
|
130
|
+
cache: "no-store",
|
|
131
|
+
headers: { accept: "application/json" },
|
|
132
|
+
});
|
|
133
|
+
const payload = await readJson(response);
|
|
134
|
+
const session = response.ok
|
|
135
|
+
? sessionFrom(payload)
|
|
136
|
+
: response.status === 401
|
|
137
|
+
? { status: "signed-out" }
|
|
138
|
+
: { status: "unavailable" };
|
|
139
|
+
// Channel/OIDC endpoints confirm absence with 401. A malformed success is an outage,
|
|
140
|
+
// and must not select a different user from a lower-priority provider.
|
|
141
|
+
if (source && response.ok && session.status === "signed-out") {
|
|
142
|
+
return { status: "unavailable" };
|
|
143
|
+
}
|
|
144
|
+
if (source === "oidc" &&
|
|
145
|
+
(session.status === "signed-out" || session.status === "unavailable") &&
|
|
146
|
+
payload &&
|
|
147
|
+
typeof payload === "object" &&
|
|
148
|
+
payload.source === "oidc") {
|
|
149
|
+
return { ...session, source: "oidc" };
|
|
150
|
+
}
|
|
151
|
+
return session.status === "signed-in" && source
|
|
152
|
+
? {
|
|
153
|
+
...session,
|
|
154
|
+
sessionId: `${source}:${session.sessionId}`,
|
|
155
|
+
source,
|
|
156
|
+
}
|
|
157
|
+
: session;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return { status: "unavailable" };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function loadSession() {
|
|
164
|
+
const generation = ++sessionGeneration;
|
|
165
|
+
const config = await appSignInConfiguration();
|
|
166
|
+
let session = { status: config ? "signed-out" : "unavailable" };
|
|
167
|
+
// Same precedence as ontology cookie authentication and app server helpers: channel, OIDC, Neon.
|
|
168
|
+
if (config && Object.values(config.channels).some((enabled) => enabled === true)) {
|
|
169
|
+
session = await discover(CHANNEL_SESSION_PATH, "channel");
|
|
170
|
+
}
|
|
171
|
+
if (session.status === "signed-out" && config?.enabled) {
|
|
172
|
+
session = await discover("/api/segmentstream/session", "oidc");
|
|
173
|
+
}
|
|
174
|
+
if (session.status === "signed-out" && session.source !== "oidc" && config?.neon) {
|
|
175
|
+
session = await discover(`${AUTH_API_PATH}/get-session`);
|
|
176
|
+
}
|
|
177
|
+
if (generation !== sessionGeneration) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (session.status === "signed-in") {
|
|
181
|
+
explicitlySignedOut = false;
|
|
182
|
+
}
|
|
183
|
+
update({ session });
|
|
184
|
+
}
|
|
185
|
+
function sessionFrom(payload) {
|
|
186
|
+
if (typeof payload !== "object" || payload === null) {
|
|
187
|
+
return { status: "signed-out" };
|
|
188
|
+
}
|
|
189
|
+
const { session, user } = payload;
|
|
190
|
+
if (typeof session !== "object" ||
|
|
191
|
+
session === null ||
|
|
192
|
+
typeof user !== "object" ||
|
|
193
|
+
user === null) {
|
|
194
|
+
return { status: "signed-out" };
|
|
195
|
+
}
|
|
196
|
+
const { id: sessionId, expires_at } = session;
|
|
197
|
+
const { id, email, name } = user;
|
|
198
|
+
if (typeof sessionId !== "string" || typeof id !== "string") {
|
|
199
|
+
return { status: "signed-out" };
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
status: "signed-in",
|
|
203
|
+
sessionId,
|
|
204
|
+
...(typeof expires_at === "string" && Number.isFinite(Date.parse(expires_at))
|
|
205
|
+
? { expiresAt: expires_at }
|
|
206
|
+
: {}),
|
|
207
|
+
user: {
|
|
208
|
+
id,
|
|
209
|
+
email: typeof email === "string" ? email : null,
|
|
210
|
+
name: typeof name === "string" ? name : null,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/** Starts loading `/me` for a session unless it is loaded or loading. */
|
|
215
|
+
export function ensureUser(sessionId, source) {
|
|
216
|
+
if (snapshot.users.has(sessionId) || loadingUsers.has(sessionId)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
loadingUsers.add(sessionId);
|
|
220
|
+
void loadUser(source).then((state) => {
|
|
221
|
+
if (!loadingUsers.delete(sessionId)) {
|
|
222
|
+
return; // Signed out meanwhile.
|
|
223
|
+
}
|
|
224
|
+
update({ users: new Map(snapshot.users).set(sessionId, state) });
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/** The session's JWT from Neon Auth, then the ontology's account of the user. */
|
|
228
|
+
async function loadUser(source) {
|
|
229
|
+
try {
|
|
230
|
+
let token;
|
|
231
|
+
if (source === undefined) {
|
|
232
|
+
const tokenResponse = await fetch(`${AUTH_API_PATH}/token`, {
|
|
233
|
+
credentials: "same-origin",
|
|
234
|
+
cache: "no-store",
|
|
235
|
+
headers: { accept: "application/json" },
|
|
236
|
+
});
|
|
237
|
+
if (tokenResponse.status === 401) {
|
|
238
|
+
return SIGNED_OUT_USER;
|
|
239
|
+
}
|
|
240
|
+
const tokenPayload = await readJson(tokenResponse);
|
|
241
|
+
token =
|
|
242
|
+
typeof tokenPayload === "object" && tokenPayload !== null
|
|
243
|
+
? tokenPayload.token
|
|
244
|
+
: undefined;
|
|
245
|
+
if (!tokenResponse.ok || typeof token !== "string" || token === "") {
|
|
246
|
+
return failed("auth_unavailable", 503, "Neon Auth did not issue a token. Try again.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const response = await fetch(ME_PATH, {
|
|
250
|
+
cache: "no-store",
|
|
251
|
+
credentials: "same-origin",
|
|
252
|
+
headers: {
|
|
253
|
+
accept: "application/json",
|
|
254
|
+
...(typeof token === "string" ? { authorization: `Bearer ${token}` } : {}),
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
const payload = await readJson(response);
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
return {
|
|
260
|
+
status: "error",
|
|
261
|
+
user: null,
|
|
262
|
+
error: apiErrorFrom(response.status, payload, "The ontology service rejected the user."),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const user = parseMe(payload);
|
|
266
|
+
return user === null
|
|
267
|
+
? failed("ontology_invalid_response", 502, "GET /api/ontology/me answered an unexpected body.")
|
|
268
|
+
: { status: "signed-in", user, error: null };
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
return failed("ontology_unreachable", 503, "The app's ontology could not be reached.", error);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function failed(code, status, detail, cause) {
|
|
275
|
+
return {
|
|
276
|
+
status: "error",
|
|
277
|
+
user: null,
|
|
278
|
+
error: new SegmentStreamApiError({ code, status, detail, retryable: true, cause }),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @segmentstream/app/ui: neutral styles for the sign-in form and the user menu.
|
|
3
|
+
* Each rule is one class, so the app's own class selectors override it; the custom properties
|
|
4
|
+
* below change colours, radius and font in one place.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--ss-font: inherit;
|
|
9
|
+
--ss-text: #111111;
|
|
10
|
+
--ss-muted: #5f5f5f;
|
|
11
|
+
--ss-surface: #ffffff;
|
|
12
|
+
--ss-border: #d4d4d4;
|
|
13
|
+
--ss-accent: #111111;
|
|
14
|
+
--ss-accent-text: #ffffff;
|
|
15
|
+
--ss-danger: #b42318;
|
|
16
|
+
--ss-focus: #2563eb;
|
|
17
|
+
--ss-radius: 0.5rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root {
|
|
22
|
+
--ss-text: #f5f5f5;
|
|
23
|
+
--ss-muted: #a3a3a3;
|
|
24
|
+
--ss-surface: #171717;
|
|
25
|
+
--ss-border: #3f3f3f;
|
|
26
|
+
--ss-accent: #f5f5f5;
|
|
27
|
+
--ss-accent-text: #111111;
|
|
28
|
+
--ss-danger: #f97066;
|
|
29
|
+
--ss-focus: #60a5fa;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ss-card {
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
width: min(24rem, 100%);
|
|
36
|
+
margin: 0 auto;
|
|
37
|
+
padding: 2rem;
|
|
38
|
+
border: 1px solid var(--ss-border);
|
|
39
|
+
border-radius: calc(var(--ss-radius) * 2);
|
|
40
|
+
background: var(--ss-surface);
|
|
41
|
+
color: var(--ss-text);
|
|
42
|
+
font-family: var(--ss-font);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.ss-title {
|
|
46
|
+
margin: 0 0 0.5rem;
|
|
47
|
+
font-size: 1.5rem;
|
|
48
|
+
line-height: 1.25;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.ss-text {
|
|
52
|
+
margin: 0 0 1.25rem;
|
|
53
|
+
color: var(--ss-muted);
|
|
54
|
+
line-height: 1.5;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ss-text strong {
|
|
58
|
+
color: var(--ss-text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.ss-form {
|
|
62
|
+
display: grid;
|
|
63
|
+
gap: 1rem;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ss-field {
|
|
67
|
+
display: grid;
|
|
68
|
+
gap: 0.375rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.ss-label {
|
|
72
|
+
font-size: 0.875rem;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.ss-input {
|
|
77
|
+
box-sizing: border-box;
|
|
78
|
+
width: 100%;
|
|
79
|
+
padding: 0.625rem 0.75rem;
|
|
80
|
+
border: 1px solid var(--ss-border);
|
|
81
|
+
border-radius: var(--ss-radius);
|
|
82
|
+
background: transparent;
|
|
83
|
+
color: inherit;
|
|
84
|
+
font: inherit;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ss-input--code {
|
|
88
|
+
font-size: 1.5rem;
|
|
89
|
+
font-variant-numeric: tabular-nums;
|
|
90
|
+
letter-spacing: 0.5em;
|
|
91
|
+
text-align: center;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.ss-input[aria-invalid="true"] {
|
|
95
|
+
border-color: var(--ss-danger);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.ss-input:focus-visible,
|
|
99
|
+
.ss-button:focus-visible,
|
|
100
|
+
.ss-link:focus-visible,
|
|
101
|
+
.ss-user-menu-trigger:focus-visible {
|
|
102
|
+
outline: 2px solid var(--ss-focus);
|
|
103
|
+
outline-offset: 2px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.ss-button {
|
|
107
|
+
display: inline-flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
box-sizing: border-box;
|
|
111
|
+
padding: 0.625rem 1rem;
|
|
112
|
+
border: 1px solid var(--ss-accent);
|
|
113
|
+
border-radius: var(--ss-radius);
|
|
114
|
+
background: var(--ss-accent);
|
|
115
|
+
color: var(--ss-accent-text);
|
|
116
|
+
font: inherit;
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
text-decoration: none;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.ss-button--secondary {
|
|
123
|
+
background: transparent;
|
|
124
|
+
color: var(--ss-text);
|
|
125
|
+
border-color: var(--ss-border);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ss-button:disabled,
|
|
129
|
+
.ss-link:disabled {
|
|
130
|
+
cursor: not-allowed;
|
|
131
|
+
opacity: 0.6;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.ss-link {
|
|
135
|
+
padding: 0;
|
|
136
|
+
border: 0;
|
|
137
|
+
background: none;
|
|
138
|
+
color: var(--ss-text);
|
|
139
|
+
font: inherit;
|
|
140
|
+
font-size: 0.875rem;
|
|
141
|
+
text-decoration: underline;
|
|
142
|
+
text-underline-offset: 0.2em;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.ss-actions {
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-wrap: wrap;
|
|
149
|
+
gap: 1rem;
|
|
150
|
+
align-items: center;
|
|
151
|
+
margin-top: 1.25rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.ss-error {
|
|
155
|
+
margin: 0;
|
|
156
|
+
color: var(--ss-danger);
|
|
157
|
+
font-size: 0.875rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.ss-notice {
|
|
161
|
+
margin: 0;
|
|
162
|
+
color: var(--ss-muted);
|
|
163
|
+
font-size: 0.875rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.ss-user-menu {
|
|
167
|
+
position: relative;
|
|
168
|
+
display: inline-block;
|
|
169
|
+
color: var(--ss-text);
|
|
170
|
+
font-family: var(--ss-font);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.ss-user-menu-trigger {
|
|
174
|
+
display: inline-flex;
|
|
175
|
+
gap: 0.5rem;
|
|
176
|
+
align-items: center;
|
|
177
|
+
padding: 0.25rem 0.5rem 0.25rem 0.25rem;
|
|
178
|
+
border: 1px solid var(--ss-border);
|
|
179
|
+
border-radius: 999px;
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
list-style: none;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.ss-user-menu-trigger::-webkit-details-marker {
|
|
185
|
+
display: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.ss-avatar {
|
|
189
|
+
display: inline-grid;
|
|
190
|
+
place-items: center;
|
|
191
|
+
width: 2rem;
|
|
192
|
+
height: 2rem;
|
|
193
|
+
border-radius: 50%;
|
|
194
|
+
background: var(--ss-accent);
|
|
195
|
+
color: var(--ss-accent-text);
|
|
196
|
+
font-size: 0.75rem;
|
|
197
|
+
font-weight: 700;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.ss-user-menu-label {
|
|
201
|
+
max-width: 12rem;
|
|
202
|
+
overflow: hidden;
|
|
203
|
+
font-size: 0.875rem;
|
|
204
|
+
text-overflow: ellipsis;
|
|
205
|
+
white-space: nowrap;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.ss-user-menu-panel {
|
|
209
|
+
position: absolute;
|
|
210
|
+
right: 0;
|
|
211
|
+
z-index: 10;
|
|
212
|
+
display: grid;
|
|
213
|
+
gap: 0.5rem;
|
|
214
|
+
min-width: 14rem;
|
|
215
|
+
margin-top: 0.5rem;
|
|
216
|
+
padding: 1rem;
|
|
217
|
+
border: 1px solid var(--ss-border);
|
|
218
|
+
border-radius: var(--ss-radius);
|
|
219
|
+
background: var(--ss-surface);
|
|
220
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.ss-user-menu-name {
|
|
224
|
+
margin: 0;
|
|
225
|
+
font-weight: 600;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.ss-user-menu-email {
|
|
229
|
+
margin: 0;
|
|
230
|
+
color: var(--ss-muted);
|
|
231
|
+
font-size: 0.875rem;
|
|
232
|
+
overflow-wrap: anywhere;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.ss-badges {
|
|
236
|
+
display: flex;
|
|
237
|
+
flex-wrap: wrap;
|
|
238
|
+
gap: 0.25rem;
|
|
239
|
+
margin: 0;
|
|
240
|
+
padding: 0;
|
|
241
|
+
list-style: none;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.ss-badge {
|
|
245
|
+
padding: 0.125rem 0.5rem;
|
|
246
|
+
border: 1px solid var(--ss-border);
|
|
247
|
+
border-radius: 999px;
|
|
248
|
+
font-size: 0.75rem;
|
|
249
|
+
}
|
|
@@ -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,52 @@
|
|
|
1
|
+
export type BrowserSignInMethod = "email" | "segmentstream";
|
|
2
|
+
export interface BrowserSignInOptions {
|
|
3
|
+
readonly returnTo?: string;
|
|
4
|
+
readonly error?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Headless browser-method discovery. The workspace supplies every message and control. */
|
|
7
|
+
export declare function useBrowserSignIn(options?: BrowserSignInOptions): {
|
|
8
|
+
status: "unavailable" | "loading" | "ready";
|
|
9
|
+
methods: readonly BrowserSignInMethod[];
|
|
10
|
+
primary: "email" | "segmentstream";
|
|
11
|
+
returnTo: string;
|
|
12
|
+
error: string | null;
|
|
13
|
+
segmentstreamSignInUrl: string | null;
|
|
14
|
+
connectAccountUrl: string | null;
|
|
15
|
+
accountSwitchUrl: string | null;
|
|
16
|
+
emailSignInUrl: string | null;
|
|
17
|
+
};
|
|
18
|
+
export interface LinkAccountIdentity {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly email?: string;
|
|
21
|
+
readonly name?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface IdentityLinkState {
|
|
24
|
+
readonly status: "loading" | "ready" | "unavailable";
|
|
25
|
+
readonly pending: boolean;
|
|
26
|
+
readonly proofReady: boolean;
|
|
27
|
+
readonly emailEnabled: boolean;
|
|
28
|
+
readonly identity: LinkAccountIdentity | null;
|
|
29
|
+
readonly workspaceUser: LinkAccountIdentity | null;
|
|
30
|
+
readonly expiresAt: number | null;
|
|
31
|
+
readonly returnTo: string | null;
|
|
32
|
+
}
|
|
33
|
+
/** Linking never confirms during a GET. Post the form to confirmationUrl after explicit consent. */
|
|
34
|
+
export declare function useIdentityLink(): {
|
|
35
|
+
confirmationUrl: string;
|
|
36
|
+
emailProofUrl: string;
|
|
37
|
+
status: "loading" | "ready" | "unavailable";
|
|
38
|
+
pending: boolean;
|
|
39
|
+
proofReady: boolean;
|
|
40
|
+
emailEnabled: boolean;
|
|
41
|
+
identity: LinkAccountIdentity | null;
|
|
42
|
+
workspaceUser: LinkAccountIdentity | null;
|
|
43
|
+
expiresAt: number | null;
|
|
44
|
+
returnTo: string | null;
|
|
45
|
+
};
|
|
46
|
+
/** Explicit reverse linking. Email possession is proof only; the OIDC session selects the user. */
|
|
47
|
+
export declare function useEmailIdentityLink(): {
|
|
48
|
+
status: "error" | "pending" | "linked" | "idle";
|
|
49
|
+
error: string | null;
|
|
50
|
+
confirm: () => Promise<boolean>;
|
|
51
|
+
requestProof: (email: string, returnTo?: string) => Promise<boolean>;
|
|
52
|
+
};
|