canopycms 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +185 -46
- package/dist/api/branch-review.d.ts +11 -1
- package/dist/api/branch-review.js +96 -18
- package/dist/api/branch-status.d.ts +26 -2
- package/dist/api/branch-status.js +38 -2
- package/dist/api/branch-withdraw.js +4 -7
- package/dist/api/branch.d.ts +2 -0
- package/dist/api/client.d.ts +3 -3
- package/dist/api/client.js +4 -4
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/comments.js +4 -0
- package/dist/api/content.d.ts +2 -2
- package/dist/api/github-sync.d.ts +4 -0
- package/dist/api/github-sync.js +38 -6
- package/dist/api/pr-comment-sync.d.ts +18 -0
- package/dist/api/pr-comment-sync.js +77 -0
- package/dist/api/request-deadline.d.ts +15 -7
- package/dist/api/request-deadline.js +15 -7
- package/dist/api/schema.d.ts +16 -16
- package/dist/authorization/review-role.d.ts +33 -0
- package/dist/authorization/review-role.js +17 -0
- package/dist/branch-metadata-file.d.ts +3 -0
- package/dist/branch-metadata-file.js +1 -0
- package/dist/cli/cli.js +704 -131
- package/dist/cli/generate-ai-content.js +524 -59
- package/dist/cli/github-app-output.d.ts +34 -0
- package/dist/cli/github-app-output.js +85 -0
- package/dist/cli/init-github-app.d.ts +18 -0
- package/dist/cli/init-github-app.js +66 -58
- package/dist/cli/init.js +15 -2
- package/dist/cli/template-files/cms-stack.ts.template +3 -0
- package/dist/cli/templates.js +5 -1
- package/dist/comment-store.d.ts +5 -0
- package/dist/comment-store.js +24 -0
- package/dist/config/helpers.js +1 -3
- package/dist/config/schemas/collection.d.ts +2 -2
- package/dist/config/schemas/config.d.ts +3 -0
- package/dist/config/schemas/config.js +2 -1
- package/dist/config/schemas/url.d.ts +6 -0
- package/dist/config/schemas/url.js +16 -0
- package/dist/config/types.d.ts +12 -1
- package/dist/content-id-index.d.ts +9 -0
- package/dist/content-id-index.js +31 -0
- package/dist/editor/BranchManager.d.ts +3 -0
- package/dist/editor/BranchManager.js +75 -44
- package/dist/editor/CanopyEditor.js +2 -2
- package/dist/editor/CanopyEditorPage.js +3 -4
- package/dist/editor/Editor.js +47 -8
- package/dist/editor/branch-lock.d.ts +7 -0
- package/dist/editor/branch-lock.js +9 -2
- package/dist/editor/client-reference-resolver.d.ts +5 -3
- package/dist/editor/client-reference-resolver.js +10 -5
- package/dist/editor/components/BranchesLoadFailedNotice.d.ts +13 -0
- package/dist/editor/components/BranchesLoadFailedNotice.js +12 -0
- package/dist/editor/components/EditorHeader.d.ts +10 -4
- package/dist/editor/components/EditorHeader.js +108 -28
- package/dist/editor/components/EntryHeader.d.ts +5 -1
- package/dist/editor/components/EntryHeader.js +3 -1
- package/dist/editor/components/ReasonedMenuItem.d.ts +8 -0
- package/dist/editor/components/ReasonedMenuItem.js +2 -2
- package/dist/editor/components/ReviewDialogs.d.ts +19 -0
- package/dist/editor/components/ReviewDialogs.js +41 -0
- package/dist/editor/copy.d.ts +4 -0
- package/dist/editor/copy.js +4 -0
- package/dist/editor/fields/MarkdownField.js +1 -1
- package/dist/editor/hooks/create-branch-request.d.ts +6 -2
- package/dist/editor/hooks/create-branch-request.js +7 -2
- package/dist/editor/hooks/useBranchManager.d.ts +7 -2
- package/dist/editor/hooks/useBranchManager.js +252 -37
- package/dist/editor-url.d.ts +20 -0
- package/dist/editor-url.js +71 -0
- package/dist/git-manager.js +10 -6
- package/dist/github-service.d.ts +2 -2
- package/dist/github-service.js +4 -2
- package/dist/pr-comments.d.ts +33 -0
- package/dist/pr-comments.js +41 -0
- package/dist/schema/schema-store.d.ts +8 -8
- package/dist/services.d.ts +2 -17
- package/dist/services.js +38 -1
- package/dist/settings-workspace.d.ts +21 -7
- package/dist/settings-workspace.js +91 -16
- package/dist/submission-attribution.d.ts +38 -1
- package/dist/submission-attribution.js +197 -18
- package/dist/task-queue/cms-task-queue.d.ts +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/utils/content-serialize.js +7 -6
- package/dist/utils/markdown-body-splice.d.ts +15 -10
- package/dist/utils/markdown-body-splice.js +71 -28
- package/dist/utils/markdown-export-options.d.ts +13 -0
- package/dist/utils/markdown-export-options.js +13 -0
- package/dist/utils/markdown-restyle.d.ts +58 -0
- package/dist/utils/markdown-restyle.js +482 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/bundle-header.d.ts +13 -9
- package/dist/worker/bundle-header.js +17 -11
- package/dist/worker/git-process.d.ts +2 -2
- package/dist/worker/github-gateway-http.js +5 -4
- package/dist/worker/github-gateway-wire.d.ts +3 -0
- package/dist/worker/github-gateway-wire.js +1 -0
- package/dist/worker/github-gateway.d.ts +1 -1
- package/dist/worker/github-gateway.js +2 -2
- package/dist/worker/github-transfer.js +72 -2
- package/dist/worker/task-runner.js +53 -3
- package/package.json +2 -2
- package/dist/editor/fields/markdown-export-options.d.ts +0 -2
- package/dist/editor/fields/markdown-export-options.js +0 -8
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `init-github-app` says to the operator, as strings. Each stream gets
|
|
3
|
+
* its own palette, on only when that stream is a TTY (or FORCE_COLOR asks for
|
|
4
|
+
* it) and NO_COLOR is unset. picocolors' own default also turns colour on for
|
|
5
|
+
* `CI` and on Windows whatever the stream is, which would put escape codes in
|
|
6
|
+
* a piped log.
|
|
7
|
+
*/
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
type Palette = ReturnType<typeof pc.createColors>;
|
|
10
|
+
/** For anything printed with `console.log`. */
|
|
11
|
+
export declare const outColors: import("picocolors/types").Colors;
|
|
12
|
+
/** For anything printed with `console.error`. */
|
|
13
|
+
export declare const errColors: import("picocolors/types").Colors;
|
|
14
|
+
export declare const tick: (text: string) => string;
|
|
15
|
+
export declare const failMark: string;
|
|
16
|
+
export declare const cross: (text: string) => string;
|
|
17
|
+
/** `palette` is the stream the line goes to. */
|
|
18
|
+
export declare const link: (url: string, palette?: Palette) => string;
|
|
19
|
+
export declare const step: (n: number, text: string) => string;
|
|
20
|
+
/** Printed before the browser opens, so it survives a terminal that is gone by the time it matters. */
|
|
21
|
+
export declare function recoveryPreamble(): string;
|
|
22
|
+
export type RegistrationMessageInput = {
|
|
23
|
+
name: string;
|
|
24
|
+
owner: string;
|
|
25
|
+
repo: string;
|
|
26
|
+
/** `describeDestination`'s wording of where the key is going. */
|
|
27
|
+
destination: string;
|
|
28
|
+
formFile: string;
|
|
29
|
+
callbackPort: number;
|
|
30
|
+
timeoutMinutes: number;
|
|
31
|
+
};
|
|
32
|
+
export declare function registrationMessage(input: RegistrationMessageInput): string;
|
|
33
|
+
export declare function deploymentSettingsMessage(appId: number, installationId: number | null): string;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `init-github-app` says to the operator, as strings. Each stream gets
|
|
3
|
+
* its own palette, on only when that stream is a TTY (or FORCE_COLOR asks for
|
|
4
|
+
* it) and NO_COLOR is unset. picocolors' own default also turns colour on for
|
|
5
|
+
* `CI` and on Windows whatever the stream is, which would put escape codes in
|
|
6
|
+
* a piped log.
|
|
7
|
+
*/
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { CANOPY_APP_PERMISSIONS } from './github-app-manifest.js';
|
|
10
|
+
function colorsFor(stream) {
|
|
11
|
+
const { NO_COLOR, FORCE_COLOR, TERM } = process.env;
|
|
12
|
+
if (NO_COLOR)
|
|
13
|
+
return pc.createColors(false);
|
|
14
|
+
if (FORCE_COLOR)
|
|
15
|
+
return pc.createColors(FORCE_COLOR !== '0' && FORCE_COLOR !== 'false');
|
|
16
|
+
return pc.createColors(Boolean(stream.isTTY) && TERM !== 'dumb');
|
|
17
|
+
}
|
|
18
|
+
/** For anything printed with `console.log`. */
|
|
19
|
+
export const outColors = colorsFor(process.stdout);
|
|
20
|
+
/** For anything printed with `console.error`. */
|
|
21
|
+
export const errColors = colorsFor(process.stderr);
|
|
22
|
+
export const tick = (text) => `${outColors.green('✓')} ${text}`;
|
|
23
|
+
export const failMark = errColors.red('✗');
|
|
24
|
+
export const cross = (text) => `${failMark} ${text}`;
|
|
25
|
+
/** `palette` is the stream the line goes to. */
|
|
26
|
+
export const link = (url, palette = outColors) => palette.cyan(palette.underline(url));
|
|
27
|
+
export const step = (n, text) => outColors.bold(outColors.cyan(`${n}.`) + ` ${text}`);
|
|
28
|
+
/** Printed before the browser opens, so it survives a terminal that is gone by the time it matters. */
|
|
29
|
+
export function recoveryPreamble() {
|
|
30
|
+
return [
|
|
31
|
+
outColors.bold(outColors.yellow('If this goes wrong after you click Create')),
|
|
32
|
+
' The App will exist, but this command will not have its private key. To recover, either:',
|
|
33
|
+
' • delete the App in its settings and re-run this command, or',
|
|
34
|
+
' • open the App, scroll to "Private keys" and click "Generate a private key". The',
|
|
35
|
+
' .pem it downloads is your only chance to save it.',
|
|
36
|
+
outColors.dim(' (Not the "client secret" a few sections above: that is for OAuth user flows, is unused\n' +
|
|
37
|
+
' by this App, and fails confusingly when used to sign.)'),
|
|
38
|
+
'',
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
export function registrationMessage(input) {
|
|
42
|
+
const width = Math.max(...Object.keys(CANOPY_APP_PERMISSIONS).map((name) => name.length));
|
|
43
|
+
const permissions = Object.entries(CANOPY_APP_PERMISSIONS).map(([name, level]) => ` ${name.padEnd(width)} ${outColors.bold(level)}`);
|
|
44
|
+
return [
|
|
45
|
+
outColors.bold(`Registering "${input.name}" for ${input.owner}/${input.repo}`),
|
|
46
|
+
'',
|
|
47
|
+
`${outColors.bold('Permissions')} ${outColors.dim('(the part that matters)')}`,
|
|
48
|
+
...permissions,
|
|
49
|
+
'',
|
|
50
|
+
`${outColors.bold('Private key')} goes to ${input.destination}`,
|
|
51
|
+
outColors.dim(' If that fails you are asked for somewhere else before the key is discarded.'),
|
|
52
|
+
'',
|
|
53
|
+
step(1, 'Open this file in a browser:'),
|
|
54
|
+
'',
|
|
55
|
+
` ${link(input.formFile)}`,
|
|
56
|
+
'',
|
|
57
|
+
' The browser matters, twice over. It must be:',
|
|
58
|
+
` • ${outColors.bold('logged in to GitHub')} as an owner of ${input.owner}`,
|
|
59
|
+
` • running ${outColors.bold('on this machine')}: the redirect returns to 127.0.0.1:${input.callbackPort}, which only a local browser can reach`,
|
|
60
|
+
outColors.dim(' An embedded or app-internal browser bounces off the GitHub login wall and appears\n' +
|
|
61
|
+
' to do nothing when you click Create.'),
|
|
62
|
+
'',
|
|
63
|
+
' The page posts the manifest to GitHub, which shows the App pre-filled with exactly',
|
|
64
|
+
' those permissions. Review them and click Create.',
|
|
65
|
+
'',
|
|
66
|
+
outColors.dim(`Waiting for GitHub to redirect back (up to ${input.timeoutMinutes} minutes)...`),
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
export function deploymentSettingsMessage(appId, installationId) {
|
|
70
|
+
return [
|
|
71
|
+
'',
|
|
72
|
+
outColors.bold('Set these on your deployment'),
|
|
73
|
+
'',
|
|
74
|
+
` GITHUB_APP_ID=${appId}`,
|
|
75
|
+
` GITHUB_APP_INSTALLATION_ID=${installationId ?? '<run `verify` to read it>'}`,
|
|
76
|
+
` GITHUB_APP_PRIVATE_KEY_SECRET_ARN=<wherever you just put the key>`,
|
|
77
|
+
'',
|
|
78
|
+
outColors.dim(' Both ids are numeric. The App ID is NOT the "Client ID" (`Iv1.…`) shown beside it,\n' +
|
|
79
|
+
' and the installation id is NOT the App ID: an App installed twice has one App ID\n' +
|
|
80
|
+
' and two installation ids.'),
|
|
81
|
+
'',
|
|
82
|
+
outColors.bold('Re-check this any time, without creating anything'),
|
|
83
|
+
` canopycms init-github-app verify --app-id ${appId} --key-file <path>`,
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
@@ -43,6 +43,24 @@ type KeyDestination = {
|
|
|
43
43
|
kind: 'file';
|
|
44
44
|
filePath: string;
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* The App's own settings page.
|
|
48
|
+
* @internal Exported for tests.
|
|
49
|
+
*/
|
|
50
|
+
export declare function appSettingsUrl(target: AppTarget, slug: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* The recovery note printed when the key could not be stored.
|
|
53
|
+
* @internal Exported for tests.
|
|
54
|
+
*/
|
|
55
|
+
export declare function keyNotStoredMessage(target: AppTarget, created: {
|
|
56
|
+
id: number;
|
|
57
|
+
slug: string;
|
|
58
|
+
}): string;
|
|
59
|
+
/**
|
|
60
|
+
* Step 2 of `create`: where to install the App.
|
|
61
|
+
* @internal Exported for tests.
|
|
62
|
+
*/
|
|
63
|
+
export declare function installStepMessage(target: AppTarget, slug: string): string;
|
|
46
64
|
/**
|
|
47
65
|
* Where the manifest form posts: user-owned and organisation repositories use
|
|
48
66
|
* different URLs, and posting the wrong one fails at the form.
|
|
@@ -33,20 +33,56 @@ import { Readable } from 'node:stream';
|
|
|
33
33
|
import { getErrorMessage, isNodeError, redactCredentials } from '../utils/error.js';
|
|
34
34
|
import { normalizeGitHubAppPrivateKey } from '../worker/github-auth.js';
|
|
35
35
|
import { APP_NAME_MAX_LENGTH, CANOPY_APP_PERMISSIONS, appDescription, appName, appSlug, readbackVerdict, } from './github-app-manifest.js';
|
|
36
|
+
import { cross, deploymentSettingsMessage, errColors, failMark, link, outColors, recoveryPreamble, registrationMessage, step, tick, } from './github-app-output.js';
|
|
36
37
|
import { askLine, pressEnter } from './prompt.js';
|
|
37
38
|
/** How long `create` waits for the browser round trip before giving up. */
|
|
38
39
|
const CALLBACK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
39
40
|
/** How long any single call to api.github.com may take. */
|
|
40
41
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
42
|
+
/**
|
|
43
|
+
* Organisation-owned and user-owned Apps live under different paths, and each
|
|
44
|
+
* form 404s or fails for the other's. Every App URL the command builds starts here.
|
|
45
|
+
*/
|
|
46
|
+
function appsBaseUrl(target) {
|
|
47
|
+
return target.isOrganization
|
|
48
|
+
? `https://github.com/organizations/${encodeURIComponent(target.owner)}/settings/apps`
|
|
49
|
+
: 'https://github.com/settings/apps';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The App's own settings page.
|
|
53
|
+
* @internal Exported for tests.
|
|
54
|
+
*/
|
|
55
|
+
export function appSettingsUrl(target, slug) {
|
|
56
|
+
return `${appsBaseUrl(target)}/${encodeURIComponent(slug)}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The recovery note printed when the key could not be stored.
|
|
60
|
+
* @internal Exported for tests.
|
|
61
|
+
*/
|
|
62
|
+
export function keyNotStoredMessage(target, created) {
|
|
63
|
+
return (`\n${errColors.bold(errColors.red('Stopping here:'))} the App exists but its key was not stored, so installing it now\n` +
|
|
64
|
+
' would achieve nothing. To recover, open the App and generate a private key from\n' +
|
|
65
|
+
' its "Private keys" section, then run `verify`; or delete the App and start again.\n\n' +
|
|
66
|
+
` GITHUB_APP_ID=${created.id}\n` +
|
|
67
|
+
` App settings: ${link(appSettingsUrl(target, created.slug), errColors)}`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Step 2 of `create`: where to install the App.
|
|
71
|
+
* @internal Exported for tests.
|
|
72
|
+
*/
|
|
73
|
+
export function installStepMessage(target, slug) {
|
|
74
|
+
return (`\n${step(2, `Install it on ${target.owner}/${target.repo}:`)}\n\n` +
|
|
75
|
+
` ${link(`${appSettingsUrl(target, slug)}/installations`)}\n\n` +
|
|
76
|
+
' That page shows an Install button first — the repository picker only appears after\n' +
|
|
77
|
+
' you click it. Choose "Only select repositories" and pick that one repository.\n');
|
|
78
|
+
}
|
|
41
79
|
/**
|
|
42
80
|
* Where the manifest form posts: user-owned and organisation repositories use
|
|
43
81
|
* different URLs, and posting the wrong one fails at the form.
|
|
44
82
|
* @internal Exported for tests.
|
|
45
83
|
*/
|
|
46
84
|
export function manifestPostUrl(target, state) {
|
|
47
|
-
const base = target
|
|
48
|
-
? `https://github.com/organizations/${encodeURIComponent(target.owner)}/settings/apps/new`
|
|
49
|
-
: 'https://github.com/settings/apps/new';
|
|
85
|
+
const base = `${appsBaseUrl(target)}/new`;
|
|
50
86
|
// The `state` rides on the ACTION URL, not inside the manifest body: it is a
|
|
51
87
|
// query parameter GitHub echoes back on the redirect, and putting it in the
|
|
52
88
|
// JSON would look implemented while protecting nothing.
|
|
@@ -582,7 +618,7 @@ export async function handOffWithRetry(pem, destination) {
|
|
|
582
618
|
for (;;) {
|
|
583
619
|
const result = await handOffKey(pem, attempt);
|
|
584
620
|
if (result.stored) {
|
|
585
|
-
console.log(`\n
|
|
621
|
+
console.log(`\n${tick(`Private key: ${result.detail}`)}`);
|
|
586
622
|
return true;
|
|
587
623
|
}
|
|
588
624
|
console.error(`\n THE PRIVATE KEY WAS NOT STORED: ${result.detail}\n\n` +
|
|
@@ -718,17 +754,17 @@ async function readBackInstallation(appId, privateKey, target) {
|
|
|
718
754
|
return { ok: false, installationId: null };
|
|
719
755
|
}
|
|
720
756
|
const installation = found.body;
|
|
721
|
-
console.log(`\n
|
|
757
|
+
console.log(`\n${outColors.bold(`Installation ${installation.id}`)} on ${target.owner}/${target.repo}`);
|
|
722
758
|
const findings = readbackVerdict(installation);
|
|
723
759
|
if (findings.length === 0) {
|
|
724
|
-
console.log(' grant matches exactly what this package needs:');
|
|
760
|
+
console.log(' The grant matches exactly what this package needs:');
|
|
725
761
|
for (const [name, level] of Object.entries(CANOPY_APP_PERMISSIONS)) {
|
|
726
|
-
console.log(`
|
|
762
|
+
console.log(` ${tick(`${name}: ${level}`)}`);
|
|
727
763
|
}
|
|
728
764
|
}
|
|
729
765
|
else {
|
|
730
766
|
for (const finding of findings) {
|
|
731
|
-
console.error(`
|
|
767
|
+
console.error(` ${cross(finding.message)}`);
|
|
732
768
|
}
|
|
733
769
|
}
|
|
734
770
|
// The one-App-per-site invariant, observed not asserted: EXACTLY one
|
|
@@ -737,30 +773,30 @@ async function readBackInstallation(appId, privateKey, target) {
|
|
|
737
773
|
let installationsOk = false;
|
|
738
774
|
const installations = await installationCount(jwt);
|
|
739
775
|
if (installations === null) {
|
|
740
|
-
console.error(
|
|
776
|
+
console.error(` ${failMark} could not list this App's installations, so "installed once" is UNCHECKED.\n` +
|
|
741
777
|
' Reported as a failure rather than passed over: this is the check that observes\n' +
|
|
742
778
|
' the one-App-per-site rule, and an unread check is not a satisfied one.');
|
|
743
779
|
}
|
|
744
780
|
else if (installations === 1) {
|
|
745
781
|
installationsOk = true;
|
|
746
|
-
console.log('
|
|
782
|
+
console.log(` ${tick('installed once, so this key reaches no other account')}`);
|
|
747
783
|
}
|
|
748
784
|
else if (installations > 1) {
|
|
749
|
-
console.error(`
|
|
785
|
+
console.error(` ${failMark} this App is installed ${installations} times, and should be installed ONCE.\n` +
|
|
750
786
|
' Its private key is App-level: whoever holds it can mint a token for any of\n' +
|
|
751
787
|
' those installations. Register a separate App per site instead, and remove\n' +
|
|
752
788
|
' the installations that do not belong to this one.');
|
|
753
789
|
}
|
|
754
790
|
else {
|
|
755
|
-
console.error(`
|
|
791
|
+
console.error(` ${failMark} GitHub reports ${installations} installations of this App, yet one was just read\n` +
|
|
756
792
|
' back for this repository. Something is inconsistent; re-run before trusting it.');
|
|
757
793
|
}
|
|
758
794
|
const mint = await proveTokenMint(jwt, installation.id, target.repo);
|
|
759
795
|
if (mint.ok) {
|
|
760
|
-
console.log(`
|
|
796
|
+
console.log(` ${tick(`installation token ${mint.detail}`)}`);
|
|
761
797
|
}
|
|
762
798
|
else {
|
|
763
|
-
console.error(`
|
|
799
|
+
console.error(` ${failMark} could not mint an installation token — ${mint.detail}\n` +
|
|
764
800
|
' If that message says the requested permissions are not granted to this\n' +
|
|
765
801
|
' installation, that is a GitHub behaviour rather than a bug here: adding a\n' +
|
|
766
802
|
' permission to an App does NOT apply to existing installations until an account\n' +
|
|
@@ -854,14 +890,7 @@ async function createCommand(options) {
|
|
|
854
890
|
// PRINTED BEFORE THE BROWSER OPENS, not on failure: if the redirect never
|
|
855
891
|
// arrives, the App HAS still been created with no key held, and printing
|
|
856
892
|
// the recovery only then could target a terminal that is already gone.
|
|
857
|
-
console.log(
|
|
858
|
-
' The App will exist and this command will not have its private key. That is\n' +
|
|
859
|
-
' recoverable. Either:\n' +
|
|
860
|
-
' - delete the App in its settings and re-run this command, or\n' +
|
|
861
|
-
' - open the App, scroll to "Private keys", and click "Generate a private key".\n' +
|
|
862
|
-
' It downloads a .pem and that is the only chance to save it. This is NOT the\n' +
|
|
863
|
-
' "client secret" listed a few sections above: that is for OAuth user flows, is\n' +
|
|
864
|
-
' unused by this App, and fails confusingly when used to sign.\n');
|
|
893
|
+
console.log(recoveryPreamble());
|
|
865
894
|
const state = randomUUID();
|
|
866
895
|
let formDir;
|
|
867
896
|
let callback;
|
|
@@ -890,21 +919,15 @@ async function createCommand(options) {
|
|
|
890
919
|
formDir = await mkdtemp(path.join(tmpdir(), 'canopycms-app-'));
|
|
891
920
|
const formFile = path.join(formDir, `create-${slug}.html`);
|
|
892
921
|
await writeFile(formFile, creationForm(postUrl, manifest), { mode: 0o600, flag: 'wx' });
|
|
893
|
-
console.log(
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
` 127.0.0.1:${callback.port}, which only a local browser can reach. An embedded or\n` +
|
|
903
|
-
' app-internal browser bounces off the GitHub login wall and simply appears to do\n' +
|
|
904
|
-
' nothing when you click Create.\n\n' +
|
|
905
|
-
' The page posts the manifest to GitHub, which shows you the App pre-filled with\n' +
|
|
906
|
-
' exactly those permissions. Review them and click Create.\n\n' +
|
|
907
|
-
`Waiting for GitHub to redirect back (up to ${CALLBACK_TIMEOUT_MS / 60000} minutes)...`);
|
|
922
|
+
console.log(registrationMessage({
|
|
923
|
+
name,
|
|
924
|
+
owner: target.owner,
|
|
925
|
+
repo: target.repo,
|
|
926
|
+
destination: describeDestination(destination),
|
|
927
|
+
formFile,
|
|
928
|
+
callbackPort: callback.port,
|
|
929
|
+
timeoutMinutes: CALLBACK_TIMEOUT_MS / 60000,
|
|
930
|
+
}));
|
|
908
931
|
let code;
|
|
909
932
|
try {
|
|
910
933
|
code = await callback.code;
|
|
@@ -918,7 +941,7 @@ async function createCommand(options) {
|
|
|
918
941
|
' See the recovery note printed above if the App was in fact created.');
|
|
919
942
|
return 1;
|
|
920
943
|
}
|
|
921
|
-
console.log('
|
|
944
|
+
console.log(`\n${tick('Got the manifest code; exchanging it for the App credentials')}`);
|
|
922
945
|
const created = await convertManifest(code);
|
|
923
946
|
if (!created) {
|
|
924
947
|
console.error(' the exchange failed, so this command never received the private key.\n\n' +
|
|
@@ -929,8 +952,8 @@ async function createCommand(options) {
|
|
|
929
952
|
' start over.');
|
|
930
953
|
return 1;
|
|
931
954
|
}
|
|
932
|
-
console.log(`
|
|
933
|
-
` ${describeDestination(destination)}`);
|
|
955
|
+
console.log(tick(`Created App ${outColors.bold(created.slug)} (id ${created.id}); private key captured, sending it to` +
|
|
956
|
+
` ${describeDestination(destination)}`));
|
|
934
957
|
// THE KEY IS HANDED OFF FIRST, before the install prompt and the readback,
|
|
935
958
|
// because until it returns this process holds the ONLY copy of the key
|
|
936
959
|
// while the install step waits on a human indefinitely and the readback
|
|
@@ -939,17 +962,10 @@ async function createCommand(options) {
|
|
|
939
962
|
if (!stored) {
|
|
940
963
|
// No point installing an App whose key was just discarded. Say what IS
|
|
941
964
|
// still actionable — the App id, the handle for a replacement key.
|
|
942
|
-
console.error(
|
|
943
|
-
' would achieve nothing. To recover, open the App and generate a private key from\n' +
|
|
944
|
-
' its "Private keys" section, then run `verify`; or delete the App and start again.\n\n' +
|
|
945
|
-
` GITHUB_APP_ID=${created.id}\n` +
|
|
946
|
-
` App settings: https://github.com/settings/apps/${created.slug}`);
|
|
965
|
+
console.error(keyNotStoredMessage(target, created));
|
|
947
966
|
return 1;
|
|
948
967
|
}
|
|
949
|
-
console.log(
|
|
950
|
-
` https://github.com/settings/apps/${created.slug}/installations\n\n` +
|
|
951
|
-
' That page shows an Install button first — the repository picker only appears after\n' +
|
|
952
|
-
' you click it. Choose "Only select repositories" and pick that one repository.\n');
|
|
968
|
+
console.log(installStepMessage(target, created.slug));
|
|
953
969
|
await pressEnter(' Press Enter once it is installed.');
|
|
954
970
|
// Wrapped so a rejection here cannot change what was already reported about
|
|
955
971
|
// the key: by this point it is stored (or deliberately not), and a
|
|
@@ -963,15 +979,7 @@ async function createCommand(options) {
|
|
|
963
979
|
' This says nothing about the App or the key — re-run `canopycms init-github-app verify`.');
|
|
964
980
|
readback = { ok: false, installationId: null };
|
|
965
981
|
}
|
|
966
|
-
console.log(
|
|
967
|
-
` GITHUB_APP_ID=${created.id}\n` +
|
|
968
|
-
` GITHUB_APP_INSTALLATION_ID=${readback.installationId ?? '<run `verify` to read it>'}\n` +
|
|
969
|
-
' GITHUB_APP_PRIVATE_KEY_SECRET_ARN=<wherever you just put the key>\n\n' +
|
|
970
|
-
' Both ids are numeric. The App ID is NOT the "Client ID" (`Iv1.…`) shown beside it,\n' +
|
|
971
|
-
' and the installation id is NOT the App ID — an App installed twice has one App ID\n' +
|
|
972
|
-
' and two installation ids.\n\n' +
|
|
973
|
-
'Re-check this any time, without creating anything:\n' +
|
|
974
|
-
` canopycms init-github-app verify --app-id ${created.id} --key-file <path>`);
|
|
982
|
+
console.log(deploymentSettingsMessage(created.id, readback.installationId));
|
|
975
983
|
return stored && readback.ok ? 0 : 1;
|
|
976
984
|
}
|
|
977
985
|
finally {
|
package/dist/cli/init.js
CHANGED
|
@@ -391,7 +391,7 @@ var init_sanitize_href = __esm({
|
|
|
391
391
|
|
|
392
392
|
// dist/config/schemas/url.js
|
|
393
393
|
import { z as z3 } from "zod";
|
|
394
|
-
var uploadTargetUrlSchema, previewPrefixSchema;
|
|
394
|
+
var uploadTargetUrlSchema, previewPrefixSchema, editorUrlSchema;
|
|
395
395
|
var init_url = __esm({
|
|
396
396
|
"dist/config/schemas/url.js"() {
|
|
397
397
|
"use strict";
|
|
@@ -402,6 +402,14 @@ var init_url = __esm({
|
|
|
402
402
|
previewPrefixSchema = z3.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value), {
|
|
403
403
|
message: 'editor.previewPrefix must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment'
|
|
404
404
|
});
|
|
405
|
+
editorUrlSchema = z3.string().trim().refine((value) => {
|
|
406
|
+
if (!isHttpUrlOrSameOriginPath(value) || !/^https?:\/\//i.test(value))
|
|
407
|
+
return false;
|
|
408
|
+
const url = new URL(value);
|
|
409
|
+
return !url.username && !url.password;
|
|
410
|
+
}, {
|
|
411
|
+
message: "editorUrl must be an absolute http(s) URL with no credentials, query or fragment"
|
|
412
|
+
});
|
|
405
413
|
}
|
|
406
414
|
});
|
|
407
415
|
|
|
@@ -552,6 +560,7 @@ var init_config = __esm({
|
|
|
552
560
|
contentRoot: contentRootSchema.default("content"),
|
|
553
561
|
sourceRoot: sourceRootSchema.optional(),
|
|
554
562
|
basePath: basePathSchema,
|
|
563
|
+
editorUrl: editorUrlSchema.optional(),
|
|
555
564
|
unauthenticatedStatus: z5.union([z5.literal(401), z5.literal(419)]).optional(),
|
|
556
565
|
danglingReferences: z5.enum(["error", "warn"]).optional(),
|
|
557
566
|
editor: editorConfigSchema.optional(),
|
|
@@ -657,9 +666,13 @@ async function canopyContext(options) {
|
|
|
657
666
|
"// authenticated request instead of silently falling back to unauthenticated dev auth.",
|
|
658
667
|
"// CLERK_SECRET_KEY is read only where Clerk's backend API is called: the auth-cache",
|
|
659
668
|
"// refresh (the worker in prod, lazily on the dev server in dev).",
|
|
669
|
+
"// useOrganizationsAsGroups needs Organizations enabled on the Clerk instance and offers",
|
|
670
|
+
"// every organization in the instance as a group. In an AWS deployment the worker reads",
|
|
671
|
+
"// the setting from `clerkOrganizationsAsGroups` in the CDK stack, which governs there;",
|
|
672
|
+
"// keep the two equal.",
|
|
660
673
|
"const authPlugin =",
|
|
661
674
|
" config.server.mode === 'prod' || process.env.CANOPY_AUTH_MODE === 'clerk'",
|
|
662
|
-
" ? createClerkAuthPlugin({ useOrganizationsAsGroups:
|
|
675
|
+
" ? createClerkAuthPlugin({ useOrganizationsAsGroups: false })",
|
|
663
676
|
" : createDevAuthPlugin()"
|
|
664
677
|
].join("\n") : "const authPlugin = createDevAuthPlugin()";
|
|
665
678
|
return template.replace("{{AUTH_IMPORTS}}", authImports).replace("{{AUTH_PLUGIN_SETUP}}", authPluginSetup).replace("{{CONFIG_IMPORT}}", options.configImport);
|
|
@@ -172,6 +172,9 @@ export class CmsStack extends Stack {
|
|
|
172
172
|
].filter((arn): arn is string => arn !== undefined),
|
|
173
173
|
githubTokenSecretArn: githubToken?.secretArn,
|
|
174
174
|
clerkSecretKeySecretArn: clerkSecretKey.secretArn,
|
|
175
|
+
// Whether the worker maps Clerk Organizations onto Canopy groups. It needs Organizations
|
|
176
|
+
// enabled on the Clerk instance; keep it equal to useOrganizationsAsGroups in canopy.ts.
|
|
177
|
+
clerkOrganizationsAsGroups: false,
|
|
175
178
|
// GitHub App auth, when configured instead of the token above. Undefined
|
|
176
179
|
// unless the adopter set all three; CanopyCmsService refuses a partial
|
|
177
180
|
// set, and refuses an App alongside githubTokenSecretArn.
|
package/dist/cli/templates.js
CHANGED
|
@@ -41,9 +41,13 @@ export async function canopyContext(options) {
|
|
|
41
41
|
'// authenticated request instead of silently falling back to unauthenticated dev auth.',
|
|
42
42
|
"// CLERK_SECRET_KEY is read only where Clerk's backend API is called: the auth-cache",
|
|
43
43
|
'// refresh (the worker in prod, lazily on the dev server in dev).',
|
|
44
|
+
'// useOrganizationsAsGroups needs Organizations enabled on the Clerk instance and offers',
|
|
45
|
+
'// every organization in the instance as a group. In an AWS deployment the worker reads',
|
|
46
|
+
'// the setting from `clerkOrganizationsAsGroups` in the CDK stack, which governs there;',
|
|
47
|
+
'// keep the two equal.',
|
|
44
48
|
'const authPlugin =',
|
|
45
49
|
" config.server.mode === 'prod' || process.env.CANOPY_AUTH_MODE === 'clerk'",
|
|
46
|
-
' ? createClerkAuthPlugin({ useOrganizationsAsGroups:
|
|
50
|
+
' ? createClerkAuthPlugin({ useOrganizationsAsGroups: false })',
|
|
47
51
|
' : createDevAuthPlugin()',
|
|
48
52
|
].join('\n')
|
|
49
53
|
: 'const authPlugin = createDevAuthPlugin()';
|
package/dist/comment-store.d.ts
CHANGED
|
@@ -60,6 +60,11 @@ export declare class CommentStore {
|
|
|
60
60
|
constructor(branchRoot: string, options?: {
|
|
61
61
|
settleMs?: number;
|
|
62
62
|
});
|
|
63
|
+
/**
|
|
64
|
+
* Every thread, for a summary that must not mistake a failed read for no comments: only a
|
|
65
|
+
* missing file reads as none, and an unreadable or malformed one throws, unlike load().
|
|
66
|
+
*/
|
|
67
|
+
listThreadsStrict(): Promise<CommentThread[]>;
|
|
63
68
|
/** Read-only load. */
|
|
64
69
|
load(): Promise<CommentsFile>;
|
|
65
70
|
/**
|
package/dist/comment-store.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { withLock } from './utils/async-mutex.js';
|
|
5
|
+
import { isNotFoundError } from './utils/error.js';
|
|
5
6
|
import { writeOccJsonFile, withOccRetry, withOccFileLock, OccWriteConflictError, } from './utils/occ-json-write.js';
|
|
6
7
|
/** A concurrent modification that survived every OCC retry. */
|
|
7
8
|
class CommentStoreConflictError extends Error {
|
|
@@ -45,6 +46,29 @@ export class CommentStore {
|
|
|
45
46
|
this.filePath = path.join(path.resolve(branchRoot), '.canopy-meta', 'comments.json');
|
|
46
47
|
this.settleMs = options?.settleMs;
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Every thread, for a summary that must not mistake a failed read for no comments: only a
|
|
51
|
+
* missing file reads as none, and an unreadable or malformed one throws, unlike load().
|
|
52
|
+
*/
|
|
53
|
+
async listThreadsStrict() {
|
|
54
|
+
let content;
|
|
55
|
+
try {
|
|
56
|
+
content = await fs.readFile(this.filePath, 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (isNotFoundError(err))
|
|
60
|
+
return [];
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
const parsed = JSON.parse(content);
|
|
64
|
+
const threads = typeof parsed === 'object' && parsed !== null
|
|
65
|
+
? parsed.threads
|
|
66
|
+
: undefined;
|
|
67
|
+
if (typeof threads !== 'object' || threads === null) {
|
|
68
|
+
throw new Error('comments.json has no threads object');
|
|
69
|
+
}
|
|
70
|
+
return Object.values(threads);
|
|
71
|
+
}
|
|
48
72
|
/** Read-only load. */
|
|
49
73
|
async load() {
|
|
50
74
|
const { data } = await this.loadWithVersion();
|
package/dist/config/helpers.js
CHANGED
|
@@ -33,11 +33,9 @@ export function defineCanopyConfig(config) {
|
|
|
33
33
|
server: validated,
|
|
34
34
|
// Client config helper - extracts safe subset and merges overrides
|
|
35
35
|
client: (clientOverrides) => {
|
|
36
|
-
const {
|
|
36
|
+
const { contentRoot, editor, mode, entryLinkUrl, basePath, mdxAllow } = validated;
|
|
37
37
|
// flatSchema is loaded dynamically by the editor via API (from .collection.json files)
|
|
38
38
|
const clientConfig = {
|
|
39
|
-
defaultBaseBranch,
|
|
40
|
-
defaultActiveBranch,
|
|
41
39
|
contentRoot,
|
|
42
40
|
editor,
|
|
43
41
|
mode,
|
|
@@ -13,7 +13,7 @@ export declare const entryTypeSchema: z.ZodObject<{
|
|
|
13
13
|
maxItems: z.ZodOptional<z.ZodNumber>;
|
|
14
14
|
}, "strip", z.ZodTypeAny, {
|
|
15
15
|
name: string;
|
|
16
|
-
format: "
|
|
16
|
+
format: "md" | "mdx" | "json" | "yaml";
|
|
17
17
|
schema: any[];
|
|
18
18
|
description?: string | undefined;
|
|
19
19
|
default?: boolean | undefined;
|
|
@@ -21,7 +21,7 @@ export declare const entryTypeSchema: z.ZodObject<{
|
|
|
21
21
|
maxItems?: number | undefined;
|
|
22
22
|
}, {
|
|
23
23
|
name: string;
|
|
24
|
-
format: "
|
|
24
|
+
format: "md" | "mdx" | "json" | "yaml";
|
|
25
25
|
schema: any[];
|
|
26
26
|
description?: string | undefined;
|
|
27
27
|
default?: boolean | undefined;
|
|
@@ -71,6 +71,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
71
71
|
contentRoot: z.ZodDefault<z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, string, string>>>;
|
|
72
72
|
sourceRoot: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
73
73
|
basePath: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
74
|
+
editorUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
74
75
|
unauthenticatedStatus: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<401>, z.ZodLiteral<419>]>>;
|
|
75
76
|
danglingReferences: z.ZodOptional<z.ZodEnum<["error", "warn"]>>;
|
|
76
77
|
editor: z.ZodOptional<z.ZodObject<{
|
|
@@ -201,6 +202,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
201
202
|
deploymentName?: string | undefined;
|
|
202
203
|
sourceRoot?: string | undefined;
|
|
203
204
|
basePath?: string | undefined;
|
|
205
|
+
editorUrl?: string | undefined;
|
|
204
206
|
unauthenticatedStatus?: 401 | 419 | undefined;
|
|
205
207
|
danglingReferences?: "error" | "warn" | undefined;
|
|
206
208
|
editor?: {
|
|
@@ -268,6 +270,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
268
270
|
contentRoot?: string | undefined;
|
|
269
271
|
sourceRoot?: string | undefined;
|
|
270
272
|
basePath?: string | undefined;
|
|
273
|
+
editorUrl?: string | undefined;
|
|
271
274
|
unauthenticatedStatus?: 401 | 419 | undefined;
|
|
272
275
|
danglingReferences?: "error" | "warn" | undefined;
|
|
273
276
|
editor?: {
|
|
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
import { relativePathSchema } from './collection.js';
|
|
6
6
|
import { mdxAllowlistSchema } from './field.js';
|
|
7
7
|
import { mediaSchema } from './media.js';
|
|
8
|
-
import { previewPrefixSchema } from './url.js';
|
|
8
|
+
import { editorUrlSchema, previewPrefixSchema } from './url.js';
|
|
9
9
|
const defaultBranchAccessSchema = z.enum(['allow', 'deny']).default('deny');
|
|
10
10
|
const defaultPathAccessLevelSchema = z.enum(['allow', 'deny']);
|
|
11
11
|
// Per-level object form: an omitted level stays undefined after parse (no per-field
|
|
@@ -123,6 +123,7 @@ export const CanopyConfigSchema = z
|
|
|
123
123
|
contentRoot: contentRootSchema.default('content'),
|
|
124
124
|
sourceRoot: sourceRootSchema.optional(),
|
|
125
125
|
basePath: basePathSchema,
|
|
126
|
+
editorUrl: editorUrlSchema.optional(),
|
|
126
127
|
unauthenticatedStatus: z.union([z.literal(401), z.literal(419)]).optional(),
|
|
127
128
|
danglingReferences: z.enum(['error', 'warn']).optional(),
|
|
128
129
|
editor: editorConfigSchema.optional(),
|
|
@@ -19,3 +19,9 @@ import { z } from 'zod';
|
|
|
19
19
|
export declare const uploadTargetUrlSchema: z.ZodEffects<z.ZodString, string, string>;
|
|
20
20
|
/** `editor.previewPrefix`, by `uploadTargetUrlSchema`'s rule: `//host` leaves the scheme open. */
|
|
21
21
|
export declare const previewPrefixSchema: z.ZodEffects<z.ZodString, string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* `editorUrl`: the absolute http(s) URL of the page that mounts the Editor, by
|
|
24
|
+
* `uploadTargetUrlSchema`'s rule plus a scheme and no credentials, since it is written into
|
|
25
|
+
* pull request bodies as a link.
|
|
26
|
+
*/
|
|
27
|
+
export declare const editorUrlSchema: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -30,3 +30,19 @@ export const previewPrefixSchema = z
|
|
|
30
30
|
.refine((value) => isHttpUrlOrSameOriginPath(value), {
|
|
31
31
|
message: 'editor.previewPrefix must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment',
|
|
32
32
|
});
|
|
33
|
+
/**
|
|
34
|
+
* `editorUrl`: the absolute http(s) URL of the page that mounts the Editor, by
|
|
35
|
+
* `uploadTargetUrlSchema`'s rule plus a scheme and no credentials, since it is written into
|
|
36
|
+
* pull request bodies as a link.
|
|
37
|
+
*/
|
|
38
|
+
export const editorUrlSchema = z
|
|
39
|
+
.string()
|
|
40
|
+
.trim()
|
|
41
|
+
.refine((value) => {
|
|
42
|
+
if (!isHttpUrlOrSameOriginPath(value) || !/^https?:\/\//i.test(value))
|
|
43
|
+
return false;
|
|
44
|
+
const url = new URL(value);
|
|
45
|
+
return !url.username && !url.password;
|
|
46
|
+
}, {
|
|
47
|
+
message: 'editorUrl must be an absolute http(s) URL with no credentials, query or fragment',
|
|
48
|
+
});
|
package/dist/config/types.d.ts
CHANGED
|
@@ -478,6 +478,8 @@ export interface CanopyConfig {
|
|
|
478
478
|
sourceRoot?: SourceRoot;
|
|
479
479
|
/** Deployment prefix the host app is served under — see {@link CanopyConfigInput.basePath}. */
|
|
480
480
|
basePath?: string;
|
|
481
|
+
/** The editor page's public URL — see {@link CanopyConfigInput.editorUrl}. */
|
|
482
|
+
editorUrl?: string;
|
|
481
483
|
/** HTTP status of an unauthenticated API response — see {@link CanopyConfigInput.unauthenticatedStatus}. */
|
|
482
484
|
unauthenticatedStatus?: 401 | 419;
|
|
483
485
|
/** What a production build does on a dangling reference — see {@link CanopyConfigInput.danglingReferences}. */
|
|
@@ -554,6 +556,13 @@ export interface CanopyConfigInput {
|
|
|
554
556
|
* silently filter out every entry (zero static params, a build that goes green with no pages).
|
|
555
557
|
*/
|
|
556
558
|
basePath?: string;
|
|
559
|
+
/**
|
|
560
|
+
* The absolute URL of the page that mounts the Editor (e.g. `https://cms.example.org/edit`),
|
|
561
|
+
* for the editor links in pull request bodies. Unset, the URL the editor reports on submit is
|
|
562
|
+
* used when its host is the one the request was addressed to (`X-Forwarded-Host`, else `Host`);
|
|
563
|
+
* set this when a proxy in front of the CMS sets neither to the editor's public host.
|
|
564
|
+
*/
|
|
565
|
+
editorUrl?: string;
|
|
557
566
|
/**
|
|
558
567
|
* HTTP status of an API response to a request with no accepted session. Default 401.
|
|
559
568
|
*
|
|
@@ -634,8 +643,10 @@ export type FlatSchemaItem = {
|
|
|
634
643
|
* Client config - subset safe for browser (DRY - derived from CanopyConfig)
|
|
635
644
|
* Use flatSchema for O(1) cached access to the flattened schema structure.
|
|
636
645
|
* Schema is loaded from .collection.json files on the server and provided as flatSchema.
|
|
646
|
+
* It names no branch: the config evaluated in the browser lacks the server's environment, so an
|
|
647
|
+
* editor no `?branch=` or `branchName` pins adopts the branches API's `defaultBranch`.
|
|
637
648
|
*/
|
|
638
|
-
export type CanopyClientConfig = Pick<CanopyConfig, '
|
|
649
|
+
export type CanopyClientConfig = Pick<CanopyConfig, 'contentRoot' | 'editor' | 'mode' | 'entryLinkUrl' | 'basePath' | 'mdxAllow'> & {
|
|
639
650
|
flatSchema: FlatSchemaItem[];
|
|
640
651
|
};
|
|
641
652
|
export interface ClientOnlyFields {
|