canopycms 0.1.0-int.115 → 0.2.0-int.119
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +370 -231
- package/README.md +0 -1
- package/dist/ai/references.js +3 -1
- package/dist/api/assets.d.ts +27 -7
- package/dist/api/assets.js +99 -38
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch.js +1 -1
- package/dist/api/client.d.ts +5 -3
- package/dist/api/client.js +7 -3
- package/dist/api/content.js +1 -1
- package/dist/api/entries-constants.d.ts +1 -1
- package/dist/api/entries-constants.js +1 -1
- package/dist/api/entries.d.ts +2 -2
- package/dist/api/index.d.ts +1 -1
- package/dist/api/permissions.d.ts +2 -2
- package/dist/api/types.d.ts +3 -2
- package/dist/assets/asset-prefixes.d.ts +4 -1
- package/dist/assets/asset-prefixes.js +3 -1
- package/dist/assets/finalize.js +14 -1
- package/dist/assets/keys.d.ts +4 -1
- package/dist/assets/keys.js +4 -1
- package/dist/assets/store-local.d.ts +18 -9
- package/dist/assets/store-local.js +58 -26
- package/dist/assets/store-s3.d.ts +19 -4
- package/dist/assets/store-s3.js +56 -13
- package/dist/assets/types.d.ts +26 -2
- package/dist/cli/cli.js +128 -48
- package/dist/cli/generate-ai-content.js +7 -5
- package/dist/cli/init.js +4 -4
- package/dist/client.d.ts +0 -1
- package/dist/client.js +0 -1
- package/dist/config/schemas/config.d.ts +0 -5
- package/dist/config/schemas/config.js +0 -1
- package/dist/config/types.d.ts +0 -1
- package/dist/content-index-generation.d.ts +1 -1
- package/dist/content-index-generation.js +1 -1
- package/dist/content-listing.js +1 -1
- package/dist/content-reader.js +1 -1
- package/dist/content-store.d.ts +1 -1
- package/dist/content-store.js +1 -1
- package/dist/dev-content-watcher.d.ts +1 -1
- package/dist/dev-content-watcher.js +1 -1
- package/dist/editor/BranchManager.d.ts +2 -0
- package/dist/editor/BranchManager.js +6 -4
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -2
- package/dist/editor/CommentsPanel.js +69 -34
- package/dist/editor/Editor.d.ts +3 -2
- package/dist/editor/Editor.js +361 -191
- package/dist/editor/EditorAuthGate.js +8 -3
- package/dist/editor/EntryNavigator.d.ts +18 -1
- package/dist/editor/EntryNavigator.js +82 -18
- package/dist/editor/FormHeader.d.ts +2 -1
- package/dist/editor/FormHeader.js +10 -6
- package/dist/editor/FormRenderer.d.ts +7 -2
- package/dist/editor/FormRenderer.js +51 -16
- package/dist/editor/PreviewFrame.d.ts +15 -2
- package/dist/editor/PreviewFrame.js +22 -5
- package/dist/editor/PreviewPane.d.ts +61 -0
- package/dist/editor/PreviewPane.js +194 -0
- package/dist/editor/PreviewToolbarTools.d.ts +30 -0
- package/dist/editor/PreviewToolbarTools.js +81 -0
- package/dist/editor/branch-status.d.ts +6 -0
- package/dist/editor/branch-status.js +10 -0
- package/dist/editor/comments/BranchComments.d.ts +2 -4
- package/dist/editor/comments/BranchComments.js +7 -11
- package/dist/editor/comments/ThreadCarousel.d.ts +2 -8
- package/dist/editor/comments/ThreadCarousel.js +4 -49
- package/dist/editor/comments/ThreadReplyControls.js +5 -2
- package/dist/editor/comments/form-comments.js +2 -1
- package/dist/editor/components/BranchLockBanner.d.ts +24 -0
- package/dist/editor/components/BranchLockBanner.js +43 -0
- package/dist/editor/components/BranchPill.d.ts +33 -0
- package/dist/editor/components/BranchPill.js +31 -0
- package/dist/editor/components/BranchesDrawer.js +3 -3
- package/dist/editor/components/ConfirmDeleteModal.d.ts +3 -1
- package/dist/editor/components/ConfirmDeleteModal.js +3 -2
- package/dist/editor/components/EditorHeader.d.ts +47 -109
- package/dist/editor/components/EditorHeader.js +128 -148
- package/dist/editor/components/EditorRail.d.ts +48 -0
- package/dist/editor/components/EditorRail.js +64 -0
- package/dist/editor/components/EntryBreadcrumb.d.ts +32 -0
- package/dist/editor/components/EntryBreadcrumb.js +53 -0
- package/dist/editor/components/QuickOpenPalette.d.ts +8 -2
- package/dist/editor/components/QuickOpenPalette.js +8 -2
- package/dist/editor/components/RenameEntryModal.d.ts +8 -1
- package/dist/editor/components/RenameEntryModal.js +21 -5
- package/dist/editor/components/SaveAllResult.d.ts +25 -0
- package/dist/editor/components/SaveAllResult.js +86 -0
- package/dist/editor/components/SaveSplitButton.d.ts +31 -0
- package/dist/editor/components/SaveSplitButton.js +121 -0
- package/dist/editor/components/StagedChangesDrawer.d.ts +0 -1
- package/dist/editor/components/StagedChangesDrawer.js +5 -4
- package/dist/editor/components/UndoToastMessage.d.ts +6 -5
- package/dist/editor/components/UndoToastMessage.js +6 -4
- package/dist/editor/components/UnsavedMark.d.ts +9 -0
- package/dist/editor/components/UnsavedMark.js +20 -0
- package/dist/editor/components/UserBadge.js +13 -11
- package/dist/editor/components/index.d.ts +2 -2
- package/dist/editor/components/index.js +2 -2
- package/dist/editor/context/EditorStateContext.d.ts +4 -4
- package/dist/editor/context/EditorStateContext.js +4 -4
- package/dist/editor/copy.d.ts +7 -0
- package/dist/editor/copy.js +7 -0
- package/dist/editor/editor-utils.d.ts +52 -7
- package/dist/editor/editor-utils.js +122 -30
- package/dist/editor/entry-search.d.ts +4 -2
- package/dist/editor/entry-search.js +2 -3
- package/dist/editor/fields/FieldLabel.d.ts +1 -1
- package/dist/editor/fields/MarkdownField.js +3 -0
- package/dist/editor/fields/ObjectField.d.ts +13 -2
- package/dist/editor/fields/ObjectField.js +38 -9
- package/dist/editor/fields/entry-link/EntryLinkContext.d.ts +2 -0
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -1
- package/dist/editor/group-manager/index.js +1 -1
- package/dist/editor/hooks/save-all.d.ts +68 -0
- package/dist/editor/hooks/save-all.js +61 -0
- package/dist/editor/hooks/useCommentSystem.d.ts +2 -0
- package/dist/editor/hooks/useCommentSystem.js +3 -0
- package/dist/editor/hooks/useDraftManager.d.ts +24 -1
- package/dist/editor/hooks/useDraftManager.js +297 -16
- package/dist/editor/hooks/useEditorLayout.d.ts +19 -11
- package/dist/editor/hooks/useEditorLayout.js +18 -25
- package/dist/editor/hooks/useEntryManager.d.ts +6 -0
- package/dist/editor/hooks/useEntryManager.js +37 -12
- package/dist/editor/media/AssetCard.d.ts +6 -3
- package/dist/editor/media/AssetCard.js +20 -7
- package/dist/editor/media/AssetListRow.d.ts +9 -4
- package/dist/editor/media/AssetListRow.js +14 -7
- package/dist/editor/media/MediaLibrary.d.ts +1 -1
- package/dist/editor/media/MediaLibrary.js +5 -4
- package/dist/editor/media/MediaLibraryBody.d.ts +1 -1
- package/dist/editor/media/MediaLibraryBody.js +65 -37
- package/dist/editor/media/useAssetLibrary.d.ts +6 -3
- package/dist/editor/media/useAssetLibrary.js +17 -10
- package/dist/editor/pane-header.d.ts +6 -0
- package/dist/editor/pane-header.js +6 -0
- package/dist/editor/permission-manager/index.js +1 -1
- package/dist/editor/preview-bridge.d.ts +16 -0
- package/dist/editor/preview-bridge.js +12 -3
- package/dist/editor/preview-path.d.ts +22 -3
- package/dist/editor/preview-path.js +92 -5
- package/dist/editor/preview-scale.d.ts +27 -0
- package/dist/editor/preview-scale.js +34 -0
- package/dist/editor/shell/ActivityRail.d.ts +35 -0
- package/dist/editor/shell/ActivityRail.js +91 -0
- package/dist/editor/shell/EditorShell.d.ts +32 -0
- package/dist/editor/shell/EditorShell.js +47 -0
- package/dist/editor/shell/OverlayHeader.d.ts +19 -0
- package/dist/editor/shell/OverlayHeader.js +15 -0
- package/dist/editor/shell/PaneLayout.d.ts +33 -0
- package/dist/editor/shell/PaneLayout.js +171 -0
- package/dist/editor/shell/SidePanelHost.d.ts +37 -0
- package/dist/editor/shell/SidePanelHost.js +221 -0
- package/dist/editor/shell/TopBar.d.ts +12 -0
- package/dist/editor/shell/TopBar.js +15 -0
- package/dist/editor/shell/host-chrome.d.ts +16 -0
- package/dist/editor/shell/host-chrome.js +69 -0
- package/dist/editor/theme.js +20 -3
- package/dist/editor/undo-removal.d.ts +19 -10
- package/dist/editor/undo-removal.js +44 -16
- package/dist/git-manager.d.ts +1 -1
- package/dist/git-manager.js +2 -2
- package/dist/operating-mode/types.d.ts +1 -1
- package/dist/resolve-canopy-user.d.ts +1 -1
- package/dist/resolve-canopy-user.js +1 -1
- package/dist/schema/resolver.d.ts +1 -1
- package/dist/schema/resolver.js +1 -1
- package/dist/schema/schema-store.d.ts +1 -1
- package/dist/schema/schema-store.js +2 -2
- package/dist/url-collision.js +3 -3
- package/dist/url-exclusivity-fixtures.d.ts +1 -1
- package/dist/url-exclusivity-fixtures.js +1 -1
- package/dist/utils/content-write-lock.d.ts +1 -1
- package/dist/utils/content-write-lock.js +1 -1
- package/dist/utils/debug.d.ts +1 -1
- package/dist/utils/debug.js +5 -3
- package/dist/utils/git.d.ts +4 -0
- package/dist/utils/git.js +27 -5
- package/dist/validation/block-structural-keys.d.ts +1 -1
- package/dist/validation/block-structural-keys.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/cms-worker.d.ts +1 -1
- package/dist/worker/cms-worker.js +2 -2
- package/dist/worker/github-auth.d.ts +1 -1
- package/dist/worker/github-auth.js +1 -1
- package/dist/worker/github-gateway-wire.d.ts +2 -1
- package/dist/worker/github-gateway-wire.js +7 -3
- package/dist/worker/github-gateway.js +1 -1
- package/dist/worker/github-mirror.d.ts +2 -2
- package/dist/worker/github-mirror.js +2 -2
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/self-checks.d.ts +1 -1
- package/dist/worker/shared-repo-git.js +1 -1
- package/package.json +5 -4
- package/dist/editor/EditorPanes.d.ts +0 -20
- package/dist/editor/EditorPanes.js +0 -124
- package/dist/editor/components/EditorFooter.d.ts +0 -11
- package/dist/editor/components/EditorFooter.js +0 -9
- package/dist/editor/components/EditorSidebar.d.ts +0 -70
- package/dist/editor/components/EditorSidebar.js +0 -50
package/README.md
CHANGED
package/dist/ai/references.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getDefaultEntryType } from '../content-store.js';
|
|
|
10
10
|
import { extractEntryTypeFromFilename } from '../content-id-index.js';
|
|
11
11
|
import { RESTRICTED_REFERENCE_MARKER } from '../entry-schema.js';
|
|
12
12
|
import { extractEntryLinkIds } from '../entry-link-resolver.js';
|
|
13
|
+
import { trimSlashes } from '../paths/normalize.js';
|
|
13
14
|
import { getErrorMessage } from '../utils/error.js';
|
|
14
15
|
import { flattenGroupFields } from '../utils/flatten-group-fields.js';
|
|
15
16
|
import { resolveEntryTitle } from '../utils/title-field.js';
|
|
@@ -183,7 +184,8 @@ export async function decideReferenceLinks(referenceLink, entries, subjectOf, en
|
|
|
183
184
|
* this export wrote for the target (`files`: content id to output path), under `mountPath`.
|
|
184
185
|
*/
|
|
185
186
|
export function createReferenceRendering(idIndex, flatSchema, entryLinkUrl, markdownCopies, linkFor) {
|
|
186
|
-
const
|
|
187
|
+
const mountSegment = trimSlashes(markdownCopies.mountPath);
|
|
188
|
+
const mount = mountSegment === '' ? '' : `/${mountSegment}`;
|
|
187
189
|
const entryTypeOf = (id) => {
|
|
188
190
|
const location = typeof id === 'string' ? idIndex.findById(id) : null;
|
|
189
191
|
if (!location?.collection)
|
package/dist/api/assets.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ApiResponse } from './types.js';
|
|
3
3
|
import type { RouteDefinition } from '../http/router.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { ListedAssetMeta, StagedUploadTarget } from '../assets/types.js';
|
|
5
5
|
/** An asset's persisted meta plus its computed, root-relative public URL. */
|
|
6
|
-
export type AssetRecord =
|
|
6
|
+
export type AssetRecord = ListedAssetMeta & {
|
|
7
7
|
src: string;
|
|
8
8
|
};
|
|
9
9
|
/** Response type for listing assets */
|
|
@@ -19,9 +19,9 @@ export type PresignAssetResponse = ApiResponse<{
|
|
|
19
19
|
export type FinalizeAssetResponse = ApiResponse<{
|
|
20
20
|
asset: AssetRecord;
|
|
21
21
|
}>;
|
|
22
|
-
/** Response type for
|
|
23
|
-
export type
|
|
24
|
-
|
|
22
|
+
/** Response type for removing a file from the library or restoring it: whether it is now hidden */
|
|
23
|
+
export type AssetVisibilityResponse = ApiResponse<{
|
|
24
|
+
hidden: boolean;
|
|
25
25
|
}>;
|
|
26
26
|
declare const presignAssetBodySchema: z.ZodObject<{
|
|
27
27
|
filename: z.ZodString;
|
|
@@ -48,6 +48,14 @@ declare const finalizeAssetBodySchema: z.ZodObject<{
|
|
|
48
48
|
stagingKey: string;
|
|
49
49
|
}>;
|
|
50
50
|
export type FinalizeAssetBody = z.infer<typeof finalizeAssetBodySchema>;
|
|
51
|
+
declare const assetKeyBodySchema: z.ZodObject<{
|
|
52
|
+
key: z.ZodString;
|
|
53
|
+
}, "strip", z.ZodTypeAny, {
|
|
54
|
+
key: string;
|
|
55
|
+
}, {
|
|
56
|
+
key: string;
|
|
57
|
+
}>;
|
|
58
|
+
export type AssetKeyBody = z.infer<typeof assetKeyBodySchema>;
|
|
51
59
|
/**
|
|
52
60
|
* Exported routes for router registration and client codegen.
|
|
53
61
|
*/
|
|
@@ -79,20 +87,32 @@ export declare const ASSET_ROUTES: {
|
|
|
79
87
|
readonly list: import("./route-builder.js").RouteDefinition<z.ZodObject<{
|
|
80
88
|
cursor: z.ZodOptional<z.ZodString>;
|
|
81
89
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
90
|
+
/** Admin only: also list files removed from the library, each marked `hidden: true`. */
|
|
91
|
+
includeHidden: z.ZodOptional<z.ZodEnum<["true", "false"]>>;
|
|
82
92
|
}, "strip", z.ZodTypeAny, {
|
|
83
93
|
cursor?: string | undefined;
|
|
84
94
|
limit?: number | undefined;
|
|
95
|
+
includeHidden?: "true" | "false" | undefined;
|
|
85
96
|
}, {
|
|
86
97
|
cursor?: string | undefined;
|
|
87
98
|
limit?: number | undefined;
|
|
99
|
+
includeHidden?: "true" | "false" | undefined;
|
|
88
100
|
}>, undefined, AssetsListResponse>;
|
|
89
|
-
readonly
|
|
101
|
+
readonly hide: import("./route-builder.js").RouteDefinition<undefined, z.ZodObject<{
|
|
102
|
+
key: z.ZodString;
|
|
103
|
+
}, "strip", z.ZodTypeAny, {
|
|
104
|
+
key: string;
|
|
105
|
+
}, {
|
|
106
|
+
key: string;
|
|
107
|
+
}>, AssetVisibilityResponse>;
|
|
108
|
+
readonly restore: import("./route-builder.js").RouteDefinition<undefined, z.ZodObject<{
|
|
90
109
|
key: z.ZodString;
|
|
91
110
|
}, "strip", z.ZodTypeAny, {
|
|
92
111
|
key: string;
|
|
93
112
|
}, {
|
|
94
113
|
key: string;
|
|
95
|
-
}>,
|
|
114
|
+
}>, AssetVisibilityResponse>;
|
|
115
|
+
readonly deleteGone: RouteDefinition;
|
|
96
116
|
};
|
|
97
117
|
/**
|
|
98
118
|
* GET /assets/raw/{key...} - see `rawAssetHandler` above for why this is
|
package/dist/api/assets.js
CHANGED
|
@@ -42,12 +42,14 @@ const finalizeAssetBodySchema = z.object({
|
|
|
42
42
|
const listAssetsParamsSchema = z.object({
|
|
43
43
|
cursor: z.string().optional(),
|
|
44
44
|
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
45
|
+
/** Admin only: also list files removed from the library, each marked `hidden: true`. */
|
|
46
|
+
includeHidden: z.enum(['true', 'false']).optional(),
|
|
45
47
|
});
|
|
46
48
|
/** hash32 is a sha-256 truncated to 32 hex chars (keys.ts's `hashBytes`) - never any other shape. */
|
|
47
49
|
const hash32Schema = z
|
|
48
50
|
.string()
|
|
49
51
|
.regex(/^[a-f0-9]{32}$/, 'key must be a 32-character lowercase hex string');
|
|
50
|
-
const
|
|
52
|
+
const assetKeyBodySchema = z.object({ key: hash32Schema });
|
|
51
53
|
/**
|
|
52
54
|
* Presign a direct (or proxied) upload target. Any authenticated user - there
|
|
53
55
|
* is no finer "editor" role, and upload needs to work for every non-admin
|
|
@@ -75,7 +77,8 @@ const presignAssetHandler = async (ctx, _req, body) => {
|
|
|
75
77
|
/**
|
|
76
78
|
* Finalize a staged upload: read the staged bytes, run the finalize pipeline
|
|
77
79
|
* (sniff/hash/dims/sanitize), write originals + meta, and return the result.
|
|
78
|
-
* Any authenticated user (same rationale as presign).
|
|
80
|
+
* Any authenticated user (same rationale as presign). Uploading a file that was
|
|
81
|
+
* removed from the library puts it back; `finalizeAsset` says why.
|
|
79
82
|
*/
|
|
80
83
|
const finalizeAssetHandler = async (ctx, req, body) => {
|
|
81
84
|
if (!ctx.assetStore)
|
|
@@ -168,49 +171,82 @@ const uploadProxiedHandler = async (ctx, req) => {
|
|
|
168
171
|
/**
|
|
169
172
|
* List assets - any authenticated user can list assets (key enumeration is
|
|
170
173
|
* accepted: unlisted != private, see .claude/future-tasks/resolved/assets-media-system.md).
|
|
174
|
+
* Files removed from the library are left out; only an admin may ask for them too.
|
|
171
175
|
*/
|
|
172
|
-
const listAssetsHandler = async (ctx,
|
|
176
|
+
const listAssetsHandler = async (ctx, req, params) => {
|
|
173
177
|
if (!ctx.assetStore)
|
|
174
178
|
return { ok: false, status: 501, error: 'Asset store not configured' };
|
|
179
|
+
const includeHidden = params.includeHidden === 'true';
|
|
180
|
+
if (includeHidden && !isAdmin(req.user.groups)) {
|
|
181
|
+
return { ok: false, status: 403, error: 'Only admins can list removed files' };
|
|
182
|
+
}
|
|
175
183
|
const { items, nextCursor } = await ctx.assetStore.listMeta({
|
|
176
184
|
cursor: params.cursor,
|
|
177
185
|
limit: params.limit,
|
|
186
|
+
includeHidden,
|
|
178
187
|
});
|
|
179
188
|
return { ok: true, status: 200, data: { assets: items.map(toAssetRecord), nextCursor } };
|
|
180
189
|
};
|
|
181
190
|
/**
|
|
182
|
-
*
|
|
183
|
-
* recorded `uploadedBy` is them. `
|
|
184
|
-
* Deletes the meta sidecar only - blobs are immortal until a future GC worker task (see
|
|
185
|
-
* .claude/future-tasks/resolved/assets-media-system.md).
|
|
191
|
+
* Who may remove a file from the library or restore it: an admin may act on any file; anyone else
|
|
192
|
+
* only on a file whose recorded `uploadedBy` is them. `null` when the caller may act.
|
|
186
193
|
*
|
|
187
|
-
* The
|
|
188
|
-
*
|
|
194
|
+
* The check lives here, not in a declarative guard, because it needs the asset's meta, which
|
|
195
|
+
* guards can't read. Two fail-closed choices:
|
|
189
196
|
* - Meta with no `uploadedBy` (the field is optional) is admin-only — defaulting to "anyone may
|
|
190
|
-
*
|
|
197
|
+
* act" would open every legacy asset to everyone.
|
|
191
198
|
* - A missing asset returns the same 403 as an unowned one for non-admins, so the endpoint isn't
|
|
192
|
-
* an existence oracle over a content-addressed keyspace.
|
|
199
|
+
* an existence oracle over a content-addressed keyspace. An admin, who can list every file,
|
|
200
|
+
* gets a 404, so no marker is ever written for a file that does not exist.
|
|
193
201
|
*
|
|
194
202
|
* `uploadedBy` records the FIRST uploader only: finalizeAsset dedups on content hash, so a second
|
|
195
|
-
* person uploading an identical file gains no
|
|
196
|
-
*
|
|
197
|
-
*
|
|
203
|
+
* person uploading an identical file gains no rights over it — benign (a 403 where they expected
|
|
204
|
+
* success, never the reverse; see safeinsights/canopycms#572).
|
|
205
|
+
*
|
|
206
|
+
* Coupled to the blob-GC follow-up in safeinsights/canopycms#575: this permission is safe because
|
|
207
|
+
* removal hides a file and destroys nothing, so nothing any branch or build references breaks. If
|
|
208
|
+
* GC ever deletes blobs, it needs a reference check of its own.
|
|
209
|
+
*/
|
|
210
|
+
async function visibilityRefusal(store, req, key, verb) {
|
|
211
|
+
const meta = await store.getMeta(key);
|
|
212
|
+
if (isAdmin(req.user.groups)) {
|
|
213
|
+
return meta ? null : { ok: false, status: 404, error: 'File not found' };
|
|
214
|
+
}
|
|
215
|
+
if (!meta?.uploadedBy || meta.uploadedBy !== req.user.userId) {
|
|
216
|
+
return { ok: false, status: 403, error: `You can only ${verb} files you uploaded` };
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Remove a file from the library: write its hidden marker and leave its meta, originals and
|
|
222
|
+
* public objects alone, so every URL and every transform, stored or not yet computed, keeps
|
|
223
|
+
* working for content on any branch and in any build. `visibilityRefusal` says who may.
|
|
198
224
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
225
|
+
* The marker is create-only, so a second remove finds it and changes nothing. Remove, restore
|
|
226
|
+
* and a re-upload's un-hide touch only the marker, and nothing but the library listing reads it:
|
|
227
|
+
* the worst interleaving of two of them lists or omits one file until the next action.
|
|
202
228
|
*/
|
|
203
|
-
const
|
|
229
|
+
const hideAssetHandler = async (ctx, req, body) => {
|
|
204
230
|
if (!ctx.assetStore)
|
|
205
231
|
return { ok: false, status: 501, error: 'Asset store not configured' };
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return { ok: true, status: 200, data: {
|
|
232
|
+
const refusal = await visibilityRefusal(ctx.assetStore, req, body.key, 'remove');
|
|
233
|
+
if (refusal)
|
|
234
|
+
return refusal;
|
|
235
|
+
await ctx.assetStore.hideAsset(body.key, {
|
|
236
|
+
hiddenAt: new Date().toISOString(),
|
|
237
|
+
hiddenBy: req.user.userId,
|
|
238
|
+
});
|
|
239
|
+
return { ok: true, status: 200, data: { hidden: true } };
|
|
240
|
+
};
|
|
241
|
+
/** Put a removed file back in the library by deleting its hidden marker; see `hideAssetHandler`. */
|
|
242
|
+
const restoreAssetHandler = async (ctx, req, body) => {
|
|
243
|
+
if (!ctx.assetStore)
|
|
244
|
+
return { ok: false, status: 501, error: 'Asset store not configured' };
|
|
245
|
+
const refusal = await visibilityRefusal(ctx.assetStore, req, body.key, 'restore');
|
|
246
|
+
if (refusal)
|
|
247
|
+
return refusal;
|
|
248
|
+
await ctx.assetStore.unhideAsset(body.key);
|
|
249
|
+
return { ok: true, status: 200, data: { hidden: false } };
|
|
214
250
|
};
|
|
215
251
|
/**
|
|
216
252
|
* The largest body this route returns inline. The CMS Lambda's Function URL buffers its response
|
|
@@ -382,20 +418,43 @@ const listAssets = defineEndpoint({
|
|
|
382
418
|
defaultMockData: { assets: [] },
|
|
383
419
|
handler: listAssetsHandler,
|
|
384
420
|
});
|
|
385
|
-
|
|
386
|
-
* DELETE /assets?key={hash32}
|
|
387
|
-
*/
|
|
388
|
-
const deleteAsset = defineEndpoint({
|
|
421
|
+
const hideAsset = defineEndpoint({
|
|
389
422
|
namespace: 'assets',
|
|
390
|
-
name: '
|
|
391
|
-
method: '
|
|
392
|
-
path: '/assets',
|
|
393
|
-
|
|
394
|
-
|
|
423
|
+
name: 'hide',
|
|
424
|
+
method: 'POST',
|
|
425
|
+
path: '/assets/hide',
|
|
426
|
+
body: assetKeyBodySchema,
|
|
427
|
+
bodyType: 'AssetKeyBody',
|
|
428
|
+
responseType: 'AssetVisibilityResponse',
|
|
395
429
|
response: {},
|
|
396
|
-
defaultMockData: {
|
|
397
|
-
handler:
|
|
430
|
+
defaultMockData: { hidden: true },
|
|
431
|
+
handler: hideAssetHandler,
|
|
398
432
|
});
|
|
433
|
+
const restoreAsset = defineEndpoint({
|
|
434
|
+
namespace: 'assets',
|
|
435
|
+
name: 'restore',
|
|
436
|
+
method: 'POST',
|
|
437
|
+
path: '/assets/restore',
|
|
438
|
+
body: assetKeyBodySchema,
|
|
439
|
+
bodyType: 'AssetKeyBody',
|
|
440
|
+
responseType: 'AssetVisibilityResponse',
|
|
441
|
+
response: {},
|
|
442
|
+
defaultMockData: { hidden: false },
|
|
443
|
+
handler: restoreAssetHandler,
|
|
444
|
+
});
|
|
445
|
+
/**
|
|
446
|
+
* DELETE /assets answers 410 instead of falling through to `DELETE /:branch` as a branch named
|
|
447
|
+
* "assets". Hand-built like `assetRawRoute`, so the client generator emits no method for it.
|
|
448
|
+
*/
|
|
449
|
+
const deleteAssetGone = {
|
|
450
|
+
method: 'DELETE',
|
|
451
|
+
pattern: ['assets'],
|
|
452
|
+
handler: async () => ({
|
|
453
|
+
ok: false,
|
|
454
|
+
status: 410,
|
|
455
|
+
error: 'DELETE /assets is gone: remove a file from the library with POST /assets/hide',
|
|
456
|
+
}),
|
|
457
|
+
};
|
|
399
458
|
/**
|
|
400
459
|
* Exported routes for router registration and client codegen.
|
|
401
460
|
*/
|
|
@@ -404,7 +463,9 @@ export const ASSET_ROUTES = {
|
|
|
404
463
|
finalize: finalizeAsset,
|
|
405
464
|
uploadProxied,
|
|
406
465
|
list: listAssets,
|
|
407
|
-
|
|
466
|
+
hide: hideAsset,
|
|
467
|
+
restore: restoreAsset,
|
|
468
|
+
deleteGone: deleteAssetGone,
|
|
408
469
|
};
|
|
409
470
|
/**
|
|
410
471
|
* GET /assets/raw/{key...} - see `rawAssetHandler` above for why this is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ApiResponse } from './types.js';
|
|
3
|
-
/** @internal No importer; deletion candidate in
|
|
3
|
+
/** @internal No importer; deletion candidate in safeinsights/canopycms#725. */
|
|
4
4
|
export interface MarkAsMergedParams {
|
|
5
5
|
branch: string;
|
|
6
6
|
}
|
package/dist/api/branch.js
CHANGED
|
@@ -514,7 +514,7 @@ export const deleteBranchHandler = async (ctx, req, params) => {
|
|
|
514
514
|
// writableBranch/submittableBranch guards elsewhere use 403 for the same
|
|
515
515
|
// "protected" category (tracked as a deferred, deliberately out-of-scope
|
|
516
516
|
// cleanup in
|
|
517
|
-
//
|
|
517
|
+
// safeinsights/canopycms#781); extending
|
|
518
518
|
// this check to 'approved' keeps it consistent with its own immediate
|
|
519
519
|
// neighbour rather than introducing a fresh 400/403 split between two
|
|
520
520
|
// arms of the same guard.
|
package/dist/api/client.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { ContentReadResponse, ContentWriteResponse, ReferenceValidationResp
|
|
|
15
15
|
import type { ReferenceOptionsResponse } from './reference-options.js';
|
|
16
16
|
import type { ResolveReferencesBody, ResolveReferencesResponse } from './resolve-references.js';
|
|
17
17
|
import type { DeleteEntryResponse, EntriesResponse } from './entries.js';
|
|
18
|
-
import type {
|
|
18
|
+
import type { AssetKeyBody, AssetVisibilityResponse, AssetsListResponse, FinalizeAssetBody, FinalizeAssetResponse, PresignAssetBody, PresignAssetResponse } from './assets.js';
|
|
19
19
|
import type { BatchGetUserMetadataBody, BatchGetUserMetadataResponse, GetUserMetadataResponse, ListGroupsResponse, PermissionsResponse, SearchUsersResponse, UpdatePermissionsBody } from './permissions.js';
|
|
20
20
|
import type { ExternalGroupsResponse, InternalGroupsResponse, UpdateInternalGroupsBody, UpdateInternalGroupsResponse } from './groups.js';
|
|
21
21
|
import type { UserInfoResponse } from './user.js';
|
|
@@ -131,8 +131,10 @@ export declare class CanopyApiClient {
|
|
|
131
131
|
} & RequestOptions) => Promise<FinalizeAssetResponse>;
|
|
132
132
|
/** GET /assets */
|
|
133
133
|
list: (params: Record<string, string>, options?: RequestOptions) => Promise<AssetsListResponse>;
|
|
134
|
-
/**
|
|
135
|
-
|
|
134
|
+
/** POST /assets/hide */
|
|
135
|
+
hide: (body: AssetKeyBody, options?: RequestOptions) => Promise<AssetVisibilityResponse>;
|
|
136
|
+
/** POST /assets/restore */
|
|
137
|
+
restore: (body: AssetKeyBody, options?: RequestOptions) => Promise<AssetVisibilityResponse>;
|
|
136
138
|
};
|
|
137
139
|
readonly permissions: {
|
|
138
140
|
/** GET /permissions */
|
package/dist/api/client.js
CHANGED
|
@@ -139,9 +139,13 @@ export class CanopyApiClient {
|
|
|
139
139
|
list: (params, options) => {
|
|
140
140
|
return this.request('GET', this.buildPath('/assets', params), undefined, 'standard', options);
|
|
141
141
|
},
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
return this.request('
|
|
142
|
+
/** POST /assets/hide */
|
|
143
|
+
hide: (body, options) => {
|
|
144
|
+
return this.request('POST', '/assets/hide', body, 'standard', options);
|
|
145
|
+
},
|
|
146
|
+
/** POST /assets/restore */
|
|
147
|
+
restore: (body, options) => {
|
|
148
|
+
return this.request('POST', '/assets/restore', body, 'standard', options);
|
|
145
149
|
},
|
|
146
150
|
};
|
|
147
151
|
this.permissions = {
|
package/dist/api/content.js
CHANGED
|
@@ -506,7 +506,7 @@ const writeContentHandler = async (gc, ctx, req, params, body) => {
|
|
|
506
506
|
ok: false,
|
|
507
507
|
status: 409,
|
|
508
508
|
error: err.message,
|
|
509
|
-
|
|
509
|
+
code: err.outcome === 'unknown' ? 'WRITE_OUTCOME_UNKNOWN' : 'BRANCH_SYNCING',
|
|
510
510
|
};
|
|
511
511
|
}
|
|
512
512
|
// [F1] Also not an editor collision: this content ID is quarantined on two files
|
|
@@ -14,7 +14,7 @@ export declare const DEFAULT_ENTRIES_LIMIT = 50;
|
|
|
14
14
|
* The generic 409 for a conflicting change to an entry, usually its version moving on since the
|
|
15
15
|
* caller read it. Shared with the editor, whose own pre-save staleness check reports the same.
|
|
16
16
|
*/
|
|
17
|
-
export declare const ENTRY_CHANGED_MESSAGE = "This entry changed since you opened it.
|
|
17
|
+
export declare const ENTRY_CHANGED_MESSAGE = "This entry changed since you opened it. Choose \"Refresh from server\" to see the latest version (your unsaved edits will be lost).";
|
|
18
18
|
/**
|
|
19
19
|
* The 409 for an update whose entry is gone: deleted or moved since the caller read it (API code
|
|
20
20
|
* `ENTRY_GONE`). The save is refused rather than re-creating the entry; re-creating it is a create
|
|
@@ -14,7 +14,7 @@ export const DEFAULT_ENTRIES_LIMIT = 50;
|
|
|
14
14
|
* The generic 409 for a conflicting change to an entry, usually its version moving on since the
|
|
15
15
|
* caller read it. Shared with the editor, whose own pre-save staleness check reports the same.
|
|
16
16
|
*/
|
|
17
|
-
export const ENTRY_CHANGED_MESSAGE = 'This entry changed since you opened it.
|
|
17
|
+
export const ENTRY_CHANGED_MESSAGE = 'This entry changed since you opened it. Choose "Refresh from server" to see the latest version (your unsaved edits will be lost).';
|
|
18
18
|
/**
|
|
19
19
|
* The 409 for an update whose entry is gone: deleted or moved since the caller read it (API code
|
|
20
20
|
* `ENTRY_GONE`). The save is refused rather than re-creating the entry; re-creating it is a create
|
package/dist/api/entries.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { LogicalPath, PhysicalPath, Slug, ContentId } from '../paths/types.
|
|
|
5
5
|
/**
|
|
6
6
|
* Summary of an entry type for client display.
|
|
7
7
|
* Simplified from EntryTypeConfig - doesn't include full field definitions.
|
|
8
|
-
* @internal No importer; deletion candidate in
|
|
8
|
+
* @internal No importer; deletion candidate in safeinsights/canopycms#725.
|
|
9
9
|
*/
|
|
10
10
|
export interface EntryTypeSummary {
|
|
11
11
|
name: string;
|
|
@@ -28,7 +28,7 @@ export interface CollectionItem {
|
|
|
28
28
|
exists?: boolean;
|
|
29
29
|
canEdit?: boolean;
|
|
30
30
|
}
|
|
31
|
-
/** @internal No importer; deletion candidate in
|
|
31
|
+
/** @internal No importer; deletion candidate in safeinsights/canopycms#725. */
|
|
32
32
|
export interface ListEntriesParams {
|
|
33
33
|
branch: string;
|
|
34
34
|
collection?: LogicalPath;
|
package/dist/api/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { CommentsResponse, AddCommentResponse, ResolveCommentResponse } fro
|
|
|
5
5
|
export type { EntriesResponse, ListEntriesResponse, CollectionItem, ListEntriesParams, DeleteEntryResponse, } from './entries.js';
|
|
6
6
|
export type { PermissionsResponse, UpdatePermissionsBody, SearchUsersParams, GetUserMetadataResponse, GetUserMetadataParams, } from './permissions.js';
|
|
7
7
|
export type { UserInfoResponse } from './user.js';
|
|
8
|
-
export type { AssetRecord, AssetsListResponse, PresignAssetResponse, PresignAssetBody, FinalizeAssetResponse, FinalizeAssetBody,
|
|
8
|
+
export type { AssetRecord, AssetsListResponse, PresignAssetResponse, PresignAssetBody, FinalizeAssetResponse, FinalizeAssetBody, AssetKeyBody, AssetVisibilityResponse, } from './assets.js';
|
|
9
9
|
export type { InternalGroupsResponse, ExternalGroupsResponse, UpdateInternalGroupsBody, SearchExternalGroupsParams, ExternalGroup, } from './groups.js';
|
|
10
10
|
export type { ReferenceOptionsResponse } from './reference-options.js';
|
|
11
11
|
export type { ResolveReferencesResponse } from './resolve-references.js';
|
|
@@ -154,10 +154,10 @@ declare const batchGetUserMetadataBodySchema: z.ZodObject<{
|
|
|
154
154
|
userIds: string[];
|
|
155
155
|
}>;
|
|
156
156
|
export type UpdatePermissionsBody = z.infer<typeof updatePermissionsBodySchema>;
|
|
157
|
-
/** @internal No importer; deletion candidate in
|
|
157
|
+
/** @internal No importer; deletion candidate in safeinsights/canopycms#725. */
|
|
158
158
|
export type SearchUsersParams = z.infer<typeof searchUsersParamsSchema>;
|
|
159
159
|
export type BatchGetUserMetadataBody = z.infer<typeof batchGetUserMetadataBodySchema>;
|
|
160
|
-
/** @internal No importer; deletion candidate in
|
|
160
|
+
/** @internal No importer; deletion candidate in safeinsights/canopycms#725. */
|
|
161
161
|
export type GetUserMetadataParams = z.infer<typeof getUserMetadataParamsSchema>;
|
|
162
162
|
/**
|
|
163
163
|
* Exported routes for router registration
|
package/dist/api/types.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export interface ApiResponse<TData = unknown> {
|
|
|
63
63
|
* server runs (a 412, see operating-mode/editor-mode-check.ts); only a rebuild fixes it.
|
|
64
64
|
* `SETTINGS_CHANGED`: a groups or permissions save named a version the file has moved on from (a
|
|
65
65
|
* 409, see `SettingsVersionConflictError`); nothing was saved, and a reload shows the other edit.
|
|
66
|
-
* The other settings 409, a busy file lock, carries no code.
|
|
66
|
+
* The other settings 409, a busy file lock, carries no code. `BRANCH_SYNCING`: an entry save was
|
|
67
|
+
* refused because the branch is mid-rebase (a 409); nothing was saved, and a retry soon can pass.
|
|
67
68
|
*/
|
|
68
|
-
export type ApiErrorCode = 'SCHEMA_UNAVAILABLE' | 'WORKER_FAILED' | 'WRITE_OUTCOME_UNKNOWN' | 'ENTRY_GONE' | 'EDITOR_MODE_MISMATCH' | 'SETTINGS_CHANGED';
|
|
69
|
+
export type ApiErrorCode = 'SCHEMA_UNAVAILABLE' | 'WORKER_FAILED' | 'WRITE_OUTCOME_UNKNOWN' | 'ENTRY_GONE' | 'EDITOR_MODE_MISMATCH' | 'SETTINGS_CHANGED' | 'BRANCH_SYNCING';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The six S3/local bucket-prefix strings (see keys.ts's module doc for the full layout).
|
|
3
3
|
*
|
|
4
4
|
* Split into its own dependency-free file so isomorphic modules (transform-directives.ts,
|
|
5
5
|
* asset-url.ts) can import the prefixes without pulling keys.ts's `node:crypto` import into a
|
|
@@ -9,6 +9,8 @@ export declare const ASSET_PREFIXES: {
|
|
|
9
9
|
readonly originals: "asset-originals";
|
|
10
10
|
readonly staging: "asset-staging";
|
|
11
11
|
readonly meta: "asset-meta";
|
|
12
|
+
/** One marker per file removed from the media library; its meta stays (see `hideAsset`). */
|
|
13
|
+
readonly hidden: "asset-hidden";
|
|
12
14
|
readonly public: "assets";
|
|
13
15
|
/** Transform-output prefix; directive-aware key building lives in transform-directives.ts. */
|
|
14
16
|
readonly transform: "assets/t";
|
|
@@ -21,6 +23,7 @@ export interface AssetPrefixes {
|
|
|
21
23
|
originals: string;
|
|
22
24
|
staging: string;
|
|
23
25
|
meta: string;
|
|
26
|
+
hidden: string;
|
|
24
27
|
public: string;
|
|
25
28
|
transform: string;
|
|
26
29
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The six S3/local bucket-prefix strings (see keys.ts's module doc for the full layout).
|
|
3
3
|
*
|
|
4
4
|
* Split into its own dependency-free file so isomorphic modules (transform-directives.ts,
|
|
5
5
|
* asset-url.ts) can import the prefixes without pulling keys.ts's `node:crypto` import into a
|
|
@@ -9,6 +9,8 @@ export const ASSET_PREFIXES = {
|
|
|
9
9
|
originals: 'asset-originals',
|
|
10
10
|
staging: 'asset-staging',
|
|
11
11
|
meta: 'asset-meta',
|
|
12
|
+
/** One marker per file removed from the media library; its meta stays (see `hideAsset`). */
|
|
13
|
+
hidden: 'asset-hidden',
|
|
12
14
|
public: 'assets',
|
|
13
15
|
/** Transform-output prefix; directive-aware key building lives in transform-directives.ts. */
|
|
14
16
|
transform: 'assets/t',
|
package/dist/assets/finalize.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { runFinalizePipeline } from './pipeline.js';
|
|
7
7
|
import { ASSET_PREFIXES } from './keys.js';
|
|
8
|
+
import { getErrorMessage } from '../utils/error.js';
|
|
9
|
+
import { canopyLogWarn } from '../utils/logger.js';
|
|
8
10
|
/**
|
|
9
11
|
* A client-supplied staging key must be exactly `asset-staging/{uuid}` — the
|
|
10
12
|
* shape beginUpload generates. Without this check, finalize's read + cleanup
|
|
@@ -37,8 +39,19 @@ export async function finalizeAsset(store, input) {
|
|
|
37
39
|
return result;
|
|
38
40
|
const { meta, data, publicObject } = result;
|
|
39
41
|
const existing = await store.getMeta(meta.hash32);
|
|
40
|
-
if (existing)
|
|
42
|
+
if (existing) {
|
|
43
|
+
// Uploading a file removed from the library puts it back. Hiding is a de-list, not access
|
|
44
|
+
// control: whoever uploads the bytes has the file, and a picker selecting an upload the
|
|
45
|
+
// library never shows would be a dead end. A failed un-hide only leaves the file out of the
|
|
46
|
+
// listing, so the upload still succeeds.
|
|
47
|
+
try {
|
|
48
|
+
await store.unhideAsset(meta.hash32);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
canopyLogWarn(`[canopycms] could not restore removed asset ${meta.hash32} to the library on re-upload: ${getErrorMessage(err)}`);
|
|
52
|
+
}
|
|
41
53
|
return { ok: true, meta: existing };
|
|
54
|
+
}
|
|
42
55
|
// Either put may find its key taken (`already-exists`): by an earlier attempt that stopped
|
|
43
56
|
// before the meta commit, or by a concurrent identical upload. The object kept there is this
|
|
44
57
|
// content's, so finalize goes on to commit the meta either way.
|
package/dist/assets/keys.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* asset-originals/{hash32}.{ext} private; full-fidelity originals
|
|
8
8
|
* asset-staging/{uuid} presigned-POST target
|
|
9
9
|
* asset-meta/{hash32}.json private; filename/uploader/dims/mime
|
|
10
|
+
* asset-hidden/{hash32}.json private; marks a file removed from the library
|
|
10
11
|
* assets/t/{directives}/{hash32}/{slug} transform outputs (see transform-directives.ts)
|
|
11
12
|
* assets/{hash32}/{slug}.{ext} public static: sanitized SVG + PDF only
|
|
12
13
|
*
|
|
@@ -50,8 +51,10 @@ export declare function createKeyBuilders(prefixes?: AssetPrefixes): {
|
|
|
50
51
|
stagingKey: (uuid: string) => string;
|
|
51
52
|
metaKey: (hash32: string) => string;
|
|
52
53
|
metaPrefix: () => string;
|
|
54
|
+
hiddenKey: (hash32: string) => string;
|
|
55
|
+
hiddenPrefix: () => string;
|
|
53
56
|
publicKey: (hash32: string, slug: string, ext: string) => string;
|
|
54
57
|
};
|
|
55
|
-
export declare const originalKey: (hash32: string, ext: string) => string, stagingKey: (uuid: string) => string, metaKey: (hash32: string) => string, publicKey: (hash32: string, slug: string, ext: string) => string;
|
|
58
|
+
export declare const originalKey: (hash32: string, ext: string) => string, stagingKey: (uuid: string) => string, metaKey: (hash32: string) => string, hiddenKey: (hash32: string) => string, publicKey: (hash32: string, slug: string, ext: string) => string;
|
|
56
59
|
/** @internal Exported for tests. */
|
|
57
60
|
export declare const originalPrefix: (hash32: string) => string, metaPrefix: () => string;
|
package/dist/assets/keys.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* asset-originals/{hash32}.{ext} private; full-fidelity originals
|
|
8
8
|
* asset-staging/{uuid} presigned-POST target
|
|
9
9
|
* asset-meta/{hash32}.json private; filename/uploader/dims/mime
|
|
10
|
+
* asset-hidden/{hash32}.json private; marks a file removed from the library
|
|
10
11
|
* assets/t/{directives}/{hash32}/{slug} transform outputs (see transform-directives.ts)
|
|
11
12
|
* assets/{hash32}/{slug}.{ext} public static: sanitized SVG + PDF only
|
|
12
13
|
*
|
|
@@ -81,10 +82,12 @@ export function createKeyBuilders(prefixes = ASSET_PREFIXES) {
|
|
|
81
82
|
stagingKey: (uuid) => `${prefixes.staging}/${uuid}`,
|
|
82
83
|
metaKey: (hash32) => `${prefixes.meta}/${hash32}.json`,
|
|
83
84
|
metaPrefix: () => `${prefixes.meta}/`,
|
|
85
|
+
hiddenKey: (hash32) => `${prefixes.hidden}/${hash32}.json`,
|
|
86
|
+
hiddenPrefix: () => `${prefixes.hidden}/`,
|
|
84
87
|
publicKey: (hash32, slug, ext) => `${prefixes.public}/${hash32}/${slug}.${ext}`,
|
|
85
88
|
};
|
|
86
89
|
}
|
|
87
90
|
const keyBuilders = createKeyBuilders();
|
|
88
|
-
export const { originalKey, stagingKey, metaKey, publicKey } = keyBuilders;
|
|
91
|
+
export const { originalKey, stagingKey, metaKey, hiddenKey, publicKey } = keyBuilders;
|
|
89
92
|
/** @internal Exported for tests. */
|
|
90
93
|
export const { originalPrefix, metaPrefix } = keyBuilders;
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* bucket-prefix layout as S3AssetStore (see keys.ts) so content behaves
|
|
4
4
|
* identically across adapters.
|
|
5
5
|
*/
|
|
6
|
-
import type { AssetMeta, AssetStore, BeginUploadInput, CreateOnlyResult, PublicObject, StagedUploadTarget } from './types.js';
|
|
6
|
+
import type { AssetMeta, AssetStore, BeginUploadInput, CreateOnlyResult, HiddenAssetMarker, ListedAssetMeta, PublicObject, StagedUploadTarget } from './types.js';
|
|
7
7
|
export interface LocalAssetStoreOptions {
|
|
8
|
-
/** Root directory. The
|
|
8
|
+
/** Root directory. The six bucket prefixes are created as subdirectories under it. */
|
|
9
9
|
root: string;
|
|
10
10
|
}
|
|
11
11
|
export declare class LocalAssetStore implements AssetStore {
|
|
@@ -33,11 +33,12 @@ export declare class LocalAssetStore implements AssetStore {
|
|
|
33
33
|
/** Link `tempPath` in at `filePath`; `false` when something is already there. */
|
|
34
34
|
private linkIfAbsent;
|
|
35
35
|
/**
|
|
36
|
-
* Create-only write of a blob and its headers sidecar. Each is written
|
|
37
|
-
* `link()`ed into place, which fails with EEXIST rather than replacing
|
|
38
|
-
* The sidecar is linked first, so a reader never finds a blob without
|
|
39
|
-
* already there was linked by an earlier writer of this key and is kept,
|
|
40
|
-
* first writer's headers win, as on S3 (a static's Content-Disposition names
|
|
36
|
+
* Create-only write of a blob and, when `headers` is given, its headers sidecar. Each is written
|
|
37
|
+
* whole to a temp file, then `link()`ed into place, which fails with EEXIST rather than replacing
|
|
38
|
+
* (see docs/concurrency.md). The sidecar is linked first, so a reader never finds a blob without
|
|
39
|
+
* its headers. A sidecar already there was linked by an earlier writer of this key and is kept,
|
|
40
|
+
* as the blob is: the first writer's headers win, as on S3 (a static's Content-Disposition names
|
|
41
|
+
* its uploaded file).
|
|
41
42
|
*/
|
|
42
43
|
private createExclusive;
|
|
43
44
|
beginUpload(_input: BeginUploadInput): Promise<StagedUploadTarget>;
|
|
@@ -85,12 +86,20 @@ export declare class LocalAssetStore implements AssetStore {
|
|
|
85
86
|
*/
|
|
86
87
|
putMetaIfAbsent(hash32: string, meta: AssetMeta): Promise<CreateOnlyResult>;
|
|
87
88
|
getMeta(hash32: string): Promise<AssetMeta | null>;
|
|
89
|
+
/**
|
|
90
|
+
* The cursor names the last item returned, and the next page starts after it in sort order, so a
|
|
91
|
+
* page boundary holds when that item is hidden in between: its meta is never deleted, and the
|
|
92
|
+
* comparison needs only its place, not its presence in the filtered list.
|
|
93
|
+
*/
|
|
88
94
|
listMeta(input?: {
|
|
89
95
|
cursor?: string;
|
|
90
96
|
limit?: number;
|
|
97
|
+
includeHidden?: boolean;
|
|
91
98
|
}): Promise<{
|
|
92
|
-
items:
|
|
99
|
+
items: ListedAssetMeta[];
|
|
93
100
|
nextCursor?: string;
|
|
94
101
|
}>;
|
|
95
|
-
|
|
102
|
+
private hiddenHashes;
|
|
103
|
+
hideAsset(hash32: string, marker: HiddenAssetMarker): Promise<CreateOnlyResult>;
|
|
104
|
+
unhideAsset(hash32: string): Promise<boolean>;
|
|
96
105
|
}
|