ai-atlas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +52 -0
  2. package/dist/src/client.d.ts +8 -0
  3. package/dist/src/client.d.ts.map +1 -0
  4. package/dist/src/client.js +37 -0
  5. package/dist/src/client.js.map +1 -0
  6. package/dist/src/index.d.ts +3 -0
  7. package/dist/src/index.d.ts.map +1 -0
  8. package/dist/src/index.js +3 -0
  9. package/dist/src/index.js.map +1 -0
  10. package/dist/src/internal/defaults.generated.d.ts +2 -0
  11. package/dist/src/internal/defaults.generated.d.ts.map +1 -0
  12. package/dist/src/internal/defaults.generated.js +9 -0
  13. package/dist/src/internal/defaults.generated.js.map +1 -0
  14. package/dist/src/internal/localBetterAuthUrl.d.ts +9 -0
  15. package/dist/src/internal/localBetterAuthUrl.d.ts.map +1 -0
  16. package/dist/src/internal/localBetterAuthUrl.js +79 -0
  17. package/dist/src/internal/localBetterAuthUrl.js.map +1 -0
  18. package/dist/src/next/callback.d.ts +10 -0
  19. package/dist/src/next/callback.d.ts.map +1 -0
  20. package/dist/src/next/callback.js +101 -0
  21. package/dist/src/next/callback.js.map +1 -0
  22. package/dist/src/next/index.d.ts +2 -0
  23. package/dist/src/next/index.d.ts.map +1 -0
  24. package/dist/src/next/index.js +2 -0
  25. package/dist/src/next/index.js.map +1 -0
  26. package/dist/src/next/oauth.d.ts +20 -0
  27. package/dist/src/next/oauth.d.ts.map +1 -0
  28. package/dist/src/next/oauth.js +231 -0
  29. package/dist/src/next/oauth.js.map +1 -0
  30. package/dist/src/react-shim.d.ts +18 -0
  31. package/dist/src/react-shim.d.ts.map +1 -0
  32. package/dist/src/react-shim.js +4 -0
  33. package/dist/src/react-shim.js.map +1 -0
  34. package/dist/src/server.d.ts +33 -0
  35. package/dist/src/server.d.ts.map +1 -0
  36. package/dist/src/server.js +168 -0
  37. package/dist/src/server.js.map +1 -0
  38. package/dist/tsconfig.tsbuildinfo +1 -0
  39. package/package.json +51 -0
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # ai-atlas
2
+
3
+ Atlas tool + UI helpers for the Vercel AI SDK (`ai@6` / `@ai-sdk/react@3`).
4
+
5
+ ## What it does
6
+
7
+ - Exports an AI SDK tool named `Atlas` (`atlasTools.Atlas`).
8
+ - When invoked, your chat UI can render an iframe that loads the `apps/web` sign-in page (`/auth/sign-in`).
9
+ - Supports Vercel Deployment Protection via a server-side unlock redirect that sets the bypass cookie.
10
+
11
+ ## Environment variables
12
+
13
+ ### BETTER_AUTH_URL (optional at runtime; baked in for published builds)
14
+
15
+ `ai-atlas` needs to know the origin of `apps/web` to construct the Atlas connect URL.
16
+
17
+ Resolution order:
18
+
19
+ 1. `process.env.BETTER_AUTH_URL` (runtime override; useful for local dev)
20
+ 2. `DEFAULT_BETTER_AUTH_URL` baked into the published package during CI publish (pulled from Vercel env)
21
+
22
+ So in deployed consumers (like `apps/demo` on Vercel), you should **not** need to set `BETTER_AUTH_URL` as long as you’re using the published package.
23
+
24
+ Local dev example (`apps/demo/.env.local`):
25
+
26
+ ```bash
27
+ BETTER_AUTH_URL=http://localhost:3001
28
+ ```
29
+
30
+ ### VERCEL_PROTECTION_BYPASS_TOKEN (optional)
31
+
32
+ If `apps/web` is behind Vercel Deployment Protection, set:
33
+
34
+ - `VERCEL_PROTECTION_BYPASS_TOKEN`
35
+ - The “Protection Bypass for Automation” token.
36
+ - **Must remain server-side.** Never put it directly into an iframe URL.
37
+
38
+ ## Dist-tags (prod vs staging)
39
+
40
+ This package is published to npm with dist-tags:
41
+
42
+ - `latest`: prod (baked `BETTER_AUTH_URL` from the production `apps/web` Vercel env)
43
+ - `preview`: preview/staging (baked `BETTER_AUTH_URL` from the preview `apps/web` Vercel env)
44
+
45
+ ## Exports
46
+
47
+ - `atlasTools` (server): the tool set to pass into `streamText({ tools })`
48
+ - `AtlasUIMessage` (types): a typed `UIMessage` that includes `tool-Atlas` parts
49
+ - `AtlasToolCard` (client): a small UI component that renders the iframe and provides an Unlock button
50
+ - `atlasConfigResponse()` / `atlasUnlockResponse(req)` (server helpers): implement `/api/atlas/config` and `/api/atlas/unlock`
51
+
52
+
@@ -0,0 +1,8 @@
1
+ import "./react-shim";
2
+ export type AtlasToolCardProps = {
3
+ signInUrl: string;
4
+ needsUnlock?: boolean;
5
+ unlockEndpoint?: string;
6
+ };
7
+ export declare function AtlasToolCard(props: AtlasToolCardProps): any;
8
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.tsx"],"names":[],"mappings":"AACA,OAAO,cAAc,CAAC;AAItB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,OAwCtD"}
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import "./react-shim";
4
+ import { useEffect, useMemo } from "react";
5
+ export function AtlasToolCard(props) {
6
+ const unlockEndpoint = props.unlockEndpoint ?? "/api/chat?atlasUnlock=1";
7
+ const needsUnlock = props.needsUnlock ?? false;
8
+ const iframeSrc = useMemo(() => {
9
+ const u = new URL(props.signInUrl, window.location.origin);
10
+ // Tell the embedded app what origin to postMessage back to.
11
+ u.searchParams.set("clientOrigin", window.location.origin);
12
+ return u.toString();
13
+ }, [props.signInUrl]);
14
+ const unlockUrl = useMemo(() => {
15
+ const u = new URL(unlockEndpoint, window.location.origin);
16
+ u.searchParams.set("returnTo", window.location.href);
17
+ return u.toString();
18
+ }, [unlockEndpoint]);
19
+ useEffect(() => {
20
+ if (!needsUnlock)
21
+ return;
22
+ // If Deployment Protection is enabled, we need a top-level navigation once to set
23
+ // the bypass cookie on the `apps/web` origin. Do it automatically, once per session.
24
+ try {
25
+ const alreadyAttempted = window.sessionStorage.getItem("atlas_unlock_attempted") === "1";
26
+ if (alreadyAttempted)
27
+ return;
28
+ window.sessionStorage.setItem("atlas_unlock_attempted", "1");
29
+ }
30
+ catch {
31
+ // If sessionStorage is unavailable, still attempt once.
32
+ }
33
+ window.location.assign(unlockUrl);
34
+ }, [needsUnlock, unlockUrl]);
35
+ return (_jsx("iframe", { title: "Atlas Sign In", src: iframeSrc, className: "h-[540px] w-full bg-white" }));
36
+ }
37
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AACb,OAAO,cAAc,CAAC;AAEtB,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAQ3C,MAAM,UAAU,aAAa,CAAC,KAAyB;IACrD,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,IAAI,yBAAyB,CAAC;IACzE,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC;IAE/C,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE;QAC7B,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC3D,4DAA4D;QAC5D,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC3D,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IACtB,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IAEtB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE;QAC7B,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC1D,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACrD,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IACtB,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC;IAErB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,WAAW;YAAE,OAAO;QAEzB,kFAAkF;QAClF,qFAAqF;QACrF,IAAI,CAAC;YACH,MAAM,gBAAgB,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,wBAAwB,CAAC,KAAK,GAAG,CAAC;YACzF,IAAI,gBAAgB;gBAAE,OAAO;YAC7B,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,wDAAwD;QAC1D,CAAC;QAED,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpC,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC;IAE7B,OAAO,CACL,iBACE,KAAK,EAAC,eAAe,EACrB,GAAG,EAAE,SAAS,EACd,SAAS,EAAC,2BAA2B,GACrC,CACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./server";
2
+ export * from "./client";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./server";
2
+ export * from "./client";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_BETTER_AUTH_URL: "https://atlas-platform-v2-web-git-dev-atlas-1cf296a1.vercel.app";
2
+ //# sourceMappingURL=defaults.generated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.generated.d.ts","sourceRoot":"","sources":["../../../src/internal/defaults.generated.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,uBAAuB,EAAG,iEAA0E,CAAC"}
@@ -0,0 +1,9 @@
1
+ // This file is generated during build/publish.
2
+ //
3
+ // Precedence at runtime:
4
+ // 1) process.env.BETTER_AUTH_URL (local/dev override)
5
+ // 2) DEFAULT_BETTER_AUTH_URL (baked at build time, e.g. from Vercel env)
6
+ //
7
+ // Do not edit by hand.
8
+ export const DEFAULT_BETTER_AUTH_URL = "https://atlas-platform-v2-web-git-dev-atlas-1cf296a1.vercel.app";
9
+ //# sourceMappingURL=defaults.generated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.generated.js","sourceRoot":"","sources":["../../../src/internal/defaults.generated.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,yBAAyB;AACzB,sDAAsD;AACtD,yEAAyE;AACzE,EAAE;AACF,uBAAuB;AACvB,MAAM,CAAC,MAAM,uBAAuB,GAAG,iEAA0E,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Local-dev convenience:
3
+ * If `apps/demo` didn't set BETTER_AUTH_URL, try to load it from `apps/web/.env*`.
4
+ *
5
+ * This should only ever run in Node (not Edge). In Vercel/prod you should rely on
6
+ * runtime env or baked defaults.
7
+ */
8
+ export declare function readLocalBetterAuthUrlFromAppsWeb(): string | undefined;
9
+ //# sourceMappingURL=localBetterAuthUrl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localBetterAuthUrl.d.ts","sourceRoot":"","sources":["../../../src/internal/localBetterAuthUrl.ts"],"names":[],"mappings":"AAmDA;;;;;;GAMG;AACH,wBAAgB,iCAAiC,IAAI,MAAM,GAAG,SAAS,CAkBtE"}
@@ -0,0 +1,79 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function parseDotEnv(text) {
4
+ const out = {};
5
+ for (const rawLine of text.split(/\r?\n/)) {
6
+ const line = rawLine.trim();
7
+ if (!line || line.startsWith("#"))
8
+ continue;
9
+ const eq = line.indexOf("=");
10
+ if (eq <= 0)
11
+ continue;
12
+ const key = line.slice(0, eq).trim();
13
+ let value = line.slice(eq + 1).trim();
14
+ if (!key)
15
+ continue;
16
+ if ((value.startsWith('"') && value.endsWith('"')) ||
17
+ (value.startsWith("'") && value.endsWith("'"))) {
18
+ value = value.slice(1, -1);
19
+ }
20
+ out[key] = value;
21
+ }
22
+ return out;
23
+ }
24
+ function tryReadEnvFile(filePath) {
25
+ try {
26
+ const text = fs.readFileSync(filePath, "utf8");
27
+ const env = parseDotEnv(text);
28
+ const v = (env.BETTER_AUTH_URL ?? "").trim().replace(/\/$/, "");
29
+ return v || undefined;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ function findRepoRootFromCwd(maxDepth = 6) {
36
+ let dir = process.cwd();
37
+ for (let i = 0; i <= maxDepth; i++) {
38
+ // Common case: repo root contains apps/web
39
+ if (fs.existsSync(path.join(dir, "apps", "web")))
40
+ return dir;
41
+ // If cwd is already inside /apps/*, parent might be /apps; detect sibling "web"
42
+ if (path.basename(dir) === "apps" && fs.existsSync(path.join(dir, "web"))) {
43
+ return path.dirname(dir);
44
+ }
45
+ const parent = path.dirname(dir);
46
+ if (parent === dir)
47
+ break;
48
+ dir = parent;
49
+ }
50
+ return null;
51
+ }
52
+ /**
53
+ * Local-dev convenience:
54
+ * If `apps/demo` didn't set BETTER_AUTH_URL, try to load it from `apps/web/.env*`.
55
+ *
56
+ * This should only ever run in Node (not Edge). In Vercel/prod you should rely on
57
+ * runtime env or baked defaults.
58
+ */
59
+ export function readLocalBetterAuthUrlFromAppsWeb() {
60
+ if (process.env.VERCEL === "1")
61
+ return undefined;
62
+ if (process.env.NODE_ENV === "production")
63
+ return undefined;
64
+ const root = findRepoRootFromCwd();
65
+ if (!root)
66
+ return undefined;
67
+ const candidates = [
68
+ path.join(root, "apps", "web", ".env.local"),
69
+ path.join(root, "apps", "web", ".env.development"),
70
+ path.join(root, "apps", "web", ".env"),
71
+ ];
72
+ for (const p of candidates) {
73
+ const v = tryReadEnvFile(p);
74
+ if (v)
75
+ return v;
76
+ }
77
+ return undefined;
78
+ }
79
+ //# sourceMappingURL=localBetterAuthUrl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localBetterAuthUrl.js","sourceRoot":"","sources":["../../../src/internal/localBetterAuthUrl.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Reusable OAuth callback page for the demo app.
3
+ *
4
+ * It calls the demo's `/api/atlas/oauth/exchange` endpoint (which should be a thin wrapper around `atlasOAuthExchangePOST`),
5
+ * then notifies `window.opener` and closes itself (popup flow).
6
+ */
7
+ export declare function AtlasOAuthCallbackPage(props: {
8
+ exchangeEndpoint?: string;
9
+ }): any;
10
+ //# sourceMappingURL=callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../../src/next/callback.tsx"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,OAoI1E"}
@@ -0,0 +1,101 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ function isOkExchangeResult(value) {
5
+ return !!value && typeof value === "object" && value.ok === true;
6
+ }
7
+ function getParamsFromHref(href) {
8
+ const url = new URL(href);
9
+ return {
10
+ code: url.searchParams.get("code") ?? "",
11
+ state: url.searchParams.get("state") ?? "",
12
+ error: url.searchParams.get("error") ?? "",
13
+ errorDescription: url.searchParams.get("error_description") ?? "",
14
+ };
15
+ }
16
+ /**
17
+ * Reusable OAuth callback page for the demo app.
18
+ *
19
+ * It calls the demo's `/api/atlas/oauth/exchange` endpoint (which should be a thin wrapper around `atlasOAuthExchangePOST`),
20
+ * then notifies `window.opener` and closes itself (popup flow).
21
+ */
22
+ export function AtlasOAuthCallbackPage(props) {
23
+ const exchangeEndpoint = props.exchangeEndpoint ?? "/api/atlas/oauth/exchange";
24
+ // NOTE: Even though this file is a client component, some monorepo / bundler setups
25
+ // can still end up evaluating it during SSR. Avoid touching `window` during render.
26
+ const [hydrated, setHydrated] = useState(false);
27
+ const [{ code, state, error, errorDescription }, setParams] = useState(() => ({
28
+ code: "",
29
+ state: "",
30
+ error: "",
31
+ errorDescription: "",
32
+ }));
33
+ const [result, setResult] = useState(null);
34
+ useEffect(() => {
35
+ setHydrated(true);
36
+ if (typeof window === "undefined")
37
+ return;
38
+ setParams(getParamsFromHref(window.location.href));
39
+ }, []);
40
+ const canExchange = useMemo(() => hydrated && !!code && !!state, [hydrated, code, state]);
41
+ const hasOAuthError = useMemo(() => hydrated && !!error, [hydrated, error]);
42
+ const missingParams = useMemo(() => hydrated && !hasOAuthError && (!code || !state), [hydrated, hasOAuthError, code, state]);
43
+ useEffect(() => {
44
+ if (!canExchange)
45
+ return;
46
+ let cancelled = false;
47
+ (async () => {
48
+ try {
49
+ const res = await fetch(exchangeEndpoint, {
50
+ method: "POST",
51
+ headers: { "content-type": "application/json" },
52
+ body: JSON.stringify({ code, state }),
53
+ });
54
+ const json = (await res.json().catch(() => null));
55
+ if (cancelled)
56
+ return;
57
+ if (!res.ok || !isOkExchangeResult(json)) {
58
+ setResult(json ?? { error: "Exchange failed" });
59
+ return;
60
+ }
61
+ setResult(json);
62
+ try {
63
+ window.opener?.postMessage({ type: "atlas_oauth_complete", ok: true, scope: json.scope ?? "" }, window.location.origin);
64
+ }
65
+ catch {
66
+ // ignore
67
+ }
68
+ setTimeout(() => window.close(), 50);
69
+ }
70
+ catch (e) {
71
+ if (cancelled)
72
+ return;
73
+ setResult({ error: e instanceof Error ? e.message : "Exchange failed" });
74
+ }
75
+ })();
76
+ return () => {
77
+ cancelled = true;
78
+ };
79
+ }, [canExchange, code, state, exchangeEndpoint]);
80
+ useEffect(() => {
81
+ if (!hasOAuthError)
82
+ return;
83
+ try {
84
+ window.opener?.postMessage({ type: "atlas_oauth_complete", ok: false, error, errorDescription }, window.location.origin);
85
+ }
86
+ catch {
87
+ // ignore
88
+ }
89
+ }, [hasOAuthError, error, errorDescription]);
90
+ if (!hydrated) {
91
+ return (_jsxs("div", { className: "mx-auto max-w-md p-6", children: [_jsx("h1", { className: "text-xl font-semibold", children: "Atlas OAuth Callback" }), _jsx("p", { className: "mt-2 text-sm text-zinc-600", children: "Loading\u2026" })] }));
92
+ }
93
+ if (hasOAuthError) {
94
+ return (_jsxs("div", { className: "mx-auto max-w-md p-6", children: [_jsx("h1", { className: "text-xl font-semibold", children: "Atlas OAuth Callback" }), _jsxs("div", { className: "mt-2 text-sm text-red-700", children: [_jsxs("div", { children: ["OAuth error: ", error] }), errorDescription ? _jsx("div", { className: "mt-1 text-red-700", children: errorDescription }) : null] }), _jsx("p", { className: "mt-3 text-sm text-zinc-600", children: "You can close this window." })] }));
95
+ }
96
+ if (missingParams) {
97
+ return (_jsxs("div", { className: "mx-auto max-w-md p-6", children: [_jsx("h1", { className: "text-xl font-semibold", children: "Atlas OAuth Callback" }), _jsx("p", { className: "mt-2 text-sm text-zinc-600", children: "Missing required OAuth parameters." })] }));
98
+ }
99
+ return (_jsxs("div", { className: "mx-auto max-w-md p-6", children: [_jsx("h1", { className: "text-xl font-semibold", children: "Finishing sign-in\u2026" }), !result ? (_jsx("p", { className: "mt-2 text-sm text-zinc-600", children: "Exchanging authorization code\u2026" })) : result.ok === true ? (_jsx("p", { className: "mt-2 text-sm text-zinc-600", children: "Connected. You can close this window." })) : (_jsxs("div", { className: "mt-2 text-sm text-red-700", children: [_jsxs("div", { children: ["Failed to connect: ", result.error ?? "Unknown error"] }), result.details ? _jsx("pre", { className: "mt-2 whitespace-pre-wrap", children: result.details }) : null] }))] }));
100
+ }
101
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.js","sourceRoot":"","sources":["../../../src/next/callback.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAMrD,SAAS,kBAAkB,CAAC,KAAc;IACxC,OAAO,CAAC,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAK,KAAa,CAAC,EAAE,KAAK,IAAI,CAAC;AAC5E,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY;IAMrC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;IAC1B,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;QACxC,KAAK,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE;QAC1C,KAAK,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE;QAC1C,gBAAgB,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,EAAE;KAClE,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAoC;IACzE,MAAM,gBAAgB,GAAG,KAAK,CAAC,gBAAgB,IAAI,2BAA2B,CAAC;IAE/E,oFAAoF;IACpF,oFAAoF;IACpF,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,SAAS,CAAC,GAAG,QAAQ,CAKnE,GAAG,EAAE,CAAC,CAAC;QACR,IAAI,EAAE,EAAE;QACR,KAAK,EAAE,EAAE;QACT,KAAK,EAAE,EAAE;QACT,gBAAgB,EAAE,EAAE;KACrB,CAAC,CAAC,CAAC;IACJ,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAwB,IAAI,CAAC,CAAC;IAElE,SAAS,CAAC,GAAG,EAAE;QACb,WAAW,CAAC,IAAI,CAAC,CAAC;QAClB,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO;QAC1C,SAAS,CAAC,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IACrD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IAC1F,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IAC5E,MAAM,aAAa,GAAG,OAAO,CAC3B,GAAG,EAAE,CAAC,QAAQ,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EACrD,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,KAAK,CAAC,CACvC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,WAAW;YAAE,OAAO;QACzB,IAAI,SAAS,GAAG,KAAK,CAAC;QAEtB,CAAC,KAAK,IAAI,EAAE;YACV,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,gBAAgB,EAAE;oBACxC,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;oBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;iBACtC,CAAC,CAAC;gBACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAY,CAAC;gBAC7D,IAAI,SAAS;oBAAE,OAAO;gBAEtB,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;oBACzC,SAAS,CAAE,IAA8B,IAAI,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;oBAC3E,OAAO;gBACT,CAAC;gBAED,SAAS,CAAC,IAAI,CAAC,CAAC;gBAEhB,IAAI,CAAC;oBACH,MAAM,CAAC,MAAM,EAAE,WAAW,CACxB,EAAE,IAAI,EAAE,sBAAsB,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,EACnE,MAAM,CAAC,QAAQ,CAAC,MAAM,CACvB,CAAC;gBACJ,CAAC;gBAAC,MAAM,CAAC;oBACP,SAAS;gBACX,CAAC;gBAED,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;YACvC,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAI,SAAS;oBAAE,OAAO;gBACtB,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,EAAE,CAAC,CAAC;YAC3E,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QAEL,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAC;QACnB,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAEjD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,aAAa;YAAE,OAAO;QAC3B,IAAI,CAAC;YACH,MAAM,CAAC,MAAM,EAAE,WAAW,CACxB,EAAE,IAAI,EAAE,sBAAsB,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,EACpE,MAAM,CAAC,QAAQ,CAAC,MAAM,CACvB,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC,EAAE,CAAC,aAAa,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAE7C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CACL,eAAK,SAAS,EAAC,sBAAsB,aACnC,aAAI,SAAS,EAAC,uBAAuB,qCAA0B,EAC/D,YAAG,SAAS,EAAC,4BAA4B,8BAAa,IAClD,CACP,CAAC;IACJ,CAAC;IAED,IAAI,aAAa,EAAE,CAAC;QAClB,OAAO,CACL,eAAK,SAAS,EAAC,sBAAsB,aACnC,aAAI,SAAS,EAAC,uBAAuB,qCAA0B,EAC/D,eAAK,SAAS,EAAC,2BAA2B,aACxC,2CAAmB,KAAK,IAAO,EAC9B,gBAAgB,CAAC,CAAC,CAAC,cAAK,SAAS,EAAC,mBAAmB,YAAE,gBAAgB,GAAO,CAAC,CAAC,CAAC,IAAI,IAClF,EACN,YAAG,SAAS,EAAC,4BAA4B,2CAA+B,IACpE,CACP,CAAC;IACJ,CAAC;IAED,IAAI,aAAa,EAAE,CAAC;QAClB,OAAO,CACL,eAAK,SAAS,EAAC,sBAAsB,aACnC,aAAI,SAAS,EAAC,uBAAuB,qCAA0B,EAC/D,YAAG,SAAS,EAAC,4BAA4B,mDAAuC,IAC5E,CACP,CAAC;IACJ,CAAC;IAED,OAAO,CACL,eAAK,SAAS,EAAC,sBAAsB,aACnC,aAAI,SAAS,EAAC,uBAAuB,wCAAwB,EAC5D,CAAC,MAAM,CAAC,CAAC,CAAC,CACT,YAAG,SAAS,EAAC,4BAA4B,oDAAmC,CAC7E,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,CACvB,YAAG,SAAS,EAAC,4BAA4B,sDAA0C,CACpF,CAAC,CAAC,CAAC,CACF,eAAK,SAAS,EAAC,2BAA2B,aACxC,iDAAyB,MAAM,CAAC,KAAK,IAAI,eAAe,IAAO,EAC9D,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,cAAK,SAAS,EAAC,0BAA0B,YAAE,MAAM,CAAC,OAAO,GAAO,CAAC,CAAC,CAAC,IAAI,IACrF,CACP,IACG,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from "./callback";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/next/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from "./callback";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/next/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
@@ -0,0 +1,20 @@
1
+ import "server-only";
2
+ import { NextResponse } from "next/server";
3
+ export declare const ATLAS_COOKIE_ACCESS_TOKEN = "atlas_access_token";
4
+ export declare const ATLAS_COOKIE_GRANTED_SCOPES = "atlas_granted_scopes";
5
+ export declare const ATLAS_COOKIE_OAUTH_CLIENT_ID = "atlas_oauth_client_id_v2";
6
+ export declare const ATLAS_COOKIE_OAUTH_STATE = "atlas_oauth_state";
7
+ export declare const ATLAS_COOKIE_OAUTH_VERIFIER = "atlas_oauth_verifier";
8
+ export declare function atlasOAuthStartPOST(req: Request): Promise<NextResponse<{
9
+ authorizeUrl: string;
10
+ state: string;
11
+ }>>;
12
+ export declare function atlasOAuthExchangePOST(req: Request): Promise<NextResponse<{
13
+ error: string;
14
+ }> | NextResponse<{
15
+ ok: boolean;
16
+ tokenType: string;
17
+ expiresIn: number | undefined;
18
+ scope: string;
19
+ }>>;
20
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../../src/next/oauth.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAyB3C,eAAO,MAAM,yBAAyB,uBAAuB,CAAC;AAC9D,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AAIlE,eAAO,MAAM,4BAA4B,6BAA6B,CAAC;AACvE,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AAwFlE,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO;;;IA8CrD;AAOD,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO;;;;;;;IAkGxD"}
@@ -0,0 +1,231 @@
1
+ import "server-only";
2
+ import { cookies } from "next/headers";
3
+ import { NextResponse } from "next/server";
4
+ import { z } from "zod";
5
+ import { DEFAULT_BETTER_AUTH_URL } from "../internal/defaults.generated";
6
+ import { readLocalBetterAuthUrlFromAppsWeb } from "../internal/localBetterAuthUrl";
7
+ function base64UrlEncode(bytes) {
8
+ return Buffer.from(bytes)
9
+ .toString("base64")
10
+ .replace(/\+/g, "-")
11
+ .replace(/\//g, "_")
12
+ .replace(/=+$/g, "");
13
+ }
14
+ async function sha256Base64Url(input) {
15
+ const data = new TextEncoder().encode(input);
16
+ const digest = await crypto.subtle.digest("SHA-256", data);
17
+ return base64UrlEncode(new Uint8Array(digest));
18
+ }
19
+ function randomBase64Url(bytesLength = 32) {
20
+ const bytes = new Uint8Array(bytesLength);
21
+ crypto.getRandomValues(bytes);
22
+ return base64UrlEncode(bytes);
23
+ }
24
+ export const ATLAS_COOKIE_ACCESS_TOKEN = "atlas_access_token";
25
+ export const ATLAS_COOKIE_GRANTED_SCOPES = "atlas_granted_scopes";
26
+ // NOTE: bumping this forces re-registration for existing users so the OAuth client
27
+ // picks up newly supported scopes (the OAuth server validates requested scopes
28
+ // against the registered client scope set).
29
+ export const ATLAS_COOKIE_OAUTH_CLIENT_ID = "atlas_oauth_client_id_v2";
30
+ export const ATLAS_COOKIE_OAUTH_STATE = "atlas_oauth_state";
31
+ export const ATLAS_COOKIE_OAUTH_VERIFIER = "atlas_oauth_verifier";
32
+ function getAtlasAuthBaseUrl() {
33
+ // Precedence:
34
+ // 1) Runtime env override
35
+ // 2) Baked default from CI publish
36
+ // 3) Local-dev fallback from apps/web/.env*
37
+ // NOTE: We intentionally use `||` (not `??`) so empty-string defaults don't block fallbacks.
38
+ const raw = ((process.env.BETTER_AUTH_URL ?? "").trim() ||
39
+ DEFAULT_BETTER_AUTH_URL ||
40
+ readLocalBetterAuthUrlFromAppsWeb() ||
41
+ "").replace(/\/$/, "");
42
+ if (!raw)
43
+ throw new Error("BETTER_AUTH_URL is not set (and no baked default is available)");
44
+ // If it already points to the issuer path, keep it; otherwise append /api/auth.
45
+ const base = raw.endsWith("/api/auth") ? raw : `${raw}/api/auth`;
46
+ return base.replace(/\/$/, "");
47
+ }
48
+ function expectedConvexIssuer() {
49
+ // Must match `issuer` configured in `packages/auth` jwt() plugin.
50
+ return getAtlasAuthBaseUrl();
51
+ }
52
+ function decodeJwtPayload(token) {
53
+ const parts = token.split(".");
54
+ if (parts.length !== 3)
55
+ return null;
56
+ try {
57
+ const json = Buffer.from(parts[1], "base64url").toString("utf8");
58
+ return JSON.parse(json);
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ async function ensureOAuthClient(args) {
65
+ const jar = await cookies();
66
+ const existing = jar.get(ATLAS_COOKIE_OAUTH_CLIENT_ID)?.value;
67
+ if (existing)
68
+ return existing;
69
+ // Register the demo client with the *full* set of scopes it may ever request.
70
+ // Better Auth validates authorize requests against the registered client scope set.
71
+ const registrationScope = [
72
+ "openid",
73
+ "profile",
74
+ "email",
75
+ "offline_access",
76
+ "atlas:gmail.readonly",
77
+ "atlas:drive.readonly",
78
+ ].join(" ");
79
+ const registerUrl = `${getAtlasAuthBaseUrl()}/oauth2/register`;
80
+ const res = await fetch(registerUrl, {
81
+ method: "POST",
82
+ headers: { "content-type": "application/json" },
83
+ body: JSON.stringify({
84
+ client_name: "Atlas Demo",
85
+ redirect_uris: [args.redirectUri],
86
+ token_endpoint_auth_method: "none",
87
+ grant_types: ["authorization_code"],
88
+ response_types: ["code"],
89
+ scope: registrationScope,
90
+ }),
91
+ });
92
+ if (!res.ok) {
93
+ const text = await res.text().catch(() => "");
94
+ throw new Error(`Client registration failed: ${res.status} ${text}`);
95
+ }
96
+ const json = (await res.json());
97
+ const clientId = json.client_id;
98
+ if (!clientId)
99
+ throw new Error("Client registration returned no client_id");
100
+ jar.set(ATLAS_COOKIE_OAUTH_CLIENT_ID, clientId, {
101
+ httpOnly: true,
102
+ sameSite: "lax",
103
+ secure: process.env.NODE_ENV === "production",
104
+ path: "/",
105
+ });
106
+ return clientId;
107
+ }
108
+ const startBodySchema = z.object({
109
+ scopes: z.array(z.string()).optional(),
110
+ });
111
+ export async function atlasOAuthStartPOST(req) {
112
+ const body = startBodySchema.parse(await req.json().catch(() => ({})));
113
+ const demoOrigin = new URL(req.url).origin;
114
+ const redirectUri = new URL("/atlas/callback", demoOrigin).toString();
115
+ const clientId = await ensureOAuthClient({ redirectUri });
116
+ const codeVerifier = randomBase64Url(48);
117
+ const codeChallenge = await sha256Base64Url(codeVerifier);
118
+ const state = crypto.randomUUID();
119
+ const jar = await cookies();
120
+ jar.set(ATLAS_COOKIE_OAUTH_STATE, state, {
121
+ httpOnly: true,
122
+ sameSite: "lax",
123
+ secure: process.env.NODE_ENV === "production",
124
+ path: "/",
125
+ maxAge: 10 * 60,
126
+ });
127
+ jar.set(ATLAS_COOKIE_OAUTH_VERIFIER, codeVerifier, {
128
+ httpOnly: true,
129
+ sameSite: "lax",
130
+ secure: process.env.NODE_ENV === "production",
131
+ path: "/",
132
+ maxAge: 10 * 60,
133
+ });
134
+ // Always include `openid` so the server can behave as OIDC (and clients can rely on `sub`).
135
+ const requestedScopes = ["openid", ...(body.scopes ?? [])]
136
+ .map((s) => s.trim())
137
+ .filter(Boolean);
138
+ const scope = Array.from(new Set(requestedScopes)).join(" ").trim();
139
+ const authorizeUrl = new URL(`${getAtlasAuthBaseUrl()}/oauth2/authorize`);
140
+ authorizeUrl.searchParams.set("response_type", "code");
141
+ authorizeUrl.searchParams.set("client_id", clientId);
142
+ authorizeUrl.searchParams.set("redirect_uri", redirectUri);
143
+ authorizeUrl.searchParams.set("state", state);
144
+ authorizeUrl.searchParams.set("code_challenge", codeChallenge);
145
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
146
+ if (scope)
147
+ authorizeUrl.searchParams.set("scope", scope);
148
+ authorizeUrl.searchParams.set("resource", "convex");
149
+ authorizeUrl.searchParams.set("prompt", "consent");
150
+ return NextResponse.json({ authorizeUrl: authorizeUrl.toString(), state });
151
+ }
152
+ const exchangeBodySchema = z.object({
153
+ code: z.string().min(1),
154
+ state: z.string().min(1),
155
+ });
156
+ export async function atlasOAuthExchangePOST(req) {
157
+ const { code, state } = exchangeBodySchema.parse(await req.json());
158
+ const jar = await cookies();
159
+ const expectedState = jar.get(ATLAS_COOKIE_OAUTH_STATE)?.value ?? "";
160
+ const codeVerifier = jar.get(ATLAS_COOKIE_OAUTH_VERIFIER)?.value ?? "";
161
+ const clientId = jar.get(ATLAS_COOKIE_OAUTH_CLIENT_ID)?.value ?? "";
162
+ if (!expectedState || state !== expectedState) {
163
+ return NextResponse.json({ error: "Invalid state" }, { status: 400 });
164
+ }
165
+ if (!codeVerifier || !clientId) {
166
+ return NextResponse.json({ error: "Missing PKCE session" }, { status: 400 });
167
+ }
168
+ const demoOrigin = new URL(req.url).origin;
169
+ const redirectUri = new URL("/atlas/callback", demoOrigin).toString();
170
+ const tokenUrl = `${getAtlasAuthBaseUrl()}/oauth2/token`;
171
+ const body = new URLSearchParams({
172
+ grant_type: "authorization_code",
173
+ code,
174
+ redirect_uri: redirectUri,
175
+ code_verifier: codeVerifier,
176
+ client_id: clientId,
177
+ });
178
+ const res = await fetch(tokenUrl, {
179
+ method: "POST",
180
+ headers: { "content-type": "application/x-www-form-urlencoded" },
181
+ body,
182
+ });
183
+ if (!res.ok) {
184
+ const text = await res.text().catch(() => "");
185
+ return NextResponse.json({ error: "Token exchange failed", status: res.status, details: text.slice(0, 2000) }, { status: 502 });
186
+ }
187
+ const json = (await res.json());
188
+ const accessToken = json.access_token;
189
+ if (!accessToken) {
190
+ return NextResponse.json({ error: "Malformed token response" }, { status: 502 });
191
+ }
192
+ const payload = decodeJwtPayload(accessToken);
193
+ if (!payload || typeof payload !== "object") {
194
+ return NextResponse.json({ error: "Expected JWT access_token for Convex, got non-JWT token" }, { status: 502 });
195
+ }
196
+ const aud = payload.aud;
197
+ const iss = payload.iss;
198
+ const hasConvexAud = aud === "convex" || (Array.isArray(aud) && aud.includes("convex"));
199
+ if (!hasConvexAud) {
200
+ return NextResponse.json({ error: "Access token audience is not convex", aud }, { status: 502 });
201
+ }
202
+ const expectedIss = expectedConvexIssuer();
203
+ if (iss !== expectedIss) {
204
+ return NextResponse.json({ error: "Access token issuer mismatch", iss, expectedIss }, { status: 502 });
205
+ }
206
+ const maxAge = typeof json.expires_in === "number" ? Math.max(60, json.expires_in) : undefined;
207
+ jar.set(ATLAS_COOKIE_ACCESS_TOKEN, accessToken, {
208
+ httpOnly: true,
209
+ sameSite: "lax",
210
+ secure: process.env.NODE_ENV === "production",
211
+ path: "/",
212
+ maxAge,
213
+ });
214
+ jar.set(ATLAS_COOKIE_GRANTED_SCOPES, json.scope ?? "", {
215
+ httpOnly: true,
216
+ sameSite: "lax",
217
+ secure: process.env.NODE_ENV === "production",
218
+ path: "/",
219
+ maxAge,
220
+ });
221
+ // Clean up one-time PKCE cookies.
222
+ jar.set(ATLAS_COOKIE_OAUTH_STATE, "", { httpOnly: true, path: "/", maxAge: 0 });
223
+ jar.set(ATLAS_COOKIE_OAUTH_VERIFIER, "", { httpOnly: true, path: "/", maxAge: 0 });
224
+ return NextResponse.json({
225
+ ok: true,
226
+ tokenType: json.token_type ?? "Bearer",
227
+ expiresIn: json.expires_in,
228
+ scope: json.scope ?? "",
229
+ });
230
+ }
231
+ //# sourceMappingURL=oauth.js.map