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.
Files changed (203) hide show
  1. package/CHANGELOG.md +370 -231
  2. package/README.md +0 -1
  3. package/dist/ai/references.js +3 -1
  4. package/dist/api/assets.d.ts +27 -7
  5. package/dist/api/assets.js +99 -38
  6. package/dist/api/branch-merge.d.ts +1 -1
  7. package/dist/api/branch.js +1 -1
  8. package/dist/api/client.d.ts +5 -3
  9. package/dist/api/client.js +7 -3
  10. package/dist/api/content.js +1 -1
  11. package/dist/api/entries-constants.d.ts +1 -1
  12. package/dist/api/entries-constants.js +1 -1
  13. package/dist/api/entries.d.ts +2 -2
  14. package/dist/api/index.d.ts +1 -1
  15. package/dist/api/permissions.d.ts +2 -2
  16. package/dist/api/types.d.ts +3 -2
  17. package/dist/assets/asset-prefixes.d.ts +4 -1
  18. package/dist/assets/asset-prefixes.js +3 -1
  19. package/dist/assets/finalize.js +14 -1
  20. package/dist/assets/keys.d.ts +4 -1
  21. package/dist/assets/keys.js +4 -1
  22. package/dist/assets/store-local.d.ts +18 -9
  23. package/dist/assets/store-local.js +58 -26
  24. package/dist/assets/store-s3.d.ts +19 -4
  25. package/dist/assets/store-s3.js +56 -13
  26. package/dist/assets/types.d.ts +26 -2
  27. package/dist/cli/cli.js +128 -48
  28. package/dist/cli/generate-ai-content.js +7 -5
  29. package/dist/cli/init.js +4 -4
  30. package/dist/client.d.ts +0 -1
  31. package/dist/client.js +0 -1
  32. package/dist/config/schemas/config.d.ts +0 -5
  33. package/dist/config/schemas/config.js +0 -1
  34. package/dist/config/types.d.ts +0 -1
  35. package/dist/content-index-generation.d.ts +1 -1
  36. package/dist/content-index-generation.js +1 -1
  37. package/dist/content-listing.js +1 -1
  38. package/dist/content-reader.js +1 -1
  39. package/dist/content-store.d.ts +1 -1
  40. package/dist/content-store.js +1 -1
  41. package/dist/dev-content-watcher.d.ts +1 -1
  42. package/dist/dev-content-watcher.js +1 -1
  43. package/dist/editor/BranchManager.d.ts +2 -0
  44. package/dist/editor/BranchManager.js +6 -4
  45. package/dist/editor/CanopyEditor.d.ts +1 -1
  46. package/dist/editor/CanopyEditor.js +1 -2
  47. package/dist/editor/CommentsPanel.js +69 -34
  48. package/dist/editor/Editor.d.ts +3 -2
  49. package/dist/editor/Editor.js +361 -191
  50. package/dist/editor/EditorAuthGate.js +8 -3
  51. package/dist/editor/EntryNavigator.d.ts +18 -1
  52. package/dist/editor/EntryNavigator.js +82 -18
  53. package/dist/editor/FormHeader.d.ts +2 -1
  54. package/dist/editor/FormHeader.js +10 -6
  55. package/dist/editor/FormRenderer.d.ts +7 -2
  56. package/dist/editor/FormRenderer.js +51 -16
  57. package/dist/editor/PreviewFrame.d.ts +15 -2
  58. package/dist/editor/PreviewFrame.js +22 -5
  59. package/dist/editor/PreviewPane.d.ts +61 -0
  60. package/dist/editor/PreviewPane.js +194 -0
  61. package/dist/editor/PreviewToolbarTools.d.ts +30 -0
  62. package/dist/editor/PreviewToolbarTools.js +81 -0
  63. package/dist/editor/branch-status.d.ts +6 -0
  64. package/dist/editor/branch-status.js +10 -0
  65. package/dist/editor/comments/BranchComments.d.ts +2 -4
  66. package/dist/editor/comments/BranchComments.js +7 -11
  67. package/dist/editor/comments/ThreadCarousel.d.ts +2 -8
  68. package/dist/editor/comments/ThreadCarousel.js +4 -49
  69. package/dist/editor/comments/ThreadReplyControls.js +5 -2
  70. package/dist/editor/comments/form-comments.js +2 -1
  71. package/dist/editor/components/BranchLockBanner.d.ts +24 -0
  72. package/dist/editor/components/BranchLockBanner.js +43 -0
  73. package/dist/editor/components/BranchPill.d.ts +33 -0
  74. package/dist/editor/components/BranchPill.js +31 -0
  75. package/dist/editor/components/BranchesDrawer.js +3 -3
  76. package/dist/editor/components/ConfirmDeleteModal.d.ts +3 -1
  77. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  78. package/dist/editor/components/EditorHeader.d.ts +47 -109
  79. package/dist/editor/components/EditorHeader.js +128 -148
  80. package/dist/editor/components/EditorRail.d.ts +48 -0
  81. package/dist/editor/components/EditorRail.js +64 -0
  82. package/dist/editor/components/EntryBreadcrumb.d.ts +32 -0
  83. package/dist/editor/components/EntryBreadcrumb.js +53 -0
  84. package/dist/editor/components/QuickOpenPalette.d.ts +8 -2
  85. package/dist/editor/components/QuickOpenPalette.js +8 -2
  86. package/dist/editor/components/RenameEntryModal.d.ts +8 -1
  87. package/dist/editor/components/RenameEntryModal.js +21 -5
  88. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  89. package/dist/editor/components/SaveAllResult.js +86 -0
  90. package/dist/editor/components/SaveSplitButton.d.ts +31 -0
  91. package/dist/editor/components/SaveSplitButton.js +121 -0
  92. package/dist/editor/components/StagedChangesDrawer.d.ts +0 -1
  93. package/dist/editor/components/StagedChangesDrawer.js +5 -4
  94. package/dist/editor/components/UndoToastMessage.d.ts +6 -5
  95. package/dist/editor/components/UndoToastMessage.js +6 -4
  96. package/dist/editor/components/UnsavedMark.d.ts +9 -0
  97. package/dist/editor/components/UnsavedMark.js +20 -0
  98. package/dist/editor/components/UserBadge.js +13 -11
  99. package/dist/editor/components/index.d.ts +2 -2
  100. package/dist/editor/components/index.js +2 -2
  101. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  102. package/dist/editor/context/EditorStateContext.js +4 -4
  103. package/dist/editor/copy.d.ts +7 -0
  104. package/dist/editor/copy.js +7 -0
  105. package/dist/editor/editor-utils.d.ts +52 -7
  106. package/dist/editor/editor-utils.js +122 -30
  107. package/dist/editor/entry-search.d.ts +4 -2
  108. package/dist/editor/entry-search.js +2 -3
  109. package/dist/editor/fields/FieldLabel.d.ts +1 -1
  110. package/dist/editor/fields/MarkdownField.js +3 -0
  111. package/dist/editor/fields/ObjectField.d.ts +13 -2
  112. package/dist/editor/fields/ObjectField.js +38 -9
  113. package/dist/editor/fields/entry-link/EntryLinkContext.d.ts +2 -0
  114. package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -1
  115. package/dist/editor/group-manager/index.js +1 -1
  116. package/dist/editor/hooks/save-all.d.ts +68 -0
  117. package/dist/editor/hooks/save-all.js +61 -0
  118. package/dist/editor/hooks/useCommentSystem.d.ts +2 -0
  119. package/dist/editor/hooks/useCommentSystem.js +3 -0
  120. package/dist/editor/hooks/useDraftManager.d.ts +24 -1
  121. package/dist/editor/hooks/useDraftManager.js +297 -16
  122. package/dist/editor/hooks/useEditorLayout.d.ts +19 -11
  123. package/dist/editor/hooks/useEditorLayout.js +18 -25
  124. package/dist/editor/hooks/useEntryManager.d.ts +6 -0
  125. package/dist/editor/hooks/useEntryManager.js +37 -12
  126. package/dist/editor/media/AssetCard.d.ts +6 -3
  127. package/dist/editor/media/AssetCard.js +20 -7
  128. package/dist/editor/media/AssetListRow.d.ts +9 -4
  129. package/dist/editor/media/AssetListRow.js +14 -7
  130. package/dist/editor/media/MediaLibrary.d.ts +1 -1
  131. package/dist/editor/media/MediaLibrary.js +5 -4
  132. package/dist/editor/media/MediaLibraryBody.d.ts +1 -1
  133. package/dist/editor/media/MediaLibraryBody.js +65 -37
  134. package/dist/editor/media/useAssetLibrary.d.ts +6 -3
  135. package/dist/editor/media/useAssetLibrary.js +17 -10
  136. package/dist/editor/pane-header.d.ts +6 -0
  137. package/dist/editor/pane-header.js +6 -0
  138. package/dist/editor/permission-manager/index.js +1 -1
  139. package/dist/editor/preview-bridge.d.ts +16 -0
  140. package/dist/editor/preview-bridge.js +12 -3
  141. package/dist/editor/preview-path.d.ts +22 -3
  142. package/dist/editor/preview-path.js +92 -5
  143. package/dist/editor/preview-scale.d.ts +27 -0
  144. package/dist/editor/preview-scale.js +34 -0
  145. package/dist/editor/shell/ActivityRail.d.ts +35 -0
  146. package/dist/editor/shell/ActivityRail.js +91 -0
  147. package/dist/editor/shell/EditorShell.d.ts +32 -0
  148. package/dist/editor/shell/EditorShell.js +47 -0
  149. package/dist/editor/shell/OverlayHeader.d.ts +19 -0
  150. package/dist/editor/shell/OverlayHeader.js +15 -0
  151. package/dist/editor/shell/PaneLayout.d.ts +33 -0
  152. package/dist/editor/shell/PaneLayout.js +171 -0
  153. package/dist/editor/shell/SidePanelHost.d.ts +37 -0
  154. package/dist/editor/shell/SidePanelHost.js +221 -0
  155. package/dist/editor/shell/TopBar.d.ts +12 -0
  156. package/dist/editor/shell/TopBar.js +15 -0
  157. package/dist/editor/shell/host-chrome.d.ts +16 -0
  158. package/dist/editor/shell/host-chrome.js +69 -0
  159. package/dist/editor/theme.js +20 -3
  160. package/dist/editor/undo-removal.d.ts +19 -10
  161. package/dist/editor/undo-removal.js +44 -16
  162. package/dist/git-manager.d.ts +1 -1
  163. package/dist/git-manager.js +2 -2
  164. package/dist/operating-mode/types.d.ts +1 -1
  165. package/dist/resolve-canopy-user.d.ts +1 -1
  166. package/dist/resolve-canopy-user.js +1 -1
  167. package/dist/schema/resolver.d.ts +1 -1
  168. package/dist/schema/resolver.js +1 -1
  169. package/dist/schema/schema-store.d.ts +1 -1
  170. package/dist/schema/schema-store.js +2 -2
  171. package/dist/url-collision.js +3 -3
  172. package/dist/url-exclusivity-fixtures.d.ts +1 -1
  173. package/dist/url-exclusivity-fixtures.js +1 -1
  174. package/dist/utils/content-write-lock.d.ts +1 -1
  175. package/dist/utils/content-write-lock.js +1 -1
  176. package/dist/utils/debug.d.ts +1 -1
  177. package/dist/utils/debug.js +5 -3
  178. package/dist/utils/git.d.ts +4 -0
  179. package/dist/utils/git.js +27 -5
  180. package/dist/validation/block-structural-keys.d.ts +1 -1
  181. package/dist/validation/block-structural-keys.js +1 -1
  182. package/dist/version.d.ts +1 -1
  183. package/dist/version.js +1 -1
  184. package/dist/worker/cms-worker.d.ts +1 -1
  185. package/dist/worker/cms-worker.js +2 -2
  186. package/dist/worker/github-auth.d.ts +1 -1
  187. package/dist/worker/github-auth.js +1 -1
  188. package/dist/worker/github-gateway-wire.d.ts +2 -1
  189. package/dist/worker/github-gateway-wire.js +7 -3
  190. package/dist/worker/github-gateway.js +1 -1
  191. package/dist/worker/github-mirror.d.ts +2 -2
  192. package/dist/worker/github-mirror.js +2 -2
  193. package/dist/worker/history-rewrite.d.ts +1 -1
  194. package/dist/worker/history-rewrite.js +1 -1
  195. package/dist/worker/self-checks.d.ts +1 -1
  196. package/dist/worker/shared-repo-git.js +1 -1
  197. package/package.json +5 -4
  198. package/dist/editor/EditorPanes.d.ts +0 -20
  199. package/dist/editor/EditorPanes.js +0 -124
  200. package/dist/editor/components/EditorFooter.d.ts +0 -11
  201. package/dist/editor/components/EditorFooter.js +0 -9
  202. package/dist/editor/components/EditorSidebar.d.ts +0 -70
  203. package/dist/editor/components/EditorSidebar.js +0 -50
package/README.md CHANGED
@@ -87,7 +87,6 @@ export default defineCanopyConfig({
87
87
  gitBotAuthorEmail: 'canopy@example.com',
88
88
  editor: {
89
89
  title: 'CanopyCMS Editor', // optional UI defaults
90
- subtitle: 'Edit content',
91
90
  theme: {
92
91
  colors: { brand: '#4f46e5' },
93
92
  },
@@ -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 mount = `/${markdownCopies.mountPath.replace(/^\/+|\/+$/g, '')}`.replace(/^\/$/, '');
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)
@@ -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 { AssetMeta, StagedUploadTarget } from '../assets/types.js';
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 = AssetMeta & {
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 deleting an asset */
23
- export type AssetDeleteResponse = ApiResponse<{
24
- deleted: boolean;
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 delete: import("./route-builder.js").RouteDefinition<z.ZodObject<{
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
- }>, undefined, AssetDeleteResponse>;
114
+ }>, AssetVisibilityResponse>;
115
+ readonly deleteGone: RouteDefinition;
96
116
  };
97
117
  /**
98
118
  * GET /assets/raw/{key...} - see `rawAssetHandler` above for why this is
@@ -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 deleteAssetParamsSchema = z.object({ key: hash32Schema });
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, _req, params) => {
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
- * Delete asset - an Admin may delete any asset; anyone else may delete only an asset whose
183
- * recorded `uploadedBy` is them. `key` (hash32) is pre-validated by `deleteAssetParamsSchema`.
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 ownership check lives here, not in a declarative guard, because it needs the asset's meta,
188
- * which guards can't read. Two fail-closed choices:
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
- * delete" would open every legacy asset to everyone.
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 delete rights over it — benign (a 403 where they
196
- * expected success, never the reverse; see
197
- * .claude/future-tasks/asset-listing-cross-branch-exposure.md).
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
- * Coupled to the blob-GC follow-up in asset-review-followups.md: this permission is safe because
200
- * delete is a de-list, not a destroy — nothing another branch references breaks. If GC ever makes
201
- * delete destroy the blob, this needs a reference check too.
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 deleteAssetHandler = async (ctx, req, params) => {
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
- if (!isAdmin(req.user.groups)) {
207
- const meta = await ctx.assetStore.getMeta(params.key);
208
- if (!meta?.uploadedBy || meta.uploadedBy !== req.user.userId) {
209
- return { ok: false, status: 403, error: 'You can only delete assets you uploaded' };
210
- }
211
- }
212
- await ctx.assetStore.deleteMeta(params.key);
213
- return { ok: true, status: 200, data: { deleted: true } };
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: 'delete',
391
- method: 'DELETE',
392
- path: '/assets',
393
- params: deleteAssetParamsSchema,
394
- responseType: 'AssetDeleteResponse',
423
+ name: 'hide',
424
+ method: 'POST',
425
+ path: '/assets/hide',
426
+ body: assetKeyBodySchema,
427
+ bodyType: 'AssetKeyBody',
428
+ responseType: 'AssetVisibilityResponse',
395
429
  response: {},
396
- defaultMockData: { deleted: true },
397
- handler: deleteAssetHandler,
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
- delete: deleteAsset,
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 knip-no-importer-deletion-candidates.md. */
3
+ /** @internal No importer; deletion candidate in safeinsights/canopycms#725. */
4
4
  export interface MarkAsMergedParams {
5
5
  branch: string;
6
6
  }
@@ -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
- // .claude/future-tasks/protected-branch-followup-cleanups.md); extending
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.
@@ -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 { AssetDeleteResponse, AssetsListResponse, FinalizeAssetBody, FinalizeAssetResponse, PresignAssetBody, PresignAssetResponse } from './assets.js';
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
- /** DELETE /assets */
135
- delete: (params: Record<string, string>, options?: RequestOptions) => Promise<AssetDeleteResponse>;
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 */
@@ -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
- /** DELETE /assets */
143
- delete: (params, options) => {
144
- return this.request('DELETE', this.buildPath('/assets', params), undefined, 'standard', options);
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 = {
@@ -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
- ...(err.outcome === 'unknown' ? { code: 'WRITE_OUTCOME_UNKNOWN' } : {}),
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. Reload to see the latest version (your unsaved edits will be lost).";
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. Reload to see the latest version (your unsaved edits will be lost).';
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
@@ -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 knip-no-importer-deletion-candidates.md.
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 knip-no-importer-deletion-candidates.md. */
31
+ /** @internal No importer; deletion candidate in safeinsights/canopycms#725. */
32
32
  export interface ListEntriesParams {
33
33
  branch: string;
34
34
  collection?: LogicalPath;
@@ -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, AssetDeleteResponse, } from './assets.js';
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 knip-no-importer-deletion-candidates.md. */
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 knip-no-importer-deletion-candidates.md. */
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
@@ -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 five S3/local bucket-prefix strings (see keys.ts's module doc for the full layout).
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 five S3/local bucket-prefix strings (see keys.ts's module doc for the full layout).
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',
@@ -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.
@@ -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;
@@ -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 five bucket prefixes are created as subdirectories under it. */
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 whole to a temp file, then
37
- * `link()`ed into place, which fails with EEXIST rather than replacing (see docs/concurrency.md).
38
- * The sidecar is linked first, so a reader never finds a blob without its headers. A sidecar
39
- * already there was linked by an earlier writer of this key and is kept, as the blob is: the
40
- * first writer's headers win, as on S3 (a static's Content-Disposition names its uploaded file).
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: AssetMeta[];
99
+ items: ListedAssetMeta[];
93
100
  nextCursor?: string;
94
101
  }>;
95
- deleteMeta(hash32: string): Promise<void>;
102
+ private hiddenHashes;
103
+ hideAsset(hash32: string, marker: HiddenAssetMarker): Promise<CreateOnlyResult>;
104
+ unhideAsset(hash32: string): Promise<boolean>;
96
105
  }