canopycms-cdk 0.0.67 → 0.0.68-int.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/constructs/cms-distribution.d.ts +5 -4
- package/dist/constructs/cms-distribution.js +14 -61
- package/dist/constructs/cms-service.d.ts +23 -3
- package/dist/constructs/cms-service.js +24 -2
- package/dist/constructs/editor-routing.d.ts +131 -0
- package/dist/constructs/editor-routing.js +313 -0
- package/dist/index.d.ts +1 -0
- package/lambda/asset-transform/dist/handler.js +59 -29
- package/package.json +4 -4
- package/worker/dist/index.js +793 -442
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
import { Annotations, Duration, Stack, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, } from 'aws-cdk-lib';
|
|
3
|
+
import { ASSETS_PATH_PATTERN, ASSETS_TRANSFORM_PATH_PATTERN } from './asset-support.js';
|
|
4
|
+
/** CloudFront's maximum origin read timeout without a service-quota increase. */
|
|
5
|
+
export const MAX_CLOUDFRONT_ORIGIN_READ_TIMEOUT = Duration.seconds(60);
|
|
6
|
+
/**
|
|
7
|
+
* The routes the editor needs from the CMS Lambda on a distribution it shares
|
|
8
|
+
* with a site: the editor page and the catch-all API route `canopycms init`
|
|
9
|
+
* scaffolds. `/edit` and `/edit/*` rather than `/edit*`, which also matches
|
|
10
|
+
* `/editorial` and `/edit-assets/…` and so takes site pages away from the site.
|
|
11
|
+
*
|
|
12
|
+
* Not included, because on a shared distribution they belong to the site:
|
|
13
|
+
* `/_next/static/*` (the CMS build moves its chunks out of it with Next's
|
|
14
|
+
* `assetPrefix`; see `editorAssetPrefix`) and `/assets/*`
|
|
15
|
+
* (`AssetSupport.attachTo` owns it).
|
|
16
|
+
*/
|
|
17
|
+
export const EDITOR_PATH_PATTERNS = ['/edit', '/edit/*', '/api/canopycms/*'];
|
|
18
|
+
/**
|
|
19
|
+
* One request path per editor pattern that only that pattern serves. A
|
|
20
|
+
* behavior listed earlier that matches any of these takes the request first.
|
|
21
|
+
*/
|
|
22
|
+
const EDITOR_PROBE_PATHS = {
|
|
23
|
+
'/edit': ['/edit'],
|
|
24
|
+
'/edit/*': ['/edit/', '/edit/x'],
|
|
25
|
+
'/api/canopycms/*': ['/api/canopycms/x'],
|
|
26
|
+
};
|
|
27
|
+
const EDITOR_ROUTES = EDITOR_PATH_PATTERNS.map((pattern) => ({
|
|
28
|
+
pattern,
|
|
29
|
+
probes: EDITOR_PROBE_PATHS[pattern],
|
|
30
|
+
}));
|
|
31
|
+
/** Patterns a prefix option must not overlap in either direction. */
|
|
32
|
+
const RESERVED_ROUTES = [
|
|
33
|
+
...EDITOR_ROUTES,
|
|
34
|
+
{ pattern: '/_next/*', probes: ['/_next/x'] },
|
|
35
|
+
{ pattern: ASSETS_TRANSFORM_PATH_PATTERN, probes: ['/assets/t/x'] },
|
|
36
|
+
{ pattern: ASSETS_PATH_PATTERN, probes: ['/assets/x'] },
|
|
37
|
+
];
|
|
38
|
+
/** Id of the marker construct `attachEditorBehaviors` adds to the distribution. */
|
|
39
|
+
const EDITOR_ATTACHED_MARKER_ID = 'CanopyEditorBehaviorsAttached';
|
|
40
|
+
/**
|
|
41
|
+
* Whether a CloudFront path pattern matches a request path: `*` is any run of
|
|
42
|
+
* characters, `?` exactly one, matching is case-sensitive, and the pattern's
|
|
43
|
+
* leading `/` is optional.
|
|
44
|
+
*/
|
|
45
|
+
export function cloudFrontPathPatternMatches(pattern, path) {
|
|
46
|
+
const glob = pattern.startsWith('/') ? pattern : `/${pattern}`;
|
|
47
|
+
// Greedy wildcard match: on a mismatch, let the most recent `*` absorb one
|
|
48
|
+
// more character and retry from there.
|
|
49
|
+
let g = 0;
|
|
50
|
+
let p = 0;
|
|
51
|
+
let star = -1;
|
|
52
|
+
let resume = 0;
|
|
53
|
+
while (p < path.length) {
|
|
54
|
+
if (g < glob.length && (glob[g] === '?' || glob[g] === path[p])) {
|
|
55
|
+
g++;
|
|
56
|
+
p++;
|
|
57
|
+
}
|
|
58
|
+
else if (g < glob.length && glob[g] === '*') {
|
|
59
|
+
star = g++;
|
|
60
|
+
resume = p;
|
|
61
|
+
}
|
|
62
|
+
else if (star !== -1) {
|
|
63
|
+
g = star + 1;
|
|
64
|
+
p = ++resume;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
while (glob[g] === '*')
|
|
71
|
+
g++;
|
|
72
|
+
return g === glob.length;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Throws when the origin read timeout exceeds the 60s CloudFront accepts
|
|
76
|
+
* without a quota increase, which it would reject at deploy. Neither construct
|
|
77
|
+
* takes a higher, quota-raised value.
|
|
78
|
+
*/
|
|
79
|
+
export function assertOriginReadTimeout(readTimeout, owner) {
|
|
80
|
+
if (readTimeout.toSeconds() > MAX_CLOUDFRONT_ORIGIN_READ_TIMEOUT.toSeconds()) {
|
|
81
|
+
throw new Error(`${owner}: originReadTimeout is ${readTimeout.toSeconds()}s, but CloudFront ` +
|
|
82
|
+
`allows at most ${MAX_CLOUDFRONT_ORIGIN_READ_TIMEOUT.toSeconds()}s without a service-quota ` +
|
|
83
|
+
`increase, and these constructs do not take a higher value. Lower the CMS Lambda's ` +
|
|
84
|
+
`timeout to ${MAX_CLOUDFRONT_ORIGIN_READ_TIMEOUT.toSeconds()}s or less. A shorter origin ` +
|
|
85
|
+
`timeout than the Lambda's would 504 at the edge on requests that actually succeed.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The viewer-request function every Lambda behavior needs. CloudFront gives
|
|
90
|
+
* the origin the Function URL's own Host (forwarding the viewer Host would
|
|
91
|
+
* break the OAC SigV4 signature), and Lambda Web Adapter adds no
|
|
92
|
+
* `x-forwarded-*` headers, so without this Clerk/Next derive sign-in redirect
|
|
93
|
+
* URLs from the IAM-authed Function URL host and the redirect 403s.
|
|
94
|
+
*
|
|
95
|
+
* Only `x-forwarded-host`: `x-forwarded-proto` is on CloudFront Functions'
|
|
96
|
+
* disallowed list and setting it fails every request with a 502. The viewer
|
|
97
|
+
* side is HTTPS-only, so proto is unambiguous anyway.
|
|
98
|
+
*/
|
|
99
|
+
export function createForwardedHostFunction(scope, id) {
|
|
100
|
+
return new cloudfront.Function(scope, id, {
|
|
101
|
+
code: cloudfront.FunctionCode.fromInline([
|
|
102
|
+
'function handler(event) {',
|
|
103
|
+
' var request = event.request;',
|
|
104
|
+
" request.headers['x-forwarded-host'] = { value: request.headers.host.value };",
|
|
105
|
+
' return request;',
|
|
106
|
+
'}',
|
|
107
|
+
].join('\n')),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Response headers for everything the CMS Lambda serves.
|
|
112
|
+
*
|
|
113
|
+
* - `frame-ancestors 'self'` (plus `X-Frame-Options: SAMEORIGIN` for browsers
|
|
114
|
+
* that ignore it) stops other sites framing the editor. `'self'` keeps the
|
|
115
|
+
* editor's own preview iframe working, which frames same-origin site pages.
|
|
116
|
+
* - Both framing headers have `override: false`, so an origin that sends its
|
|
117
|
+
* own CSP keeps it whole; when that CSP has no `frame-ancestors` and the
|
|
118
|
+
* origin sends no `X-Frame-Options`, this one applies.
|
|
119
|
+
* - `X-Robots-Tag: noindex` overrides: nothing the Lambda serves is meant to
|
|
120
|
+
* be indexed.
|
|
121
|
+
* - No `Cross-Origin-Opener-Policy`: it can cut the `window.opener` link a
|
|
122
|
+
* popup-based OAuth sign-in reports back through.
|
|
123
|
+
*/
|
|
124
|
+
export function createEditorResponseHeadersPolicy(scope, id) {
|
|
125
|
+
return new cloudfront.ResponseHeadersPolicy(scope, id, {
|
|
126
|
+
securityHeadersBehavior: {
|
|
127
|
+
contentSecurityPolicy: { contentSecurityPolicy: "frame-ancestors 'self'", override: false },
|
|
128
|
+
frameOptions: { frameOption: cloudfront.HeadersFrameOption.SAMEORIGIN, override: false },
|
|
129
|
+
contentTypeOptions: { override: true },
|
|
130
|
+
strictTransportSecurity: {
|
|
131
|
+
accessControlMaxAge: Duration.days(365),
|
|
132
|
+
includeSubdomains: false,
|
|
133
|
+
override: false,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
customHeadersBehavior: {
|
|
137
|
+
customHeaders: [{ header: 'X-Robots-Tag', value: 'noindex', override: true }],
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A year-long cache for content-hashed build output. The cache key carries no
|
|
143
|
+
* header, cookie or query string, so every viewer shares one copy.
|
|
144
|
+
*/
|
|
145
|
+
export function createStaticCachePolicy(scope, id) {
|
|
146
|
+
return new cloudfront.CachePolicy(scope, id, {
|
|
147
|
+
defaultTtl: Duration.days(365),
|
|
148
|
+
maxTtl: Duration.days(365),
|
|
149
|
+
minTtl: Duration.days(365),
|
|
150
|
+
headerBehavior: cloudfront.CacheHeaderBehavior.none(),
|
|
151
|
+
queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
|
|
152
|
+
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Behavior options for a route the CMS Lambda serves. The managed
|
|
157
|
+
* CACHING_DISABLED policy as it is: CloudFront rejects any non-none cache-key
|
|
158
|
+
* setting on a caching-disabled policy, and the origin still gets the whole
|
|
159
|
+
* viewer request (headers, cookies, query string, minus Host, which would
|
|
160
|
+
* break the OAC signature) through ALL_VIEWER_EXCEPT_HOST_HEADER.
|
|
161
|
+
*/
|
|
162
|
+
export function lambdaBehaviorOptions(viewerRequestFunction, responseHeadersPolicy) {
|
|
163
|
+
return {
|
|
164
|
+
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
|
|
165
|
+
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
|
|
166
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
167
|
+
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
|
168
|
+
responseHeadersPolicy,
|
|
169
|
+
functionAssociations: viewerRequestFunction && [
|
|
170
|
+
{ function: viewerRequestFunction, eventType: cloudfront.FunctionEventType.VIEWER_REQUEST },
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The routes a prefix option adds, after refusing a prefix that is not a
|
|
176
|
+
* plain path below the root or that overlaps `reserved` in either direction.
|
|
177
|
+
*/
|
|
178
|
+
function prefixRoutes(option, prefix, reserved) {
|
|
179
|
+
const fail = (reason) => new Error(`CanopyCmsService.attachTo: ${option} '${prefix}' ${reason}.`);
|
|
180
|
+
if (!prefix.startsWith('/') || prefix === '/') {
|
|
181
|
+
throw fail("must start with '/' and name a path below the root");
|
|
182
|
+
}
|
|
183
|
+
if (prefix.endsWith('/'))
|
|
184
|
+
throw fail("must not end with a trailing '/'");
|
|
185
|
+
// CloudFront collapses `//` in request paths, so a pattern holding one never matches.
|
|
186
|
+
if (prefix.includes('//'))
|
|
187
|
+
throw fail("must not contain '//'");
|
|
188
|
+
if (/[*?]/.test(prefix))
|
|
189
|
+
throw fail('must not contain * or ?, which CloudFront reads as wildcards');
|
|
190
|
+
if (!/^[A-Za-z0-9_\-.$/~"'@:+&]+$/.test(prefix)) {
|
|
191
|
+
throw fail(`may hold only the characters CloudFront allows in a path pattern: A-Z a-z 0-9 _-.$/~"'@:+&`);
|
|
192
|
+
}
|
|
193
|
+
const routes = option === 'previewPrefix'
|
|
194
|
+
? [
|
|
195
|
+
{ pattern: prefix, probes: [prefix] },
|
|
196
|
+
{ pattern: `${prefix}/*`, probes: [`${prefix}/`, `${prefix}/x`] },
|
|
197
|
+
]
|
|
198
|
+
: [{ pattern: `${prefix}/*`, probes: [`${prefix}/x`] }];
|
|
199
|
+
const overlaps = (a, b) => b.probes.some((path) => cloudFrontPathPatternMatches(a.pattern, path));
|
|
200
|
+
const clash = reserved.find((route) => routes.some((own) => overlaps(route, own) || overlaps(own, route)));
|
|
201
|
+
if (clash) {
|
|
202
|
+
throw fail(`overlaps '${clash.pattern}', so CloudFront would send one route's requests to the ` +
|
|
203
|
+
`other's behavior. Pick a prefix of its own, such as '/edit-assets' or '/preview'`);
|
|
204
|
+
}
|
|
205
|
+
return routes;
|
|
206
|
+
}
|
|
207
|
+
function isRecord(value) {
|
|
208
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
209
|
+
}
|
|
210
|
+
/** One list from a distribution's L1 config, resolved but still camelCased. */
|
|
211
|
+
function resolvedConfigList(distribution, field) {
|
|
212
|
+
const cfn = distribution.node.defaultChild;
|
|
213
|
+
if (!(cfn instanceof cloudfront.CfnDistribution))
|
|
214
|
+
return [];
|
|
215
|
+
const config = cfn.distributionConfig;
|
|
216
|
+
if (!isRecord(config))
|
|
217
|
+
return [];
|
|
218
|
+
const list = Stack.of(distribution).resolve(config[field]);
|
|
219
|
+
return Array.isArray(list) ? list.filter(isRecord) : [];
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Error messages for every behavior that CloudFront would match before one of
|
|
223
|
+
* the editor's, which leaves that editor route unreachable.
|
|
224
|
+
*/
|
|
225
|
+
function shadowedEditorRoutes(distribution, routes) {
|
|
226
|
+
const patterns = resolvedConfigList(distribution, 'cacheBehaviors')
|
|
227
|
+
.map((behavior) => behavior.pathPattern)
|
|
228
|
+
.filter((pattern) => typeof pattern === 'string');
|
|
229
|
+
const errors = [];
|
|
230
|
+
for (const { pattern: editorPattern, probes } of routes) {
|
|
231
|
+
// The last occurrence, so a duplicate of an editor pattern on either side
|
|
232
|
+
// of it is reported too.
|
|
233
|
+
const index = patterns.lastIndexOf(editorPattern);
|
|
234
|
+
const shadow = patterns
|
|
235
|
+
.slice(0, index)
|
|
236
|
+
.find((earlier) => probes.some((path) => cloudFrontPathPatternMatches(earlier, path)));
|
|
237
|
+
if (index !== -1 && shadow !== undefined) {
|
|
238
|
+
errors.push(`CanopyCmsService.attachTo: the distribution's '${shadow}' behavior is listed before ` +
|
|
239
|
+
`the editor's '${editorPattern}' and matches the same requests, so CloudFront never ` +
|
|
240
|
+
`reaches the editor behavior. Remove hand-wired editor behaviors (such as '/edit*') ` +
|
|
241
|
+
`and let attachTo add them.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return errors;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The status codes of custom error responses that replace the origin's
|
|
248
|
+
* response. A TTL-only entry changes caching, not the response.
|
|
249
|
+
*/
|
|
250
|
+
function rewrittenErrorCodes(distribution) {
|
|
251
|
+
return resolvedConfigList(distribution, 'customErrorResponses')
|
|
252
|
+
.filter((entry) => entry.responseCode !== undefined || entry.responsePagePath !== undefined)
|
|
253
|
+
.map((entry) => entry.errorCode)
|
|
254
|
+
.filter((code) => typeof code === 'number');
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Attach the editor's behaviors to a distribution. See
|
|
258
|
+
* `CanopyCmsService.attachTo`, which is the public entry point.
|
|
259
|
+
*/
|
|
260
|
+
export function attachEditorBehaviors(distribution, functionUrl, readTimeout, options = {}) {
|
|
261
|
+
if (distribution.node.tryFindChild(EDITOR_ATTACHED_MARKER_ID)) {
|
|
262
|
+
throw new Error('CanopyCmsService: attachTo() was already called for this distribution. Each editor ' +
|
|
263
|
+
'pattern would be attached twice and CloudFront rejects duplicate path patterns at ' +
|
|
264
|
+
'deploy time.');
|
|
265
|
+
}
|
|
266
|
+
if (options.viewerRequestFunction && options.behaviorOverrides?.functionAssociations) {
|
|
267
|
+
throw new Error('CanopyCmsService.attachTo: pass viewerRequestFunction or ' +
|
|
268
|
+
'behaviorOverrides.functionAssociations, not both. The overrides replace the whole ' +
|
|
269
|
+
'functionAssociations list, so the viewerRequestFunction would be silently dropped.');
|
|
270
|
+
}
|
|
271
|
+
assertOriginReadTimeout(readTimeout, 'CanopyCmsService.attachTo');
|
|
272
|
+
const assetRoutes = options.editorAssetPrefix === undefined
|
|
273
|
+
? []
|
|
274
|
+
: prefixRoutes('editorAssetPrefix', options.editorAssetPrefix, RESERVED_ROUTES);
|
|
275
|
+
const previewRoutes = options.previewPrefix === undefined
|
|
276
|
+
? []
|
|
277
|
+
: prefixRoutes('previewPrefix', options.previewPrefix, [...RESERVED_ROUTES, ...assetRoutes]);
|
|
278
|
+
const marker = new Construct(distribution, EDITOR_ATTACHED_MARKER_ID);
|
|
279
|
+
// Scoped to the distribution so everything lands in its stack, which may
|
|
280
|
+
// not be the service's.
|
|
281
|
+
// None at all when the overrides bring their own associations, which would
|
|
282
|
+
// otherwise leave a deployed function associated with nothing.
|
|
283
|
+
const viewerRequestFunction = options.viewerRequestFunction ??
|
|
284
|
+
(options.behaviorOverrides?.functionAssociations
|
|
285
|
+
? undefined
|
|
286
|
+
: createForwardedHostFunction(distribution, 'CanopyEditorForwardedHostFunction'));
|
|
287
|
+
const responseHeadersPolicy = options.behaviorOverrides?.responseHeadersPolicy ??
|
|
288
|
+
createEditorResponseHeadersPolicy(distribution, 'CanopyEditorResponseHeadersPolicy');
|
|
289
|
+
const behavior = lambdaBehaviorOptions(viewerRequestFunction, responseHeadersPolicy);
|
|
290
|
+
const definedOverrides = Object.fromEntries(Object.entries(options.behaviorOverrides ?? {}).filter(([, value]) => value !== undefined));
|
|
291
|
+
const origin = origins.FunctionUrlOrigin.withOriginAccessControl(functionUrl, { readTimeout });
|
|
292
|
+
for (const { pattern } of [...EDITOR_ROUTES, ...previewRoutes]) {
|
|
293
|
+
distribution.addBehavior(pattern, origin, { ...behavior, ...definedOverrides });
|
|
294
|
+
}
|
|
295
|
+
for (const { pattern } of assetRoutes) {
|
|
296
|
+
distribution.addBehavior(pattern, origin, {
|
|
297
|
+
cachePolicy: createStaticCachePolicy(distribution, 'CanopyEditorAssetCachePolicy'),
|
|
298
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
299
|
+
responseHeadersPolicy,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const routes = [...EDITOR_ROUTES, ...previewRoutes, ...assetRoutes];
|
|
303
|
+
// Custom error responses are fixed at construction, so they are known now.
|
|
304
|
+
const rewritten = rewrittenErrorCodes(distribution);
|
|
305
|
+
if (rewritten.length > 0) {
|
|
306
|
+
Annotations.of(distribution).addWarningV2('canopycms:editor-custom-error-responses', `This distribution rewrites ${rewritten.join(', ')} responses, and CloudFront custom ` +
|
|
307
|
+
`error responses are distribution-wide: they also replace the editor API's JSON errors, ` +
|
|
308
|
+
`so the editor reports "Unexpected response from server" instead of the real error. ` +
|
|
309
|
+
`See "Custom error responses" in docs/deploying-to-aws.md.`);
|
|
310
|
+
}
|
|
311
|
+
// Behaviors can still be added after this call, so shadowing is checked at synth.
|
|
312
|
+
marker.node.addValidation({ validate: () => shadowedEditorRoutes(distribution, routes) });
|
|
313
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { CmsWorker } from './worker.js';
|
|
|
2
2
|
export type { CmsWorkerConfig } from './worker.js';
|
|
3
3
|
export { CanopyCmsService } from './constructs/cms-service.js';
|
|
4
4
|
export type { CanopyCmsServiceProps } from './constructs/cms-service.js';
|
|
5
|
+
export type { CanopyCmsAttachOptions } from './constructs/editor-routing.js';
|
|
5
6
|
export { CanopyCmsDistribution } from './constructs/cms-distribution.js';
|
|
6
7
|
export type { CanopyCmsDistributionProps } from './constructs/cms-distribution.js';
|
|
7
8
|
export { AssetSupport, assetUploadBehavior } from './constructs/asset-support.js';
|
|
@@ -4304,7 +4304,8 @@ var init_field = __esm({
|
|
|
4304
4304
|
});
|
|
4305
4305
|
objectFieldSchema = fieldBaseSchema.extend({
|
|
4306
4306
|
type: external_exports.literal("object"),
|
|
4307
|
-
fields: external_exports.array(external_exports.lazy(() => fieldHolder[0])).min(1)
|
|
4307
|
+
fields: external_exports.array(external_exports.lazy(() => fieldHolder[0])).min(1),
|
|
4308
|
+
itemTitleField: external_exports.string().min(1).optional()
|
|
4308
4309
|
});
|
|
4309
4310
|
inlineGroupFieldSchema = external_exports.object({
|
|
4310
4311
|
type: external_exports.literal("group"),
|
|
@@ -4418,7 +4419,7 @@ var init_sanitize_href = __esm({
|
|
|
4418
4419
|
});
|
|
4419
4420
|
|
|
4420
4421
|
// ../canopycms/src/config/schemas/url.ts
|
|
4421
|
-
var uploadTargetUrlSchema, assetMountUrlSchema;
|
|
4422
|
+
var uploadTargetUrlSchema, assetMountUrlSchema, previewPrefixSchema;
|
|
4422
4423
|
var init_url = __esm({
|
|
4423
4424
|
"../canopycms/src/config/schemas/url.ts"() {
|
|
4424
4425
|
"use strict";
|
|
@@ -4430,6 +4431,9 @@ var init_url = __esm({
|
|
|
4430
4431
|
assetMountUrlSchema = external_exports.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value, { allowProtocolRelative: true }), {
|
|
4431
4432
|
message: 'must be an absolute http(s) URL, a protocol-relative "//host" URL, or a site-relative path beginning with a single "/", with no query or fragment'
|
|
4432
4433
|
});
|
|
4434
|
+
previewPrefixSchema = external_exports.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value), {
|
|
4435
|
+
message: 'editor.previewPrefix must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment'
|
|
4436
|
+
});
|
|
4433
4437
|
}
|
|
4434
4438
|
});
|
|
4435
4439
|
|
|
@@ -4487,6 +4491,7 @@ var init_config = __esm({
|
|
|
4487
4491
|
init_zod();
|
|
4488
4492
|
init_collection();
|
|
4489
4493
|
init_media();
|
|
4494
|
+
init_url();
|
|
4490
4495
|
defaultBranchAccessSchema = external_exports.enum(["allow", "deny"]).default("deny");
|
|
4491
4496
|
defaultPathAccessLevelSchema = external_exports.enum(["allow", "deny"]);
|
|
4492
4497
|
defaultPathAccessLevelsSchema = external_exports.object({
|
|
@@ -4518,6 +4523,7 @@ var init_config = __esm({
|
|
|
4518
4523
|
subtitle: external_exports.string().optional(),
|
|
4519
4524
|
theme: external_exports.unknown().optional(),
|
|
4520
4525
|
previewBase: external_exports.record(external_exports.string()).optional(),
|
|
4526
|
+
previewPrefix: previewPrefixSchema.optional(),
|
|
4521
4527
|
// UI handler functions (runtime only, don't serialize)
|
|
4522
4528
|
onAccountClick: external_exports.function().returns(external_exports.void()).optional(),
|
|
4523
4529
|
onLogoutClick: external_exports.function().returns(external_exports.void()).optional(),
|
|
@@ -4545,6 +4551,9 @@ var init_config = __esm({
|
|
|
4545
4551
|
defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
|
|
4546
4552
|
gitBotAuthorName: gitBotAuthorNameSchema,
|
|
4547
4553
|
gitBotAuthorEmail: gitBotAuthorEmailSchema,
|
|
4554
|
+
// Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
|
|
4555
|
+
gitEditedByTrailers: external_exports.boolean().optional(),
|
|
4556
|
+
gitCoAuthoredByTrailers: external_exports.boolean().optional(),
|
|
4548
4557
|
githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
|
|
4549
4558
|
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
4550
4559
|
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
@@ -4556,7 +4565,6 @@ var init_config = __esm({
|
|
|
4556
4565
|
// Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
|
|
4557
4566
|
allowNetworkRemoteInProd: external_exports.boolean().optional(),
|
|
4558
4567
|
settingsBranch: external_exports.string().optional(),
|
|
4559
|
-
autoCreateSettingsPR: external_exports.boolean().optional(),
|
|
4560
4568
|
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
4561
4569
|
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
4562
4570
|
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
@@ -6858,7 +6866,7 @@ var require_node = __commonJS({
|
|
|
6858
6866
|
var tty = require("tty");
|
|
6859
6867
|
var util2 = require("util");
|
|
6860
6868
|
exports2.init = init;
|
|
6861
|
-
exports2.log =
|
|
6869
|
+
exports2.log = log11;
|
|
6862
6870
|
exports2.formatArgs = formatArgs;
|
|
6863
6871
|
exports2.save = save;
|
|
6864
6872
|
exports2.load = load;
|
|
@@ -6993,7 +7001,7 @@ var require_node = __commonJS({
|
|
|
6993
7001
|
}
|
|
6994
7002
|
return (/* @__PURE__ */ new Date()).toISOString() + " ";
|
|
6995
7003
|
}
|
|
6996
|
-
function
|
|
7004
|
+
function log11(...args) {
|
|
6997
7005
|
return process.stderr.write(util2.formatWithOptions(exports2.inspectOpts, ...args) + "\n");
|
|
6998
7006
|
}
|
|
6999
7007
|
function save(namespaces) {
|
|
@@ -7047,27 +7055,27 @@ var require_src2 = __commonJS({
|
|
|
7047
7055
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
7048
7056
|
var fs_1 = require("fs");
|
|
7049
7057
|
var debug_1 = __importDefault(require_src());
|
|
7050
|
-
var
|
|
7058
|
+
var log11 = debug_1.default("@kwsites/file-exists");
|
|
7051
7059
|
function check(path3, isFile, isDirectory) {
|
|
7052
|
-
|
|
7060
|
+
log11(`checking %s`, path3);
|
|
7053
7061
|
try {
|
|
7054
7062
|
const stat = fs_1.statSync(path3);
|
|
7055
7063
|
if (stat.isFile() && isFile) {
|
|
7056
|
-
|
|
7064
|
+
log11(`[OK] path represents a file`);
|
|
7057
7065
|
return true;
|
|
7058
7066
|
}
|
|
7059
7067
|
if (stat.isDirectory() && isDirectory) {
|
|
7060
|
-
|
|
7068
|
+
log11(`[OK] path represents a directory`);
|
|
7061
7069
|
return true;
|
|
7062
7070
|
}
|
|
7063
|
-
|
|
7071
|
+
log11(`[FAIL] path represents something other than a file or directory`);
|
|
7064
7072
|
return false;
|
|
7065
7073
|
} catch (e) {
|
|
7066
7074
|
if (e.code === "ENOENT") {
|
|
7067
|
-
|
|
7075
|
+
log11(`[FAIL] path is not accessible: %o`, e);
|
|
7068
7076
|
return false;
|
|
7069
7077
|
}
|
|
7070
|
-
|
|
7078
|
+
log11(`[FATAL] %o`, e);
|
|
7071
7079
|
throw e;
|
|
7072
7080
|
}
|
|
7073
7081
|
}
|
|
@@ -12661,7 +12669,7 @@ var require_merge2 = __commonJS({
|
|
|
12661
12669
|
var require_addPairToJSMap = __commonJS({
|
|
12662
12670
|
"../../node_modules/.pnpm/yaml@2.8.3/node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports2) {
|
|
12663
12671
|
"use strict";
|
|
12664
|
-
var
|
|
12672
|
+
var log11 = require_log();
|
|
12665
12673
|
var merge = require_merge2();
|
|
12666
12674
|
var stringify = require_stringify2();
|
|
12667
12675
|
var identity = require_identity();
|
|
@@ -12710,7 +12718,7 @@ var require_addPairToJSMap = __commonJS({
|
|
|
12710
12718
|
let jsonStr = JSON.stringify(strKey);
|
|
12711
12719
|
if (jsonStr.length > 40)
|
|
12712
12720
|
jsonStr = jsonStr.substring(0, 36) + '..."';
|
|
12713
|
-
|
|
12721
|
+
log11.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
|
|
12714
12722
|
ctx.mapKeyWarned = true;
|
|
12715
12723
|
}
|
|
12716
12724
|
return strKey;
|
|
@@ -18106,7 +18114,7 @@ var require_public_api = __commonJS({
|
|
|
18106
18114
|
var composer = require_composer();
|
|
18107
18115
|
var Document = require_Document();
|
|
18108
18116
|
var errors = require_errors();
|
|
18109
|
-
var
|
|
18117
|
+
var log11 = require_log();
|
|
18110
18118
|
var identity = require_identity();
|
|
18111
18119
|
var lineCounter = require_line_counter();
|
|
18112
18120
|
var parser4 = require_parser();
|
|
@@ -18158,7 +18166,7 @@ var require_public_api = __commonJS({
|
|
|
18158
18166
|
const doc = parseDocument2(src, options2);
|
|
18159
18167
|
if (!doc)
|
|
18160
18168
|
return null;
|
|
18161
|
-
doc.warnings.forEach((warning) =>
|
|
18169
|
+
doc.warnings.forEach((warning) => log11.warn(doc.options.logLevel, warning));
|
|
18162
18170
|
if (doc.errors.length > 0) {
|
|
18163
18171
|
if (doc.options.logLevel !== "silent")
|
|
18164
18172
|
throw doc.errors[0];
|
|
@@ -19195,7 +19203,7 @@ var require_dist_node5 = __commonJS({
|
|
|
19195
19203
|
}
|
|
19196
19204
|
function fetchWrapper(requestOptions) {
|
|
19197
19205
|
var _a3, _b, _c, _d;
|
|
19198
|
-
const
|
|
19206
|
+
const log11 = requestOptions.request && requestOptions.request.log ? requestOptions.request.log : console;
|
|
19199
19207
|
const parseSuccessResponseBody = ((_a3 = requestOptions.request) == null ? void 0 : _a3.parseSuccessResponseBody) !== false;
|
|
19200
19208
|
if (isPlainObject(requestOptions.body) || Array.isArray(requestOptions.body)) {
|
|
19201
19209
|
requestOptions.body = JSON.stringify(requestOptions.body);
|
|
@@ -19230,7 +19238,7 @@ var require_dist_node5 = __commonJS({
|
|
|
19230
19238
|
if ("deprecation" in headers) {
|
|
19231
19239
|
const matches = headers.link && headers.link.match(/<([^<>]+)>; rel="deprecation"/);
|
|
19232
19240
|
const deprecationLink = matches && matches.pop();
|
|
19233
|
-
|
|
19241
|
+
log11.warn(
|
|
19234
19242
|
`[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${headers.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`
|
|
19235
19243
|
);
|
|
19236
19244
|
}
|
|
@@ -24030,6 +24038,7 @@ init_validation();
|
|
|
24030
24038
|
|
|
24031
24039
|
// ../canopycms/src/paths/branch.ts
|
|
24032
24040
|
init_operating_mode();
|
|
24041
|
+
init_error();
|
|
24033
24042
|
|
|
24034
24043
|
// ../canopycms/src/branch-metadata-file.ts
|
|
24035
24044
|
init_error();
|
|
@@ -24093,6 +24102,10 @@ var DebugLogger = class {
|
|
|
24093
24102
|
throw new Error(errorMsg);
|
|
24094
24103
|
}
|
|
24095
24104
|
}
|
|
24105
|
+
/**
|
|
24106
|
+
* Keyed by label alone, so two overlapping timers with one label overwrite each other;
|
|
24107
|
+
* anything that can run concurrently uses {@link timed}.
|
|
24108
|
+
*/
|
|
24096
24109
|
time(label) {
|
|
24097
24110
|
this.timers.set(label, Date.now());
|
|
24098
24111
|
}
|
|
@@ -24107,12 +24120,16 @@ var DebugLogger = class {
|
|
|
24107
24120
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
24108
24121
|
return duration;
|
|
24109
24122
|
}
|
|
24123
|
+
/**
|
|
24124
|
+
* Log `<label> completed {durationMs}` once `fn` settles. The start time is local to the
|
|
24125
|
+
* call, so concurrent spans with the same label on one logger never overwrite each other.
|
|
24126
|
+
*/
|
|
24110
24127
|
async timed(category, label, fn) {
|
|
24111
|
-
|
|
24128
|
+
const start = Date.now();
|
|
24112
24129
|
try {
|
|
24113
24130
|
return await fn();
|
|
24114
24131
|
} finally {
|
|
24115
|
-
this.
|
|
24132
|
+
this.debug(category, `${label} completed`, { durationMs: Date.now() - start });
|
|
24116
24133
|
}
|
|
24117
24134
|
}
|
|
24118
24135
|
};
|
|
@@ -25722,11 +25739,11 @@ function taskCallback(task, response, callback = NOOP) {
|
|
|
25722
25739
|
response.then(onSuccess, onError2);
|
|
25723
25740
|
}
|
|
25724
25741
|
function addDeprecationNoticeToError(err2) {
|
|
25725
|
-
let
|
|
25742
|
+
let log11 = (name) => {
|
|
25726
25743
|
console.warn(
|
|
25727
25744
|
`simple-git deprecation notice: accessing GitResponseError.${name} should be GitResponseError.git.${name}, this will no longer be available in version 3`
|
|
25728
25745
|
);
|
|
25729
|
-
|
|
25746
|
+
log11 = NOOP;
|
|
25730
25747
|
};
|
|
25731
25748
|
return Object.create(err2, Object.getOwnPropertyNames(err2.git).reduce(descriptorReducer, {}));
|
|
25732
25749
|
function descriptorReducer(all, name) {
|
|
@@ -25737,7 +25754,7 @@ function addDeprecationNoticeToError(err2) {
|
|
|
25737
25754
|
enumerable: false,
|
|
25738
25755
|
configurable: false,
|
|
25739
25756
|
get() {
|
|
25740
|
-
|
|
25757
|
+
log11(name);
|
|
25741
25758
|
return err2.git[name];
|
|
25742
25759
|
}
|
|
25743
25760
|
};
|
|
@@ -28400,6 +28417,11 @@ init_flatten();
|
|
|
28400
28417
|
// ../canopycms/src/validation/entry-type-reference-validator.ts
|
|
28401
28418
|
init_validation2();
|
|
28402
28419
|
|
|
28420
|
+
// ../canopycms/src/utils/request-timing.ts
|
|
28421
|
+
var import_node_async_hooks = require("node:async_hooks");
|
|
28422
|
+
var storage = new import_node_async_hooks.AsyncLocalStorage();
|
|
28423
|
+
var log4 = createDebugLogger({ prefix: "CanopyCMS" });
|
|
28424
|
+
|
|
28403
28425
|
// ../canopycms/src/git-manager.ts
|
|
28404
28426
|
init_error();
|
|
28405
28427
|
|
|
@@ -28409,10 +28431,10 @@ init_error();
|
|
|
28409
28431
|
init_logger();
|
|
28410
28432
|
|
|
28411
28433
|
// ../canopycms/src/git-manager.ts
|
|
28412
|
-
var
|
|
28434
|
+
var log5 = createDebugLogger({ prefix: "GitManager" });
|
|
28413
28435
|
|
|
28414
28436
|
// ../canopycms/src/branch-workspace.ts
|
|
28415
|
-
var
|
|
28437
|
+
var log6 = createDebugLogger({ prefix: "BranchWorkspace" });
|
|
28416
28438
|
|
|
28417
28439
|
// ../canopycms/src/content-store.ts
|
|
28418
28440
|
var import_gray_matter2 = __toESM(require_gray_matter(), 1);
|
|
@@ -28523,7 +28545,7 @@ init_normalize();
|
|
|
28523
28545
|
init_error();
|
|
28524
28546
|
|
|
28525
28547
|
// ../canopycms/src/entry-link-resolver.ts
|
|
28526
|
-
var
|
|
28548
|
+
var log7 = createDebugLogger({ prefix: "EntryLinks" });
|
|
28527
28549
|
var BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
28528
28550
|
var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
|
|
28529
28551
|
|
|
@@ -30162,7 +30184,7 @@ init_operating_mode();
|
|
|
30162
30184
|
|
|
30163
30185
|
// ../canopycms/src/settings-workspace.ts
|
|
30164
30186
|
init_error();
|
|
30165
|
-
var
|
|
30187
|
+
var log8 = createDebugLogger({ prefix: "SettingsWorkspace" });
|
|
30166
30188
|
|
|
30167
30189
|
// ../canopycms/src/github-service.ts
|
|
30168
30190
|
var import_rest = __toESM(require_dist_node12(), 1);
|
|
@@ -30170,6 +30192,11 @@ var import_plugin_throttling = __toESM(require_dist_node13(), 1);
|
|
|
30170
30192
|
init_operating_mode();
|
|
30171
30193
|
init_error();
|
|
30172
30194
|
init_logger();
|
|
30195
|
+
|
|
30196
|
+
// ../canopycms/src/submission-attribution.ts
|
|
30197
|
+
init_logger();
|
|
30198
|
+
|
|
30199
|
+
// ../canopycms/src/github-service.ts
|
|
30173
30200
|
var ThrottledOctokit = import_rest.Octokit.plugin(import_plugin_throttling.throttling);
|
|
30174
30201
|
|
|
30175
30202
|
// ../canopycms/src/services.ts
|
|
@@ -30181,6 +30208,9 @@ var debugLogger = createDebugLogger({ prefix: "TaskQueue" });
|
|
|
30181
30208
|
// ../canopycms/src/task-queue/task-queue-config.ts
|
|
30182
30209
|
init_config3();
|
|
30183
30210
|
|
|
30211
|
+
// ../canopycms/src/services.ts
|
|
30212
|
+
init_error();
|
|
30213
|
+
|
|
30184
30214
|
// ../canopycms/src/url-path-resolver.ts
|
|
30185
30215
|
init_normalize();
|
|
30186
30216
|
|
|
@@ -30189,10 +30219,10 @@ var import_gray_matter3 = __toESM(require_gray_matter(), 1);
|
|
|
30189
30219
|
var import_yaml3 = __toESM(require_dist4(), 1);
|
|
30190
30220
|
init_content_id_index();
|
|
30191
30221
|
init_error();
|
|
30192
|
-
var
|
|
30222
|
+
var log9 = createDebugLogger({ prefix: "ContentListing" });
|
|
30193
30223
|
|
|
30194
30224
|
// ../canopycms/src/context.ts
|
|
30195
|
-
var
|
|
30225
|
+
var log10 = createDebugLogger({ prefix: "Context" });
|
|
30196
30226
|
|
|
30197
30227
|
// ../canopycms/src/server.ts
|
|
30198
30228
|
init_operating_mode();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canopycms-cdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.68-int.94",
|
|
4
4
|
"description": "AWS CDK constructs and EC2 worker for CanopyCMS deployment",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"aws-cdk-lib": "^2.260.0",
|
|
33
33
|
"constructs": "^10.0.0",
|
|
34
|
-
"canopycms": "0.0.
|
|
34
|
+
"canopycms": "0.0.68-int.94"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"aws-cdk-lib": {
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"tsx": "^4.19.0",
|
|
57
57
|
"typescript": "^5.6.3",
|
|
58
58
|
"vitest": "^4.1.0",
|
|
59
|
-
"canopycms": "0.0.
|
|
60
|
-
"canopycms-auth-clerk": "0.0.
|
|
59
|
+
"canopycms": "0.0.68-int.94",
|
|
60
|
+
"canopycms-auth-clerk": "0.0.68-int.94"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "rm -rf dist && tsc && node ../../scripts/add-js-extensions.mjs dist",
|