canopycms 0.0.67 → 0.0.68-int.100
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 +21 -14
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +111 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +10 -1
- package/dist/api/github-sync.js +20 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +122 -0
- package/dist/assets/materialize.js +305 -0
- package/dist/assets/store-local.d.ts +4 -0
- package/dist/assets/store-local.js +11 -0
- package/dist/assets/store-s3.d.ts +10 -1
- package/dist/assets/store-s3.js +55 -3
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +14 -1
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +30 -0
- package/dist/cli/asset-refs.js +168 -0
- package/dist/cli/cli.d.ts +2 -1
- package/dist/cli/cli.js +4843 -1870
- package/dist/cli/generate-ai-content.js +1584 -444
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +24 -20
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +11 -0
- package/dist/editor/hooks/useBranchManager.js +80 -2
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +8 -1
- package/dist/editor/preview-bridge.js +15 -2
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +24 -9
- package/dist/server.js +22 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +39 -4
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +2 -1
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ContentStore } from '../content-store.js';
|
|
3
3
|
import { defineEndpoint } from './route-builder.js';
|
|
4
|
-
import { ReferenceResolver } from '../reference-resolver.js';
|
|
5
|
-
import { buildResolvedReference } from '../entry-schema.js';
|
|
6
|
-
import { computeEntryUrl } from '../utils/entry-url.js';
|
|
7
4
|
import { branchNameSchema, contentIdSchema } from './validators.js';
|
|
8
5
|
/**
|
|
9
6
|
* Resolution does sequential per-ID file I/O, so the request body caps how much filesystem work
|
|
@@ -20,49 +17,25 @@ const resolveReferencesBodySchema = z.object({
|
|
|
20
17
|
const resolveReferencesHandler = async (gc, ctx, req, params, body) => {
|
|
21
18
|
const { branchContext } = gc;
|
|
22
19
|
const { ids } = body;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const store = new ContentStore(branchContext.branchRoot, flatSchema, {
|
|
26
|
-
contentRootName,
|
|
20
|
+
const store = new ContentStore(branchContext.branchRoot, branchContext.flatSchema, {
|
|
21
|
+
contentRootName: ctx.services.config.contentRoot || 'content',
|
|
27
22
|
});
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
// Resolve each ID to full document
|
|
31
|
-
const resolver = new ReferenceResolver(store, idIndex);
|
|
32
|
-
// Build the access checker once, reused for every id instead of re-loading per id in the loop.
|
|
33
|
-
// A failure here (e.g. settings workspace unavailable) surfaces as a handler error, not
|
|
34
|
-
// swallowed silently per id.
|
|
23
|
+
// Built once for every id. A failure here (e.g. settings workspace unavailable) surfaces as a
|
|
24
|
+
// handler error rather than being swallowed per id.
|
|
35
25
|
const checkAccess = await ctx.services.createContentAccessChecker(branchContext, branchContext.branchRoot, req.user);
|
|
26
|
+
const access = (logicalPath) => checkAccess(logicalPath, 'read').allowed;
|
|
27
|
+
// The resolution `read()` applies to a reference field, so live preview shows this user what
|
|
28
|
+
// `read()` would: the target's data, or a `RestrictedReference` for a target they may not
|
|
29
|
+
// read. The target's own references stay ids, as in `read()`.
|
|
36
30
|
const resolved = {};
|
|
37
31
|
for (const id of ids) {
|
|
38
32
|
try {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
const resolvedPath = await store.resolveDocumentPath(result.collection, result.slug);
|
|
43
|
-
const access = checkAccess(resolvedPath.relativePath, 'read');
|
|
44
|
-
if (!access.allowed)
|
|
45
|
-
continue;
|
|
46
|
-
// `resolveReferences: false` matches the server-side resolver (content-store.ts's
|
|
47
|
-
// resolveSingleReferenceOnce), so a nested reference inside a target renders the same way
|
|
48
|
-
// in live preview as on the published site, instead of resolving one level deeper.
|
|
49
|
-
const doc = await store.read(result.collection, result.slug, { resolveReferences: false });
|
|
50
|
-
if (doc && doc.data) {
|
|
51
|
-
// Same shape as the server-side resolver: target data first, then the reserved keys.
|
|
52
|
-
// Order is the corruption guard (a target modelling `id` as content must not shadow the
|
|
53
|
-
// real content ID); the extra keys (incl. `urlPath`) keep live preview and production
|
|
54
|
-
// in sync for consumers like client-reference-resolver.ts.
|
|
55
|
-
resolved[id] = buildResolvedReference(doc.data, {
|
|
56
|
-
id,
|
|
57
|
-
slug: result.slug,
|
|
58
|
-
collection: result.collection,
|
|
59
|
-
urlPath: computeEntryUrl(result.collection, result.slug, contentRootName),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
33
|
+
const value = await store.resolveReferenceTarget(id, access);
|
|
34
|
+
if (value)
|
|
35
|
+
resolved[id] = value;
|
|
63
36
|
}
|
|
64
37
|
catch (error) {
|
|
65
|
-
//
|
|
38
|
+
// An id that fails to resolve is omitted rather than failing the whole request.
|
|
66
39
|
console.error(`Failed to resolve reference ID ${id}:`, error);
|
|
67
40
|
}
|
|
68
41
|
}
|
|
@@ -73,7 +46,8 @@ const resolveReferencesHandler = async (gc, ctx, req, params, body) => {
|
|
|
73
46
|
};
|
|
74
47
|
};
|
|
75
48
|
/**
|
|
76
|
-
* Resolve reference IDs
|
|
49
|
+
* Resolve reference IDs as a reference field would: full target data, or title + URL tagged
|
|
50
|
+
* `unavailable` for a target the user may not read. An id naming no entry is omitted.
|
|
77
51
|
* POST /:branch/resolve-references
|
|
78
52
|
* Body: { ids: string[] }
|
|
79
53
|
*/
|
|
@@ -30,7 +30,7 @@ export interface CommitSettingsResult {
|
|
|
30
30
|
/**
|
|
31
31
|
* Commit and push settings changes based on the mode.
|
|
32
32
|
* Both prod and dev use commitToSettingsBranch.
|
|
33
|
-
*
|
|
33
|
+
* Settings changes are never reviewed through a PR (see commitToSettingsBranch).
|
|
34
34
|
*/
|
|
35
35
|
export declare function commitSettings(ctx: ApiContext, options: {
|
|
36
36
|
context: {
|
|
@@ -23,7 +23,7 @@ export async function getSettingsBranchContext(ctx) {
|
|
|
23
23
|
/**
|
|
24
24
|
* Commit and push settings changes based on the mode.
|
|
25
25
|
* Both prod and dev use commitToSettingsBranch.
|
|
26
|
-
*
|
|
26
|
+
* Settings changes are never reviewed through a PR (see commitToSettingsBranch).
|
|
27
27
|
*/
|
|
28
28
|
export async function commitSettings(ctx, options) {
|
|
29
29
|
const strategy = operatingStrategy(options.mode);
|
|
@@ -38,9 +38,6 @@ export async function commitSettings(ctx, options) {
|
|
|
38
38
|
branchRoot: options.branchRoot,
|
|
39
39
|
files: options.fileName,
|
|
40
40
|
message: options.message,
|
|
41
|
-
createPR: strategy.shouldCreateSettingsPR({
|
|
42
|
-
autoCreateSettingsPR: ctx.services.config.autoCreateSettingsPR,
|
|
43
|
-
}),
|
|
44
41
|
});
|
|
45
42
|
if (!result.pushed) {
|
|
46
43
|
console.warn(`${options.message} committed but not pushed:`, result.error);
|
package/dist/api/user.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ApiResponse } from './types.js';
|
|
2
|
+
import type { BuildIdentity } from '../types.js';
|
|
2
3
|
export type UserInfoResponse = ApiResponse<{
|
|
3
4
|
userId: string;
|
|
4
5
|
groups: string[];
|
|
6
|
+
/** Admins only: the deployed version is reconnaissance for anyone else. */
|
|
7
|
+
build?: BuildIdentity;
|
|
5
8
|
}>;
|
|
6
9
|
export declare const USER_ROUTES: {
|
|
7
10
|
readonly whoami: import("./route-builder.js").RouteDefinition<undefined, undefined, UserInfoResponse>;
|
package/dist/api/user.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { defineEndpoint } from './route-builder.js';
|
|
2
|
+
import { isAdmin } from '../authorization/helpers.js';
|
|
3
|
+
import { getBuildIdentity } from '../build-identity.js';
|
|
2
4
|
/**
|
|
3
5
|
* This is a PUBLIC endpoint - no special permissions required
|
|
4
6
|
*/
|
|
@@ -10,6 +12,7 @@ const getUserInfoHandler = async (ctx, req) => {
|
|
|
10
12
|
data: {
|
|
11
13
|
userId: req.user.userId,
|
|
12
14
|
groups: req.user.groups,
|
|
15
|
+
...(isAdmin(req.user.groups) ? { build: getBuildIdentity() } : {}),
|
|
13
16
|
},
|
|
14
17
|
};
|
|
15
18
|
};
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build/adjust transform URLs for `<img>`/srcset without pulling in the
|
|
3
|
-
* server-only transform engine. Isomorphic -
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* import from client (editor) code as well as during static builds.
|
|
3
|
+
* server-only transform engine. Isomorphic - none of its imports reach a node
|
|
4
|
+
* builtin, so this is safe to import from client (editor) code as well as during
|
|
5
|
+
* static builds.
|
|
7
6
|
*/
|
|
8
7
|
import { type CropRect, type OutputFormat } from './transform-directives.js';
|
|
9
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The minimal shape `assetUrl`/`assetSrcSet` need from a stored asset reference. An
|
|
10
|
+
* `ImageFieldValue` is one, so passing a field value renders the crop the editor stored.
|
|
11
|
+
*/
|
|
10
12
|
export interface AssetRef {
|
|
11
13
|
src: string;
|
|
14
|
+
crop?: CropRect;
|
|
12
15
|
}
|
|
13
16
|
export interface AssetUrlOptions {
|
|
14
17
|
width?: number;
|
|
@@ -23,16 +26,14 @@ export interface AssetUrlOptions {
|
|
|
23
26
|
* option beside it. Two shapes are legitimate, and they are alternatives, never composed:
|
|
24
27
|
*
|
|
25
28
|
* - An absolute origin (`https://assets.example.com`) — assets served from another origin.
|
|
26
|
-
* `media.publicBaseUrl` is one source of this, and is the source the editor uses; it is
|
|
27
|
-
* validated as an absolute URL, so it structurally cannot carry the second shape.
|
|
28
29
|
* - A same-origin path prefix (`/preview-123`) — the site is deployed under a Next `basePath`
|
|
29
30
|
* AND its assets are served by Next (`withCanopy`'s `/assets/:path*` rewrite, which Next
|
|
30
31
|
* auto-prefixes). NOT the right value on a CloudFront/CDK deployment, where the asset
|
|
31
32
|
* behaviors are anchored at the distribution root and a `basePath` does not move them —
|
|
32
33
|
* there the correct value is none at all. See the README's asset-mount table.
|
|
33
34
|
*
|
|
34
|
-
* It is a per-render option rather than a config field
|
|
35
|
-
*
|
|
35
|
+
* It is a per-render option rather than a config field because different renderers see the
|
|
36
|
+
* `/assets` space at different places.
|
|
36
37
|
*
|
|
37
38
|
* **Render-time only — never stored.** A stored `src` is always root-relative (see
|
|
38
39
|
* `assets/asset-src.ts`), because content moves between branches and environments. Nothing
|
|
@@ -41,16 +42,21 @@ export interface AssetUrlOptions {
|
|
|
41
42
|
baseUrl?: string;
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
|
-
* Build a transform URL, merging `opts` over the directives already present
|
|
45
|
-
* in `ref.src` (
|
|
46
|
-
* or any src that isn't one of our own transform URLs) the src is
|
|
47
|
-
* unchanged and
|
|
45
|
+
* Build a transform URL, merging `opts` and `ref.crop` over the directives already present
|
|
46
|
+
* in `ref.src` (see `mergeDirectives` for precedence). For static srcs (svg/pdf under
|
|
47
|
+
* `/assets/{hash}/...`, or any src that isn't one of our own transform URLs) the src is
|
|
48
|
+
* returned unchanged and every directive is ignored - there is nothing to transform.
|
|
49
|
+
*
|
|
50
|
+
* In a live preview, a transform URL goes behind the editor's authenticated route
|
|
51
|
+
* (`editor/preview-asset-base.ts`) instead of `opts.baseUrl`: a draft's crop or width may exist
|
|
52
|
+
* nowhere a build put it. Static srcs keep `opts.baseUrl`, since finalize wrote them.
|
|
48
53
|
*/
|
|
49
54
|
export declare function assetUrl(ref: AssetRef, opts?: AssetUrlOptions): string;
|
|
50
55
|
/**
|
|
51
56
|
* Build a comma-joined `url w` srcset descriptor list. `widths` must all be
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
57
|
+
* integers in [1, 8192] (the `any` width policy) - this is developer-facing (a
|
|
58
|
+
* host app's own responsive-image markup), so an invalid width throws rather
|
|
59
|
+
* than silently dropping it. A site serving the opt-in lazy public path, which
|
|
60
|
+
* transforms only allowlisted widths, must pick widths from that allowlist.
|
|
55
61
|
*/
|
|
56
62
|
export declare function assetSrcSet(ref: AssetRef, widths: readonly number[], opts?: Omit<AssetUrlOptions, 'width'>): string;
|
package/dist/assets/asset-url.js
CHANGED
|
@@ -1,36 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build/adjust transform URLs for `<img>`/srcset without pulling in the
|
|
3
|
-
* server-only transform engine. Isomorphic -
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* import from client (editor) code as well as during static builds.
|
|
3
|
+
* server-only transform engine. Isomorphic - none of its imports reach a node
|
|
4
|
+
* builtin, so this is safe to import from client (editor) code as well as during
|
|
5
|
+
* static builds.
|
|
7
6
|
*/
|
|
8
7
|
import { isUnprefixablePath, joinUrlPrefix, sanitizeUnprefixedPath, stripTrailingSlashes, } from '../utils/url-prefix.js';
|
|
8
|
+
import { getPreviewAssetBase } from '../editor/preview-asset-base.js';
|
|
9
9
|
import { ASSET_PREFIXES } from './asset-prefixes.js';
|
|
10
10
|
import { formatDirectives, isAllowedTransformWidth, parseTransformPath, } from './transform-directives.js';
|
|
11
11
|
const TRANSFORM_URL_PREFIX = `/${ASSET_PREFIXES.transform}/`;
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* opts - only to override it).
|
|
13
|
+
* Test-only. A function stored on `globalThis` under this symbol is called with every URL
|
|
14
|
+
* `assetUrl` returns; the dual-build fixture installs one during a static export to prove
|
|
15
|
+
* `collect-asset-refs` finds every URL its pages emitted.
|
|
17
16
|
*/
|
|
18
|
-
|
|
17
|
+
const EMITTED_URL_LISTENER = Symbol.for('canopycms.assetUrl.emitted');
|
|
18
|
+
function emitted(url) {
|
|
19
|
+
const listener = globalThis[EMITTED_URL_LISTENER];
|
|
20
|
+
if (typeof listener === 'function')
|
|
21
|
+
listener(url);
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Merge `opts` and the ref's crop over the directives already in its src. Precedence per
|
|
26
|
+
* directive: `opts`, then `ref.crop` (crop only), then the src. There is no way to clear a
|
|
27
|
+
* directive, only to override it.
|
|
28
|
+
*/
|
|
29
|
+
function mergeDirectives(current, refCrop, opts) {
|
|
19
30
|
const existing = current.identity ? undefined : current;
|
|
20
31
|
const width = opts.width ?? existing?.width;
|
|
21
32
|
const format = opts.format ?? existing?.format;
|
|
22
33
|
const quality = opts.quality ?? existing?.quality;
|
|
23
|
-
const crop = opts.crop ?? existing?.crop;
|
|
34
|
+
const crop = opts.crop ?? refCrop ?? existing?.crop;
|
|
24
35
|
if (width === undefined && format === undefined && quality === undefined && crop === undefined) {
|
|
25
36
|
return { identity: true };
|
|
26
37
|
}
|
|
27
38
|
return { identity: false, width, format, quality, crop };
|
|
28
39
|
}
|
|
29
40
|
/**
|
|
30
|
-
* Build a transform URL, merging `opts` over the directives already present
|
|
31
|
-
* in `ref.src` (
|
|
32
|
-
* or any src that isn't one of our own transform URLs) the src is
|
|
33
|
-
* unchanged and
|
|
41
|
+
* Build a transform URL, merging `opts` and `ref.crop` over the directives already present
|
|
42
|
+
* in `ref.src` (see `mergeDirectives` for precedence). For static srcs (svg/pdf under
|
|
43
|
+
* `/assets/{hash}/...`, or any src that isn't one of our own transform URLs) the src is
|
|
44
|
+
* returned unchanged and every directive is ignored - there is nothing to transform.
|
|
45
|
+
*
|
|
46
|
+
* In a live preview, a transform URL goes behind the editor's authenticated route
|
|
47
|
+
* (`editor/preview-asset-base.ts`) instead of `opts.baseUrl`: a draft's crop or width may exist
|
|
48
|
+
* nowhere a build put it. Static srcs keep `opts.baseUrl`, since finalize wrote them.
|
|
34
49
|
*/
|
|
35
50
|
export function assetUrl(ref, opts = {}) {
|
|
36
51
|
const { src } = ref;
|
|
@@ -46,35 +61,37 @@ export function assetUrl(ref, opts = {}) {
|
|
|
46
61
|
// `joinUrlPrefix`'s own contract. Plain `!opts.baseUrl` made '' and '/' disagree.
|
|
47
62
|
const mount = opts.baseUrl ? stripTrailingSlashes(opts.baseUrl) : '';
|
|
48
63
|
if (!mount || isUnprefixablePath(src))
|
|
49
|
-
return sanitizeUnprefixedPath(src);
|
|
50
|
-
return joinUrlPrefix(opts.baseUrl, src);
|
|
64
|
+
return emitted(sanitizeUnprefixedPath(src));
|
|
65
|
+
return emitted(joinUrlPrefix(opts.baseUrl, src));
|
|
51
66
|
}
|
|
67
|
+
const base = getPreviewAssetBase() ?? opts.baseUrl;
|
|
52
68
|
const rest = src.slice(TRANSFORM_URL_PREFIX.length);
|
|
53
|
-
const parsed = parseTransformPath(rest.split('/'));
|
|
69
|
+
const parsed = parseTransformPath(rest.split('/'), 'any');
|
|
54
70
|
if (!parsed.ok) {
|
|
55
71
|
// Malformed src (shouldn't happen for a src canopycms itself wrote) -
|
|
56
72
|
// nothing sensible to merge onto, so return it unchanged rather than throw.
|
|
57
|
-
return joinUrlPrefix(
|
|
73
|
+
return emitted(joinUrlPrefix(base, src));
|
|
58
74
|
}
|
|
59
|
-
const merged = mergeDirectives(parsed.directives, opts);
|
|
75
|
+
const merged = mergeDirectives(parsed.directives, ref.crop, opts);
|
|
60
76
|
// Ext follows the format: an explicit format (new or carried over) always
|
|
61
77
|
// wins; with no format at all, the ext must keep preserving the source's
|
|
62
78
|
// real extension, which is exactly what `parsed.ext` already is here.
|
|
63
79
|
const ext = !merged.identity && merged.format !== undefined ? merged.format : parsed.ext;
|
|
64
80
|
const newSrc = `${TRANSFORM_URL_PREFIX}${formatDirectives(merged)}/${parsed.hash32}/${parsed.slug}.${ext}`;
|
|
65
|
-
return joinUrlPrefix(
|
|
81
|
+
return emitted(joinUrlPrefix(base, newSrc));
|
|
66
82
|
}
|
|
67
83
|
/**
|
|
68
84
|
* Build a comma-joined `url w` srcset descriptor list. `widths` must all be
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
85
|
+
* integers in [1, 8192] (the `any` width policy) - this is developer-facing (a
|
|
86
|
+
* host app's own responsive-image markup), so an invalid width throws rather
|
|
87
|
+
* than silently dropping it. A site serving the opt-in lazy public path, which
|
|
88
|
+
* transforms only allowlisted widths, must pick widths from that allowlist.
|
|
72
89
|
*/
|
|
73
90
|
export function assetSrcSet(ref, widths, opts = {}) {
|
|
74
91
|
return widths
|
|
75
92
|
.map((width) => {
|
|
76
|
-
if (!isAllowedTransformWidth(width)) {
|
|
77
|
-
throw new Error(`assetSrcSet: width ${width} is not allowed (must be
|
|
93
|
+
if (!isAllowedTransformWidth(width, 'any')) {
|
|
94
|
+
throw new Error(`assetSrcSet: width ${width} is not allowed (must be an integer between 1 and 8192)`);
|
|
78
95
|
}
|
|
79
96
|
return `${assetUrl(ref, { ...opts, width })} ${width}w`;
|
|
80
97
|
})
|
package/dist/assets/factory.d.ts
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the one place that turns the (currently-unconsumed) `media` config
|
|
5
5
|
* into a real store — see .claude/future-tasks/resolved/assets-media-system.md.
|
|
6
|
+
*
|
|
7
|
+
* The store is branch-agnostic: a local one is rooted at the dev workspace's
|
|
8
|
+
* `assets/` or at `media.directory` resolved against the process's working
|
|
9
|
+
* directory, never inside a branch clone. So asset writes take no [SYNC-C1]
|
|
10
|
+
* content-write lock — the worker's rebase never touches them.
|
|
6
11
|
*/
|
|
7
12
|
import type { MediaConfig } from '../config/types.js';
|
|
8
13
|
import type { AssetStore } from './types.js';
|
package/dist/assets/factory.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the one place that turns the (currently-unconsumed) `media` config
|
|
5
5
|
* into a real store — see .claude/future-tasks/resolved/assets-media-system.md.
|
|
6
|
+
*
|
|
7
|
+
* The store is branch-agnostic: a local one is rooted at the dev workspace's
|
|
8
|
+
* `assets/` or at `media.directory` resolved against the process's working
|
|
9
|
+
* directory, never inside a branch clone. So asset writes take no [SYNC-C1]
|
|
10
|
+
* content-write lock — the worker's rebase never touches them.
|
|
6
11
|
*/
|
|
7
12
|
import { LocalAssetStore } from './store-local.js';
|
|
8
13
|
import { S3AssetStore } from './store-s3.js';
|
package/dist/assets/index.d.ts
CHANGED
|
@@ -14,6 +14,6 @@ export { ALLOWED_UPLOAD_CONTENT_TYPES, runFinalizePipeline, type FinalizeInput,
|
|
|
14
14
|
export { finalizeAsset, finalizeStagedUpload, type FinalizeAssetResult } from './finalize.js';
|
|
15
15
|
export { sanitizeSvg } from './svg-sanitizer.js';
|
|
16
16
|
export { assetSrc } from './asset-src.js';
|
|
17
|
-
export { formatDirectives, isAllowedTransformWidth, isValidCropRect, parseTransformPath, type CropRect, type OutputFormat, type ParsedTransformPath, type ParseTransformPathResult, type TransformDirectives, } from './transform-directives.js';
|
|
17
|
+
export { formatDirectives, isAllowedTransformWidth, isValidCropRect, parseTransformPath, type CropRect, type OutputFormat, type ParsedTransformPath, type ParseTransformPathResult, type TransformDirectives, type TransformWidthPolicy, } from './transform-directives.js';
|
|
18
18
|
export { applyTransform, type ApplyTransformInput, type TransformResult } from './transform.js';
|
|
19
19
|
export { assetUrl, assetSrcSet, type AssetRef, type AssetUrlOptions } from './asset-url.js';
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing transform outputs into the store. Server-only.
|
|
3
|
+
*
|
|
4
|
+
* `storeTransform` computes one key and is shared by the authenticated raw route
|
|
5
|
+
* (api/assets.ts), `materializeAssets` (the batch run that writes every key a build references
|
|
6
|
+
* before that build is released) and canopycms-cdk's lazy transform Lambda.
|
|
7
|
+
*/
|
|
8
|
+
import { type ParsedTransformPath } from './transform-directives.js';
|
|
9
|
+
import type { AssetStore } from './types.js';
|
|
10
|
+
/** Every transform output is stored under a content-addressed key, so it never changes. */
|
|
11
|
+
export declare const TRANSFORM_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
12
|
+
/**
|
|
13
|
+
* The object tag on every derivative the lazy transform Lambda writes. Its bucket's `assets/t/`
|
|
14
|
+
* expiry filters on it, so what `materializeAssets` writes, which the Lambda's allowlist may refuse
|
|
15
|
+
* to recompute, is never expired. canopycms-cdk's `AssetSupport` copies these two strings as
|
|
16
|
+
* literals.
|
|
17
|
+
*/
|
|
18
|
+
export declare const LAZY_TRANSFORM_TAG: {
|
|
19
|
+
readonly key: "canopy-transform";
|
|
20
|
+
readonly value: "lazy";
|
|
21
|
+
};
|
|
22
|
+
export type StoreTransformResult = {
|
|
23
|
+
ok: true;
|
|
24
|
+
data: Uint8Array;
|
|
25
|
+
contentType: string;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
status: 400 | 404 | 413 | 422;
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Compute the transform `parsed` names from the asset's original and write it under
|
|
33
|
+
* `canonicalKey`. `parsed` must come from `canonicalizeTransformPath`, so the stored pixels
|
|
34
|
+
* always match their key. A rejection is a fact about the asset or the URL and is never worth
|
|
35
|
+
* retrying; a thrown error belongs to the store or the environment. A 404's `error` names what
|
|
36
|
+
* was missing, so a caller answering a browser replaces it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function storeTransform(store: AssetStore, parsed: ParsedTransformPath, canonicalKey: string, options?: {
|
|
39
|
+
tags?: Readonly<Record<string, string>>;
|
|
40
|
+
}): Promise<StoreTransformResult>;
|
|
41
|
+
/** @internal One key a build references, and where it was referenced, for the failure report. */
|
|
42
|
+
export interface MaterializeTarget {
|
|
43
|
+
/** `assets/t/{directives}/{hash32}/{slug}.{ext}`. */
|
|
44
|
+
key: string;
|
|
45
|
+
routes: readonly string[];
|
|
46
|
+
files: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `content`: the key cannot be produced from what the store holds (asset deleted, wrong slug,
|
|
50
|
+
* undecodable input, malformed key), so a retry would fail the same way and the page must change.
|
|
51
|
+
* `store`: the store kept failing, or refused the request outright; rerunning may succeed.
|
|
52
|
+
*/
|
|
53
|
+
type MaterializeFailureKind = 'content' | 'store';
|
|
54
|
+
export type MaterializeResult = {
|
|
55
|
+
key: string;
|
|
56
|
+
routes: string[];
|
|
57
|
+
files: string[];
|
|
58
|
+
} & ({
|
|
59
|
+
status: 'existed' | 'created';
|
|
60
|
+
} | {
|
|
61
|
+
status: 'failed';
|
|
62
|
+
failure: MaterializeFailureKind;
|
|
63
|
+
error: string;
|
|
64
|
+
});
|
|
65
|
+
export interface MaterializeReport {
|
|
66
|
+
summary: {
|
|
67
|
+
total: number;
|
|
68
|
+
existed: number;
|
|
69
|
+
created: number;
|
|
70
|
+
failed: number;
|
|
71
|
+
contentFailures: number;
|
|
72
|
+
storeFailures: number;
|
|
73
|
+
};
|
|
74
|
+
/** Sorted by key. */
|
|
75
|
+
results: MaterializeResult[];
|
|
76
|
+
}
|
|
77
|
+
export interface MaterializeOptions {
|
|
78
|
+
store: AssetStore;
|
|
79
|
+
targets: readonly MaterializeTarget[];
|
|
80
|
+
/**
|
|
81
|
+
* `assets/{hash32}/{slug}.{ext}` keys (svg, pdf) a build references. These are only checked:
|
|
82
|
+
* finalize writes them at upload, so a missing one is a content failure nothing here can fix,
|
|
83
|
+
* unless the bucket itself is missing, which is a store failure.
|
|
84
|
+
*/
|
|
85
|
+
statics?: readonly MaterializeTarget[];
|
|
86
|
+
/** Store requests in flight at once. Default 8. */
|
|
87
|
+
concurrency?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Transforms in flight at once. Each holds a decoded image, up to about 1.2 GiB at
|
|
90
|
+
* `MAX_INPUT_PIXELS`, so this stays small whatever `concurrency` is. Default 2.
|
|
91
|
+
*/
|
|
92
|
+
transformConcurrency?: number;
|
|
93
|
+
/**
|
|
94
|
+
* A directive string referenced by at least this many keys has its whole `assets/t/{directives}/`
|
|
95
|
+
* prefix listed once instead of a HEAD per key, when the store can list. Default 100.
|
|
96
|
+
*/
|
|
97
|
+
listThreshold?: number;
|
|
98
|
+
/** Attempts per store operation, including the first. Default 3. */
|
|
99
|
+
attempts?: number;
|
|
100
|
+
/** First retry delay; each later one doubles. Default 500. */
|
|
101
|
+
baseDelayMs?: number;
|
|
102
|
+
/** @internal Test seam for the retry delay. */
|
|
103
|
+
sleep?: (ms: number) => Promise<void>;
|
|
104
|
+
}
|
|
105
|
+
/** sharp cannot load in this process, so nothing missing can be produced here. */
|
|
106
|
+
export declare class SharpUnavailableError extends Error {
|
|
107
|
+
constructor(cause: unknown);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* @internal Exported for tests. Throttling, 5xx and dropped connections are transient. Any other
|
|
111
|
+
* 4xx (AccessDenied, NoSuchBucket) is a configuration fault that a retry only delays.
|
|
112
|
+
*/
|
|
113
|
+
export declare function isTransientStoreError(err: unknown): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Make every referenced transform key exist in `store`, and check every static key does. A key
|
|
116
|
+
* already stored is left alone:
|
|
117
|
+
* keys are content-addressed, so an existing object is the right one. sharp is loaded only when
|
|
118
|
+
* something is missing, and a sharp that cannot load throws `SharpUnavailableError` rather than
|
|
119
|
+
* failing each key.
|
|
120
|
+
*/
|
|
121
|
+
export declare function materializeAssets(options: MaterializeOptions): Promise<MaterializeReport>;
|
|
122
|
+
export {};
|