@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,89 @@
1
+ import { cookies } from "next/headers";
2
+ import { NextResponse } from "next/server";
3
+ import { CHANNEL_LOGOUT_PATH, channelCookieName, channelCookieAttributes, } from "./channel-protocol.js";
4
+ import { apiErrorFrom, readJson, SegmentStreamApiError } from "./errors.js";
5
+ import { isSameOrigin } from "./decision-pages.js";
6
+ import { parseMe } from "./me.js";
7
+ import { createOntologyTransport, ontologyOrigin } from "./ontology-transport.js";
8
+ export { channelCookieName } from "./channel-protocol.js";
9
+ /** Workspace-owned opaque app sessions; every discovery checks current membership and revocation. */
10
+ export function createChannelSession(options) {
11
+ const name = channelCookieName(options.workspaceId);
12
+ const token = async () => {
13
+ if (!options.enabled) {
14
+ return null;
15
+ }
16
+ const credential = (await cookies()).get(name)?.value;
17
+ return credential?.startsWith("sso_") ? credential : null;
18
+ };
19
+ const transport = createOntologyTransport({
20
+ ...options,
21
+ credentials: { token },
22
+ });
23
+ const read = async () => {
24
+ const credential = await token();
25
+ if (!credential) {
26
+ return null;
27
+ }
28
+ const response = await transport.fetch("/me", { method: "GET" });
29
+ if (response.status === 401 || response.status === 403) {
30
+ return null;
31
+ }
32
+ const payload = await readJson(response);
33
+ if (!response.ok) {
34
+ throw apiErrorFrom(response.status, payload, "session_unavailable");
35
+ }
36
+ const user = parseMe(payload);
37
+ if (!user) {
38
+ throw new SegmentStreamApiError({
39
+ code: "ontology_invalid_response",
40
+ status: 502,
41
+ detail: "ontology_invalid_response",
42
+ });
43
+ }
44
+ return { token: credential, user };
45
+ };
46
+ const signOut = async () => {
47
+ if (!(await token())) {
48
+ return;
49
+ }
50
+ const origin = await ontologyOrigin(undefined);
51
+ const response = await transport.fetch(CHANNEL_LOGOUT_PATH.replace("/api/ontology", ""), {
52
+ method: "POST",
53
+ headers: { origin },
54
+ });
55
+ if (!response.ok) {
56
+ throw apiErrorFrom(response.status, await readJson(response), "logout_failed");
57
+ }
58
+ (await cookies()).set(name, "", { ...channelCookieAttributes, maxAge: 0 });
59
+ };
60
+ const proxy = async (request) => {
61
+ if (!options.enabled) {
62
+ return undefined;
63
+ }
64
+ const credential = request.cookies.get(name)?.value;
65
+ if (!credential?.startsWith("sso_")) {
66
+ return undefined;
67
+ }
68
+ if (!["GET", "HEAD", "OPTIONS"].includes(request.method) && !isSameOrigin(request)) {
69
+ return NextResponse.json({ error: "cross_origin_session" }, { status: 403 });
70
+ }
71
+ const response = await options.fetch(`${await ontologyOrigin(options.ontologyUrl ?? request.nextUrl.origin)}/api/ontology/me`, {
72
+ headers: {
73
+ authorization: `Bearer ${credential}`,
74
+ "x-forwarded-host": request.nextUrl.host,
75
+ "x-forwarded-proto": request.nextUrl.protocol.slice(0, -1),
76
+ },
77
+ cache: "no-store",
78
+ redirect: "error",
79
+ });
80
+ if (response.ok && parseMe(await readJson(response))) {
81
+ return NextResponse.next();
82
+ }
83
+ if (response.status !== 401 && response.status !== 403) {
84
+ return NextResponse.json({ error: "session_unavailable" }, { status: 503 });
85
+ }
86
+ return undefined;
87
+ };
88
+ return { token, transport, read, signOut, proxy };
89
+ }
@@ -0,0 +1,53 @@
1
+ export declare const DEFAULT_BASE_URL = "https://cloud.segmentstream.com";
2
+ export interface SegmentStreamAppConfig {
3
+ /** The SegmentStream workspace that owns this app. */
4
+ readonly workspaceId: string;
5
+ /** The SegmentStream organization that owns the workspace. */
6
+ readonly organizationId: string;
7
+ /** SegmentStream origin. Defaults to `SEGMENTSTREAM_URL`, then `https://cloud.segmentstream.com`. */
8
+ readonly baseUrl?: string;
9
+ /** Explicit channel app opt-in; omitted providers stay disabled. */
10
+ readonly channels?: Readonly<Record<string, boolean>>;
11
+ }
12
+ export interface ResolvedConfig {
13
+ readonly workspaceId: string;
14
+ readonly organizationId: string;
15
+ readonly baseUrl: string;
16
+ readonly issuer: string;
17
+ readonly channels?: Readonly<Record<string, boolean>>;
18
+ }
19
+ /** Environment variables, `process.env` unless a test supplies its own. */
20
+ export type Environment = Readonly<Record<string, string | undefined>>;
21
+ export declare function processEnvironment(): Environment;
22
+ /** Neon Auth for the app's users, configured when `NEON_AUTH_BASE_URL` is set. */
23
+ export interface NeonAuthEnvironment {
24
+ /** `NEON_AUTH_BASE_URL`: the branch's Auth URL. */
25
+ readonly baseUrl: string;
26
+ /** `SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET`: signs the session cache cookie. */
27
+ readonly cookieSecret: string | undefined;
28
+ /** `SEGMENTSTREAM_ONTOLOGY_URL`: the ontology service when it is not beside the app. */
29
+ readonly ontologyUrl: string | undefined;
30
+ }
31
+ export declare function resolveConfig(config: SegmentStreamAppConfig, environment?: Environment): ResolvedConfig;
32
+ /** `null` without `NEON_AUTH_BASE_URL`: the app then signs in with SegmentStream, as in 0.2. */
33
+ export declare function resolveNeonAuth(environment: Environment): NeonAuthEnvironment | null;
34
+ /** Linear-time trailing slash removal; `/\/+$/` backtracks quadratically on slash runs. */
35
+ export declare function trimTrailingSlashes(value: string): string;
36
+ /** A variable's trimmed value; unset and blank are the same. */
37
+ export declare function environmentValue(environment: Environment, name: string): string | undefined;
38
+ /** Opt-ins are booleans, separate from ontology admission policy. */
39
+ export declare function hasChannelApps(channels: Readonly<Record<string, boolean>> | undefined): boolean;
40
+ /** Derived from the deployed ontology; never authored as an app-side override. */
41
+ export interface BrowserAuthBinding {
42
+ readonly version: 1;
43
+ readonly manifest_digest: string;
44
+ readonly browser: {
45
+ readonly methods: readonly ("email" | "segmentstream")[];
46
+ readonly primary: "email" | "segmentstream";
47
+ readonly auto_redirect: boolean;
48
+ readonly local?: {
49
+ readonly port: number;
50
+ };
51
+ };
52
+ }
53
+ export declare function resolveBrowserAuth(environment: Environment): BrowserAuthBinding | undefined;
package/dist/config.js ADDED
@@ -0,0 +1,112 @@
1
+ export const DEFAULT_BASE_URL = "https://cloud.segmentstream.com";
2
+ export function processEnvironment() {
3
+ return typeof process === "undefined" ? {} : process.env;
4
+ }
5
+ export function resolveConfig(config, environment = processEnvironment()) {
6
+ const workspaceId = requireIdentifier("workspaceId", config.workspaceId);
7
+ const organizationId = requireIdentifier("organizationId", config.organizationId);
8
+ const baseUrl = normalizeBaseUrl(config.baseUrl ?? environmentValue(environment, "SEGMENTSTREAM_URL") ?? DEFAULT_BASE_URL);
9
+ const issuer = environmentValue(environment, "SEGMENTSTREAM_APP_TOKEN_ISSUER") ?? new URL(baseUrl).origin;
10
+ return {
11
+ workspaceId,
12
+ organizationId,
13
+ baseUrl,
14
+ issuer,
15
+ ...(config.channels ? { channels: config.channels } : {}),
16
+ };
17
+ }
18
+ /** `null` without `NEON_AUTH_BASE_URL`: the app then signs in with SegmentStream, as in 0.2. */
19
+ export function resolveNeonAuth(environment) {
20
+ const baseUrl = environmentValue(environment, "NEON_AUTH_BASE_URL");
21
+ if (baseUrl === undefined) {
22
+ return null;
23
+ }
24
+ const ontologyUrl = environmentValue(environment, "SEGMENTSTREAM_ONTOLOGY_URL");
25
+ return {
26
+ baseUrl: normalizeUrl("NEON_AUTH_BASE_URL", baseUrl),
27
+ cookieSecret: environmentValue(environment, "SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET"),
28
+ ontologyUrl: ontologyUrl === undefined
29
+ ? undefined
30
+ : new URL(normalizeUrl("SEGMENTSTREAM_ONTOLOGY_URL", ontologyUrl)).origin,
31
+ };
32
+ }
33
+ function requireIdentifier(name, value) {
34
+ const trimmed = typeof value === "string" ? value.trim() : "";
35
+ if (trimmed === "") {
36
+ throw new Error(`createSegmentStreamApp requires a non-empty ${name}.`);
37
+ }
38
+ return trimmed;
39
+ }
40
+ /** Linear-time trailing slash removal; `/\/+$/` backtracks quadratically on slash runs. */
41
+ export function trimTrailingSlashes(value) {
42
+ let end = value.length;
43
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
44
+ end -= 1;
45
+ }
46
+ return value.slice(0, end);
47
+ }
48
+ function normalizeBaseUrl(value) {
49
+ return normalizeUrl("SegmentStream base URL", value);
50
+ }
51
+ function normalizeUrl(name, value) {
52
+ const trimmed = trimTrailingSlashes(value.trim());
53
+ let url;
54
+ try {
55
+ url = new URL(trimmed);
56
+ }
57
+ catch (error) {
58
+ throw new Error(`${name} is not a valid URL: ${trimmed}`, { cause: error });
59
+ }
60
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
61
+ throw new Error(`${name} must use http or https: ${trimmed}`);
62
+ }
63
+ return trimmed;
64
+ }
65
+ /** A variable's trimmed value; unset and blank are the same. */
66
+ export function environmentValue(environment, name) {
67
+ const value = environment[name];
68
+ return value === undefined || value.trim() === "" ? undefined : value.trim();
69
+ }
70
+ /** Opt-ins are booleans, separate from ontology admission policy. */
71
+ export function hasChannelApps(channels) {
72
+ return Object.values(channels ?? {}).some((enabled) => enabled === true);
73
+ }
74
+ export function resolveBrowserAuth(environment) {
75
+ const raw = environmentValue(environment, "SEGMENTSTREAM_BROWSER_AUTH");
76
+ if (!raw)
77
+ return undefined;
78
+ let value;
79
+ try {
80
+ value = JSON.parse(raw);
81
+ }
82
+ catch (cause) {
83
+ throw new Error("browser_auth_configuration_invalid", { cause });
84
+ }
85
+ if (!value || typeof value !== "object")
86
+ throw new Error("browser_auth_configuration_invalid");
87
+ const binding = value;
88
+ const browser = binding.browser;
89
+ if (binding.version !== 1 ||
90
+ typeof binding.manifest_digest !== "string" ||
91
+ !binding.manifest_digest ||
92
+ !browser ||
93
+ !Array.isArray(browser.methods) ||
94
+ browser.methods.length === 0 ||
95
+ browser.methods.some((method) => method !== "email" && method !== "segmentstream") ||
96
+ new Set(browser.methods).size !== browser.methods.length ||
97
+ !browser.methods.includes(browser.primary) ||
98
+ typeof browser.auto_redirect !== "boolean" ||
99
+ (browser.auto_redirect &&
100
+ (browser.methods.length !== 1 || browser.methods[0] !== "segmentstream")) ||
101
+ (browser.local !== undefined &&
102
+ (!browser.methods.includes("segmentstream") ||
103
+ !browser.local ||
104
+ typeof browser.local !== "object" ||
105
+ Array.isArray(browser.local) ||
106
+ !Number.isInteger(browser.local.port) ||
107
+ browser.local.port < 1 ||
108
+ browser.local.port > 65535))) {
109
+ throw new Error("browser_auth_configuration_invalid");
110
+ }
111
+ return binding;
112
+ }
@@ -0,0 +1,10 @@
1
+ import type { ResolvedConfig } from "./config.js";
2
+ import type { TokenExchange, TokenGrant } from "./sessions.js";
3
+ import type { TokenVerification } from "./tokens.js";
4
+ /** The resolved configuration and capabilities shared by the proxy, handlers, and server helpers. */
5
+ export interface AppContext {
6
+ readonly config: ResolvedConfig;
7
+ readonly verifyToken: (token: string) => Promise<TokenVerification>;
8
+ readonly requestToken: (grant: TokenGrant) => Promise<TokenExchange>;
9
+ readonly revokeSession: (refreshToken: string) => Promise<void>;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
1
+ import type { NextResponse } from "next/server";
2
+ export { safeReturnTo } from "./return-to.js";
3
+ export declare const REFRESH_COOKIE_MAX_AGE_SECONDS: number;
4
+ export declare const AUTH_FLOW_COOKIE_MAX_AGE_SECONDS: number;
5
+ /** Sign-ins one browser can have in progress at once; one more abandons them all. */
6
+ export declare const MAX_PENDING_AUTH_FLOWS = 4;
7
+ export interface CookieNames {
8
+ readonly session: string;
9
+ readonly refresh: string;
10
+ /** Each sign-in in progress has its own cookie: this prefix followed by its `state`. */
11
+ readonly authFlowPrefix: string;
12
+ }
13
+ /** Cookie names and attributes for one app on one request origin. */
14
+ export interface CookiePolicy {
15
+ readonly secure: boolean;
16
+ readonly names: CookieNames;
17
+ }
18
+ /** The PKCE flow state kept between starting sign-in and the callback. */
19
+ export interface AuthFlowState {
20
+ readonly state: string;
21
+ readonly codeVerifier: string;
22
+ readonly redirectUri: string;
23
+ readonly returnTo: string;
24
+ }
25
+ export interface SessionTokens {
26
+ readonly accessToken: string;
27
+ readonly expiresIn: number;
28
+ readonly refreshToken: string;
29
+ }
30
+ export declare function isLoopbackHostname(hostname: string): boolean;
31
+ /**
32
+ * Browsers reject `Secure` and `__Host-` cookies on plain-HTTP origins, except that
33
+ * local development on http://localhost should still work. Every other origin uses
34
+ * `__Host-` cookies, which cannot be set by sibling subdomains.
35
+ *
36
+ * Browsers share cookies between ports, so apps on `localhost:3000` and `localhost:3001`
37
+ * see one cookie jar. Names carry the workspace id to keep each app's session its own.
38
+ */
39
+ export declare function cookiePolicyFor(url: URL, workspaceId: string): CookiePolicy;
40
+ /** Candidate cookie names for code that only knows the request host (Server Components). */
41
+ export declare function sessionCookieCandidates(hostname: string, workspaceId: string): readonly string[];
42
+ /** The cookie holding one sign-in's flow, or undefined for a `state` this SDK never issues. */
43
+ export declare function authFlowCookieName(policy: CookiePolicy, state: string): string | undefined;
44
+ /** The sign-in flow cookies a request carries, one per sign-in still in progress. */
45
+ export declare function pendingAuthFlowCookies(policy: CookiePolicy, requestCookieNames: readonly string[]): string[];
46
+ export declare function setSessionCookies(response: NextResponse, policy: CookiePolicy, tokens: SessionTokens): void;
47
+ export declare function clearSessionCookies(response: NextResponse, policy: CookiePolicy): void;
48
+ export declare function setAuthFlowCookie(response: NextResponse, policy: CookiePolicy, flow: AuthFlowState): void;
49
+ export declare function clearCookie(response: NextResponse, policy: CookiePolicy, name: string): void;
50
+ export declare function encodeAuthFlowState(flow: AuthFlowState): string;
51
+ export declare function decodeAuthFlowState(value: string | undefined): AuthFlowState | null;
52
+ /**
53
+ * Replaces the named cookies in a raw `Cookie` request header while preserving every
54
+ * other cookie byte for byte.
55
+ */
56
+ export declare function replaceRequestCookies(cookieHeader: string | null, replacements: Record<string, string>): string;
@@ -0,0 +1,131 @@
1
+ import { base64UrlDecode, base64UrlEncode } from "./pkce.js";
2
+ import { safeReturnTo } from "./return-to.js";
3
+ export { safeReturnTo } from "./return-to.js";
4
+ export const REFRESH_COOKIE_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
5
+ export const AUTH_FLOW_COOKIE_MAX_AGE_SECONDS = 10 * 60;
6
+ /** Sign-ins one browser can have in progress at once; one more abandons them all. */
7
+ export const MAX_PENDING_AUTH_FLOWS = 4;
8
+ const SECURE_PREFIX = "__Host-";
9
+ const AUTH_FLOW_STATE = /^[A-Za-z0-9_-]{1,128}$/;
10
+ export function isLoopbackHostname(hostname) {
11
+ const normalized = hostname.toLowerCase();
12
+ return (normalized === "localhost" ||
13
+ normalized.endsWith(".localhost") ||
14
+ normalized === "[::1]" ||
15
+ normalized === "::1" ||
16
+ /^127(?:\.\d{1,3}){3}$/.test(normalized));
17
+ }
18
+ /**
19
+ * Browsers reject `Secure` and `__Host-` cookies on plain-HTTP origins, except that
20
+ * local development on http://localhost should still work. Every other origin uses
21
+ * `__Host-` cookies, which cannot be set by sibling subdomains.
22
+ *
23
+ * Browsers share cookies between ports, so apps on `localhost:3000` and `localhost:3001`
24
+ * see one cookie jar. Names carry the workspace id to keep each app's session its own.
25
+ */
26
+ export function cookiePolicyFor(url, workspaceId) {
27
+ const insecureLoopback = url.protocol === "http:" && isLoopbackHostname(url.hostname);
28
+ return {
29
+ secure: !insecureLoopback,
30
+ names: cookieNames(insecureLoopback ? "" : SECURE_PREFIX, workspaceId),
31
+ };
32
+ }
33
+ /** Candidate cookie names for code that only knows the request host (Server Components). */
34
+ export function sessionCookieCandidates(hostname, workspaceId) {
35
+ const secure = cookieNames(SECURE_PREFIX, workspaceId).session;
36
+ return isLoopbackHostname(hostname) ? [secure, cookieNames("", workspaceId).session] : [secure];
37
+ }
38
+ function cookieNames(prefix, workspaceId) {
39
+ // Workspace ids are UUIDs; anything else is kept to cookie-name characters.
40
+ const scope = workspaceId.replace(/[^A-Za-z0-9_-]/g, "_");
41
+ return {
42
+ session: `${prefix}ss_app_session.${scope}`,
43
+ refresh: `${prefix}ss_app_refresh.${scope}`,
44
+ authFlowPrefix: `${prefix}ss_app_auth.${scope}.`,
45
+ };
46
+ }
47
+ /** The cookie holding one sign-in's flow, or undefined for a `state` this SDK never issues. */
48
+ export function authFlowCookieName(policy, state) {
49
+ return AUTH_FLOW_STATE.test(state) ? `${policy.names.authFlowPrefix}${state}` : undefined;
50
+ }
51
+ /** The sign-in flow cookies a request carries, one per sign-in still in progress. */
52
+ export function pendingAuthFlowCookies(policy, requestCookieNames) {
53
+ return requestCookieNames.filter((name) => name.startsWith(policy.names.authFlowPrefix));
54
+ }
55
+ function cookieAttributes(policy, maxAge) {
56
+ return {
57
+ httpOnly: true,
58
+ secure: policy.secure,
59
+ sameSite: "lax",
60
+ path: "/",
61
+ maxAge,
62
+ };
63
+ }
64
+ export function setSessionCookies(response, policy, tokens) {
65
+ const accessMaxAge = Math.max(1, Math.floor(tokens.expiresIn));
66
+ response.cookies.set(policy.names.session, tokens.accessToken, cookieAttributes(policy, accessMaxAge));
67
+ response.cookies.set(policy.names.refresh, tokens.refreshToken, cookieAttributes(policy, REFRESH_COOKIE_MAX_AGE_SECONDS));
68
+ }
69
+ export function clearSessionCookies(response, policy) {
70
+ clearCookie(response, policy, policy.names.session);
71
+ clearCookie(response, policy, policy.names.refresh);
72
+ }
73
+ export function setAuthFlowCookie(response, policy, flow) {
74
+ response.cookies.set(`${policy.names.authFlowPrefix}${flow.state}`, encodeAuthFlowState(flow), cookieAttributes(policy, AUTH_FLOW_COOKIE_MAX_AGE_SECONDS));
75
+ }
76
+ // `__Host-` cookies are only replaced when the clearing cookie repeats Secure and Path=/.
77
+ export function clearCookie(response, policy, name) {
78
+ response.cookies.set(name, "", { ...cookieAttributes(policy, 0), expires: new Date(0) });
79
+ }
80
+ export function encodeAuthFlowState(flow) {
81
+ return base64UrlEncode(new TextEncoder().encode(JSON.stringify(flow)));
82
+ }
83
+ export function decodeAuthFlowState(value) {
84
+ if (value === undefined || value === "") {
85
+ return null;
86
+ }
87
+ let candidate;
88
+ try {
89
+ candidate = JSON.parse(new TextDecoder().decode(base64UrlDecode(value)));
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ if (!isAuthFlowState(candidate)) {
95
+ return null;
96
+ }
97
+ return {
98
+ state: candidate.state,
99
+ codeVerifier: candidate.codeVerifier,
100
+ redirectUri: candidate.redirectUri,
101
+ returnTo: safeReturnTo(candidate.returnTo),
102
+ };
103
+ }
104
+ function isAuthFlowState(value) {
105
+ if (typeof value !== "object" || value === null) {
106
+ return false;
107
+ }
108
+ const record = value;
109
+ return (typeof record.state === "string" &&
110
+ typeof record.codeVerifier === "string" &&
111
+ typeof record.redirectUri === "string" &&
112
+ typeof record.returnTo === "string");
113
+ }
114
+ /**
115
+ * Replaces the named cookies in a raw `Cookie` request header while preserving every
116
+ * other cookie byte for byte.
117
+ */
118
+ export function replaceRequestCookies(cookieHeader, replacements) {
119
+ const replacedNames = new Set(Object.keys(replacements));
120
+ const preserved = (cookieHeader ?? "")
121
+ .split(";")
122
+ .map((pair) => pair.trim())
123
+ .filter((pair) => pair !== "")
124
+ .filter((pair) => !replacedNames.has(cookiePairName(pair)));
125
+ const replaced = Object.entries(replacements).map(([name, value]) => `${name}=${value}`);
126
+ return [...preserved, ...replaced].join("; ");
127
+ }
128
+ function cookiePairName(pair) {
129
+ const separator = pair.indexOf("=");
130
+ return (separator === -1 ? pair : pair.slice(0, separator)).trim();
131
+ }