camox 0.76.0 → 0.76.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/dist/core/editing/createEditableBlock.js +1077 -1681
- package/dist/core/editing/fieldSlot.js +251 -0
- package/dist/core/editing/useFieldSlot.js +159 -0
- package/dist/core/layoutContext.d.ts +2 -0
- package/dist/core/layoutContext.js +27 -1
- package/dist/features/canvas/CanvasPageFrame.js +2 -2
- package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
- package/dist/features/canvas/canvasCameraController.js +595 -0
- package/dist/features/canvas/useCanvasCamera.js +141 -587
- package/dist/features/canvas/useCanvasFrame.js +1 -1
- package/dist/features/page/DerivedPageContent.js +4 -8
- package/dist/features/preview/components/AssetLightbox.js +6 -65
- package/dist/features/preview/components/CanvasPreview.js +2 -2
- package/dist/features/preview/components/Frame.js +3 -3
- package/dist/features/preview/components/ItemFieldsEditor.js +376 -460
- package/dist/features/preview/components/Overlays.js +4 -9
- package/dist/features/preview/components/PreviewPanel.js +3 -3
- package/dist/features/preview/components/RemovedBlockPlaceholder.js +2 -2
- package/dist/features/preview/components/SelectionEditor.js +285 -339
- package/dist/features/preview/components/ZoomableImage.js +105 -0
- package/dist/features/preview/components/contentFieldSchema.js +3 -1
- package/dist/features/preview/components/useFieldWriter.js +30 -51
- package/dist/features/preview/components/useRecordView.js +3 -3
- package/dist/features/preview/overlayMessages.js +20 -58
- package/dist/features/preview/previewStore.js +6 -123
- package/dist/features/preview/repeatableItemPath.js +47 -0
- package/dist/features/renderer/RendererApp.js +6 -6
- package/dist/features/renderer/RendererFrames.js +2 -2
- package/dist/features/renderer/rendererHost.js +1 -1
- package/dist/features/routes/pageRuntime.d.ts +2 -8
- package/dist/features/routes/pageRuntime.js +21 -105
- package/dist/features/runtime/pageApp.js +1 -1
- package/dist/features/runtime/pageRead.js +105 -0
- package/dist/features/runtime/reader.d.ts +25 -0
- package/dist/features/runtime/reader.js +142 -0
- package/dist/features/runtime/runtime.d.ts +9 -3
- package/dist/features/runtime/runtime.js +608 -776
- package/dist/features/sdkBundle/buildSdkBundle.d.ts +5 -0
- package/dist/features/sdkBundle/buildSdkBundle.js +9 -3
- package/dist/features/studio/components/DraftsSheet.js +124 -65
- package/dist/features/studio/components/ThreadMessages.js +5 -9
- package/dist/features/studio/showInCanvas.js +9 -19
- package/dist/features/studio/useEditorSelection.js +8 -19
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +4 -4
- package/dist/core/hooks/useFieldSelection.js +0 -67
- package/dist/core/hooks/useIsEditable.js +0 -21
- package/dist/core/hooks/useOverlayMessage.js +0 -70
|
@@ -36,10 +36,16 @@ function parseMarkdownPath(pathname) {
|
|
|
36
36
|
if (pathname === "/index.md") return "/";
|
|
37
37
|
return pathname.slice(0, -3) || null;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The API's client for one kind of read: with a credential, in a draft or Live, in a locale. Only
|
|
41
|
+
* a credential can read a draft, so a draft is only ever named with one; a Live read names the
|
|
42
|
+
* draft this Worker publishes, if any.
|
|
43
|
+
*/
|
|
44
|
+
function createServerApiClient(apiUrl, { credential, draftId, locale } = {}) {
|
|
45
|
+
const headers = localeHeaders(locale);
|
|
46
|
+
const session = credential && "session" in credential ? credential.session : void 0;
|
|
47
|
+
if (credential && "lookToken" in credential) headers[LOOK_TOKEN_HEADER] = credential.lookToken;
|
|
48
|
+
if (draftId && credential) headers[DRAFT_HEADER] = draftId;
|
|
43
49
|
else {
|
|
44
50
|
const publishing = publishingDraft();
|
|
45
51
|
if (publishing) headers[PUBLISHING_DRAFT_HEADER] = publishing;
|
|
@@ -48,7 +54,7 @@ function createServerApiClient(apiUrl, options) {
|
|
|
48
54
|
url: `${apiUrl}/rpc`,
|
|
49
55
|
headers,
|
|
50
56
|
fetch: (request, init) => {
|
|
51
|
-
if (
|
|
57
|
+
if (session && request instanceof Request) request.headers.set("Better-Auth-Cookie", session);
|
|
52
58
|
return fetch(request, init);
|
|
53
59
|
}
|
|
54
60
|
}));
|
|
@@ -92,42 +98,16 @@ function draftRefusalNotice(error, draftId) {
|
|
|
92
98
|
if (code === "FORBIDDEN" || status === 403) return { reason: "forbidden" };
|
|
93
99
|
if (code === "UNAUTHORIZED" || status === 401) return null;
|
|
94
100
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Reads with the draft a URL names, or Live (ADR 0011). Only a member's request names the draft;
|
|
97
|
-
* anyone else reads Live. When the API refuses the draft, the same response reads Live instead.
|
|
98
|
-
*/
|
|
99
|
-
async function readDraftOrLive(draftId, member, read) {
|
|
100
|
-
if (!draftId) return { value: await read(void 0) };
|
|
101
|
-
if (!member) return {
|
|
102
|
-
value: await read(void 0),
|
|
103
|
-
refusedDraft: { notice: null }
|
|
104
|
-
};
|
|
105
|
-
try {
|
|
106
|
-
return {
|
|
107
|
-
value: await read(draftId),
|
|
108
|
-
draftId
|
|
109
|
-
};
|
|
110
|
-
} catch (error) {
|
|
111
|
-
const notice = draftRefusalNotice(error, draftId);
|
|
112
|
-
if (notice === void 0) throw error;
|
|
113
|
-
return {
|
|
114
|
-
value: await read(void 0),
|
|
115
|
-
refusedDraft: { notice }
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
101
|
/** The URL a render shows: without the draft param when the draft couldn't be read. */
|
|
120
102
|
function renderedHref(url, read) {
|
|
121
103
|
return read.refusedDraft ? withoutDraft(url) : url;
|
|
122
104
|
}
|
|
123
|
-
async function loadCamoxPage({
|
|
105
|
+
async function loadCamoxPage({ reader, draftId, locale, pathname, projectUuid, queryClient }) {
|
|
124
106
|
const studioCache = createServerStudioCache(queryClient, {
|
|
125
|
-
|
|
126
|
-
authCookieHeader,
|
|
127
|
-
lookToken,
|
|
107
|
+
reader,
|
|
128
108
|
locale
|
|
129
109
|
});
|
|
130
|
-
const inDraft = { in: draftId
|
|
110
|
+
const inDraft = { in: draftId ?? null };
|
|
131
111
|
const fallback = locale && linkFallbackLocale(locale);
|
|
132
112
|
const [page] = await Promise.all([
|
|
133
113
|
studioCache.ensure(reads.pageStructure({
|
|
@@ -143,16 +123,14 @@ async function loadCamoxPage({ apiUrl, authCookieHeader, lookToken, draftId, loc
|
|
|
143
123
|
return page;
|
|
144
124
|
}
|
|
145
125
|
/**
|
|
146
|
-
* The studio's reads for one server request, read
|
|
147
|
-
*
|
|
126
|
+
* The studio's reads for one server request, read as its reader. Each operation names the draft
|
|
127
|
+
* it acts in, once the request settled which one it reads.
|
|
148
128
|
*/
|
|
149
|
-
function createServerStudioCache(queryClient, {
|
|
129
|
+
function createServerStudioCache(queryClient, { reader, locale }) {
|
|
150
130
|
return createStudioCache({
|
|
151
131
|
queryClient,
|
|
152
132
|
scope: createDraftScope({ queryClient }),
|
|
153
|
-
api: (draft, inLocale) =>
|
|
154
|
-
authCookieHeader,
|
|
155
|
-
lookToken,
|
|
133
|
+
api: (draft, inLocale) => reader.api({
|
|
156
134
|
draftId: draft ?? void 0,
|
|
157
135
|
locale: inLocale
|
|
158
136
|
}),
|
|
@@ -160,45 +138,6 @@ function createServerStudioCache(queryClient, { apiUrl, authCookieHeader, lookTo
|
|
|
160
138
|
});
|
|
161
139
|
}
|
|
162
140
|
/**
|
|
163
|
-
* The locales of the scope a request reads: the URL's draft's for a session naming one, else
|
|
164
|
-
* Live's. A draft the API refuses reads Live's, as the request's other reads then do. Live's read
|
|
165
|
-
* keeps the session: a member's studio routes the locales Live doesn't serve yet, which visitors
|
|
166
|
-
* never see (ADR 0043), and a render with a session is private.
|
|
167
|
-
*/
|
|
168
|
-
async function readScopeLocales({ apiUrl, authCookieHeader, draftId, projectUuid }) {
|
|
169
|
-
const list = (session, readDraftId) => createServerApiClient(apiUrl, {
|
|
170
|
-
authCookieHeader: session,
|
|
171
|
-
draftId: readDraftId
|
|
172
|
-
}).locales.list({ projectUuid });
|
|
173
|
-
const { value } = await readDraftOrLive(draftId, !!authCookieHeader, async (readDraftId) => {
|
|
174
|
-
if (readDraftId || !authCookieHeader) return list(authCookieHeader, readDraftId);
|
|
175
|
-
try {
|
|
176
|
-
return await list(authCookieHeader, void 0);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
if (!isAuthSessionError(error)) throw error;
|
|
179
|
-
return list(void 0, void 0);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
return value;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* The project, when the auth cookie is a valid session of one of its members. A successful
|
|
186
|
-
* protected project lookup validates both session and project access: cookie presence alone
|
|
187
|
-
* is not proof of authentication. An invalid session asks for the cookie to be cleared.
|
|
188
|
-
*/
|
|
189
|
-
async function resolveMemberProject({ apiUrl, authCookieHeader, projectUuid }) {
|
|
190
|
-
if (!authCookieHeader) return { clearAuthCookie: false };
|
|
191
|
-
try {
|
|
192
|
-
return {
|
|
193
|
-
project: await createServerApiClient(apiUrl, { authCookieHeader }).projects.getByUuid({ projectUuid }),
|
|
194
|
-
clearAuthCookie: false
|
|
195
|
-
};
|
|
196
|
-
} catch (error) {
|
|
197
|
-
if (!isAuthSessionError(error)) throw error;
|
|
198
|
-
return { clearAuthCookie: true };
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
141
|
* The project's favicon, which falls back to the default favicon when none was uploaded.
|
|
203
142
|
* Pass `withDefault: false` to get a 404 instead, to learn whether there is one.
|
|
204
143
|
*/
|
|
@@ -217,36 +156,13 @@ function buildCamoxPageLoaderData(apiUrl, page, origin) {
|
|
|
217
156
|
faviconUrl: buildFaviconUrl(apiUrl, page.project)
|
|
218
157
|
};
|
|
219
158
|
}
|
|
220
|
-
async function loadCamoxPageForRequest({ apiUrl, authCookieHeader, draftId, locale, origin, pathname, projectUuid, queryClient }) {
|
|
221
|
-
const member = await resolveMemberProject({
|
|
222
|
-
apiUrl,
|
|
223
|
-
authCookieHeader,
|
|
224
|
-
projectUuid
|
|
225
|
-
});
|
|
226
|
-
if (member.clearAuthCookie) console.warn("[camox] Ignoring stale camox_auth_cookie.");
|
|
227
|
-
const { value: page, ...read } = await readDraftOrLive(draftId, !!member.project, (readDraftId) => loadCamoxPage({
|
|
228
|
-
apiUrl,
|
|
229
|
-
authCookieHeader: member.project ? authCookieHeader : void 0,
|
|
230
|
-
draftId: readDraftId,
|
|
231
|
-
locale,
|
|
232
|
-
pathname,
|
|
233
|
-
projectUuid,
|
|
234
|
-
queryClient
|
|
235
|
-
}));
|
|
236
|
-
return {
|
|
237
|
-
...read,
|
|
238
|
-
data: buildCamoxPageLoaderData(apiUrl, page, origin),
|
|
239
|
-
project: member.project,
|
|
240
|
-
shouldClearAuthCookie: member.clearAuthCookie
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
159
|
/**
|
|
244
160
|
* Reads the Markdown Representation of the page at a runtime path, with its locale's prefix, in
|
|
245
161
|
* one API read: the API finds the locale the prefix names, the page there, its Markdown and what
|
|
246
162
|
* its front matter is built from. Markdown is a public output, so it reads Live whoever asks.
|
|
247
163
|
*/
|
|
248
|
-
function readPageMarkdown({
|
|
249
|
-
return
|
|
164
|
+
function readPageMarkdown({ reader, path, projectUuid }) {
|
|
165
|
+
return reader.api().pages.getMarkdown({
|
|
250
166
|
path,
|
|
251
167
|
projectUuid
|
|
252
168
|
});
|
|
@@ -366,4 +282,4 @@ function buildCamoxPageHead(camoxApp, loaderData, locale) {
|
|
|
366
282
|
}
|
|
367
283
|
|
|
368
284
|
//#endregion
|
|
369
|
-
export { buildCamoxPageHead, buildCamoxPageLoaderData, buildDefaultFaviconUrl, buildFaviconUrl, buildPageHead, buildPageMeta, createServerApiClient, createServerStudioCache, curatedMarkdownResponse, curatedPageMetaInput, declarePage, getMarkdownPath, isAuthSessionError, isNotFoundError, loadCamoxPage,
|
|
285
|
+
export { buildCamoxPageHead, buildCamoxPageLoaderData, buildDefaultFaviconUrl, buildFaviconUrl, buildPageHead, buildPageMeta, createServerApiClient, createServerStudioCache, curatedMarkdownResponse, curatedPageMetaInput, declarePage, draftRefusalNotice, getMarkdownPath, isAuthSessionError, isNotFoundError, loadCamoxPage, parseMarkdownPath, parseQuality, prefersMarkdown, readPageMarkdown, renderedHref, withFrontMatter };
|
|
@@ -2,8 +2,8 @@ import { DraftScopeContext } from "../../lib/draft-scope.js";
|
|
|
2
2
|
import { useAuthState, useSignInRedirect } from "../../lib/auth.js";
|
|
3
3
|
import { ArtifactCaptureContext } from "../../core/hooks/useArtifactHost.js";
|
|
4
4
|
import { useSignInShortcut } from "../provider/signInShortcut.js";
|
|
5
|
-
import { PreviewActivation } from "../preview/components/PreviewActivation.js";
|
|
6
5
|
import { PreviewDocumentContext } from "./PreviewDocumentContext.js";
|
|
6
|
+
import { PreviewActivation } from "../preview/components/PreviewActivation.js";
|
|
7
7
|
import { Frame } from "../preview/components/Frame.js";
|
|
8
8
|
import { PreviewSignInPage } from "../provider/PreviewSignInPage.js";
|
|
9
9
|
import { CoreCamoxProvider } from "../provider/CoreCamoxProvider.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createRequestContext } from "../../core/requestContext.js";
|
|
2
|
+
import { asCollectionItem } from "../../core/recordItem.js";
|
|
3
|
+
import { matchDerivedLayout } from "../../core/derivedRoutes.js";
|
|
4
|
+
import { layoutPageContext } from "../../core/layoutContext.js";
|
|
5
|
+
import { isNotFoundError } from "../routes/pageRuntime.js";
|
|
6
|
+
|
|
7
|
+
//#region src/features/runtime/pageRead.ts
|
|
8
|
+
/**
|
|
9
|
+
* What a path shows, in the Locale the path's prefix named: a Singleton Layout's page first, then
|
|
10
|
+
* the curated page at the path, then a Derived Layout's page. Each output reads the curated page
|
|
11
|
+
* and the layout's blocks as it needs them; Page Read decides which it shows and assembles a
|
|
12
|
+
* Layout Page. A Layout Page's loader runs in the Draft its blocks are read in: alongside them
|
|
13
|
+
* when the reader knows the Draft, after them when their read settles it.
|
|
14
|
+
*/
|
|
15
|
+
async function readPage({ app, path, pathname, reader, curated, layout }) {
|
|
16
|
+
const match = app && matchDerivedLayout(app.getLayouts(), path);
|
|
17
|
+
const layoutPage = () => readLayoutPage({
|
|
18
|
+
app,
|
|
19
|
+
match,
|
|
20
|
+
pathname,
|
|
21
|
+
reader,
|
|
22
|
+
readBlocks: layout
|
|
23
|
+
});
|
|
24
|
+
if (!mayShowCuratedPage(app, path)) return layoutPage();
|
|
25
|
+
try {
|
|
26
|
+
const { value: page, ...read } = await curated();
|
|
27
|
+
if (page !== null) return {
|
|
28
|
+
kind: "curated",
|
|
29
|
+
page,
|
|
30
|
+
...read
|
|
31
|
+
};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (!isNotFoundError(error)) throw error;
|
|
34
|
+
}
|
|
35
|
+
return match ? layoutPage() : { kind: "none" };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether a path may show a curated page, so reading one there may be worth starting early: not
|
|
39
|
+
* a Singleton Layout's route.
|
|
40
|
+
*/
|
|
41
|
+
function mayShowCuratedPage(app, path) {
|
|
42
|
+
return (app && matchDerivedLayout(app.getLayouts(), path))?.layout._internal.kind !== "singleton";
|
|
43
|
+
}
|
|
44
|
+
async function readLayoutPage({ app, match, pathname, reader, readBlocks }) {
|
|
45
|
+
const layout = match.layout._internal;
|
|
46
|
+
const blocksInScope = () => reader.inScope((draftId) => readBlocks(draftId, layout.id));
|
|
47
|
+
const load = (draftId) => loadLayoutResult(app, match, createRequestContext({ records: reader.records(draftId) }), reader.locale);
|
|
48
|
+
let result;
|
|
49
|
+
let read;
|
|
50
|
+
try {
|
|
51
|
+
if (reader.draft) [result, read] = await Promise.all([load(reader.draft.id), blocksInScope()]);
|
|
52
|
+
else {
|
|
53
|
+
read = await blocksInScope();
|
|
54
|
+
result = await load(read.draftId);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (isNotFoundError(error)) return {
|
|
58
|
+
kind: "none",
|
|
59
|
+
layout: match.layout
|
|
60
|
+
};
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const { value: blocks, ...draftRead } = read;
|
|
64
|
+
return {
|
|
65
|
+
kind: "layout",
|
|
66
|
+
layout: match.layout,
|
|
67
|
+
params: match.params,
|
|
68
|
+
blocks,
|
|
69
|
+
result,
|
|
70
|
+
...layoutPageContext(app, layout, {
|
|
71
|
+
path: pathname,
|
|
72
|
+
locale: reader.locale.tag,
|
|
73
|
+
projectName: blocks.projectName,
|
|
74
|
+
params: match.params,
|
|
75
|
+
result
|
|
76
|
+
}),
|
|
77
|
+
...draftRead
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const RECORD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
81
|
+
/** An envelope names its record; it doesn't authorize anything, the API does. */
|
|
82
|
+
function isValidCollectionItem(app, result) {
|
|
83
|
+
const data = result.data;
|
|
84
|
+
return typeof result.collectionId === "string" && !!app.getCollectionById(result.collectionId) && typeof data?.id === "string" && RECORD_ID.test(data.id) && typeof data.content === "object" && data.content !== null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Runs a Layout Page's loader in the page's locale. The envelope survives SSR, navigation and
|
|
88
|
+
* hydration, and its data renders as returned: Camox never translates it.
|
|
89
|
+
*/
|
|
90
|
+
async function loadLayoutResult(app, match, context, locale) {
|
|
91
|
+
const { loader, id } = match.layout._internal;
|
|
92
|
+
if (!loader) return void 0;
|
|
93
|
+
const result = await loader({
|
|
94
|
+
params: match.params,
|
|
95
|
+
context,
|
|
96
|
+
locale: locale.tag
|
|
97
|
+
});
|
|
98
|
+
const serializedResult = result === void 0 ? void 0 : JSON.parse(JSON.stringify(result));
|
|
99
|
+
const record = asCollectionItem(serializedResult);
|
|
100
|
+
if (!(record ? isValidCollectionItem(app, record) : serializedResult?.kind === "data" && Object.hasOwn(serializedResult, "data"))) throw new Error(`Layout "${id}" loader must return { kind: "data", data } with JSON-serializable data, or a collection-item envelope naming a registered collection and a record id`);
|
|
101
|
+
return serializedResult;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
export { mayShowCuratedPage, readPage };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LocaleView } from "../../lib/locale-path.js";
|
|
2
|
+
import "../routes/pageRuntime.js";
|
|
3
|
+
import { Router } from "@camox/api-contract/router";
|
|
4
|
+
import { RouterClient } from "@orpc/server";
|
|
5
|
+
//#region src/features/runtime/reader.d.ts
|
|
6
|
+
/** The API's procedures, as the content port answers them. */
|
|
7
|
+
export type ContentClient = RouterClient<Router>;
|
|
8
|
+
/** What proves a read may see more than Live: a Member's session, or a look token (ADR 0032). */
|
|
9
|
+
export type Credential = {
|
|
10
|
+
session: string;
|
|
11
|
+
} | {
|
|
12
|
+
lookToken: string;
|
|
13
|
+
};
|
|
14
|
+
/** Which reads a client makes: with which credential, in which Draft (Live without), in which Locale. */
|
|
15
|
+
export interface ContentAccess {
|
|
16
|
+
credential?: Credential;
|
|
17
|
+
draftId?: string;
|
|
18
|
+
locale?: LocaleView;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The content port: the API's reads, as one credential sees them, in Live or a Draft, in a
|
|
22
|
+
* Locale. Production reads the API (`apiContent`); tests read an in-memory one.
|
|
23
|
+
*/
|
|
24
|
+
export type Content = (access: ContentAccess) => ContentClient;
|
|
25
|
+
//#endregion
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { draftNamedBy } from "../../lib/draft-scope.js";
|
|
2
|
+
import { buildClearServerAuthCookieHeader, getLookToken, getServerAuthCookieHeader } from "../../lib/server-auth-cookie.js";
|
|
3
|
+
import { createServerApiClient, draftRefusalNotice, isAuthSessionError } from "../routes/pageRuntime.js";
|
|
4
|
+
import { DRAFT_SEARCH_PARAM } from "@camox/api-contract/drafts";
|
|
5
|
+
|
|
6
|
+
//#region src/features/runtime/reader.ts
|
|
7
|
+
/** The content port's production adapter: the API at `apiUrl`, over oRPC. */
|
|
8
|
+
function apiContent(apiUrl) {
|
|
9
|
+
return (access) => createServerApiClient(apiUrl, access);
|
|
10
|
+
}
|
|
11
|
+
function contentOf(options) {
|
|
12
|
+
return options.content ?? (options.apiUrl ? apiContent(options.apiUrl) : void 0);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether a request may see what only it sees, so no shared cache keeps its response (ADR 0022).
|
|
16
|
+
* Known before any read, so a cache can decide before the runtime answers.
|
|
17
|
+
*/
|
|
18
|
+
function isPrivateRequest(request) {
|
|
19
|
+
return !!getServerAuthCookieHeader(request.headers) || draftNamedBy(request.url) !== null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The request's Reader: a Member when the API confirms its session for the project, which it
|
|
23
|
+
* asks once, else a Visitor. A session the API refuses is cleared. Without a project or a way to
|
|
24
|
+
* read content, nobody is a Member.
|
|
25
|
+
*/
|
|
26
|
+
async function resolveReader(request, options) {
|
|
27
|
+
const content = contentOf(options);
|
|
28
|
+
const session = getServerAuthCookieHeader(request.headers);
|
|
29
|
+
const named = draftNamedBy(request.url) ?? void 0;
|
|
30
|
+
const isPrivate = isPrivateRequest(request);
|
|
31
|
+
if (!content || !options.projectUuid || !session) return visitor(content, options, {
|
|
32
|
+
named,
|
|
33
|
+
private: isPrivate
|
|
34
|
+
});
|
|
35
|
+
let member;
|
|
36
|
+
try {
|
|
37
|
+
member = await content({ credential: { session } }).projects.getByUuid({ projectUuid: options.projectUuid });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (!isAuthSessionError(error)) throw error;
|
|
40
|
+
console.warn("[camox] Ignoring stale camox_auth_cookie.");
|
|
41
|
+
return visitor(content, options, {
|
|
42
|
+
named,
|
|
43
|
+
private: isPrivate,
|
|
44
|
+
setCookie: buildClearServerAuthCookieHeader()
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const credential = { session };
|
|
48
|
+
const api = ({ draftId, locale } = {}) => content({
|
|
49
|
+
credential,
|
|
50
|
+
draftId,
|
|
51
|
+
locale
|
|
52
|
+
});
|
|
53
|
+
return withLocales(options, {
|
|
54
|
+
member,
|
|
55
|
+
draftId: named,
|
|
56
|
+
private: isPrivate,
|
|
57
|
+
setCookie: void 0,
|
|
58
|
+
api,
|
|
59
|
+
read: async (read) => {
|
|
60
|
+
if (!named) return { value: await read(void 0) };
|
|
61
|
+
try {
|
|
62
|
+
return {
|
|
63
|
+
value: await read(named),
|
|
64
|
+
draftId: named
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const notice = draftRefusalNotice(error, named);
|
|
68
|
+
if (notice === void 0) throw error;
|
|
69
|
+
return {
|
|
70
|
+
value: await read(void 0),
|
|
71
|
+
refusedDraft: { notice }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A Visitor's Reader, for what reads Live whoever asks (sitemaps, llms.txt, Markdown): no
|
|
79
|
+
* session check, no credential.
|
|
80
|
+
*/
|
|
81
|
+
function visitorReader(options) {
|
|
82
|
+
return visitor(contentOf(options), options, {
|
|
83
|
+
named: void 0,
|
|
84
|
+
private: false
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The look document's Reader (ADR 0032): the look token the request's cookie carries, for the
|
|
89
|
+
* Draft the URL names. Null without both.
|
|
90
|
+
*/
|
|
91
|
+
function lookReader(request, options) {
|
|
92
|
+
const content = contentOf(options);
|
|
93
|
+
const lookToken = getLookToken(request.headers);
|
|
94
|
+
const draftId = new URL(request.url).searchParams.get(DRAFT_SEARCH_PARAM) || void 0;
|
|
95
|
+
if (!content || !lookToken || !draftId) return null;
|
|
96
|
+
const credential = { lookToken };
|
|
97
|
+
return withLocales(options, {
|
|
98
|
+
member: void 0,
|
|
99
|
+
draftId,
|
|
100
|
+
private: true,
|
|
101
|
+
setCookie: void 0,
|
|
102
|
+
api: ({ locale } = {}) => content({
|
|
103
|
+
credential,
|
|
104
|
+
draftId,
|
|
105
|
+
locale
|
|
106
|
+
}),
|
|
107
|
+
read: async (read) => ({
|
|
108
|
+
value: await read(draftId),
|
|
109
|
+
draftId
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function visitor(content, options, { named, private: isPrivate, setCookie }) {
|
|
114
|
+
const api = ({ locale } = {}) => {
|
|
115
|
+
if (!content) throw new Error("Camox has no content to read: the runtime names no API.");
|
|
116
|
+
return content({ locale });
|
|
117
|
+
};
|
|
118
|
+
return withLocales(options, {
|
|
119
|
+
member: void 0,
|
|
120
|
+
draftId: void 0,
|
|
121
|
+
private: isPrivate,
|
|
122
|
+
setCookie,
|
|
123
|
+
api,
|
|
124
|
+
read: async (read) => ({
|
|
125
|
+
value: await read(void 0),
|
|
126
|
+
...named && { refusedDraft: { notice: null } }
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function withLocales(options, reader) {
|
|
131
|
+
return {
|
|
132
|
+
...reader,
|
|
133
|
+
locales: async () => {
|
|
134
|
+
const projectUuid = options.projectUuid;
|
|
135
|
+
const { value } = await reader.read((draftId) => reader.api({ draftId }).locales.list({ projectUuid }));
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
export { apiContent, isPrivateRequest, lookReader, resolveReader, visitorReader };
|
|
@@ -3,9 +3,10 @@ import { AlternatePath, LocaleView } from "../../lib/locale-path.js";
|
|
|
3
3
|
import { AnyLayoutLoaderResult } from "../../core/createLayout.js";
|
|
4
4
|
import { CamoxApp } from "../../core/createApp.js";
|
|
5
5
|
import { AnalyticsProviders } from "./analytics.js";
|
|
6
|
+
import { Content, ContentClient } from "./reader.js";
|
|
6
7
|
import { RuntimeOwnedRouteKind } from "./runtimeRoutes.js";
|
|
7
8
|
import { StudioRenderInput } from "./studioApp.js";
|
|
8
|
-
import { DraftRead, MemberProject, PageHead
|
|
9
|
+
import { DraftRead, MemberProject, PageHead } from "../routes/pageRuntime.js";
|
|
9
10
|
import { LookPlaceholderHeight } from "@camox/api-contract/drafts";
|
|
10
11
|
import { UseHeadInput } from "unhead/types";
|
|
11
12
|
//#region src/features/runtime/runtime.d.ts
|
|
@@ -47,7 +48,7 @@ export interface PageRenderInput extends DraftRead {
|
|
|
47
48
|
*/
|
|
48
49
|
lookLayout?: {
|
|
49
50
|
layoutId: string;
|
|
50
|
-
layout: Awaited<ReturnType<
|
|
51
|
+
layout: Awaited<ReturnType<ContentClient["layouts"]["getUnplaced"]>>["layout"];
|
|
51
52
|
placeholder: LookPlaceholderHeight;
|
|
52
53
|
path: string;
|
|
53
54
|
projectName: string;
|
|
@@ -74,7 +75,7 @@ export interface PageRenderInput extends DraftRead {
|
|
|
74
75
|
projectName: string;
|
|
75
76
|
};
|
|
76
77
|
result?: AnyLayoutLoaderResult;
|
|
77
|
-
layout: Awaited<ReturnType<
|
|
78
|
+
layout: Awaited<ReturnType<ContentClient["layouts"]["get"]>>["layout"];
|
|
78
79
|
};
|
|
79
80
|
apiUrl: string;
|
|
80
81
|
authenticationUrl: string;
|
|
@@ -97,6 +98,11 @@ export interface PageRenderInput extends DraftRead {
|
|
|
97
98
|
}
|
|
98
99
|
export interface RuntimeOptions {
|
|
99
100
|
apiUrl?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Where the runtime reads content, as each request's Reader sees it. Without it, the API at
|
|
103
|
+
* `apiUrl`. Tests read an in-memory one.
|
|
104
|
+
*/
|
|
105
|
+
content?: Content;
|
|
100
106
|
authenticationUrl?: string;
|
|
101
107
|
/** Where the browser loads each block from, so a page can preload the blocks it renders. */
|
|
102
108
|
blockPreloads?: {
|