@puckeditor/cloud-client 0.9.0-canary.d4a0bc62 → 0.9.0-canary.d582a110
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 +104 -0
- package/dist/auth.d.mts +46 -0
- package/dist/auth.d.ts +46 -0
- package/dist/auth.js +311 -0
- package/dist/auth.mjs +267 -0
- package/dist/chunk-AQBXFPC2.mjs +19 -0
- package/dist/{chunk-RQBVR6IF.mjs → chunk-J3SJQJK6.mjs} +167 -87
- package/dist/cli/index.js +182 -0
- package/dist/experimental.d.mts +29 -6
- package/dist/experimental.d.ts +29 -6
- package/dist/experimental.js +297 -186
- package/dist/experimental.mjs +65 -36
- package/dist/index.d.mts +12 -739
- package/dist/index.d.ts +12 -739
- package/dist/index.js +170 -84
- package/dist/index.mjs +2 -1
- package/dist/types-BMWPvaOC.d.mts +726 -0
- package/dist/types-BMWPvaOC.d.ts +726 -0
- package/package.json +18 -7
package/README.md
CHANGED
|
@@ -1 +1,105 @@
|
|
|
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 branch.
|
|
22
|
+
|
|
23
|
+
Each API key belongs to one branch of a project, chosen when you create it under **Settings › Project › API Keys**. Its requests read and write that branch.
|
|
24
|
+
|
|
25
|
+
Direct `chat()` calls also require authentication and return a promise:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const response = await chat(params, {
|
|
29
|
+
authenticate: () => authenticate({ request }),
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Sign in with Puck
|
|
34
|
+
|
|
35
|
+
Let members of your Puck Cloud organization sign in to your editor with their Puck account. Sessions edit the branch of your `PUCK_API_KEY`.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { authenticate } from "@puckeditor/cloud-client/auth";
|
|
39
|
+
import { puckHandler } from "@puckeditor/cloud-client/experimental";
|
|
40
|
+
|
|
41
|
+
export const GET = (request: Request) => puckHandler(request, { authenticate });
|
|
42
|
+
export const POST = GET;
|
|
43
|
+
export const DELETE = GET;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Gate the editor page:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { getSession, loginUrl } from "@puckeditor/cloud-client/auth";
|
|
50
|
+
|
|
51
|
+
const session = await getSession(await headers());
|
|
52
|
+
if (!session) redirect(loginUrl("/puck"));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Protect custom routes with the same function:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const user = await authenticate({ request });
|
|
59
|
+
if (user instanceof Response) return user;
|
|
60
|
+
await generate({ prompt, user });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The helper serves these routes through `puckHandler`:
|
|
64
|
+
|
|
65
|
+
| Route | Behavior |
|
|
66
|
+
| ----------------------------- | ----------------------------------------------------- |
|
|
67
|
+
| `GET /api/puck/auth/login` | Redirect to Puck Cloud (authorization code with PKCE) |
|
|
68
|
+
| `GET /api/puck/auth/callback` | Exchange the code and set an HttpOnly session cookie |
|
|
69
|
+
| `GET /api/puck/auth/session` | Current user, project, branch and expiry, or 401 |
|
|
70
|
+
| `POST /api/puck/auth/logout` | Revoke the session and clear the cookie |
|
|
71
|
+
|
|
72
|
+
Each sign-in asks for the Puck account again and starts a Puck Cloud session of its own, separate from any Puck Cloud dashboard session in the same browser. Accounts outside the organization that owns `PUCK_API_KEY` are turned away with a prompt to sign in with another account. Sessions last up to seven days and end when the user signs out of the app or leaves the organization. Cloud verifies the user on every request, and external-owner filtering does not apply. These functions read `PUCK_API_KEY` and `PUCK_HOST`, which every cloud-client call uses. Use `createPuckAuth(options)` to pass them explicitly.
|
|
73
|
+
|
|
74
|
+
Before each sign-in, the app registers its callback with its API key, so the editor can run on localhost or any HTTPS origin. Switching to a key for another branch signs the app out.
|
|
75
|
+
|
|
76
|
+
The pages plugin needs no configuration: outside the Puck Cloud editor frame it relies on the app's session cookie. Add `createAuthPlugin` from `@puckeditor/plugin-auth` to show the signed-in account in the editor's header, with links to Puck Cloud and sign-out. List it after the pages plugin, which replaces the header's actions:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { createAuthPlugin } from "@puckeditor/plugin-auth";
|
|
80
|
+
import "@puckeditor/plugin-auth/styles.css";
|
|
81
|
+
|
|
82
|
+
<Puck plugins={[createPagesPlugin(), createAuthPlugin()]} />;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Pass `host` if the auth routes aren't served at `/api/puck/auth`. The menu shows the branch being edited in development, or anywhere with `showBranch: true`.
|
|
86
|
+
|
|
87
|
+
## Creating pages in bulk
|
|
88
|
+
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
npx @puckeditor/cloud-client import database.json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The CLI reads `PUCK_API_KEY` and `PUCK_HOST` from the environment, then from `.env.local` and `.env`. Pages go to the API key's branch. Use `createPages` from scripts:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { createPages } from "@puckeditor/cloud-client/experimental";
|
|
99
|
+
import pages from "./database.json";
|
|
100
|
+
|
|
101
|
+
// Pages are published unless status is "unpublished"
|
|
102
|
+
const { created, skipped } = await createPages({ pages });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
In Puck Cloud, **Pages › Import pages** does the same for the selected branch.
|
package/dist/auth.d.mts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { A as AuthenticateFn } from './types-BMWPvaOC.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
|
+
/** The branch this session reads and writes. */
|
|
13
|
+
branch: string;
|
|
14
|
+
organizationId: string;
|
|
15
|
+
/** ISO timestamp. */
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
/** Puck Cloud dashboard origin, for linking back. */
|
|
18
|
+
cloudUrl: string;
|
|
19
|
+
};
|
|
20
|
+
type PuckAuthOptions = {
|
|
21
|
+
/** Members sign in to the key's project and edit its branch. Defaults to PUCK_API_KEY. */
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
/** Puck Cloud API host. Defaults to PUCK_HOST or https://cloud.puckeditor.com/api. */
|
|
24
|
+
host?: string;
|
|
25
|
+
/** Public origin of this app when it cannot be derived from the request, e.g. behind a proxy. */
|
|
26
|
+
appUrl?: string;
|
|
27
|
+
};
|
|
28
|
+
type PuckAuth = {
|
|
29
|
+
/** Pass to `puckHandler`, or call directly to protect your own routes. */
|
|
30
|
+
authenticate: AuthenticateFn;
|
|
31
|
+
getSession: (headers: Headers) => Promise<PuckSession | null>;
|
|
32
|
+
loginUrl: (returnTo?: string) => string;
|
|
33
|
+
};
|
|
34
|
+
/** Only same-origin paths, and never back into the auth routes. */
|
|
35
|
+
declare const safeReturnTo: (value: string | null | undefined) => string;
|
|
36
|
+
/**
|
|
37
|
+
* Sign in with Puck. `authenticate` also serves the login, callback, logout
|
|
38
|
+
* and session routes under /api/puck/auth. Use the exported functions unless you need options.
|
|
39
|
+
*/
|
|
40
|
+
declare function createPuckAuth(options?: PuckAuthOptions): PuckAuth;
|
|
41
|
+
/** Pass to `puckHandler`, or call directly to protect your own routes. */
|
|
42
|
+
declare const authenticate: PuckAuth["authenticate"];
|
|
43
|
+
declare const getSession: PuckAuth["getSession"];
|
|
44
|
+
declare const loginUrl: PuckAuth["loginUrl"];
|
|
45
|
+
|
|
46
|
+
export { type PuckAuth, type PuckAuthOptions, type PuckSession, authenticate, createPuckAuth, getSession, loginUrl, safeReturnTo };
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { A as AuthenticateFn } from './types-BMWPvaOC.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
|
+
/** The branch this session reads and writes. */
|
|
13
|
+
branch: string;
|
|
14
|
+
organizationId: string;
|
|
15
|
+
/** ISO timestamp. */
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
/** Puck Cloud dashboard origin, for linking back. */
|
|
18
|
+
cloudUrl: string;
|
|
19
|
+
};
|
|
20
|
+
type PuckAuthOptions = {
|
|
21
|
+
/** Members sign in to the key's project and edit its branch. Defaults to PUCK_API_KEY. */
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
/** Puck Cloud API host. Defaults to PUCK_HOST or https://cloud.puckeditor.com/api. */
|
|
24
|
+
host?: string;
|
|
25
|
+
/** Public origin of this app when it cannot be derived from the request, e.g. behind a proxy. */
|
|
26
|
+
appUrl?: string;
|
|
27
|
+
};
|
|
28
|
+
type PuckAuth = {
|
|
29
|
+
/** Pass to `puckHandler`, or call directly to protect your own routes. */
|
|
30
|
+
authenticate: AuthenticateFn;
|
|
31
|
+
getSession: (headers: Headers) => Promise<PuckSession | null>;
|
|
32
|
+
loginUrl: (returnTo?: string) => string;
|
|
33
|
+
};
|
|
34
|
+
/** Only same-origin paths, and never back into the auth routes. */
|
|
35
|
+
declare const safeReturnTo: (value: string | null | undefined) => string;
|
|
36
|
+
/**
|
|
37
|
+
* Sign in with Puck. `authenticate` also serves the login, callback, logout
|
|
38
|
+
* and session routes under /api/puck/auth. Use the exported functions unless you need options.
|
|
39
|
+
*/
|
|
40
|
+
declare function createPuckAuth(options?: PuckAuthOptions): PuckAuth;
|
|
41
|
+
/** Pass to `puckHandler`, or call directly to protect your own routes. */
|
|
42
|
+
declare const authenticate: PuckAuth["authenticate"];
|
|
43
|
+
declare const getSession: PuckAuth["getSession"];
|
|
44
|
+
declare const loginUrl: PuckAuth["loginUrl"];
|
|
45
|
+
|
|
46
|
+
export { type PuckAuth, type PuckAuthOptions, type PuckSession, authenticate, createPuckAuth, getSession, loginUrl, safeReturnTo };
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
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
|
+
authenticate: () => authenticate,
|
|
34
|
+
createPuckAuth: () => createPuckAuth,
|
|
35
|
+
getSession: () => getSession,
|
|
36
|
+
loginUrl: () => loginUrl,
|
|
37
|
+
safeReturnTo: () => safeReturnTo
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(auth_exports);
|
|
40
|
+
|
|
41
|
+
// ../tsup-config/react-import.js
|
|
42
|
+
var import_react = __toESM(require("react"));
|
|
43
|
+
|
|
44
|
+
// src/lib/get-api-key.ts
|
|
45
|
+
var getApiKey = () => process.env.PUCK_API_KEY;
|
|
46
|
+
|
|
47
|
+
// src/lib/get-host.ts
|
|
48
|
+
var getDefaultHost = () => (process.env.PUCK_HOST ?? "https://cloud.puckeditor.com/api").replace(
|
|
49
|
+
/\/+$/,
|
|
50
|
+
""
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// src/auth.ts
|
|
54
|
+
var BASE_PATH = "/api/puck/auth";
|
|
55
|
+
var SESSION_COOKIE = "puck-session";
|
|
56
|
+
var LOGIN_COOKIE = "puck-login";
|
|
57
|
+
var LOGIN_TTL_SECONDS = 600;
|
|
58
|
+
var TOKEN_PATTERN = /^[A-Za-z0-9_.~-]{16,512}$/;
|
|
59
|
+
var noStore = { "cache-control": "no-store" };
|
|
60
|
+
var base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
61
|
+
var randomToken = () => base64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
62
|
+
var sha256 = async (value) => base64url(
|
|
63
|
+
new Uint8Array(
|
|
64
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
var encodeCookie = (value) => base64url(new TextEncoder().encode(JSON.stringify(value)));
|
|
68
|
+
var decodeCookie = (value) => {
|
|
69
|
+
try {
|
|
70
|
+
const binary = atob((value ?? "").replace(/-/g, "+").replace(/_/g, "/"));
|
|
71
|
+
return JSON.parse(
|
|
72
|
+
new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)))
|
|
73
|
+
);
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var readCookie = (headers, name) => headers.get("cookie")?.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
79
|
+
var safeReturnTo = (value) => {
|
|
80
|
+
if (!value || value.length > 512) return "/";
|
|
81
|
+
try {
|
|
82
|
+
const url = new URL(value, "https://puck.invalid");
|
|
83
|
+
if (url.origin === "https://puck.invalid" && !url.pathname.startsWith(BASE_PATH)) {
|
|
84
|
+
return `${url.pathname}${url.search}`;
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
return "/";
|
|
89
|
+
};
|
|
90
|
+
function createPuckAuth(options = {}) {
|
|
91
|
+
const apiKey = () => {
|
|
92
|
+
const key = options.apiKey ?? getApiKey();
|
|
93
|
+
if (!key) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
"Sign in with Puck requires an API key. Set PUCK_API_KEY or pass apiKey."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return key;
|
|
99
|
+
};
|
|
100
|
+
const host = (options.host ?? getDefaultHost()).replace(/\/+$/, "");
|
|
101
|
+
const cloudUrl = new URL(host).origin;
|
|
102
|
+
const appOrigin = (request) => new URL(options.appUrl ?? request.url).origin;
|
|
103
|
+
const isSecure = (request) => appOrigin(request).startsWith("https:");
|
|
104
|
+
const callbackUrl = (request) => `${appOrigin(request)}${BASE_PATH}/callback`;
|
|
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, clientId } = decodeCookie(
|
|
112
|
+
readCookie(headers, `__Host-${SESSION_COOKIE}`) ?? readCookie(headers, SESSION_COOKIE)
|
|
113
|
+
);
|
|
114
|
+
return typeof token === "string" && TOKEN_PATTERN.test(token) && typeof clientId === "string" ? { token, clientId } : void 0;
|
|
115
|
+
};
|
|
116
|
+
const loginUrl2 = (returnTo = "/") => `${BASE_PATH}/login?returnTo=${encodeURIComponent(safeReturnTo(returnTo))}`;
|
|
117
|
+
const html = (message, status, action = "Sign in") => new Response(
|
|
118
|
+
`<!doctype html><html lang="en"><meta charset="utf-8"><title>Puck</title><p>${message} <a href="${loginUrl2()}">${action}</a>.</p></html>`,
|
|
119
|
+
{
|
|
120
|
+
status,
|
|
121
|
+
headers: { ...noStore, "content-type": "text/html; charset=utf-8" }
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
const unauthorized = () => Response.json(
|
|
125
|
+
{ error: "unauthorized", loginUrl: loginUrl2() },
|
|
126
|
+
{ status: 401, headers: noStore }
|
|
127
|
+
);
|
|
128
|
+
const fetchCloudSession = async (accessToken) => {
|
|
129
|
+
const response = await fetch(`${host}/oauth/session`, {
|
|
130
|
+
headers: {
|
|
131
|
+
authorization: `Bearer ${accessToken}`,
|
|
132
|
+
"x-api-key": apiKey()
|
|
133
|
+
},
|
|
134
|
+
cache: "no-store",
|
|
135
|
+
signal: AbortSignal.timeout(1e4)
|
|
136
|
+
});
|
|
137
|
+
if (response.status === 401) return null;
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Puck Cloud session check failed (HTTP ${response.status}).`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const session = await response.json();
|
|
144
|
+
return session?.user?.id ? { ...session, cloudUrl } : null;
|
|
145
|
+
};
|
|
146
|
+
const resolve = async (headers) => {
|
|
147
|
+
const stored = readSessionCookie(headers);
|
|
148
|
+
if (!stored) return null;
|
|
149
|
+
const session = await fetchCloudSession(stored.token);
|
|
150
|
+
return session ? { session, accessToken: stored.token } : null;
|
|
151
|
+
};
|
|
152
|
+
const revoke = (accessToken, clientId) => fetch(`${host}/auth/oauth2/revoke`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
155
|
+
body: new URLSearchParams({ token: accessToken, client_id: clientId }),
|
|
156
|
+
signal: AbortSignal.timeout(1e4)
|
|
157
|
+
}).catch(() => void 0);
|
|
158
|
+
const registerClient = async (request) => {
|
|
159
|
+
const response = await fetch(`${host}/oauth/client`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: { "content-type": "application/json", "x-api-key": apiKey() },
|
|
162
|
+
body: JSON.stringify({ callbackUrl: callbackUrl(request) }),
|
|
163
|
+
cache: "no-store",
|
|
164
|
+
signal: AbortSignal.timeout(1e4)
|
|
165
|
+
});
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Puck Cloud couldn't set up Sign in with Puck (HTTP ${response.status}). Check PUCK_API_KEY.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return await response.json();
|
|
172
|
+
};
|
|
173
|
+
const login = async (request, url) => {
|
|
174
|
+
const state = randomToken();
|
|
175
|
+
const verifier = randomToken();
|
|
176
|
+
const { clientId, redirectUri } = await registerClient(request);
|
|
177
|
+
const transaction = encodeCookie({
|
|
178
|
+
state,
|
|
179
|
+
verifier,
|
|
180
|
+
clientId,
|
|
181
|
+
redirectUri,
|
|
182
|
+
returnTo: safeReturnTo(url.searchParams.get("returnTo"))
|
|
183
|
+
});
|
|
184
|
+
const authorize = new URL(`${host}/auth/oauth2/authorize`);
|
|
185
|
+
authorize.search = new URLSearchParams({
|
|
186
|
+
client_id: clientId,
|
|
187
|
+
redirect_uri: redirectUri,
|
|
188
|
+
response_type: "code",
|
|
189
|
+
scope: "profile email",
|
|
190
|
+
state,
|
|
191
|
+
code_challenge: await sha256(verifier),
|
|
192
|
+
code_challenge_method: "S256"
|
|
193
|
+
}).toString();
|
|
194
|
+
return new Response(null, {
|
|
195
|
+
status: 302,
|
|
196
|
+
headers: {
|
|
197
|
+
...noStore,
|
|
198
|
+
location: authorize.toString(),
|
|
199
|
+
"set-cookie": cookie(
|
|
200
|
+
LOGIN_COOKIE,
|
|
201
|
+
transaction,
|
|
202
|
+
LOGIN_TTL_SECONDS,
|
|
203
|
+
isSecure(request)
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
const callback = async (request, url) => {
|
|
209
|
+
const secure = isSecure(request);
|
|
210
|
+
const error = url.searchParams.get("error");
|
|
211
|
+
if (error === "access_denied") {
|
|
212
|
+
return html(
|
|
213
|
+
"You don't have access to this project.",
|
|
214
|
+
403,
|
|
215
|
+
"Sign in with another account"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (error) return html("Sign-in was cancelled or failed.", 400);
|
|
219
|
+
const { state, verifier, clientId, redirectUri, returnTo } = decodeCookie(readCookie(request.headers, cookieName(LOGIN_COOKIE, secure)));
|
|
220
|
+
const code = url.searchParams.get("code");
|
|
221
|
+
if (!code || typeof state !== "string" || typeof verifier !== "string" || typeof clientId !== "string" || typeof redirectUri !== "string" || state !== url.searchParams.get("state")) {
|
|
222
|
+
return html("Sign-in expired or was cancelled.", 400);
|
|
223
|
+
}
|
|
224
|
+
const tokenResponse = await fetch(`${host}/auth/oauth2/token`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
227
|
+
body: new URLSearchParams({
|
|
228
|
+
grant_type: "authorization_code",
|
|
229
|
+
code,
|
|
230
|
+
redirect_uri: redirectUri,
|
|
231
|
+
client_id: clientId,
|
|
232
|
+
code_verifier: verifier
|
|
233
|
+
}),
|
|
234
|
+
signal: AbortSignal.timeout(1e4)
|
|
235
|
+
});
|
|
236
|
+
const tokens = tokenResponse.ok ? await tokenResponse.json() : null;
|
|
237
|
+
const accessToken = tokens?.access_token;
|
|
238
|
+
if (typeof accessToken !== "string" || !TOKEN_PATTERN.test(accessToken)) {
|
|
239
|
+
return html("Sign-in expired or was cancelled.", 400);
|
|
240
|
+
}
|
|
241
|
+
const session = await fetchCloudSession(accessToken);
|
|
242
|
+
const maxAge = Math.floor(
|
|
243
|
+
(Date.parse(session?.expiresAt ?? "") - Date.now()) / 1e3
|
|
244
|
+
);
|
|
245
|
+
if (!session || !(maxAge > 0)) {
|
|
246
|
+
await revoke(accessToken, clientId);
|
|
247
|
+
return html("Sign-in expired or was cancelled.", 400);
|
|
248
|
+
}
|
|
249
|
+
const headers = new Headers({
|
|
250
|
+
...noStore,
|
|
251
|
+
location: new URL(safeReturnTo(returnTo), appOrigin(request)).toString()
|
|
252
|
+
});
|
|
253
|
+
headers.append("set-cookie", cookie(LOGIN_COOKIE, "", 0, secure));
|
|
254
|
+
headers.append(
|
|
255
|
+
"set-cookie",
|
|
256
|
+
cookie(
|
|
257
|
+
SESSION_COOKIE,
|
|
258
|
+
encodeCookie({ token: accessToken, clientId }),
|
|
259
|
+
maxAge,
|
|
260
|
+
secure
|
|
261
|
+
)
|
|
262
|
+
);
|
|
263
|
+
return new Response(null, { status: 303, headers });
|
|
264
|
+
};
|
|
265
|
+
const logout = async (request) => {
|
|
266
|
+
const stored = readSessionCookie(request.headers);
|
|
267
|
+
if (stored) await revoke(stored.token, stored.clientId);
|
|
268
|
+
return new Response(null, {
|
|
269
|
+
status: 303,
|
|
270
|
+
headers: {
|
|
271
|
+
...noStore,
|
|
272
|
+
location: "/",
|
|
273
|
+
"set-cookie": cookie(SESSION_COOKIE, "", 0, isSecure(request))
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
const authenticate2 = async ({ request }) => {
|
|
278
|
+
const url = new URL(request.url);
|
|
279
|
+
if (url.pathname.startsWith(`${BASE_PATH}/`)) {
|
|
280
|
+
const route = `${request.method} ${url.pathname.slice(BASE_PATH.length)}`;
|
|
281
|
+
if (route === "GET /login") return login(request, url);
|
|
282
|
+
if (route === "GET /callback") return callback(request, url);
|
|
283
|
+
if (route === "POST /logout") return logout(request);
|
|
284
|
+
if (route === "GET /session") {
|
|
285
|
+
const resolved2 = await resolve(request.headers);
|
|
286
|
+
return resolved2 ? Response.json(resolved2.session, { headers: noStore }) : unauthorized();
|
|
287
|
+
}
|
|
288
|
+
return Response.json({ error: "Not found" }, { status: 404 });
|
|
289
|
+
}
|
|
290
|
+
const resolved = await resolve(request.headers);
|
|
291
|
+
return resolved ? { ...resolved.session.user, accessToken: resolved.accessToken } : unauthorized();
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
authenticate: authenticate2,
|
|
295
|
+
getSession: async (headers) => (await resolve(headers))?.session ?? null,
|
|
296
|
+
loginUrl: loginUrl2
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
var defaultAuth;
|
|
300
|
+
var getDefaultAuth = () => defaultAuth ??= createPuckAuth();
|
|
301
|
+
var authenticate = (context) => getDefaultAuth().authenticate(context);
|
|
302
|
+
var getSession = (headers) => getDefaultAuth().getSession(headers);
|
|
303
|
+
var loginUrl = (returnTo) => getDefaultAuth().loginUrl(returnTo);
|
|
304
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
305
|
+
0 && (module.exports = {
|
|
306
|
+
authenticate,
|
|
307
|
+
createPuckAuth,
|
|
308
|
+
getSession,
|
|
309
|
+
loginUrl,
|
|
310
|
+
safeReturnTo
|
|
311
|
+
});
|