canopycms 0.0.67 → 0.0.68-int.101
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 +24 -17
- 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 +118 -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 +19 -1
- package/dist/api/github-sync.js +57 -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/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- 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 +31 -4
- 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 +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- 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/client.d.ts +1 -1
- package/dist/client.js +1 -1
- 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 +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- 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 +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- 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 +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- 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 +15 -0
- package/dist/github-service.js +19 -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/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- 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 +64 -9
- package/dist/server.js +48 -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-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- 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/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -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 +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -1,46 +1,15 @@
|
|
|
1
1
|
import nodePath from 'node:path';
|
|
2
2
|
import { extractSlugFromFilename, extractEntryTypeFromFilename } from './content-id-index.js';
|
|
3
|
+
import { entryLogicalPath } from './paths/index.js';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Loads the options a reference field's picker offers. Resolving a stored reference to its
|
|
6
|
+
* target is `ContentStore`'s job (`read()`, `resolveReferenceTarget`).
|
|
5
7
|
*/
|
|
6
8
|
export class ReferenceResolver {
|
|
7
9
|
constructor(store, idIndex) {
|
|
8
10
|
this.store = store;
|
|
9
11
|
this.idIndex = idIndex;
|
|
10
12
|
}
|
|
11
|
-
/**
|
|
12
|
-
* Resolve a content ID to a display value.
|
|
13
|
-
* Returns null if the ID doesn't exist or points to a collection.
|
|
14
|
-
*/
|
|
15
|
-
async resolve(id, displayField = 'title') {
|
|
16
|
-
const location = this.idIndex.findById(id);
|
|
17
|
-
if (!location || location.type !== 'entry') {
|
|
18
|
-
return {
|
|
19
|
-
id,
|
|
20
|
-
exists: false,
|
|
21
|
-
displayValue: id, // Fallback to showing the ID itself
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const doc = await this.store.read(location.collection, location.slug);
|
|
26
|
-
const displayValue = String(doc.data[displayField] || doc.data.title || location.slug);
|
|
27
|
-
return {
|
|
28
|
-
id,
|
|
29
|
-
exists: true,
|
|
30
|
-
displayValue,
|
|
31
|
-
collection: location.collection,
|
|
32
|
-
slug: location.slug,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
console.error('Failed to resolve reference:', { id, error });
|
|
37
|
-
return {
|
|
38
|
-
id,
|
|
39
|
-
exists: false,
|
|
40
|
-
displayValue: id,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
13
|
/**
|
|
45
14
|
* Load all available reference options for a reference field.
|
|
46
15
|
*
|
|
@@ -48,7 +17,7 @@ export class ReferenceResolver {
|
|
|
48
17
|
* At least one of `collections` or `entryTypes` should be provided.
|
|
49
18
|
*
|
|
50
19
|
* @param canAccess - Optional permission predicate, called with each candidate's
|
|
51
|
-
*
|
|
20
|
+
* logical path (`entryLogicalPath`) before it is read. Returning false skips the entry entirely --
|
|
52
21
|
* no file I/O, no label, no option -- so a caller who can't read a path never
|
|
53
22
|
* triggers a read for content they won't be allowed to see anyway.
|
|
54
23
|
*/
|
|
@@ -74,7 +43,7 @@ export class ReferenceResolver {
|
|
|
74
43
|
if (!location.collection || !location.slug)
|
|
75
44
|
continue;
|
|
76
45
|
// Skip denied paths before any file I/O.
|
|
77
|
-
if (canAccess && !canAccess(location.
|
|
46
|
+
if (canAccess && !canAccess(entryLogicalPath(location.collection, location.slug)))
|
|
78
47
|
continue;
|
|
79
48
|
const id = this.idIndex.findByPath(location.relativePath);
|
|
80
49
|
if (!id)
|
|
@@ -82,7 +51,10 @@ export class ReferenceResolver {
|
|
|
82
51
|
try {
|
|
83
52
|
const filename = nodePath.basename(location.relativePath);
|
|
84
53
|
const normalizedSlug = extractSlugFromFilename(filename);
|
|
85
|
-
|
|
54
|
+
// Only the label is read, so the candidate's own references stay unresolved.
|
|
55
|
+
const doc = await this.store.read(location.collection, normalizedSlug, {
|
|
56
|
+
resolveReferences: false,
|
|
57
|
+
});
|
|
86
58
|
const label = String(doc.data[displayField] || doc.data.title || normalizedSlug);
|
|
87
59
|
if (search && !label.toLowerCase().includes(search.toLowerCase())) {
|
|
88
60
|
continue;
|
|
@@ -104,11 +76,4 @@ export class ReferenceResolver {
|
|
|
104
76
|
}
|
|
105
77
|
return options.sort((a, b) => a.label.localeCompare(b.label));
|
|
106
78
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Resolve multiple IDs at once.
|
|
109
|
-
* Useful for displaying lists of referenced items.
|
|
110
|
-
*/
|
|
111
|
-
async resolveMany(ids, displayField = 'title') {
|
|
112
|
-
return Promise.all(ids.map((id) => this.resolve(id, displayField)));
|
|
113
|
-
}
|
|
114
79
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { canopyLogWarn } from './utils/logger.js';
|
|
13
13
|
import { authResultToCanopyUser } from './user.js';
|
|
14
14
|
import { loadInternalGroups, RESERVED_GROUPS } from './authorization/index.js';
|
|
15
|
+
import { timeRequestPhase } from './utils/request-timing.js';
|
|
15
16
|
/** Module-level: warn at most once per process. */
|
|
16
17
|
let warnedNoAdmins = false;
|
|
17
18
|
/**
|
|
@@ -39,7 +40,7 @@ function warnIfNoAdmins(internalGroups, bootstrapAdminIds) {
|
|
|
39
40
|
*/
|
|
40
41
|
export async function resolveCanopyUser(authResult, deps) {
|
|
41
42
|
const settingsRoot = await deps.getSettingsBranchRoot();
|
|
42
|
-
const internalGroups = await loadInternalGroups(settingsRoot, deps.mode, deps.bootstrapAdminIds);
|
|
43
|
+
const internalGroups = await timeRequestPhase('groups', () => loadInternalGroups(settingsRoot, deps.mode, deps.bootstrapAdminIds));
|
|
43
44
|
warnIfNoAdmins(internalGroups, deps.bootstrapAdminIds);
|
|
44
45
|
return authResultToCanopyUser(authResult, deps.bootstrapAdminIds, internalGroups);
|
|
45
46
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Schema Store -
|
|
3
|
-
* All mutations are branch-specific (like content edits).
|
|
2
|
+
* Schema Store - reads and writes .collection.json files, per branch.
|
|
4
3
|
*
|
|
5
4
|
* ## Concurrency
|
|
6
5
|
*
|
|
@@ -16,11 +15,11 @@
|
|
|
16
15
|
*
|
|
17
16
|
* Protection is instead layer 1 ({@link withLock}) + layer 3
|
|
18
17
|
* ({@link withOccFileLock}) on one COARSE per-branch SURROGATE lock path
|
|
19
|
-
* OUTSIDE the content tree (`{branchRoot}/.canopy-meta/schema
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* OUTSIDE the content tree (`{branchRoot}/.canopy-meta/schema`), covering
|
|
19
|
+
* every schema mutation including multi-file ones, inside the [SYNC-C1]
|
|
20
|
+
* content-write lock the worker's rebase also takes (`withBranchSchemaLock`).
|
|
21
|
+
* Layer 2 (OCC read-back) is skipped, since there's no version field to
|
|
22
|
+
* check; layer 4 (generation marker) guards the schema CACHE separately.
|
|
24
23
|
*
|
|
25
24
|
* Accepted residual: `deleteBranch` does not take this lock before its
|
|
26
25
|
* recursive `rm`, so an in-flight write can race a concurrent branch
|
|
@@ -182,7 +181,30 @@ declare const updateEntryTypeInputSchema: z.ZodObject<{
|
|
|
182
181
|
* into a 409 so the editor can retry rather than surfacing a raw 400.
|
|
183
182
|
*/
|
|
184
183
|
export declare class SchemaStoreBusyError extends Error {
|
|
185
|
-
|
|
184
|
+
/** As on `ContentWriteLockBusyError`: `'unknown'` means the mutation ran and may have landed. */
|
|
185
|
+
readonly outcome: 'not-run' | 'unknown';
|
|
186
|
+
constructor(message?: string,
|
|
187
|
+
/** As on `ContentWriteLockBusyError`: `'unknown'` means the mutation ran and may have landed. */
|
|
188
|
+
outcome?: 'not-run' | 'unknown');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Run `fn` holding every lock a schema mutation needs on `branchRoot`, in the
|
|
192
|
+
* global order (docs/concurrency.md, "Lock acquisition order"): the branch's
|
|
193
|
+
* content-write lock, then the `.canopy-meta/schema` surrogate's in-process
|
|
194
|
+
* mutex, then its lockfile. Shared with `cli/migrate.ts` so the two cannot
|
|
195
|
+
* drift.
|
|
196
|
+
*
|
|
197
|
+
* NOT re-entrant, and nothing inside `fn` may take the content-write lock
|
|
198
|
+
* (no `ContentStore` writes): proper-lockfile refuses a second acquisition in
|
|
199
|
+
* the same process, so a nested one burns its wait and fails busy.
|
|
200
|
+
*
|
|
201
|
+
* Errors propagate untranslated: `ContentWriteLockBusyError` (contention, or
|
|
202
|
+
* a compromise reported after `fn` completed) and `OccWriteConflictError`.
|
|
203
|
+
*/
|
|
204
|
+
export declare function withBranchSchemaLock<T>(branchRoot: string, fn: () => Promise<T>, contentWriteLockWaitMs?: number): Promise<T>;
|
|
205
|
+
export interface SchemaOpsOptions {
|
|
206
|
+
/** Bounded wait for the branch's content-write lock; defaults to `DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS`. */
|
|
207
|
+
contentWriteLockWaitMs?: number;
|
|
186
208
|
}
|
|
187
209
|
export declare class SchemaOps {
|
|
188
210
|
private readonly contentRoot;
|
|
@@ -199,34 +221,35 @@ export declare class SchemaOps {
|
|
|
199
221
|
* and never match the root collection's logical path).
|
|
200
222
|
*/
|
|
201
223
|
private readonly contentRootName;
|
|
202
|
-
/**
|
|
203
|
-
private readonly
|
|
204
|
-
constructor(contentRoot: string, entrySchemaRegistry: EntrySchemaRegistry, services?: CanopyServices | undefined, branchRoot?: string);
|
|
224
|
+
/** See SchemaOpsOptions.contentWriteLockWaitMs. */
|
|
225
|
+
private readonly contentWriteLockWaitMs;
|
|
226
|
+
constructor(contentRoot: string, entrySchemaRegistry: EntrySchemaRegistry, services?: CanopyServices | undefined, branchRoot?: string, options?: SchemaOpsOptions);
|
|
205
227
|
/**
|
|
206
|
-
* Serialize an entire read-modify-write schema mutation behind
|
|
207
|
-
*
|
|
208
|
-
* ({@link withLock}
|
|
209
|
-
* (
|
|
210
|
-
* exclusion, immune to NFS attribute caching), same structure as
|
|
211
|
-
* `BranchMetadataFileManager.save()` in branch-metadata.ts.
|
|
228
|
+
* Serialize an entire read-modify-write schema mutation behind
|
|
229
|
+
* {@link withBranchSchemaLock}: the content-write lock, then the surrogate
|
|
230
|
+
* ({@link withLock} wrapping {@link withOccFileLock}, as in
|
|
231
|
+
* `BranchMetadataFileManager.save()`).
|
|
212
232
|
*
|
|
213
233
|
* NOT re-entrant — callers must never invoke this from inside a callback
|
|
214
|
-
* already running under it, or
|
|
215
|
-
*
|
|
216
|
-
*
|
|
234
|
+
* already running under it, or the nested acquisition fails busy; that's
|
|
235
|
+
* why `updateOrderInner` calls `updateCollectionInner` directly instead of
|
|
236
|
+
* the public `updateCollection`.
|
|
217
237
|
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* branch-metadata.ts's): a call that passes this check can still race a
|
|
225
|
-
* `deleteBranch` `rm` starting moments later — not closed here either,
|
|
226
|
-
* since `deleteBranch` never takes this lock (see the module doc comment).
|
|
238
|
+
* `branchRoot` can be removed by a concurrent `deleteBranch` between the
|
|
239
|
+
* caller resolving its BranchContext and this call's lock acquisition. The
|
|
240
|
+
* stat below fails fast before any lock; past it, neither lock recreates a
|
|
241
|
+
* missing root (each creates only its own directory), so a later removal
|
|
242
|
+
* fails the acquisition, and any failure on a vanished root is reported as
|
|
243
|
+
* `SchemaStoreBusyError` below.
|
|
227
244
|
*
|
|
228
245
|
* Translation happens ONLY at this boundary: inner code always sees the
|
|
229
|
-
* raw {@link OccWriteConflictError} bubble up here untranslated
|
|
246
|
+
* raw {@link OccWriteConflictError} bubble up here untranslated, and a
|
|
247
|
+
* {@link ContentWriteLockBusyError} becomes a `SchemaStoreBusyError` with
|
|
248
|
+
* its message, so every caller's 409 mapping covers both.
|
|
249
|
+
*
|
|
250
|
+
* A content-lock compromise is reported after `fn` completed, so the
|
|
251
|
+
* mutation is on disk while the caller's post-lock `invalidateSchemaCache()`
|
|
252
|
+
* never runs; this runs it before throwing.
|
|
230
253
|
*/
|
|
231
254
|
private withSchemaLock;
|
|
232
255
|
/**
|
|
@@ -322,7 +345,7 @@ export declare class SchemaOps {
|
|
|
322
345
|
* Body of updateCollection, holding the schema lock for its full
|
|
323
346
|
* read-modify-write. Called directly (not via the public `updateCollection`)
|
|
324
347
|
* by `updateOrderInner` for non-root collections — `withSchemaLock` is NOT
|
|
325
|
-
* re-entrant, so going through the public method there would
|
|
348
|
+
* re-entrant, so going through the public method there would fail busy.
|
|
326
349
|
*
|
|
327
350
|
* `collectionPath` here is always already normalized (content-root prefix
|
|
328
351
|
* stripped, if it had one) by whichever public method reached this — either
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Schema Store -
|
|
3
|
-
* All mutations are branch-specific (like content edits).
|
|
2
|
+
* Schema Store - reads and writes .collection.json files, per branch.
|
|
4
3
|
*
|
|
5
4
|
* ## Concurrency
|
|
6
5
|
*
|
|
@@ -16,11 +15,11 @@
|
|
|
16
15
|
*
|
|
17
16
|
* Protection is instead layer 1 ({@link withLock}) + layer 3
|
|
18
17
|
* ({@link withOccFileLock}) on one COARSE per-branch SURROGATE lock path
|
|
19
|
-
* OUTSIDE the content tree (`{branchRoot}/.canopy-meta/schema
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* OUTSIDE the content tree (`{branchRoot}/.canopy-meta/schema`), covering
|
|
19
|
+
* every schema mutation including multi-file ones, inside the [SYNC-C1]
|
|
20
|
+
* content-write lock the worker's rebase also takes (`withBranchSchemaLock`).
|
|
21
|
+
* Layer 2 (OCC read-back) is skipped, since there's no version field to
|
|
22
|
+
* check; layer 4 (generation marker) guards the schema CACHE separately.
|
|
24
23
|
*
|
|
25
24
|
* Accepted residual: `deleteBranch` does not take this lock before its
|
|
26
25
|
* recursive `rm`, so an in-flight write can race a concurrent branch
|
|
@@ -34,6 +33,7 @@ import { z } from 'zod';
|
|
|
34
33
|
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
35
34
|
import { withLock } from '../utils/async-mutex.js';
|
|
36
35
|
import { createDebugLogger } from '../utils/debug.js';
|
|
36
|
+
import { ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, withContentWriteLock, } from '../utils/content-write-lock.js';
|
|
37
37
|
import { getErrorMessage, isNotFoundError } from '../utils/error.js';
|
|
38
38
|
import { withOccFileLock, OccWriteConflictError } from '../utils/occ-json-write.js';
|
|
39
39
|
import { resolveCollectionPath } from '../content-id-index.js';
|
|
@@ -107,13 +107,34 @@ const log = createDebugLogger({ prefix: 'SchemaOps' });
|
|
|
107
107
|
* into a 409 so the editor can retry rather than surfacing a raw 400.
|
|
108
108
|
*/
|
|
109
109
|
export class SchemaStoreBusyError extends Error {
|
|
110
|
-
constructor(message = 'Schema is being modified by another operation, try again'
|
|
110
|
+
constructor(message = 'Schema is being modified by another operation, try again',
|
|
111
|
+
/** As on `ContentWriteLockBusyError`: `'unknown'` means the mutation ran and may have landed. */
|
|
112
|
+
outcome = 'not-run') {
|
|
111
113
|
super(message);
|
|
114
|
+
this.outcome = outcome;
|
|
112
115
|
this.name = 'SchemaStoreBusyError';
|
|
113
116
|
}
|
|
114
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Run `fn` holding every lock a schema mutation needs on `branchRoot`, in the
|
|
120
|
+
* global order (docs/concurrency.md, "Lock acquisition order"): the branch's
|
|
121
|
+
* content-write lock, then the `.canopy-meta/schema` surrogate's in-process
|
|
122
|
+
* mutex, then its lockfile. Shared with `cli/migrate.ts` so the two cannot
|
|
123
|
+
* drift.
|
|
124
|
+
*
|
|
125
|
+
* NOT re-entrant, and nothing inside `fn` may take the content-write lock
|
|
126
|
+
* (no `ContentStore` writes): proper-lockfile refuses a second acquisition in
|
|
127
|
+
* the same process, so a nested one burns its wait and fails busy.
|
|
128
|
+
*
|
|
129
|
+
* Errors propagate untranslated: `ContentWriteLockBusyError` (contention, or
|
|
130
|
+
* a compromise reported after `fn` completed) and `OccWriteConflictError`.
|
|
131
|
+
*/
|
|
132
|
+
export async function withBranchSchemaLock(branchRoot, fn, contentWriteLockWaitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS) {
|
|
133
|
+
const schemaLockPath = path.join(path.resolve(branchRoot), '.canopy-meta', 'schema');
|
|
134
|
+
return withContentWriteLock(branchRoot, () => withLock(schemaLockPath, () => withOccFileLock(schemaLockPath, fn)), contentWriteLockWaitMs);
|
|
135
|
+
}
|
|
115
136
|
export class SchemaOps {
|
|
116
|
-
constructor(contentRoot, entrySchemaRegistry, services, branchRoot) {
|
|
137
|
+
constructor(contentRoot, entrySchemaRegistry, services, branchRoot, options = {}) {
|
|
117
138
|
this.contentRoot = contentRoot;
|
|
118
139
|
this.entrySchemaRegistry = entrySchemaRegistry;
|
|
119
140
|
this.services = services;
|
|
@@ -130,33 +151,35 @@ export class SchemaOps {
|
|
|
130
151
|
.relative(this.branchRoot, resolvedContentRoot)
|
|
131
152
|
.split(path.sep)
|
|
132
153
|
.join('/');
|
|
133
|
-
this.
|
|
154
|
+
this.contentWriteLockWaitMs =
|
|
155
|
+
options.contentWriteLockWaitMs ?? DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS;
|
|
134
156
|
}
|
|
135
157
|
/**
|
|
136
|
-
* Serialize an entire read-modify-write schema mutation behind
|
|
137
|
-
*
|
|
138
|
-
* ({@link withLock}
|
|
139
|
-
* (
|
|
140
|
-
* exclusion, immune to NFS attribute caching), same structure as
|
|
141
|
-
* `BranchMetadataFileManager.save()` in branch-metadata.ts.
|
|
158
|
+
* Serialize an entire read-modify-write schema mutation behind
|
|
159
|
+
* {@link withBranchSchemaLock}: the content-write lock, then the surrogate
|
|
160
|
+
* ({@link withLock} wrapping {@link withOccFileLock}, as in
|
|
161
|
+
* `BranchMetadataFileManager.save()`).
|
|
142
162
|
*
|
|
143
163
|
* NOT re-entrant — callers must never invoke this from inside a callback
|
|
144
|
-
* already running under it, or
|
|
145
|
-
*
|
|
146
|
-
*
|
|
164
|
+
* already running under it, or the nested acquisition fails busy; that's
|
|
165
|
+
* why `updateOrderInner` calls `updateCollectionInner` directly instead of
|
|
166
|
+
* the public `updateCollection`.
|
|
147
167
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* branch-metadata.ts's): a call that passes this check can still race a
|
|
155
|
-
* `deleteBranch` `rm` starting moments later — not closed here either,
|
|
156
|
-
* since `deleteBranch` never takes this lock (see the module doc comment).
|
|
168
|
+
* `branchRoot` can be removed by a concurrent `deleteBranch` between the
|
|
169
|
+
* caller resolving its BranchContext and this call's lock acquisition. The
|
|
170
|
+
* stat below fails fast before any lock; past it, neither lock recreates a
|
|
171
|
+
* missing root (each creates only its own directory), so a later removal
|
|
172
|
+
* fails the acquisition, and any failure on a vanished root is reported as
|
|
173
|
+
* `SchemaStoreBusyError` below.
|
|
157
174
|
*
|
|
158
175
|
* Translation happens ONLY at this boundary: inner code always sees the
|
|
159
|
-
* raw {@link OccWriteConflictError} bubble up here untranslated
|
|
176
|
+
* raw {@link OccWriteConflictError} bubble up here untranslated, and a
|
|
177
|
+
* {@link ContentWriteLockBusyError} becomes a `SchemaStoreBusyError` with
|
|
178
|
+
* its message, so every caller's 409 mapping covers both.
|
|
179
|
+
*
|
|
180
|
+
* A content-lock compromise is reported after `fn` completed, so the
|
|
181
|
+
* mutation is on disk while the caller's post-lock `invalidateSchemaCache()`
|
|
182
|
+
* never runs; this runs it before throwing.
|
|
160
183
|
*/
|
|
161
184
|
async withSchemaLock(fn) {
|
|
162
185
|
try {
|
|
@@ -168,13 +191,36 @@ export class SchemaOps {
|
|
|
168
191
|
}
|
|
169
192
|
throw err;
|
|
170
193
|
}
|
|
194
|
+
let landed = false;
|
|
171
195
|
try {
|
|
172
|
-
return await
|
|
196
|
+
return await withBranchSchemaLock(this.branchRoot, async () => {
|
|
197
|
+
const result = await fn();
|
|
198
|
+
landed = true;
|
|
199
|
+
return result;
|
|
200
|
+
}, this.contentWriteLockWaitMs);
|
|
173
201
|
}
|
|
174
202
|
catch (err) {
|
|
175
203
|
if (err instanceof OccWriteConflictError) {
|
|
176
204
|
throw new SchemaStoreBusyError();
|
|
177
205
|
}
|
|
206
|
+
if (err instanceof ContentWriteLockBusyError) {
|
|
207
|
+
if (landed) {
|
|
208
|
+
await this.invalidateSchemaCache().catch((invalidateErr) => {
|
|
209
|
+
log.warn('schema-cache', 'Invalidation after a lost content-write lock failed', {
|
|
210
|
+
branchRoot: this.branchRoot,
|
|
211
|
+
error: getErrorMessage(invalidateErr),
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
throw new SchemaStoreBusyError(err.outcome === 'unknown'
|
|
216
|
+
? 'This branch was being synced while the schema change was written, so it may or may not have been recorded. Reload before changing the schema again.'
|
|
217
|
+
: err.message, err.outcome);
|
|
218
|
+
}
|
|
219
|
+
// A delete that removes the branch root mid-operation fails it wherever it is (a lock's
|
|
220
|
+
// mkdir ENOENT, a vanished collection meta); the deletion is the cause, whatever the symptom.
|
|
221
|
+
const rootGone = await fs.stat(this.branchRoot).then(() => false, (statErr) => isNotFoundError(statErr));
|
|
222
|
+
if (rootGone)
|
|
223
|
+
throw new SchemaStoreBusyError('Branch no longer exists');
|
|
178
224
|
throw err;
|
|
179
225
|
}
|
|
180
226
|
}
|
|
@@ -461,7 +507,7 @@ export class SchemaOps {
|
|
|
461
507
|
* Body of updateCollection, holding the schema lock for its full
|
|
462
508
|
* read-modify-write. Called directly (not via the public `updateCollection`)
|
|
463
509
|
* by `updateOrderInner` for non-root collections — `withSchemaLock` is NOT
|
|
464
|
-
* re-entrant, so going through the public method there would
|
|
510
|
+
* re-entrant, so going through the public method there would fail busy.
|
|
465
511
|
*
|
|
466
512
|
* `collectionPath` here is always already normalized (content-root prefix
|
|
467
513
|
* stripped, if it had one) by whichever public method reached this — either
|
package/dist/server.d.ts
CHANGED
|
@@ -126,32 +126,87 @@ export { createAssetStore } from './assets/factory.js';
|
|
|
126
126
|
/**
|
|
127
127
|
* The five S3/local bucket-prefix strings (`asset-originals/`, `asset-staging/`, `asset-meta/`,
|
|
128
128
|
* `assets/`, `assets/t/`). Exported so server-only consumers outside this package — notably the
|
|
129
|
-
*
|
|
129
|
+
* lazy transform Lambda (`packages/canopycms-cdk/lambda/asset-transform`) — build and parse
|
|
130
130
|
* asset keys through the same constants `S3AssetStore` uses rather than duplicating the strings.
|
|
131
131
|
*/
|
|
132
132
|
export { ASSET_PREFIXES, type AssetPrefixes } from './assets/keys.js';
|
|
133
|
-
/** Asset metadata sidecar shape (`asset-meta/{hash32}.json`), written by the finalize pipeline and read by
|
|
133
|
+
/** Asset metadata sidecar shape (`asset-meta/{hash32}.json`), written by the finalize pipeline and read by `storeTransform`. */
|
|
134
134
|
export type { AssetMeta } from './assets/types.js';
|
|
135
135
|
/**
|
|
136
136
|
* Parse the three path segments after `assets/t/` (`{directives}/{hash32}/{slug}.{ext}`) into a
|
|
137
|
-
* validated `TransformDirectives` set, or a structured parse error
|
|
138
|
-
*
|
|
139
|
-
* URL grammar is defined in exactly one place.
|
|
137
|
+
* validated `TransformDirectives` set, or a structured parse error, so the URL grammar is
|
|
138
|
+
* defined in exactly one place.
|
|
140
139
|
*/
|
|
141
140
|
export { parseTransformPath } from './assets/transform-directives.js';
|
|
142
141
|
export type { ParsedTransformPath, ParseTransformPathResult } from './assets/transform-directives.js';
|
|
143
142
|
/**
|
|
144
143
|
* Canonical string form of a `TransformDirectives` set — the cache key every transform output is
|
|
145
144
|
* stored under (`assets/t/{formatDirectives(...)}/{hash32}/{slug}.{ext}`). Equivalent directive
|
|
146
|
-
* sets (different key order or float formatting) always format to the same string.
|
|
147
|
-
* prod transform Lambda so its writes agree with the dev-mode emulation and `assetUrl()`.
|
|
145
|
+
* sets (different key order or float formatting) always format to the same string.
|
|
148
146
|
*/
|
|
149
147
|
export { formatDirectives } from './assets/transform-directives.js';
|
|
148
|
+
/**
|
|
149
|
+
* Parse `assets/t/` path segments and resolve them to the canonical spelling their output is
|
|
150
|
+
* stored under, with directives re-parsed from that spelling. The lazy transform Lambda 301s a
|
|
151
|
+
* non-canonical request to it; the authenticated raw route serves the canonical bytes.
|
|
152
|
+
*/
|
|
153
|
+
export { canonicalizeTransformPath } from './assets/transform-directives.js';
|
|
154
|
+
export type { CanonicalTransformPathResult, TransformWidthPolicy, } from './assets/transform-directives.js';
|
|
150
155
|
/**
|
|
151
156
|
* Apply a parsed `TransformDirectives` set to source image bytes with sharp:
|
|
152
157
|
* resize/format/quality/crop, EXIF stripped on every re-encode, identity included. Server-only
|
|
153
|
-
* (sharp);
|
|
154
|
-
* Lambda.
|
|
158
|
+
* (sharp); `storeTransform` below wraps it for every caller that stores the output.
|
|
155
159
|
*/
|
|
156
160
|
export { applyTransform } from './assets/transform.js';
|
|
157
161
|
export type { ApplyTransformInput, TransformResult } from './assets/transform.js';
|
|
162
|
+
/**
|
|
163
|
+
* Compute the transform a canonical `assets/t/` path names and store it under that key: meta
|
|
164
|
+
* lookup, raster-only, slug and ext checks, then `applyTransform`. Shared by the authenticated raw
|
|
165
|
+
* route, `materialize-assets` and the lazy transform Lambda. A 404's `error` names hashes and
|
|
166
|
+
* slugs, so a caller answering a browser replaces it.
|
|
167
|
+
*/
|
|
168
|
+
export { storeTransform, type StoreTransformResult } from './assets/materialize.js';
|
|
169
|
+
/** The `Cache-Control` every stored transform output carries: content-addressed, so immutable. */
|
|
170
|
+
export { TRANSFORM_CACHE_CONTROL } from './assets/materialize.js';
|
|
171
|
+
/** The object tag the lazy transform Lambda's outputs carry, which its `assets/t/` expiry filters on. */
|
|
172
|
+
export { LAZY_TRANSFORM_TAG } from './assets/materialize.js';
|
|
173
|
+
/**
|
|
174
|
+
* Make every `assets/t/` key a build references exist in `store` (transforming only the missing
|
|
175
|
+
* ones from the stored originals) and check every referenced svg/pdf key exists. Under an
|
|
176
|
+
* `outputPrefix`, keys are written beneath it, and those production already stores are copied. A
|
|
177
|
+
* release job that bundles its own tool calls this with a store from `createAssetStore` and gates
|
|
178
|
+
* on the returned report; no site config is involved. Needs sharp only when something is missing.
|
|
179
|
+
*/
|
|
180
|
+
export { materializeAssets } from './assets/materialize.js';
|
|
181
|
+
/** The asset store `createAssetStore` returns and `materializeAssets` takes. */
|
|
182
|
+
export type { AssetStore } from './assets/types.js';
|
|
183
|
+
/** What a create-only store write did: `created`, or `already-exists` (the stored object was kept). */
|
|
184
|
+
export type { CreateOnlyResult } from './assets/types.js';
|
|
185
|
+
/** Thrown by `materializeAssets` when sharp cannot load and keys are missing, so none can be produced. */
|
|
186
|
+
export { SharpUnavailableError } from './assets/materialize.js';
|
|
187
|
+
/**
|
|
188
|
+
* Thrown by `materializeAssets` before any store request when `outputPrefix` is not a relative,
|
|
189
|
+
* `/`-terminated run of `[A-Za-z0-9._-]` segments outside every canopy prefix.
|
|
190
|
+
*/
|
|
191
|
+
export { InvalidOutputPrefixError } from './assets/materialize.js';
|
|
192
|
+
/** The `schemaVersion` carried by every `MaterializeReport`. */
|
|
193
|
+
export { MATERIALIZE_REPORT_SCHEMA_VERSION } from './assets/materialize.js';
|
|
194
|
+
/** Options for `materializeAssets`: the store, the referenced keys, an output prefix, and concurrency/retry limits. */
|
|
195
|
+
export type { MaterializeOptions } from './assets/materialize.js';
|
|
196
|
+
/** What `materializeAssets` returns: a versioned summary and one result per key, sorted by key. */
|
|
197
|
+
export type { MaterializeReport } from './assets/materialize.js';
|
|
198
|
+
/** One key's outcome in a `MaterializeReport`: `existed`, `created`, `copied` or `failed` (`content` or `store`). */
|
|
199
|
+
export type { MaterializeResult } from './assets/materialize.js';
|
|
200
|
+
/** One key a build references, with the pages and output files that reference it. */
|
|
201
|
+
export type { MaterializeTarget } from './assets/materialize.js';
|
|
202
|
+
/**
|
|
203
|
+
* Scan a static build's output for `/assets/…` URLs and write `canopy-asset-refs.json` into it.
|
|
204
|
+
* Throws `AssetRefsError` for a URL no stored object can answer (non-canonical or malformed).
|
|
205
|
+
*/
|
|
206
|
+
export { collectAssetRefs, type CollectAssetRefsResult } from './build/asset-refs.js';
|
|
207
|
+
/** Read and validate a `canopy-asset-refs.json`; throws when it is unreadable or malformed. */
|
|
208
|
+
export { readAssetRefsFile } from './build/asset-refs.js';
|
|
209
|
+
/** Thrown by `collectAssetRefs`; `problems` lists each offending file and URL. */
|
|
210
|
+
export { AssetRefsError } from './build/asset-refs.js';
|
|
211
|
+
/** The shape of `canopy-asset-refs.json`: the transform and static keys a build references. */
|
|
212
|
+
export type { AssetRefsFile } from './build/asset-refs.js';
|
package/dist/server.js
CHANGED
|
@@ -115,28 +115,68 @@ export { createAssetStore } from './assets/factory.js';
|
|
|
115
115
|
/**
|
|
116
116
|
* The five S3/local bucket-prefix strings (`asset-originals/`, `asset-staging/`, `asset-meta/`,
|
|
117
117
|
* `assets/`, `assets/t/`). Exported so server-only consumers outside this package — notably the
|
|
118
|
-
*
|
|
118
|
+
* lazy transform Lambda (`packages/canopycms-cdk/lambda/asset-transform`) — build and parse
|
|
119
119
|
* asset keys through the same constants `S3AssetStore` uses rather than duplicating the strings.
|
|
120
120
|
*/
|
|
121
121
|
export { ASSET_PREFIXES } from './assets/keys.js';
|
|
122
122
|
/**
|
|
123
123
|
* Parse the three path segments after `assets/t/` (`{directives}/{hash32}/{slug}.{ext}`) into a
|
|
124
|
-
* validated `TransformDirectives` set, or a structured parse error
|
|
125
|
-
*
|
|
126
|
-
* URL grammar is defined in exactly one place.
|
|
124
|
+
* validated `TransformDirectives` set, or a structured parse error, so the URL grammar is
|
|
125
|
+
* defined in exactly one place.
|
|
127
126
|
*/
|
|
128
127
|
export { parseTransformPath } from './assets/transform-directives.js';
|
|
129
128
|
/**
|
|
130
129
|
* Canonical string form of a `TransformDirectives` set — the cache key every transform output is
|
|
131
130
|
* stored under (`assets/t/{formatDirectives(...)}/{hash32}/{slug}.{ext}`). Equivalent directive
|
|
132
|
-
* sets (different key order or float formatting) always format to the same string.
|
|
133
|
-
* prod transform Lambda so its writes agree with the dev-mode emulation and `assetUrl()`.
|
|
131
|
+
* sets (different key order or float formatting) always format to the same string.
|
|
134
132
|
*/
|
|
135
133
|
export { formatDirectives } from './assets/transform-directives.js';
|
|
134
|
+
/**
|
|
135
|
+
* Parse `assets/t/` path segments and resolve them to the canonical spelling their output is
|
|
136
|
+
* stored under, with directives re-parsed from that spelling. The lazy transform Lambda 301s a
|
|
137
|
+
* non-canonical request to it; the authenticated raw route serves the canonical bytes.
|
|
138
|
+
*/
|
|
139
|
+
export { canonicalizeTransformPath } from './assets/transform-directives.js';
|
|
136
140
|
/**
|
|
137
141
|
* Apply a parsed `TransformDirectives` set to source image bytes with sharp:
|
|
138
142
|
* resize/format/quality/crop, EXIF stripped on every re-encode, identity included. Server-only
|
|
139
|
-
* (sharp);
|
|
140
|
-
* Lambda.
|
|
143
|
+
* (sharp); `storeTransform` below wraps it for every caller that stores the output.
|
|
141
144
|
*/
|
|
142
145
|
export { applyTransform } from './assets/transform.js';
|
|
146
|
+
/**
|
|
147
|
+
* Compute the transform a canonical `assets/t/` path names and store it under that key: meta
|
|
148
|
+
* lookup, raster-only, slug and ext checks, then `applyTransform`. Shared by the authenticated raw
|
|
149
|
+
* route, `materialize-assets` and the lazy transform Lambda. A 404's `error` names hashes and
|
|
150
|
+
* slugs, so a caller answering a browser replaces it.
|
|
151
|
+
*/
|
|
152
|
+
export { storeTransform } from './assets/materialize.js';
|
|
153
|
+
/** The `Cache-Control` every stored transform output carries: content-addressed, so immutable. */
|
|
154
|
+
export { TRANSFORM_CACHE_CONTROL } from './assets/materialize.js';
|
|
155
|
+
/** The object tag the lazy transform Lambda's outputs carry, which its `assets/t/` expiry filters on. */
|
|
156
|
+
export { LAZY_TRANSFORM_TAG } from './assets/materialize.js';
|
|
157
|
+
/**
|
|
158
|
+
* Make every `assets/t/` key a build references exist in `store` (transforming only the missing
|
|
159
|
+
* ones from the stored originals) and check every referenced svg/pdf key exists. Under an
|
|
160
|
+
* `outputPrefix`, keys are written beneath it, and those production already stores are copied. A
|
|
161
|
+
* release job that bundles its own tool calls this with a store from `createAssetStore` and gates
|
|
162
|
+
* on the returned report; no site config is involved. Needs sharp only when something is missing.
|
|
163
|
+
*/
|
|
164
|
+
export { materializeAssets } from './assets/materialize.js';
|
|
165
|
+
/** Thrown by `materializeAssets` when sharp cannot load and keys are missing, so none can be produced. */
|
|
166
|
+
export { SharpUnavailableError } from './assets/materialize.js';
|
|
167
|
+
/**
|
|
168
|
+
* Thrown by `materializeAssets` before any store request when `outputPrefix` is not a relative,
|
|
169
|
+
* `/`-terminated run of `[A-Za-z0-9._-]` segments outside every canopy prefix.
|
|
170
|
+
*/
|
|
171
|
+
export { InvalidOutputPrefixError } from './assets/materialize.js';
|
|
172
|
+
/** The `schemaVersion` carried by every `MaterializeReport`. */
|
|
173
|
+
export { MATERIALIZE_REPORT_SCHEMA_VERSION } from './assets/materialize.js';
|
|
174
|
+
/**
|
|
175
|
+
* Scan a static build's output for `/assets/…` URLs and write `canopy-asset-refs.json` into it.
|
|
176
|
+
* Throws `AssetRefsError` for a URL no stored object can answer (non-canonical or malformed).
|
|
177
|
+
*/
|
|
178
|
+
export { collectAssetRefs } from './build/asset-refs.js';
|
|
179
|
+
/** Read and validate a `canopy-asset-refs.json`; throws when it is unreadable or malformed. */
|
|
180
|
+
export { readAssetRefsFile } from './build/asset-refs.js';
|
|
181
|
+
/** Thrown by `collectAssetRefs`; `problems` lists each offending file and URL. */
|
|
182
|
+
export { AssetRefsError } from './build/asset-refs.js';
|
package/dist/services.d.ts
CHANGED
|
@@ -24,11 +24,16 @@ import { GitManager } from './git-manager.js';
|
|
|
24
24
|
import { BranchRegistry } from './branch-registry.js';
|
|
25
25
|
import { type GitHubService } from './github-service.js';
|
|
26
26
|
import { BranchSchemaCache } from './branch-schema-cache.js';
|
|
27
|
+
import { type SubmissionEditor } from './submission-attribution.js';
|
|
27
28
|
/**
|
|
28
29
|
* Bootstrap admin IDs from the environment. These users are treated as Admins
|
|
29
30
|
* whatever their group membership.
|
|
30
31
|
*/
|
|
31
32
|
export declare const getBootstrapAdminIds: () => Set<string>;
|
|
33
|
+
export interface SubmitBranchResult {
|
|
34
|
+
/** Repo-relative paths the branch changes, excluding canopycms runtime metadata. */
|
|
35
|
+
changedPaths: string[];
|
|
36
|
+
}
|
|
32
37
|
export interface CanopyServices {
|
|
33
38
|
config: CanopyConfig;
|
|
34
39
|
/**
|
|
@@ -58,27 +63,34 @@ export interface CanopyServices {
|
|
|
58
63
|
githubService?: GitHubService;
|
|
59
64
|
/** Bootstrap admin user IDs that are always treated as Admins */
|
|
60
65
|
bootstrapAdminIds: Set<string>;
|
|
61
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Commit files to git with automatic author handling. Holds the branch's
|
|
68
|
+
* content-write lock; rejects with `ContentWriteLockBusyError` when busy.
|
|
69
|
+
*/
|
|
62
70
|
commitFiles: (options: {
|
|
63
71
|
context: BranchContext;
|
|
64
72
|
files: string | string[];
|
|
65
73
|
message: string;
|
|
66
74
|
}) => Promise<void>;
|
|
67
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Submit branch: commit all changes (with the submitter's trailers) and push
|
|
77
|
+
* to remote. Resolves with every path the branch changes against its base.
|
|
78
|
+
* Holds the branch's content-write lock; rejects with
|
|
79
|
+
* `ContentWriteLockBusyError` when busy.
|
|
80
|
+
*/
|
|
68
81
|
submitBranch: (options: {
|
|
69
82
|
context: BranchContext;
|
|
83
|
+
submitter?: SubmissionEditor;
|
|
70
84
|
message?: string;
|
|
71
|
-
}) => Promise<
|
|
72
|
-
/** Commit to settings branch (for permissions/groups)
|
|
85
|
+
}) => Promise<SubmitBranchResult>;
|
|
86
|
+
/** Commit to the settings branch (for permissions/groups) and push it; never opens a PR */
|
|
73
87
|
commitToSettingsBranch: (options: {
|
|
74
88
|
branchRoot: string;
|
|
75
89
|
files: string | string[];
|
|
76
90
|
message: string;
|
|
77
|
-
createPR?: boolean;
|
|
78
91
|
}) => Promise<{
|
|
79
92
|
committed: boolean;
|
|
80
93
|
pushed: boolean;
|
|
81
|
-
prUrl?: string;
|
|
82
94
|
error?: string;
|
|
83
95
|
syncStatus?: 'pending-sync' | 'synced' | 'sync-failed';
|
|
84
96
|
}>;
|