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.
@@ -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 = log10;
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 log10(...args) {
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 log10 = debug_1.default("@kwsites/file-exists");
7058
+ var log11 = debug_1.default("@kwsites/file-exists");
7051
7059
  function check(path3, isFile, isDirectory) {
7052
- log10(`checking %s`, path3);
7060
+ log11(`checking %s`, path3);
7053
7061
  try {
7054
7062
  const stat = fs_1.statSync(path3);
7055
7063
  if (stat.isFile() && isFile) {
7056
- log10(`[OK] path represents a file`);
7064
+ log11(`[OK] path represents a file`);
7057
7065
  return true;
7058
7066
  }
7059
7067
  if (stat.isDirectory() && isDirectory) {
7060
- log10(`[OK] path represents a directory`);
7068
+ log11(`[OK] path represents a directory`);
7061
7069
  return true;
7062
7070
  }
7063
- log10(`[FAIL] path represents something other than a file or directory`);
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
- log10(`[FAIL] path is not accessible: %o`, e);
7075
+ log11(`[FAIL] path is not accessible: %o`, e);
7068
7076
  return false;
7069
7077
  }
7070
- log10(`[FATAL] %o`, e);
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 log10 = require_log();
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
- log10.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.`);
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 log10 = require_log();
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) => log10.warn(doc.options.logLevel, 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 log10 = requestOptions.request && requestOptions.request.log ? requestOptions.request.log : console;
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
- log10.warn(
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
- this.time(label);
24128
+ const start = Date.now();
24112
24129
  try {
24113
24130
  return await fn();
24114
24131
  } finally {
24115
- this.timeEnd(category, label);
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 log10 = (name) => {
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
- log10 = NOOP;
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
- log10(name);
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 log4 = createDebugLogger({ prefix: "GitManager" });
28434
+ var log5 = createDebugLogger({ prefix: "GitManager" });
28413
28435
 
28414
28436
  // ../canopycms/src/branch-workspace.ts
28415
- var log5 = createDebugLogger({ prefix: "BranchWorkspace" });
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 log6 = createDebugLogger({ prefix: "EntryLinks" });
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 log7 = createDebugLogger({ prefix: "SettingsWorkspace" });
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 log8 = createDebugLogger({ prefix: "ContentListing" });
30222
+ var log9 = createDebugLogger({ prefix: "ContentListing" });
30193
30223
 
30194
30224
  // ../canopycms/src/context.ts
30195
- var log9 = createDebugLogger({ prefix: "Context" });
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.67",
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.67"
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.67",
60
- "canopycms-auth-clerk": "0.0.67"
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",