@obelism/content 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +112 -2
- package/dist/bin/content.js +216 -0
- package/dist/bridge.d.mts +82 -0
- package/dist/bridge.d.mts.map +1 -0
- package/dist/bridge.d.ts +82 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +1061 -0
- package/dist/bridge.mjs +1056 -0
- package/dist/config-c6qktlz6.mjs +8 -0
- package/dist/config-ci2hxmas.js +13 -0
- package/dist/index.d.mts +93 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +300 -0
- package/dist/index.mjs +284 -0
- package/dist/preview-by8d54dt.js +68 -0
- package/dist/preview-kvotryzw.mjs +64 -0
- package/dist/prompt.d.mts +26 -0
- package/dist/prompt.d.mts.map +1 -0
- package/dist/prompt.d.ts +26 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +112 -0
- package/dist/prompt.mjs +108 -0
- package/dist/site.d.mts +34 -0
- package/dist/site.d.mts.map +1 -0
- package/dist/site.d.ts +34 -0
- package/dist/site.d.ts.map +1 -0
- package/dist/site.js +310 -0
- package/dist/site.mjs +302 -0
- package/dist/types-pa96z6po.d.ts +120 -0
- package/dist/types-pa96z6po.d.ts.map +1 -0
- package/package.json +91 -5
package/dist/prompt.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Your own model for prompt editing in the visual editor. Set the endpoint
|
|
2
|
+
// under Space settings → Prompt editing; the CMS then sends each prompt there
|
|
3
|
+
// instead of to Workers AI, as a signed POST (Standard Webhooks, the same
|
|
4
|
+
// scheme as webhooks), with the instructions, the conversation and the JSON
|
|
5
|
+
// Schema of the answer already written. Your endpoint only runs a model:
|
|
6
|
+
//
|
|
7
|
+
// // app/api/content-prompt/route.ts
|
|
8
|
+
// import { createPromptHandler } from '@obelism/content/prompt'
|
|
9
|
+
//
|
|
10
|
+
// export const POST = createPromptHandler({
|
|
11
|
+
// secret: process.env.CONTENT_PROMPT_SECRET,
|
|
12
|
+
// generate: async ({ system, messages, schema }) => {
|
|
13
|
+
// // … call any model; return the answer object (or its JSON text)
|
|
14
|
+
// },
|
|
15
|
+
// })
|
|
16
|
+
//
|
|
17
|
+
// The CMS checks the answer like any edit, so a bad one changes nothing.
|
|
18
|
+
class PromptVerificationError extends Error {
|
|
19
|
+
constructor(message){
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'PromptVerificationError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const SECRET_PREFIX = 'whsec_';
|
|
25
|
+
// Seconds a request's timestamp may be off, against replays.
|
|
26
|
+
const DEFAULT_TOLERANCE = 5 * 60;
|
|
27
|
+
const base64ToBytes = (value)=>Uint8Array.from(atob(value), (char)=>char.charCodeAt(0));
|
|
28
|
+
// Checks a request's Standard Webhooks signature and returns its body.
|
|
29
|
+
// Throws PromptVerificationError when it isn't from the CMS.
|
|
30
|
+
const verifyPromptRequest = async (request, secret, { tolerance = DEFAULT_TOLERANCE, now = Date.now() } = {})=>{
|
|
31
|
+
if (!secret?.startsWith(SECRET_PREFIX)) {
|
|
32
|
+
throw new Error('The prompt secret must be the whsec_… from Space settings');
|
|
33
|
+
}
|
|
34
|
+
const id = request.headers.get('webhook-id');
|
|
35
|
+
const timestamp = request.headers.get('webhook-timestamp');
|
|
36
|
+
const signatures = request.headers.get('webhook-signature');
|
|
37
|
+
if (!id || !timestamp || !signatures) {
|
|
38
|
+
throw new PromptVerificationError('Missing signature headers');
|
|
39
|
+
}
|
|
40
|
+
const seconds = Number(timestamp);
|
|
41
|
+
if (!Number.isFinite(seconds) || Math.abs(now / 1000 - seconds) > tolerance) {
|
|
42
|
+
throw new PromptVerificationError('Timestamp outside the tolerance');
|
|
43
|
+
}
|
|
44
|
+
const body = await request.text();
|
|
45
|
+
const key = await crypto.subtle.importKey('raw', base64ToBytes(secret.slice(SECRET_PREFIX.length)), {
|
|
46
|
+
name: 'HMAC',
|
|
47
|
+
hash: 'SHA-256'
|
|
48
|
+
}, false, [
|
|
49
|
+
'verify'
|
|
50
|
+
]);
|
|
51
|
+
const signed = new TextEncoder().encode(`${id}.${timestamp}.${body}`);
|
|
52
|
+
let valid = false;
|
|
53
|
+
for (const signature of signatures.split(' ')){
|
|
54
|
+
const [version, value] = signature.split(',');
|
|
55
|
+
if (version !== 'v1' || !value) continue;
|
|
56
|
+
let bytes;
|
|
57
|
+
try {
|
|
58
|
+
bytes = base64ToBytes(value);
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// crypto.subtle.verify compares in constant time.
|
|
63
|
+
if (await crypto.subtle.verify('HMAC', key, bytes, signed)) valid = true;
|
|
64
|
+
}
|
|
65
|
+
if (!valid) throw new PromptVerificationError('Invalid signature');
|
|
66
|
+
let payload;
|
|
67
|
+
try {
|
|
68
|
+
payload = JSON.parse(body);
|
|
69
|
+
} catch {
|
|
70
|
+
throw new PromptVerificationError('The body is not JSON');
|
|
71
|
+
}
|
|
72
|
+
if (payload.type !== 'content.prompt') {
|
|
73
|
+
throw new PromptVerificationError(`Unexpected type "${payload.type}"`);
|
|
74
|
+
}
|
|
75
|
+
return payload;
|
|
76
|
+
};
|
|
77
|
+
// A fetch-style handler (Request → Response) for the prompt endpoint: Next.js
|
|
78
|
+
// route handlers, Hono, Workers, Bun, Deno. Answers 401 to anything not signed
|
|
79
|
+
// by the CMS and { output } otherwise.
|
|
80
|
+
const createPromptHandler = ({ secret, generate })=>async (request)=>{
|
|
81
|
+
let payload;
|
|
82
|
+
try {
|
|
83
|
+
payload = await verifyPromptRequest(request, secret);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof PromptVerificationError) {
|
|
86
|
+
return Response.json({
|
|
87
|
+
error: error.message
|
|
88
|
+
}, {
|
|
89
|
+
status: 401
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return Response.json({
|
|
96
|
+
output: await generate(payload)
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('[@obelism/content prompt]', error);
|
|
100
|
+
return Response.json({
|
|
101
|
+
error: 'The model failed'
|
|
102
|
+
}, {
|
|
103
|
+
status: 502
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export { PromptVerificationError, createPromptHandler, verifyPromptRequest };
|
package/dist/site.d.mts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
declare const SITE_TOKEN_COOKIE = "obelism_content_token";
|
|
2
|
+
declare const SITE_EDITING_COOKIE = "obelism_content_editing";
|
|
3
|
+
declare const SITE_REQUEST_HEADER = "x-obelism-content";
|
|
4
|
+
type SiteEnvironment = 'develop' | 'staging' | 'production';
|
|
5
|
+
type SiteSession = {
|
|
6
|
+
token: string;
|
|
7
|
+
expiresAt: string;
|
|
8
|
+
spaceId: string;
|
|
9
|
+
environment: SiteEnvironment;
|
|
10
|
+
};
|
|
11
|
+
type SiteEditingOptions = {
|
|
12
|
+
spaceId: string;
|
|
13
|
+
environment: SiteEnvironment;
|
|
14
|
+
url?: string | undefined;
|
|
15
|
+
basePath?: string | undefined;
|
|
16
|
+
siteUrl?: string | undefined;
|
|
17
|
+
onSignIn?: ((session: SiteSession) => void | Promise<void>) | undefined;
|
|
18
|
+
onSignOut?: (() => void | Promise<void>) | undefined;
|
|
19
|
+
timeout?: number | undefined;
|
|
20
|
+
fetch?: typeof fetch | undefined;
|
|
21
|
+
};
|
|
22
|
+
declare const readSiteToken: (cookies: string | null | undefined | {
|
|
23
|
+
get(name: string): {
|
|
24
|
+
value: string;
|
|
25
|
+
} | undefined;
|
|
26
|
+
}) => string | null;
|
|
27
|
+
declare const pkceChallenge: (verifier: string) => Promise<string>;
|
|
28
|
+
declare const safeReturnTo: (value: string | null | undefined) => string;
|
|
29
|
+
type SiteEditingHandler = (request: Request) => Promise<Response>;
|
|
30
|
+
declare const createSiteEditing: ({ spaceId, environment, url, basePath, siteUrl, onSignIn, onSignOut, timeout, fetch: fetchImpl, }: SiteEditingOptions) => SiteEditingHandler;
|
|
31
|
+
|
|
32
|
+
export { SITE_EDITING_COOKIE, SITE_REQUEST_HEADER, SITE_TOKEN_COOKIE, createSiteEditing, pkceChallenge, readSiteToken, safeReturnTo };
|
|
33
|
+
export type { SiteEditingHandler, SiteEditingOptions, SiteEnvironment, SiteSession };
|
|
34
|
+
//# sourceMappingURL=site.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site.d.mts","sources":["../src/site.ts"],"mappings":"AAAA,cAAc,iBAAiB;AAC/B,cAAc,mBAAmB;AACjC,cAAc,mBAAmB;AACjC,KAAK,eAAe;AACpB,KAAK,WAAW;AAChB;AACA;AACA;AACA,iBAAiB,eAAe;AAChC;AACA,KAAK,kBAAkB;AACvB;AACA,iBAAiB,eAAe;AAChC;AACA;AACA;AACA,0BAA0B,WAAW,YAAY,OAAO;AACxD,8BAA8B,OAAO;AACrC;AACA,mBAAmB,KAAK;AACxB;AACA,cAAc,aAAa;AAC3B;AACA;AACA;AACA;AACA,cAAc,aAAa,wBAAwB,OAAO;AAC1D,cAAc,YAAY;AAC1B,KAAK,kBAAkB,aAAa,OAAO,KAAK,OAAO,CAAC,QAAQ;AAChE,cAAc,iBAAiB,sGAAsG,kBAAkB,KAAK,kBAAkB;;","names":[]}
|
package/dist/site.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
declare const SITE_TOKEN_COOKIE = "obelism_content_token";
|
|
2
|
+
declare const SITE_EDITING_COOKIE = "obelism_content_editing";
|
|
3
|
+
declare const SITE_REQUEST_HEADER = "x-obelism-content";
|
|
4
|
+
type SiteEnvironment = 'develop' | 'staging' | 'production';
|
|
5
|
+
type SiteSession = {
|
|
6
|
+
token: string;
|
|
7
|
+
expiresAt: string;
|
|
8
|
+
spaceId: string;
|
|
9
|
+
environment: SiteEnvironment;
|
|
10
|
+
};
|
|
11
|
+
type SiteEditingOptions = {
|
|
12
|
+
spaceId: string;
|
|
13
|
+
environment: SiteEnvironment;
|
|
14
|
+
url?: string | undefined;
|
|
15
|
+
basePath?: string | undefined;
|
|
16
|
+
siteUrl?: string | undefined;
|
|
17
|
+
onSignIn?: ((session: SiteSession) => void | Promise<void>) | undefined;
|
|
18
|
+
onSignOut?: (() => void | Promise<void>) | undefined;
|
|
19
|
+
timeout?: number | undefined;
|
|
20
|
+
fetch?: typeof fetch | undefined;
|
|
21
|
+
};
|
|
22
|
+
declare const readSiteToken: (cookies: string | null | undefined | {
|
|
23
|
+
get(name: string): {
|
|
24
|
+
value: string;
|
|
25
|
+
} | undefined;
|
|
26
|
+
}) => string | null;
|
|
27
|
+
declare const pkceChallenge: (verifier: string) => Promise<string>;
|
|
28
|
+
declare const safeReturnTo: (value: string | null | undefined) => string;
|
|
29
|
+
type SiteEditingHandler = (request: Request) => Promise<Response>;
|
|
30
|
+
declare const createSiteEditing: ({ spaceId, environment, url, basePath, siteUrl, onSignIn, onSignOut, timeout, fetch: fetchImpl, }: SiteEditingOptions) => SiteEditingHandler;
|
|
31
|
+
|
|
32
|
+
export { SITE_EDITING_COOKIE, SITE_REQUEST_HEADER, SITE_TOKEN_COOKIE, createSiteEditing, pkceChallenge, readSiteToken, safeReturnTo };
|
|
33
|
+
export type { SiteEditingHandler, SiteEditingOptions, SiteEnvironment, SiteSession };
|
|
34
|
+
//# sourceMappingURL=site.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site.d.ts","sources":["../src/site.ts"],"mappings":"AAAA,cAAc,iBAAiB;AAC/B,cAAc,mBAAmB;AACjC,cAAc,mBAAmB;AACjC,KAAK,eAAe;AACpB,KAAK,WAAW;AAChB;AACA;AACA;AACA,iBAAiB,eAAe;AAChC;AACA,KAAK,kBAAkB;AACvB;AACA,iBAAiB,eAAe;AAChC;AACA;AACA;AACA,0BAA0B,WAAW,YAAY,OAAO;AACxD,8BAA8B,OAAO;AACrC;AACA,mBAAmB,KAAK;AACxB;AACA,cAAc,aAAa;AAC3B;AACA;AACA;AACA;AACA,cAAc,aAAa,wBAAwB,OAAO;AAC1D,cAAc,YAAY;AAC1B,KAAK,kBAAkB,aAAa,OAAO,KAAK,OAAO,CAAC,QAAQ;AAChE,cAAc,iBAAiB,sGAAsG,kBAAkB,KAAK,kBAAkB;;","names":[]}
|
package/dist/site.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
|
|
3
|
+
var config = require('./config-ci2hxmas.js');
|
|
4
|
+
|
|
5
|
+
// Editing on your own site, outside the visual editor (the CMS's
|
|
6
|
+
// docs/site-editing.md). One handler serves everything under `/cms` (the
|
|
7
|
+
// `basePath`) on your site:
|
|
8
|
+
//
|
|
9
|
+
// - `/cms/login?return_to=/pricing` — sends the browser to Obelism Content
|
|
10
|
+
// to sign in, with a PKCE challenge, and…
|
|
11
|
+
// - `/cms/callback` — …takes it back: trades the one-time code for a site
|
|
12
|
+
// token, keeps it in an httpOnly cookie, calls `onSignIn` (turn on your
|
|
13
|
+
// framework's draft mode there) and returns to `return_to`.
|
|
14
|
+
// - `/cms/logout` — ends the session in the CMS, clears the cookies, calls
|
|
15
|
+
// `onSignOut`.
|
|
16
|
+
// - `/cms/api/…` — what the bridge's prompt bar calls in the browser,
|
|
17
|
+
// forwarded to the CMS with the token. The token never reaches page
|
|
18
|
+
// scripts.
|
|
19
|
+
//
|
|
20
|
+
// While signed in, render drafts by reading the delivery API with the token
|
|
21
|
+
// (`readSiteToken`), and the bridge shows the prompt bar with Save and
|
|
22
|
+
// Publish.
|
|
23
|
+
const SITE_TOKEN_COOKIE = 'obelism_content_token';
|
|
24
|
+
// Readable by the page: tells the bridge the site is being edited, and where
|
|
25
|
+
// this handler lives. Holds no secret.
|
|
26
|
+
const SITE_EDITING_COOKIE = 'obelism_content_editing';
|
|
27
|
+
const FLOW_COOKIE = 'obelism_content_flow';
|
|
28
|
+
// The bridge sends it with every write, which a cross-site form can't.
|
|
29
|
+
const SITE_REQUEST_HEADER = 'x-obelism-content';
|
|
30
|
+
const SITE_PATH = '/api/site/v1';
|
|
31
|
+
const FLOW_MAX_AGE = 10 * 60;
|
|
32
|
+
// The site token from a Cookie header (or a framework's cookie store), or
|
|
33
|
+
// null when not signed in to edit:
|
|
34
|
+
//
|
|
35
|
+
// const token = readSiteToken(request.headers.get('cookie'))
|
|
36
|
+
// const content = token ? new ContentClient({ token, init: { cache: 'no-store' } }) : delivery
|
|
37
|
+
const readSiteToken = (cookies)=>{
|
|
38
|
+
if (!cookies) return null;
|
|
39
|
+
if (typeof cookies !== 'string') return cookies.get(SITE_TOKEN_COOKIE)?.value || null;
|
|
40
|
+
return parseCookies(cookies).get(SITE_TOKEN_COOKIE) || null;
|
|
41
|
+
};
|
|
42
|
+
const parseCookies = (header)=>{
|
|
43
|
+
const cookies = new Map();
|
|
44
|
+
for (const part of header.split(';')){
|
|
45
|
+
const index = part.indexOf('=');
|
|
46
|
+
if (index < 0) continue;
|
|
47
|
+
const name = part.slice(0, index).trim();
|
|
48
|
+
if (!name || cookies.has(name)) continue;
|
|
49
|
+
try {
|
|
50
|
+
cookies.set(name, decodeURIComponent(part.slice(index + 1).trim()));
|
|
51
|
+
} catch {
|
|
52
|
+
cookies.set(name, part.slice(index + 1).trim());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return cookies;
|
|
56
|
+
};
|
|
57
|
+
const serializeCookie = (name, value, { maxAge, path, httpOnly, secure })=>[
|
|
58
|
+
`${name}=${encodeURIComponent(value)}`,
|
|
59
|
+
`Path=${path}`,
|
|
60
|
+
`Max-Age=${Math.max(0, Math.floor(maxAge))}`,
|
|
61
|
+
// Also Expires: Next.js re-serializes response cookies when draft mode
|
|
62
|
+
// changes, and drops a Max-Age of 0.
|
|
63
|
+
`Expires=${new Date(maxAge > 0 ? Date.now() + maxAge * 1000 : 0).toUTCString()}`,
|
|
64
|
+
'SameSite=Lax',
|
|
65
|
+
httpOnly && 'HttpOnly',
|
|
66
|
+
secure && 'Secure'
|
|
67
|
+
].filter(Boolean).join('; ');
|
|
68
|
+
const base64Url = (bytes)=>{
|
|
69
|
+
let binary = '';
|
|
70
|
+
for (const byte of bytes)binary += String.fromCharCode(byte);
|
|
71
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
72
|
+
};
|
|
73
|
+
const randomString = (bytes = 32)=>base64Url(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
74
|
+
const pkceChallenge = async (verifier)=>base64Url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));
|
|
75
|
+
// Only a path on this site: never an open redirect.
|
|
76
|
+
const safeReturnTo = (value)=>value && value.startsWith('/') && !value.startsWith('//') && !value.includes('\\') ? value : '/';
|
|
77
|
+
const normalizeBasePath = (basePath)=>`/${basePath.replace(/^\/+|\/+$/g, '')}`;
|
|
78
|
+
// What the browser may reach through `/cms/api/…`: reads, and the writes
|
|
79
|
+
// the prompt bar makes. Nothing else of the site API is exposed.
|
|
80
|
+
const PROXY_ROUTES = [
|
|
81
|
+
{
|
|
82
|
+
method: 'GET',
|
|
83
|
+
pattern: /^session$/
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
method: 'GET',
|
|
87
|
+
pattern: /^entries\/[\w-]+$/
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
method: 'POST',
|
|
91
|
+
pattern: /^entries\/[\w-]+\/(preview|prompt|save|publish)$/
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
// The handler for everything under `basePath`. In Next.js:
|
|
95
|
+
//
|
|
96
|
+
// // app/cms/[...path]/route.ts
|
|
97
|
+
// const handler = createSiteEditing({
|
|
98
|
+
// spaceId: process.env.CONTENT_SPACE_ID!,
|
|
99
|
+
// environment: 'production',
|
|
100
|
+
// onSignIn: async () => (await draftMode()).enable(),
|
|
101
|
+
// onSignOut: async () => (await draftMode()).disable(),
|
|
102
|
+
// })
|
|
103
|
+
// export { handler as GET, handler as POST }
|
|
104
|
+
const createSiteEditing = ({ spaceId, environment, url, basePath = '/cms', siteUrl, onSignIn, onSignOut, timeout = config.DEFAULT_TIMEOUT, fetch: fetchImpl })=>{
|
|
105
|
+
const cms = config.normalizeUrl(url || config.DEFAULT_URL);
|
|
106
|
+
const base = normalizeBasePath(basePath);
|
|
107
|
+
const doFetch = fetchImpl ?? ((...args)=>fetch(...args));
|
|
108
|
+
const callCms = (path, init = {})=>doFetch(`${cms}${SITE_PATH}/${path}`, {
|
|
109
|
+
...init,
|
|
110
|
+
redirect: 'manual',
|
|
111
|
+
signal: AbortSignal.timeout(timeout)
|
|
112
|
+
});
|
|
113
|
+
return async (request)=>{
|
|
114
|
+
const requestUrl = new URL(request.url);
|
|
115
|
+
const origin = siteUrl ? config.normalizeUrl(siteUrl) : requestUrl.origin;
|
|
116
|
+
const secure = origin.startsWith('https:');
|
|
117
|
+
const redirectUri = `${origin}${base}/callback`;
|
|
118
|
+
const cookies = parseCookies(request.headers.get('cookie') ?? '');
|
|
119
|
+
const path = requestUrl.pathname.startsWith(`${base}/`) ? requestUrl.pathname.slice(base.length + 1) : null;
|
|
120
|
+
const sessionCookies = (session)=>[
|
|
121
|
+
serializeCookie(SITE_TOKEN_COOKIE, session.token, {
|
|
122
|
+
maxAge: session.maxAge,
|
|
123
|
+
path: '/',
|
|
124
|
+
httpOnly: true,
|
|
125
|
+
secure
|
|
126
|
+
}),
|
|
127
|
+
serializeCookie(SITE_EDITING_COOKIE, session.token ? base : '', {
|
|
128
|
+
maxAge: session.maxAge,
|
|
129
|
+
path: '/',
|
|
130
|
+
httpOnly: false,
|
|
131
|
+
secure
|
|
132
|
+
})
|
|
133
|
+
];
|
|
134
|
+
const clearedCookies = ()=>sessionCookies({
|
|
135
|
+
token: '',
|
|
136
|
+
maxAge: 0
|
|
137
|
+
});
|
|
138
|
+
const redirect = (location, setCookies = [])=>{
|
|
139
|
+
const headers = new Headers({
|
|
140
|
+
Location: location,
|
|
141
|
+
'Cache-Control': 'no-store'
|
|
142
|
+
});
|
|
143
|
+
for (const cookie of setCookies)headers.append('Set-Cookie', cookie);
|
|
144
|
+
return new Response(null, {
|
|
145
|
+
status: 302,
|
|
146
|
+
headers
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
if (path === 'login' && request.method === 'GET') {
|
|
150
|
+
const state = randomString(16);
|
|
151
|
+
const verifier = randomString(48);
|
|
152
|
+
const flow = JSON.stringify({
|
|
153
|
+
state,
|
|
154
|
+
verifier,
|
|
155
|
+
returnTo: safeReturnTo(requestUrl.searchParams.get('return_to'))
|
|
156
|
+
});
|
|
157
|
+
const connect = new URL(`${cms}/site/connect`);
|
|
158
|
+
connect.search = new URLSearchParams({
|
|
159
|
+
space: spaceId,
|
|
160
|
+
environment,
|
|
161
|
+
redirect_uri: redirectUri,
|
|
162
|
+
state,
|
|
163
|
+
code_challenge: await pkceChallenge(verifier),
|
|
164
|
+
code_challenge_method: 'S256'
|
|
165
|
+
}).toString();
|
|
166
|
+
return redirect(connect.toString(), [
|
|
167
|
+
serializeCookie(FLOW_COOKIE, flow, {
|
|
168
|
+
maxAge: FLOW_MAX_AGE,
|
|
169
|
+
path: base,
|
|
170
|
+
httpOnly: true,
|
|
171
|
+
secure
|
|
172
|
+
})
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
if (path === 'callback' && request.method === 'GET') {
|
|
176
|
+
const clearFlow = serializeCookie(FLOW_COOKIE, '', {
|
|
177
|
+
maxAge: 0,
|
|
178
|
+
path: base,
|
|
179
|
+
httpOnly: true,
|
|
180
|
+
secure
|
|
181
|
+
});
|
|
182
|
+
let flow;
|
|
183
|
+
try {
|
|
184
|
+
flow = JSON.parse(cookies.get(FLOW_COOKIE) ?? '');
|
|
185
|
+
} catch {
|
|
186
|
+
flow = {};
|
|
187
|
+
}
|
|
188
|
+
const code = requestUrl.searchParams.get('code');
|
|
189
|
+
if (!code || typeof flow.state !== 'string' || typeof flow.verifier !== 'string' || flow.state !== requestUrl.searchParams.get('state')) {
|
|
190
|
+
return textResponse(400, 'This sign-in link is invalid or expired. Start again from the login link.', [
|
|
191
|
+
clearFlow
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
const response = await callCms('token', {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: {
|
|
197
|
+
'Content-Type': 'application/json'
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
code,
|
|
201
|
+
code_verifier: flow.verifier,
|
|
202
|
+
redirect_uri: redirectUri
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
return textResponse(400, 'Obelism Content did not accept the sign-in. Start again from the login link.', [
|
|
207
|
+
clearFlow
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
const session = await response.json();
|
|
211
|
+
await onSignIn?.(session);
|
|
212
|
+
const maxAge = (Date.parse(session.expiresAt) - Date.now()) / 1000;
|
|
213
|
+
return redirect(safeReturnTo(flow.returnTo), [
|
|
214
|
+
clearFlow,
|
|
215
|
+
...sessionCookies({
|
|
216
|
+
token: session.token,
|
|
217
|
+
maxAge
|
|
218
|
+
})
|
|
219
|
+
]);
|
|
220
|
+
}
|
|
221
|
+
if (path === 'logout') {
|
|
222
|
+
const token = cookies.get(SITE_TOKEN_COOKIE);
|
|
223
|
+
if (token) {
|
|
224
|
+
await callCms('session', {
|
|
225
|
+
method: 'DELETE',
|
|
226
|
+
headers: {
|
|
227
|
+
Authorization: `Bearer ${token}`
|
|
228
|
+
}
|
|
229
|
+
}).catch(()=>null);
|
|
230
|
+
}
|
|
231
|
+
await onSignOut?.();
|
|
232
|
+
return redirect(safeReturnTo(requestUrl.searchParams.get('return_to')), clearedCookies());
|
|
233
|
+
}
|
|
234
|
+
if (path?.startsWith('api/')) {
|
|
235
|
+
const route = path.slice('api/'.length);
|
|
236
|
+
if (!PROXY_ROUTES.some(({ method, pattern })=>method === request.method && pattern.test(route))) {
|
|
237
|
+
return Response.json({
|
|
238
|
+
error: 'Not found'
|
|
239
|
+
}, {
|
|
240
|
+
status: 404
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (request.method !== 'GET' && request.headers.get(SITE_REQUEST_HEADER) !== '1') {
|
|
244
|
+
return Response.json({
|
|
245
|
+
error: 'Forbidden'
|
|
246
|
+
}, {
|
|
247
|
+
status: 403
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const token = cookies.get(SITE_TOKEN_COOKIE);
|
|
251
|
+
if (!token) {
|
|
252
|
+
return Response.json({
|
|
253
|
+
error: 'Not signed in'
|
|
254
|
+
}, {
|
|
255
|
+
status: 401
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const response = await callCms(route, {
|
|
259
|
+
method: request.method,
|
|
260
|
+
headers: {
|
|
261
|
+
Authorization: `Bearer ${token}`,
|
|
262
|
+
...request.method !== 'GET' && {
|
|
263
|
+
'Content-Type': 'application/json'
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
...request.method !== 'GET' && {
|
|
267
|
+
body: await request.text()
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
const headers = new Headers({
|
|
271
|
+
'Content-Type': 'application/json',
|
|
272
|
+
'Cache-Control': 'no-store'
|
|
273
|
+
});
|
|
274
|
+
// The session ended in the CMS: stop showing the editing UI.
|
|
275
|
+
if (response.status === 401) {
|
|
276
|
+
for (const cookie of clearedCookies()){
|
|
277
|
+
headers.append('Set-Cookie', cookie);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return new Response(await response.text(), {
|
|
281
|
+
status: response.status,
|
|
282
|
+
headers
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return Response.json({
|
|
286
|
+
error: 'Not found'
|
|
287
|
+
}, {
|
|
288
|
+
status: 404
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
const textResponse = (status, text, setCookies)=>{
|
|
293
|
+
const headers = new Headers({
|
|
294
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
295
|
+
'Cache-Control': 'no-store'
|
|
296
|
+
});
|
|
297
|
+
for (const cookie of setCookies)headers.append('Set-Cookie', cookie);
|
|
298
|
+
return new Response(text, {
|
|
299
|
+
status,
|
|
300
|
+
headers
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
exports.SITE_EDITING_COOKIE = SITE_EDITING_COOKIE;
|
|
305
|
+
exports.SITE_REQUEST_HEADER = SITE_REQUEST_HEADER;
|
|
306
|
+
exports.SITE_TOKEN_COOKIE = SITE_TOKEN_COOKIE;
|
|
307
|
+
exports.createSiteEditing = createSiteEditing;
|
|
308
|
+
exports.pkceChallenge = pkceChallenge;
|
|
309
|
+
exports.readSiteToken = readSiteToken;
|
|
310
|
+
exports.safeReturnTo = safeReturnTo;
|