@segmentstream/app 0.0.0-stage → 0.38.1

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,79 @@
1
+ import type { OntologyTransport } from "./ontology.js";
2
+ export interface BlobUploadTicket {
3
+ readonly uploadId: string;
4
+ }
5
+ export interface FileRevision {
6
+ readonly id: string;
7
+ readonly filename: string;
8
+ readonly contentType: string;
9
+ readonly size: number;
10
+ readonly createdAt: string;
11
+ }
12
+ export interface FileTransfer {
13
+ readonly url: string;
14
+ readonly method: "GET" | "PUT";
15
+ readonly headers: Readonly<Record<string, string>>;
16
+ readonly expiresAt: string;
17
+ }
18
+ export interface FileDownloadOptions {
19
+ /** A retained revision of this record's blob property; omitted for the current file. */
20
+ readonly revisionId?: string;
21
+ /** Request inline display when the ontology's content-type policy permits it. */
22
+ readonly inline?: boolean;
23
+ readonly signal?: AbortSignal;
24
+ }
25
+ export interface FileUploadTarget {
26
+ readonly entity: string;
27
+ readonly property: string;
28
+ readonly action: string;
29
+ readonly recordId?: string;
30
+ readonly createIntent?: string;
31
+ }
32
+ export interface UploadSession {
33
+ readonly id: string;
34
+ readonly state: string;
35
+ readonly expiresAt: string;
36
+ readonly ticket?: BlobUploadTicket;
37
+ }
38
+ export interface OpenUploadSession extends UploadSession {
39
+ readonly filename: string;
40
+ readonly contentType: string;
41
+ readonly size: number;
42
+ readonly entity: string;
43
+ readonly property: string;
44
+ readonly action: string;
45
+ readonly recordId: string | null;
46
+ readonly createIntent: string | null;
47
+ readonly createdAt: string;
48
+ /** The bytes are uploaded; a record-saving action must still consume the ticket. */
49
+ readonly awaitingAction: boolean;
50
+ }
51
+ export interface OpenUploadSessions {
52
+ readonly uploads: ReadonlyArray<OpenUploadSession>;
53
+ readonly pending: number;
54
+ readonly max: number;
55
+ }
56
+ export interface FileUploadOptions {
57
+ readonly signal?: AbortSignal;
58
+ /** Reuse this UUID when retrying an upload and its entity action. */
59
+ readonly idempotencyKey?: string;
60
+ /** Reports transfer start and completion; supply a transfer adapter for granular progress. */
61
+ readonly onProgress?: (transferred: number, total: number) => void;
62
+ /** Keep the ID as soon as it exists, including when a later transfer fails. */
63
+ readonly onSession?: (session: UploadSession) => void;
64
+ }
65
+ /** Portable client: ontology requests use the member transport; bytes use only a scoped grant. */
66
+ export declare function createBlobStorageClient(transport: OntologyTransport, transfer?: typeof fetch): {
67
+ list(signal?: AbortSignal): Promise<OpenUploadSessions>;
68
+ status: (id: string, signal?: AbortSignal) => Promise<UploadSession>;
69
+ cancel: (id: string) => Promise<UploadSession>;
70
+ uploadForAction(target: FileUploadTarget, file: File, options?: FileUploadOptions): Promise<{
71
+ upload: UploadSession;
72
+ ticket: BlobUploadTicket;
73
+ options: {
74
+ createIntent?: string | undefined;
75
+ idempotencyKey: string;
76
+ };
77
+ }>;
78
+ download(entity: string, recordId: string, property: string, options?: FileDownloadOptions): Promise<FileTransfer>;
79
+ };
@@ -0,0 +1,180 @@
1
+ import { apiErrorFrom, readJson } from "./errors.js";
2
+ import { toSnakeCase } from "./ontology-names.js";
3
+ function waitForGrantRetry(delay, signal) {
4
+ signal?.throwIfAborted();
5
+ return new Promise((resolve, reject) => {
6
+ const abort = () => {
7
+ clearTimeout(timer);
8
+ signal?.removeEventListener("abort", abort);
9
+ reject(signal?.reason);
10
+ };
11
+ const timer = setTimeout(() => {
12
+ signal?.removeEventListener("abort", abort);
13
+ resolve();
14
+ }, delay);
15
+ signal?.addEventListener("abort", abort, { once: true });
16
+ });
17
+ }
18
+ /** Portable client: ontology requests use the member transport; bytes use only a scoped grant. */
19
+ export function createBlobStorageClient(transport, transfer = globalThis.fetch) {
20
+ const send = async (path, method, body, signal, retryGrant = false) => {
21
+ for (let attempt = 0;; attempt++) {
22
+ signal?.throwIfAborted();
23
+ const response = await transport.fetch(path, {
24
+ method,
25
+ ...(body === undefined
26
+ ? {}
27
+ : { headers: { "content-type": "application/json" }, body: JSON.stringify(body) }),
28
+ ...(signal ? { signal } : {}),
29
+ });
30
+ const data = await readJson(response);
31
+ if (!response.ok) {
32
+ const error = apiErrorFrom(response.status, data, "The ontology service rejected the file request.");
33
+ const retryAfter = response.headers.get("retry-after");
34
+ const seconds = Number(retryAfter);
35
+ const delay = retryAfter === null
36
+ ? 500 * 2 ** attempt
37
+ : Number.isFinite(seconds)
38
+ ? Math.max(0, seconds * 1000)
39
+ : Date.parse(retryAfter) - Date.now();
40
+ if (retryGrant &&
41
+ attempt < 2 &&
42
+ error.retryable &&
43
+ [429, 502, 503, 504].includes(response.status) &&
44
+ Number.isFinite(delay) &&
45
+ delay >= 0 &&
46
+ delay <= 5000) {
47
+ await waitForGrantRetry(delay, signal);
48
+ continue;
49
+ }
50
+ throw error;
51
+ }
52
+ return data;
53
+ }
54
+ };
55
+ const sessionPath = (id) => `/uploads/${encodeURIComponent(id)}`;
56
+ const readSession = async (path, method, body, signal) => {
57
+ const { expires_at, ticket, ...session } = await send(path, method, body, signal);
58
+ return {
59
+ ...session,
60
+ expiresAt: expires_at,
61
+ ...(ticket ? { ticket: { uploadId: ticket.upload_id } } : {}),
62
+ };
63
+ };
64
+ const readTransfer = async (path, body, signal, retryGrant = false) => {
65
+ const { expires_at, ...grant } = await send(path, "POST", body, signal, retryGrant);
66
+ return { ...grant, expiresAt: expires_at };
67
+ };
68
+ const cancel = (id) => readSession(`${sessionPath(id)}/cancel`, "POST");
69
+ return {
70
+ async list(signal) {
71
+ const page = await send("/uploads", "GET", undefined, signal);
72
+ return {
73
+ pending: page.pending,
74
+ max: page.max,
75
+ uploads: page.uploads.map((upload) => ({
76
+ id: upload.id,
77
+ state: upload.state,
78
+ filename: upload.filename,
79
+ contentType: upload.content_type,
80
+ size: upload.size,
81
+ entity: upload.entity,
82
+ property: upload.property,
83
+ action: upload.action,
84
+ recordId: upload.record_id,
85
+ createIntent: upload.create_intent,
86
+ createdAt: upload.created_at,
87
+ expiresAt: upload.expires_at,
88
+ awaitingAction: upload.awaiting_action,
89
+ })),
90
+ };
91
+ },
92
+ status: (id, signal) => readSession(sessionPath(id), "GET", undefined, signal),
93
+ cancel,
94
+ async uploadForAction(target, file, options = {}) {
95
+ const idempotencyKey = options.idempotencyKey ?? crypto.randomUUID();
96
+ const createIntent = target.recordId ? undefined : (target.createIntent ?? idempotencyKey);
97
+ const session = await readSession("/uploads", "POST", {
98
+ entity: toSnakeCase(target.entity),
99
+ property: toSnakeCase(target.property),
100
+ action: toSnakeCase(target.action),
101
+ ...(target.recordId ? { record_id: target.recordId } : {}),
102
+ ...(createIntent ? { create_intent: createIntent } : {}),
103
+ idempotency_key: idempotencyKey,
104
+ filename: file.name,
105
+ content_type: file.type || "application/octet-stream",
106
+ size: file.size,
107
+ }, options.signal);
108
+ options.onSession?.(session);
109
+ if (session.ticket) {
110
+ return {
111
+ upload: session,
112
+ ticket: session.ticket,
113
+ options: { idempotencyKey, ...(createIntent ? { createIntent } : {}) },
114
+ };
115
+ }
116
+ let completionStarted = false;
117
+ let transferRejected = false;
118
+ try {
119
+ if (session.state !== "completing") {
120
+ const grant = await readTransfer(`${sessionPath(session.id)}/parts`, undefined, options.signal);
121
+ options.onProgress?.(0, file.size);
122
+ const response = await transfer(grant.url, {
123
+ method: grant.method,
124
+ headers: grant.headers,
125
+ body: file,
126
+ signal: options.signal,
127
+ credentials: "omit",
128
+ referrerPolicy: "no-referrer",
129
+ });
130
+ if (!response.ok) {
131
+ transferRejected = true;
132
+ throw new Error(`File transfer failed (${response.status}).`);
133
+ }
134
+ options.onProgress?.(file.size, file.size);
135
+ }
136
+ completionStarted = true;
137
+ const completed = await readSession(`${sessionPath(session.id)}/complete`, "POST", undefined, options.signal);
138
+ if (!completed.ticket) {
139
+ throw new Error("The file upload is not ready.");
140
+ }
141
+ options.onSession?.(completed);
142
+ return {
143
+ upload: completed,
144
+ ticket: completed.ticket,
145
+ options: { idempotencyKey, ...(createIntent ? { createIntent } : {}) },
146
+ };
147
+ }
148
+ catch (error) {
149
+ if (options.signal?.aborted || transferRejected) {
150
+ // Deliberate cancellation uses a fresh request even when the signal was aborted.
151
+ await cancel(session.id).catch((cause) => {
152
+ console.warn("ontology.upload.cancel_failed", { uploadId: session.id, cause });
153
+ });
154
+ }
155
+ else if (completionStarted) {
156
+ // A lost completion response may already have produced a valid ticket.
157
+ const recovered = await readSession(sessionPath(session.id), "GET").catch(() => undefined);
158
+ if (recovered?.ticket) {
159
+ options.onSession?.(recovered);
160
+ return {
161
+ upload: recovered,
162
+ ticket: recovered.ticket,
163
+ options: { idempotencyKey, ...(createIntent ? { createIntent } : {}) },
164
+ };
165
+ }
166
+ }
167
+ // Ambiguous network failures retain the session for an explicit retry or scheduled cleanup.
168
+ throw error;
169
+ }
170
+ },
171
+ download(entity, recordId, property, options = {}) {
172
+ const { signal } = options;
173
+ const input = {
174
+ ...(options.revisionId ? { revision_id: options.revisionId } : {}),
175
+ ...(options.inline === undefined ? {} : { inline: options.inline }),
176
+ };
177
+ return readTransfer(`/${encodeURIComponent(toSnakeCase(entity))}/${encodeURIComponent(recordId)}/blobs/${encodeURIComponent(toSnakeCase(property))}/download`, input, signal, true);
178
+ },
179
+ };
180
+ }
@@ -0,0 +1,7 @@
1
+ import { type ProxyOptions, type SegmentStreamProxy } from "./proxy.js";
2
+ /**
3
+ * An unauthenticated Telegram Web iframe cannot visit the unframeable sign-in page. Keep the
4
+ * protected URL and return only an exchange script; never let the workspace render anonymously.
5
+ * A failed launch stays inert, or opens the app's public failure page when the app names one.
6
+ */
7
+ export declare function withTelegramLaunch(proxy: SegmentStreamProxy, channels: Readonly<Record<string, boolean>> | undefined, options?: ProxyOptions): SegmentStreamProxy;
@@ -0,0 +1,134 @@
1
+ import { NextResponse } from "next/server";
2
+ import { CHANNEL_SESSION_PATH, channelSignInPath } from "./channel-protocol.js";
3
+ import { isDecisionPage } from "./decision-pages.js";
4
+ import { randomToken } from "./pkce.js";
5
+ import { matchesPathPrefix, normalizePublicPath, } from "./proxy.js";
6
+ import { channelAppFraming } from "./telegram.js";
7
+ const NON_PAGE_PATHS = ["/api", "/mcp", "/.well-known", "/_next"];
8
+ /**
9
+ * The app's page for a failed launch: a plain path, never a decision page, which the proxy serves
10
+ * without a session so the frame does not return to the shell.
11
+ */
12
+ function launchFailurePath(options) {
13
+ const path = options.channelLaunchFailurePath;
14
+ if (path === undefined) {
15
+ return undefined;
16
+ }
17
+ const url = URL.parse(path, "https://app.invalid");
18
+ if (!path.startsWith("/") ||
19
+ path.startsWith("//") ||
20
+ url?.origin !== "https://app.invalid" ||
21
+ url.pathname !== path ||
22
+ isDecisionPage(path) ||
23
+ NON_PAGE_PATHS.some((prefix) => matchesPathPrefix(path, prefix))) {
24
+ throw new Error("channelLaunchFailurePath must be an app page path such as /telegram-unavailable, without a query or fragment.");
25
+ }
26
+ if (!(options.publicPaths ?? [])
27
+ .map(normalizePublicPath)
28
+ .some((publicPath) => matchesPathPrefix(path, publicPath))) {
29
+ throw new Error("channelLaunchFailurePath must be covered by publicPaths.");
30
+ }
31
+ return path;
32
+ }
33
+ /**
34
+ * An unauthenticated Telegram Web iframe cannot visit the unframeable sign-in page. Keep the
35
+ * protected URL and return only an exchange script; never let the workspace render anonymously.
36
+ * A failed launch stays inert, or opens the app's public failure page when the app names one.
37
+ */
38
+ export function withTelegramLaunch(proxy, channels, options = {}) {
39
+ const failurePath = launchFailurePath(options);
40
+ if (channels?.telegram !== true) {
41
+ return proxy;
42
+ }
43
+ return async (request) => {
44
+ const response = await proxy(request);
45
+ const pathname = request.nextUrl.pathname;
46
+ const location = response.headers.get("location");
47
+ const destination = location === null ? null : URL.parse(location, request.url);
48
+ if (request.method !== "GET" ||
49
+ request.headers.get("sec-fetch-dest") !== "iframe" ||
50
+ request.headers.get("sec-fetch-mode") !== "navigate" ||
51
+ isDecisionPage(pathname) ||
52
+ NON_PAGE_PATHS.some((path) => matchesPathPrefix(pathname, path)) ||
53
+ response.status < 300 ||
54
+ response.status >= 400 ||
55
+ destination?.origin !== request.nextUrl.origin ||
56
+ destination.pathname !== "/sign-in") {
57
+ return response;
58
+ }
59
+ const nonce = randomToken();
60
+ // Only SDK-owned constants and the validated failure path enter the script. Launch data stays
61
+ // in the browser until the POST and never enters the failure page's query.
62
+ const script = `
63
+ void (async () => {
64
+ const state = document.documentElement.dataset;
65
+ const failurePath = ${JSON.stringify(failurePath ?? null)};
66
+ const fail = (code) => {
67
+ state.channelSignIn = "error";
68
+ state.channelSignInError = code;
69
+ if (failurePath) {
70
+ location.replace(failurePath + "?" + new URLSearchParams({
71
+ error: code,
72
+ returnTo: location.pathname + location.search,
73
+ }));
74
+ }
75
+ };
76
+ const assertion = new URLSearchParams(location.hash.slice(1)).get("tgWebAppData");
77
+ if (!assertion) {
78
+ state.channelSignIn = "idle";
79
+ return;
80
+ }
81
+ state.channelSignIn = "pending";
82
+ try {
83
+ // Persist only a digest, scoped to this tab and exact launch, before the one allowed reload.
84
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(location.href));
85
+ const reloadKey = "segmentstream:telegram-launch:" + Array.from(new Uint8Array(digest),
86
+ (byte) => byte.toString(16).padStart(2, "0")).join("");
87
+ if (sessionStorage.getItem(reloadKey)) {
88
+ fail("launch_loop");
89
+ return;
90
+ }
91
+ const exchanged = await fetch(${JSON.stringify(channelSignInPath("telegram"))}, {
92
+ method: "POST",
93
+ credentials: "same-origin",
94
+ redirect: "error",
95
+ headers: { "content-type": "application/json" },
96
+ body: JSON.stringify({ assertion }),
97
+ });
98
+ if (!exchanged.ok) {
99
+ const body = await exchanged.json();
100
+ fail(typeof body.error === "string" ? body.error : "sign_in_failed");
101
+ return;
102
+ }
103
+ const discovered = await fetch(${JSON.stringify(CHANNEL_SESSION_PATH)}, {
104
+ credentials: "same-origin",
105
+ cache: "no-store",
106
+ redirect: "error",
107
+ });
108
+ const body = discovered.ok ? await discovered.json() : null;
109
+ if (typeof body?.session?.id !== "string" || typeof body?.user?.id !== "string") {
110
+ fail("session_unavailable");
111
+ return;
112
+ }
113
+ sessionStorage.setItem(reloadKey, "1");
114
+ location.reload();
115
+ } catch {
116
+ fail("sign_in_unavailable");
117
+ }
118
+ })();`;
119
+ const shell = new NextResponse(`<!doctype html><html><head><meta charset="utf-8"></head><body><script nonce="${nonce}">${script}</script></body></html>`, {
120
+ headers: {
121
+ "content-type": "text/html; charset=utf-8",
122
+ "cache-control": "no-store",
123
+ "referrer-policy": "no-referrer",
124
+ "x-content-type-options": "nosniff",
125
+ "content-security-policy": `default-src 'none'; script-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors ${channelAppFraming(channels)}`,
126
+ },
127
+ });
128
+ // Preserve stale-session clearing without carrying redirect or middleware bypass headers.
129
+ for (const cookie of response.headers.getSetCookie()) {
130
+ shell.headers.append("set-cookie", cookie);
131
+ }
132
+ return shell;
133
+ };
134
+ }
@@ -0,0 +1,2 @@
1
+ /** Provider-specific embedding policy; decision pages always override this with DENY. */
2
+ export declare function channelAppFraming(channels: Readonly<Record<string, boolean>> | undefined): string | undefined;
@@ -0,0 +1,6 @@
1
+ /** Provider-specific embedding policy; decision pages always override this with DENY. */
2
+ export function channelAppFraming(channels) {
3
+ return channels?.telegram === true
4
+ ? "'self' https://web.telegram.org https://*.web.telegram.org"
5
+ : undefined;
6
+ }
@@ -0,0 +1,51 @@
1
+ import type { JWTVerifyGetKey } from "jose";
2
+ import type { ResolvedConfig } from "./config.js";
3
+ import type { ScopedPermission } from "./me.js";
4
+ /** The SegmentStream member a legacy app session token names. */
5
+ export interface TokenUser {
6
+ readonly id: string;
7
+ readonly email?: string;
8
+ readonly name?: string;
9
+ readonly organizationId: string;
10
+ readonly workspaceId: string;
11
+ }
12
+ /**
13
+ * The signed-in user. With Neon Auth, the person's ontology `user` record with the roles and
14
+ * permissions from `GET /api/ontology/me`; with the legacy SegmentStream sign-in, the member the app
15
+ * session token names, without ontology roles.
16
+ */
17
+ export interface AppUser extends TokenUser {
18
+ /** Neon Auth: the ontology `user` record's id. Legacy sign-in: the SegmentStream user id. */
19
+ readonly id: string;
20
+ /** The ontology roles the user holds, such as `sales`. Empty under the legacy sign-in. */
21
+ readonly roles: ReadonlyArray<string>;
22
+ /** The permissions the roles grant, wildcards included. Empty under the legacy sign-in. */
23
+ readonly permissions: ReadonlyArray<string>;
24
+ /**
25
+ * The permissions of `permissions` held only for some records; see `CurrentUser.scopes`. Absent
26
+ * when none is scoped.
27
+ */
28
+ readonly scopes?: ReadonlyArray<ScopedPermission>;
29
+ /** Neon Auth: the ontology user's kind, `member` for people. */
30
+ readonly kind?: string;
31
+ /** Neon Auth: whether the ontology user is active. */
32
+ readonly active?: boolean;
33
+ /** Neon Auth: the person's Neon Auth user id, the session JWT's `sub`. */
34
+ readonly authUserId?: string;
35
+ }
36
+ export type KeySet = JWTVerifyGetKey;
37
+ export interface VerifiedToken {
38
+ readonly status: "valid";
39
+ readonly user: TokenUser;
40
+ readonly token: string;
41
+ }
42
+ export type TokenVerification = VerifiedToken | {
43
+ readonly status: "expired";
44
+ } | {
45
+ readonly status: "invalid";
46
+ } | {
47
+ readonly status: "unavailable";
48
+ };
49
+ /** One remote JWKS per SegmentStream origin, shared by every app instance in the process. */
50
+ export declare function remoteKeySet(baseUrl: string): KeySet;
51
+ export declare function createTokenVerifier(config: ResolvedConfig, keySet: KeySet): (token: string) => Promise<TokenVerification>;
package/dist/tokens.js ADDED
@@ -0,0 +1,70 @@
1
+ import { createRemoteJWKSet, errors, jwtVerify } from "jose";
2
+ const CLOCK_TOLERANCE_SECONDS = 5;
3
+ const remoteKeySets = new Map();
4
+ /** One remote JWKS per SegmentStream origin, shared by every app instance in the process. */
5
+ export function remoteKeySet(baseUrl) {
6
+ const existing = remoteKeySets.get(baseUrl);
7
+ if (existing !== undefined) {
8
+ return existing;
9
+ }
10
+ const keySet = createRemoteJWKSet(new URL(`${baseUrl}/api/v1/app-sessions/jwks`), {
11
+ timeoutDuration: 5_000,
12
+ });
13
+ remoteKeySets.set(baseUrl, keySet);
14
+ return keySet;
15
+ }
16
+ export function createTokenVerifier(config, keySet) {
17
+ return async function verifyToken(token) {
18
+ let payload;
19
+ try {
20
+ const verified = await jwtVerify(token, keySet, {
21
+ algorithms: ["ES256"],
22
+ issuer: config.issuer,
23
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
24
+ requiredClaims: ["sub", "exp", "iat", "org", "wsp"],
25
+ });
26
+ payload = verified.payload;
27
+ }
28
+ catch (error) {
29
+ return classifyVerificationFailure(error);
30
+ }
31
+ const user = appUserFromClaims(config, payload);
32
+ return user === null ? { status: "invalid" } : { status: "valid", user, token };
33
+ };
34
+ }
35
+ function classifyVerificationFailure(error) {
36
+ if (error instanceof errors.JWTExpired) {
37
+ return { status: "expired" };
38
+ }
39
+ if (error instanceof errors.JWKSTimeout) {
40
+ return { status: "unavailable" };
41
+ }
42
+ if (error instanceof errors.JOSEError) {
43
+ return { status: "invalid" };
44
+ }
45
+ // Network failures while fetching the key set are not token defects.
46
+ return { status: "unavailable" };
47
+ }
48
+ function appUserFromClaims(config, payload) {
49
+ const { sub, org, wsp, email, name } = payload;
50
+ if (typeof sub !== "string" ||
51
+ sub === "" ||
52
+ org !== config.organizationId ||
53
+ wsp !== config.workspaceId) {
54
+ return null;
55
+ }
56
+ if (!hasAppAudience(payload.aud)) {
57
+ return null;
58
+ }
59
+ return {
60
+ id: sub,
61
+ ...(typeof email === "string" ? { email } : {}),
62
+ ...(typeof name === "string" ? { name } : {}),
63
+ organizationId: org,
64
+ workspaceId: wsp,
65
+ };
66
+ }
67
+ function hasAppAudience(audience) {
68
+ const audiences = typeof audience === "string" ? [audience] : (audience ?? []);
69
+ return audiences.some((candidate) => candidate.startsWith("app:"));
70
+ }
@@ -0,0 +1,18 @@
1
+ import type { AgentPairing } from "../agent.js";
2
+ export interface AgentConnectProps {
3
+ /** The page's `code` query parameter, from the agent's link. */
4
+ readonly code: string | undefined;
5
+ /** `segmentstream.agent.pairing(code)`, or `null` when the code no longer works. */
6
+ readonly pairing: AgentPairing | null;
7
+ /** The page's `connected` query parameter: the channel just linked, after approval. */
8
+ readonly connected?: string | undefined;
9
+ /** Who is signed in, such as their email, so they see whom the chat account will act as. */
10
+ readonly member?: string | undefined;
11
+ /** Where the form posts. Defaults to `/api/agent/connect`, `segmentstream.agent.connectHandler()`. */
12
+ readonly action?: string;
13
+ }
14
+ /**
15
+ * The agent connect page in the SDK's neutral styles: what asks to be linked, an Approve button and
16
+ * the result. Render it from the Server Component at `/agent/connect` with the pairing it read.
17
+ */
18
+ export declare function AgentConnect({ code, pairing, connected, member, action, }: AgentConnectProps): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { AGENT_CONNECT_HANDLER } from "../pages.js";
5
+ /**
6
+ * The agent connect page in the SDK's neutral styles: what asks to be linked, an Approve button and
7
+ * the result. Render it from the Server Component at `/agent/connect` with the pairing it read.
8
+ */
9
+ export function AgentConnect({ code, pairing, connected, member = "you", action = AGENT_CONNECT_HANDLER, }) {
10
+ const titleId = `${useId()}-title`;
11
+ if (connected !== undefined) {
12
+ return (_jsxs("section", { className: "ss-card", "aria-labelledby": titleId, children: [_jsxs("h1", { id: titleId, className: "ss-title", children: [providerName(connected), " is connected"] }), _jsxs("p", { className: "ss-text", children: ["The agent now acts as ", _jsx("strong", { children: member }), " when you write to it there. Go back to the chat and send your message again."] })] }));
13
+ }
14
+ if (code === undefined || code === "" || pairing === null) {
15
+ return (_jsxs("section", { className: "ss-card", "aria-labelledby": titleId, children: [_jsx("h1", { id: titleId, className: "ss-title", children: "This link has expired" }), _jsx("p", { className: "ss-text", children: "The link was already used or is more than ten minutes old. Write to the agent again for a new one." })] }));
16
+ }
17
+ const provider = providerName(pairing.provider);
18
+ return (_jsxs("section", { className: "ss-card", "aria-labelledby": titleId, children: [_jsxs("h1", { id: titleId, className: "ss-title", children: ["Connect ", provider, " to this app?"] }), _jsxs("p", { className: "ss-text", children: [pairing.display === null ? (`A ${provider} account`) : (_jsxs(_Fragment, { children: [_jsx("strong", { children: pairing.display }), " on ", provider] })), " ", "asks to talk to the workspace agent as ", _jsx("strong", { children: member }), ". The agent answers it with your permissions, and history records its changes under your name. Approve only if this is your own account."] }), _jsxs("form", { className: "ss-form", method: "post", action: action, children: [_jsx("input", { type: "hidden", name: "code", value: code }), _jsx("button", { type: "submit", className: "ss-button", children: "Approve" })] })] }));
19
+ }
20
+ /** `telegram` reads as `Telegram`. */
21
+ function providerName(provider) {
22
+ return provider.charAt(0).toUpperCase() + provider.slice(1);
23
+ }
@@ -0,0 +1,27 @@
1
+ import type { ReactNode } from "react";
2
+ export interface ChildrenProps {
3
+ readonly children?: ReactNode;
4
+ }
5
+ /** Renders its children when a Neon Auth session exists in this browser. */
6
+ export declare function SignedIn({ children }: ChildrenProps): ReactNode;
7
+ /** Renders its children when nobody is signed in. */
8
+ export declare function SignedOut({ children }: ChildrenProps): ReactNode;
9
+ export interface CanProps extends ChildrenProps {
10
+ /** A permission such as `lead:changeStatus`. The user's wildcards (`lead:*`, `*:*`) count. */
11
+ readonly permission: string;
12
+ /** Rendered when the user lacks the permission. Nothing renders while permissions load. */
13
+ readonly fallback?: ReactNode;
14
+ }
15
+ /**
16
+ * Renders its children when the signed-in user holds the permission. It only hides controls: the
17
+ * ontology service checks every read and action itself.
18
+ */
19
+ export declare function Can({ permission, children, fallback }: CanProps): ReactNode;
20
+ /** Ends the selected session in this browser, then opens `redirectTo`. */
21
+ export declare function signOut(redirectTo?: string): Promise<void>;
22
+ export interface UserMenuProps {
23
+ /** Where to go after signing out. Defaults to `/sign-in`. */
24
+ readonly signedOutPath?: string;
25
+ }
26
+ /** The signed-in user's name, email and roles, and a sign-out button. */
27
+ export declare function UserMenu({ signedOutPath }: UserMenuProps): import("react").JSX.Element | null;