@puckeditor/cloud-client 0.9.0-canary.9a2a27cf → 0.9.0-canary.9ff9b8a5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/dist/auth.d.mts +52 -0
- package/dist/auth.d.ts +52 -0
- package/dist/auth.js +307 -0
- package/dist/auth.mjs +267 -0
- package/dist/{chunk-WEKY7YKO.mjs → chunk-DBZWEOBD.mjs} +189 -64
- package/dist/chunk-DZBQDMTW.mjs +14 -0
- package/dist/cli/index.js +208 -0
- package/dist/experimental.d.mts +48 -7
- package/dist/experimental.d.ts +48 -7
- package/dist/experimental.js +357 -171
- package/dist/experimental.mjs +106 -42
- package/dist/index.d.mts +16 -694
- package/dist/index.d.ts +16 -694
- package/dist/index.js +190 -63
- package/dist/index.mjs +2 -1
- package/dist/types-DRxmCSVs.d.mts +732 -0
- package/dist/types-DRxmCSVs.d.ts +732 -0
- package/package.json +17 -6
package/README.md
CHANGED
|
@@ -1 +1,93 @@
|
|
|
1
1
|
# @puckeditor/cloud-client
|
|
2
|
+
|
|
3
|
+
## Authentication
|
|
4
|
+
|
|
5
|
+
`puckHandler` requires an `authenticate` function that resolves the current user from each request. Return `{ id }` to continue, `{ id: null }` to use unowned resources, `null` to respond 401, or a `Response` to short-circuit.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { puckHandler } from "@puckeditor/cloud-client/experimental";
|
|
9
|
+
|
|
10
|
+
export const GET = (request: Request) =>
|
|
11
|
+
puckHandler(request, {
|
|
12
|
+
authenticate: async ({ request }) => {
|
|
13
|
+
const user = await getUser(request); // your own auth
|
|
14
|
+
return user ? { id: user.id } : null;
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
export const POST = GET;
|
|
18
|
+
export const DELETE = GET;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requests to Puck Cloud use your `PUCK_API_KEY` and carry the returned `id` as `x-puck-authenticated-id`. Cloud records it as the external owner of pages, chats and attachments, and only returns rows owned by that id. Returning `{ id: null }` limits API-key requests to unowned resources. Page paths stay unique per site.
|
|
22
|
+
|
|
23
|
+
Direct `chat()` calls also require authentication and return a promise:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const response = await chat(params, {
|
|
27
|
+
authenticate: () => puckAuth.authenticate({ request }),
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Sign in with Puck
|
|
32
|
+
|
|
33
|
+
Let members of your Puck Cloud organization sign in to your editor with their Puck account. Create a project in Puck Cloud, set its branches' deployment URLs under **Settings › Branches**, serve the editor at `/puck` on those origins, and set `PUCK_PROJECT_ID` from **Settings › Project**. Every cloud-client call uses this project, and `PUCK_BRANCH` picks the branch whose pages it reads and writes.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { puckAuth } from "@puckeditor/cloud-client/auth";
|
|
37
|
+
import { puckHandler } from "@puckeditor/cloud-client/experimental";
|
|
38
|
+
|
|
39
|
+
export const GET = (request: Request) =>
|
|
40
|
+
puckHandler(request, { authenticate: puckAuth });
|
|
41
|
+
export const POST = GET;
|
|
42
|
+
export const DELETE = GET;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Gate the editor page:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const session = await puckAuth.getSession(await headers());
|
|
49
|
+
if (!session) redirect(puckAuth.loginUrl("/puck"));
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Protect custom routes with the same function:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const user = await puckAuth.authenticate({ request });
|
|
56
|
+
if (user instanceof Response) return user;
|
|
57
|
+
await generate({ prompt, user });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The helper serves these routes through `puckHandler`:
|
|
61
|
+
|
|
62
|
+
| Route | Behavior |
|
|
63
|
+
| ----------------------------- | ----------------------------------------------------- |
|
|
64
|
+
| `GET /api/puck/auth/login` | Redirect to Puck Cloud (authorization code with PKCE) |
|
|
65
|
+
| `GET /api/puck/auth/callback` | Exchange the code and set an HttpOnly session cookie |
|
|
66
|
+
| `GET /api/puck/auth/session` | Current user, project, branch and expiry, or 401 |
|
|
67
|
+
| `POST /api/puck/auth/logout` | Revoke the session and clear the cookie |
|
|
68
|
+
|
|
69
|
+
Sessions last up to seven days and end when the user signs out of Puck Cloud or leaves the organization. Cloud verifies the user on every request, so no API key is needed for the editor, and external-owner filtering does not apply. `puckAuth` reads `PUCK_PROJECT_ID`, `PUCK_BRANCH` and `PUCK_HOST` (default `https://cloud.puckeditor.com/api`, shared by every cloud-client call). `createPuckAuth({ projectId, branch, host, basePath, appUrl })` builds an instance with explicit options; `basePath` defaults to `/api/puck/auth` and `appUrl` is the public origin when behind a proxy. HTTPS is required except on localhost.
|
|
70
|
+
|
|
71
|
+
Each session edits one branch. A deployment URL signs in to the branch deployed there, so a preview deployment can't edit production. Localhost needs no deployment URL and edits `PUCK_BRANCH`, or the project's default branch.
|
|
72
|
+
|
|
73
|
+
The pages plugin needs no configuration: outside the Puck Cloud editor frame it relies on the app's session cookie and shows an account menu with links to Puck Cloud and sign-out. Pass `authHost` to `createPagesPlugin` if the auth routes live somewhere other than the `/auth` sibling of the pages host.
|
|
74
|
+
|
|
75
|
+
## Creating pages in bulk
|
|
76
|
+
|
|
77
|
+
Create pages from a JSON file of `{ "/route": data }`, the shape of a Puck recipe's `database.json`. Routes the branch already has, including pages it inherits, are skipped, so it's safe to run again.
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
npx @puckeditor/cloud-client import database.json [--branch <name>]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The CLI reads `PUCK_API_KEY`, `PUCK_PROJECT_ID`, `PUCK_BRANCH` and `PUCK_HOST` from the environment, then from `.env.local` and `.env`. Use `createPages` from scripts:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { createPages } from "@puckeditor/cloud-client/experimental";
|
|
87
|
+
import pages from "./database.json";
|
|
88
|
+
|
|
89
|
+
// Pages are published unless status is "unpublished"
|
|
90
|
+
const { created, skipped } = await createPages({ pages });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
In Puck Cloud, **Pages › Import JSON** does the same for the selected branch.
|
package/dist/auth.d.mts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { A as AuthenticateFn } from './types-DRxmCSVs.mjs';
|
|
2
|
+
import '@ai-sdk/provider';
|
|
3
|
+
import '@ai-sdk/provider-utils';
|
|
4
|
+
|
|
5
|
+
type PuckSession = {
|
|
6
|
+
user: {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
email: string;
|
|
10
|
+
image?: string | null;
|
|
11
|
+
};
|
|
12
|
+
projectId: string;
|
|
13
|
+
/** The branch this session reads and writes. */
|
|
14
|
+
branch: string;
|
|
15
|
+
organizationId: string;
|
|
16
|
+
/** ISO timestamp. */
|
|
17
|
+
expiresAt: string;
|
|
18
|
+
/** Puck Cloud dashboard origin, for linking back. */
|
|
19
|
+
cloudUrl: string;
|
|
20
|
+
};
|
|
21
|
+
type PuckAuthOptions = {
|
|
22
|
+
/** Project registered in Puck Cloud; also the OAuth client id. Defaults to PUCK_PROJECT_ID. */
|
|
23
|
+
projectId?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Branch this app edits. Defaults to PUCK_BRANCH. Cloud signs a deployed URL
|
|
26
|
+
* in to the branch deployed there; on localhost this picks the branch.
|
|
27
|
+
*/
|
|
28
|
+
branch?: string;
|
|
29
|
+
/** Puck Cloud API host. Defaults to PUCK_HOST or https://cloud.puckeditor.com/api. */
|
|
30
|
+
host?: string;
|
|
31
|
+
/** Prefix for the auth routes served through puckHandler. Defaults to /api/puck/auth. */
|
|
32
|
+
basePath?: string;
|
|
33
|
+
/** Public origin of this app when it cannot be derived from the request, e.g. behind a proxy. */
|
|
34
|
+
appUrl?: string;
|
|
35
|
+
};
|
|
36
|
+
type PuckAuth = {
|
|
37
|
+
/** Pass to `puckHandler`, or call directly to protect your own routes. */
|
|
38
|
+
authenticate: AuthenticateFn;
|
|
39
|
+
getSession: (headers: Headers) => Promise<PuckSession | null>;
|
|
40
|
+
loginUrl: (returnTo?: string) => string;
|
|
41
|
+
};
|
|
42
|
+
/** Only same-origin paths, and never back into the auth routes. */
|
|
43
|
+
declare const safeReturnTo: (value: string | null | undefined, basePath: string) => string;
|
|
44
|
+
/**
|
|
45
|
+
* Sign in with Puck. `authenticate` also serves the login, callback, logout
|
|
46
|
+
* and session routes under `basePath`. Use `puckAuth` unless you need options.
|
|
47
|
+
*/
|
|
48
|
+
declare function createPuckAuth(options?: PuckAuthOptions): PuckAuth;
|
|
49
|
+
/** Sign in with Puck configured from PUCK_PROJECT_ID, PUCK_BRANCH and PUCK_HOST. */
|
|
50
|
+
declare const puckAuth: PuckAuth;
|
|
51
|
+
|
|
52
|
+
export { type PuckAuth, type PuckAuthOptions, type PuckSession, createPuckAuth, puckAuth, safeReturnTo };
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { A as AuthenticateFn } from './types-DRxmCSVs.js';
|
|
2
|
+
import '@ai-sdk/provider';
|
|
3
|
+
import '@ai-sdk/provider-utils';
|
|
4
|
+
|
|
5
|
+
type PuckSession = {
|
|
6
|
+
user: {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
email: string;
|
|
10
|
+
image?: string | null;
|
|
11
|
+
};
|
|
12
|
+
projectId: string;
|
|
13
|
+
/** The branch this session reads and writes. */
|
|
14
|
+
branch: string;
|
|
15
|
+
organizationId: string;
|
|
16
|
+
/** ISO timestamp. */
|
|
17
|
+
expiresAt: string;
|
|
18
|
+
/** Puck Cloud dashboard origin, for linking back. */
|
|
19
|
+
cloudUrl: string;
|
|
20
|
+
};
|
|
21
|
+
type PuckAuthOptions = {
|
|
22
|
+
/** Project registered in Puck Cloud; also the OAuth client id. Defaults to PUCK_PROJECT_ID. */
|
|
23
|
+
projectId?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Branch this app edits. Defaults to PUCK_BRANCH. Cloud signs a deployed URL
|
|
26
|
+
* in to the branch deployed there; on localhost this picks the branch.
|
|
27
|
+
*/
|
|
28
|
+
branch?: string;
|
|
29
|
+
/** Puck Cloud API host. Defaults to PUCK_HOST or https://cloud.puckeditor.com/api. */
|
|
30
|
+
host?: string;
|
|
31
|
+
/** Prefix for the auth routes served through puckHandler. Defaults to /api/puck/auth. */
|
|
32
|
+
basePath?: string;
|
|
33
|
+
/** Public origin of this app when it cannot be derived from the request, e.g. behind a proxy. */
|
|
34
|
+
appUrl?: string;
|
|
35
|
+
};
|
|
36
|
+
type PuckAuth = {
|
|
37
|
+
/** Pass to `puckHandler`, or call directly to protect your own routes. */
|
|
38
|
+
authenticate: AuthenticateFn;
|
|
39
|
+
getSession: (headers: Headers) => Promise<PuckSession | null>;
|
|
40
|
+
loginUrl: (returnTo?: string) => string;
|
|
41
|
+
};
|
|
42
|
+
/** Only same-origin paths, and never back into the auth routes. */
|
|
43
|
+
declare const safeReturnTo: (value: string | null | undefined, basePath: string) => string;
|
|
44
|
+
/**
|
|
45
|
+
* Sign in with Puck. `authenticate` also serves the login, callback, logout
|
|
46
|
+
* and session routes under `basePath`. Use `puckAuth` unless you need options.
|
|
47
|
+
*/
|
|
48
|
+
declare function createPuckAuth(options?: PuckAuthOptions): PuckAuth;
|
|
49
|
+
/** Sign in with Puck configured from PUCK_PROJECT_ID, PUCK_BRANCH and PUCK_HOST. */
|
|
50
|
+
declare const puckAuth: PuckAuth;
|
|
51
|
+
|
|
52
|
+
export { type PuckAuth, type PuckAuthOptions, type PuckSession, createPuckAuth, puckAuth, safeReturnTo };
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/auth.ts
|
|
31
|
+
var auth_exports = {};
|
|
32
|
+
__export(auth_exports, {
|
|
33
|
+
createPuckAuth: () => createPuckAuth,
|
|
34
|
+
puckAuth: () => puckAuth,
|
|
35
|
+
safeReturnTo: () => safeReturnTo
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(auth_exports);
|
|
38
|
+
|
|
39
|
+
// ../tsup-config/react-import.js
|
|
40
|
+
var import_react = __toESM(require("react"));
|
|
41
|
+
|
|
42
|
+
// src/lib/get-host.ts
|
|
43
|
+
var getDefaultHost = () => (process.env.PUCK_HOST ?? "https://cloud.puckeditor.com/api").replace(
|
|
44
|
+
/\/+$/,
|
|
45
|
+
""
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// src/auth.ts
|
|
49
|
+
var SESSION_COOKIE = "puck-session";
|
|
50
|
+
var LOGIN_COOKIE = "puck-login";
|
|
51
|
+
var LOGIN_TTL_SECONDS = 600;
|
|
52
|
+
var TOKEN_PATTERN = /^[A-Za-z0-9_.~-]{16,512}$/;
|
|
53
|
+
var noStore = { "cache-control": "no-store" };
|
|
54
|
+
var base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
55
|
+
var randomToken = () => base64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
56
|
+
var sha256 = async (value) => base64url(
|
|
57
|
+
new Uint8Array(
|
|
58
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
var isLoopback = (hostname) => hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(hostname);
|
|
62
|
+
var encodeCookie = (value) => base64url(new TextEncoder().encode(JSON.stringify(value)));
|
|
63
|
+
var decodeCookie = (value) => {
|
|
64
|
+
try {
|
|
65
|
+
const binary = atob((value ?? "").replace(/-/g, "+").replace(/_/g, "/"));
|
|
66
|
+
return JSON.parse(
|
|
67
|
+
new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)))
|
|
68
|
+
);
|
|
69
|
+
} catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var readCookie = (headers, name) => headers.get("cookie")?.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
74
|
+
var safeReturnTo = (value, basePath) => {
|
|
75
|
+
if (!value || value.length > 512) return "/";
|
|
76
|
+
try {
|
|
77
|
+
const url = new URL(value, "https://puck.invalid");
|
|
78
|
+
if (url.origin === "https://puck.invalid" && !url.pathname.startsWith(basePath)) {
|
|
79
|
+
return `${url.pathname}${url.search}`;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
return "/";
|
|
84
|
+
};
|
|
85
|
+
function createPuckAuth(options = {}) {
|
|
86
|
+
const projectId = options.projectId ?? process.env.PUCK_PROJECT_ID;
|
|
87
|
+
const configuredBranch = () => (options.branch ?? process.env.PUCK_BRANCH)?.trim() || null;
|
|
88
|
+
if (!projectId) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Sign in with Puck requires a project id. Set PUCK_PROJECT_ID or pass projectId."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const host = (options.host ?? getDefaultHost()).replace(/\/+$/, "");
|
|
94
|
+
const cloudUrl = new URL(host).origin;
|
|
95
|
+
const basePath = (options.basePath ?? "/api/puck/auth").replace(/\/+$/, "");
|
|
96
|
+
const appOrigin = (request) => new URL(options.appUrl ?? request.url).origin;
|
|
97
|
+
const isSecure = (request) => appOrigin(request).startsWith("https:");
|
|
98
|
+
const callbackUrl = (request, branch) => {
|
|
99
|
+
const url = new URL(`${appOrigin(request)}${basePath}/callback`);
|
|
100
|
+
if (branch && isLoopback(url.hostname)) {
|
|
101
|
+
url.searchParams.set("branch", branch);
|
|
102
|
+
}
|
|
103
|
+
return url.toString();
|
|
104
|
+
};
|
|
105
|
+
const cookieName = (name, secure) => secure ? `__Host-${name}` : name;
|
|
106
|
+
const cookie = (name, value, maxAge, secure) => `${cookieName(
|
|
107
|
+
name,
|
|
108
|
+
secure
|
|
109
|
+
)}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${secure ? "; Secure" : ""}`;
|
|
110
|
+
const readSessionCookie = (headers) => {
|
|
111
|
+
const { token, branch } = decodeCookie(
|
|
112
|
+
readCookie(headers, `__Host-${SESSION_COOKIE}`) ?? readCookie(headers, SESSION_COOKIE)
|
|
113
|
+
);
|
|
114
|
+
return typeof token === "string" && TOKEN_PATTERN.test(token) ? { token, branch: typeof branch === "string" ? branch : null } : void 0;
|
|
115
|
+
};
|
|
116
|
+
const loginUrl = (returnTo = "/") => `${basePath}/login?returnTo=${encodeURIComponent(
|
|
117
|
+
safeReturnTo(returnTo, basePath)
|
|
118
|
+
)}`;
|
|
119
|
+
const html = (message, status) => new Response(
|
|
120
|
+
`<!doctype html><html lang="en"><meta charset="utf-8"><title>Puck</title><p>${message} <a href="${loginUrl()}">Sign in</a>.</p></html>`,
|
|
121
|
+
{
|
|
122
|
+
status,
|
|
123
|
+
headers: { ...noStore, "content-type": "text/html; charset=utf-8" }
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
const unauthorized = () => Response.json(
|
|
127
|
+
{ error: "unauthorized", loginUrl: loginUrl() },
|
|
128
|
+
{ status: 401, headers: noStore }
|
|
129
|
+
);
|
|
130
|
+
const fetchCloudSession = async (accessToken) => {
|
|
131
|
+
const response = await fetch(`${host}/oauth/session`, {
|
|
132
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
133
|
+
cache: "no-store",
|
|
134
|
+
signal: AbortSignal.timeout(1e4)
|
|
135
|
+
});
|
|
136
|
+
if (response.status === 401) return null;
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Puck Cloud session check failed (HTTP ${response.status}).`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return await response.json();
|
|
143
|
+
};
|
|
144
|
+
const isOtherBranch = (session, branch) => !!branch && session.branch !== branch;
|
|
145
|
+
const toSession = (session, branch) => session?.projectId === projectId && !isOtherBranch(session, branch) && session.user?.id ? { ...session, cloudUrl } : null;
|
|
146
|
+
const resolve = async (headers) => {
|
|
147
|
+
const stored = readSessionCookie(headers);
|
|
148
|
+
if (!stored || stored.branch !== configuredBranch()) return null;
|
|
149
|
+
const session = toSession(
|
|
150
|
+
await fetchCloudSession(stored.token),
|
|
151
|
+
stored.branch
|
|
152
|
+
);
|
|
153
|
+
return session ? { session, accessToken: stored.token } : null;
|
|
154
|
+
};
|
|
155
|
+
const revoke = (accessToken) => fetch(`${host}/auth/oauth2/revoke`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
158
|
+
body: new URLSearchParams({ token: accessToken, client_id: projectId }),
|
|
159
|
+
signal: AbortSignal.timeout(1e4)
|
|
160
|
+
}).catch(() => void 0);
|
|
161
|
+
const login = async (request, url) => {
|
|
162
|
+
const state = randomToken();
|
|
163
|
+
const verifier = randomToken();
|
|
164
|
+
const branch = configuredBranch();
|
|
165
|
+
const transaction = encodeCookie({
|
|
166
|
+
state,
|
|
167
|
+
verifier,
|
|
168
|
+
branch,
|
|
169
|
+
returnTo: safeReturnTo(url.searchParams.get("returnTo"), basePath)
|
|
170
|
+
});
|
|
171
|
+
const authorize = new URL(`${host}/auth/oauth2/authorize`);
|
|
172
|
+
authorize.search = new URLSearchParams({
|
|
173
|
+
client_id: projectId,
|
|
174
|
+
redirect_uri: callbackUrl(request, branch),
|
|
175
|
+
response_type: "code",
|
|
176
|
+
scope: "profile email",
|
|
177
|
+
state,
|
|
178
|
+
code_challenge: await sha256(verifier),
|
|
179
|
+
code_challenge_method: "S256"
|
|
180
|
+
}).toString();
|
|
181
|
+
return new Response(null, {
|
|
182
|
+
status: 302,
|
|
183
|
+
headers: {
|
|
184
|
+
...noStore,
|
|
185
|
+
location: authorize.toString(),
|
|
186
|
+
"set-cookie": cookie(
|
|
187
|
+
LOGIN_COOKIE,
|
|
188
|
+
transaction,
|
|
189
|
+
LOGIN_TTL_SECONDS,
|
|
190
|
+
isSecure(request)
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
const callback = async (request, url) => {
|
|
196
|
+
const secure = isSecure(request);
|
|
197
|
+
const error = url.searchParams.get("error");
|
|
198
|
+
if (error) {
|
|
199
|
+
return html(
|
|
200
|
+
error === "access_denied" ? "Your Puck account does not have access to this project. Sign out of Puck Cloud to switch accounts." : "Sign-in was cancelled or failed.",
|
|
201
|
+
400
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const transaction = decodeCookie(readCookie(request.headers, cookieName(LOGIN_COOKIE, secure)));
|
|
205
|
+
const branch = transaction.branch ?? null;
|
|
206
|
+
const code = url.searchParams.get("code");
|
|
207
|
+
if (!code || !transaction.state || !transaction.verifier || transaction.state !== url.searchParams.get("state")) {
|
|
208
|
+
return html("Sign-in expired or was cancelled.", 400);
|
|
209
|
+
}
|
|
210
|
+
const tokenResponse = await fetch(`${host}/auth/oauth2/token`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
213
|
+
body: new URLSearchParams({
|
|
214
|
+
grant_type: "authorization_code",
|
|
215
|
+
code,
|
|
216
|
+
redirect_uri: callbackUrl(request, branch),
|
|
217
|
+
client_id: projectId,
|
|
218
|
+
code_verifier: transaction.verifier
|
|
219
|
+
}),
|
|
220
|
+
signal: AbortSignal.timeout(1e4)
|
|
221
|
+
});
|
|
222
|
+
const tokens = tokenResponse.ok ? await tokenResponse.json() : null;
|
|
223
|
+
const accessToken = tokens?.access_token;
|
|
224
|
+
if (typeof accessToken !== "string" || !TOKEN_PATTERN.test(accessToken)) {
|
|
225
|
+
return html("Sign-in expired or was cancelled.", 400);
|
|
226
|
+
}
|
|
227
|
+
const cloudSession = await fetchCloudSession(accessToken);
|
|
228
|
+
if (cloudSession && isOtherBranch(cloudSession, branch)) {
|
|
229
|
+
await revoke(accessToken);
|
|
230
|
+
return html(
|
|
231
|
+
`Puck Cloud signs this URL in to the ${cloudSession.branch} branch, but this app is set up for ${branch}. Update PUCK_BRANCH or the branch's deployment URL.`,
|
|
232
|
+
400
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const session = toSession(cloudSession, branch);
|
|
236
|
+
const maxAge = Math.floor(
|
|
237
|
+
(Date.parse(session?.expiresAt ?? "") - Date.now()) / 1e3
|
|
238
|
+
);
|
|
239
|
+
if (!session || !(maxAge > 0)) {
|
|
240
|
+
return html("Sign-in expired or was cancelled.", 400);
|
|
241
|
+
}
|
|
242
|
+
const headers = new Headers({
|
|
243
|
+
...noStore,
|
|
244
|
+
location: new URL(
|
|
245
|
+
safeReturnTo(transaction.returnTo, basePath),
|
|
246
|
+
appOrigin(request)
|
|
247
|
+
).toString()
|
|
248
|
+
});
|
|
249
|
+
headers.append("set-cookie", cookie(LOGIN_COOKIE, "", 0, secure));
|
|
250
|
+
headers.append(
|
|
251
|
+
"set-cookie",
|
|
252
|
+
cookie(
|
|
253
|
+
SESSION_COOKIE,
|
|
254
|
+
encodeCookie({ token: accessToken, branch }),
|
|
255
|
+
maxAge,
|
|
256
|
+
secure
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
return new Response(null, { status: 303, headers });
|
|
260
|
+
};
|
|
261
|
+
const logout = async (request) => {
|
|
262
|
+
const stored = readSessionCookie(request.headers);
|
|
263
|
+
if (stored) await revoke(stored.token);
|
|
264
|
+
return new Response(null, {
|
|
265
|
+
status: 303,
|
|
266
|
+
headers: {
|
|
267
|
+
...noStore,
|
|
268
|
+
location: "/",
|
|
269
|
+
"set-cookie": cookie(SESSION_COOKIE, "", 0, isSecure(request))
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
const authenticate = async ({ request }) => {
|
|
274
|
+
const url = new URL(request.url);
|
|
275
|
+
if (url.pathname.startsWith(`${basePath}/`)) {
|
|
276
|
+
const route = `${request.method} ${url.pathname.slice(basePath.length)}`;
|
|
277
|
+
if (route === "GET /login") return login(request, url);
|
|
278
|
+
if (route === "GET /callback") return callback(request, url);
|
|
279
|
+
if (route === "POST /logout") return logout(request);
|
|
280
|
+
if (route === "GET /session") {
|
|
281
|
+
const resolved2 = await resolve(request.headers);
|
|
282
|
+
return resolved2 ? Response.json(resolved2.session, { headers: noStore }) : unauthorized();
|
|
283
|
+
}
|
|
284
|
+
return Response.json({ error: "Not found" }, { status: 404 });
|
|
285
|
+
}
|
|
286
|
+
const resolved = await resolve(request.headers);
|
|
287
|
+
return resolved ? { ...resolved.session.user, accessToken: resolved.accessToken } : unauthorized();
|
|
288
|
+
};
|
|
289
|
+
return {
|
|
290
|
+
authenticate,
|
|
291
|
+
getSession: async (headers) => (await resolve(headers))?.session ?? null,
|
|
292
|
+
loginUrl
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
var defaultAuth;
|
|
296
|
+
var getDefaultAuth = () => defaultAuth ??= createPuckAuth();
|
|
297
|
+
var puckAuth = {
|
|
298
|
+
authenticate: (context) => getDefaultAuth().authenticate(context),
|
|
299
|
+
getSession: (headers) => getDefaultAuth().getSession(headers),
|
|
300
|
+
loginUrl: (returnTo) => getDefaultAuth().loginUrl(returnTo)
|
|
301
|
+
};
|
|
302
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
303
|
+
0 && (module.exports = {
|
|
304
|
+
createPuckAuth,
|
|
305
|
+
puckAuth,
|
|
306
|
+
safeReturnTo
|
|
307
|
+
});
|