@openrails/contentkit-ui 0.86.0 → 0.87.0-rc.1

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 (89) hide show
  1. package/README.md +21 -3
  2. package/dist/client/content/types.d.ts +4 -2
  3. package/dist/client/content/types.js.map +1 -1
  4. package/dist/client/discovery.js +3 -0
  5. package/dist/client/discovery.js.map +1 -1
  6. package/dist/client/errors.d.ts +6 -0
  7. package/dist/client/errors.js +6 -2
  8. package/dist/client/errors.js.map +1 -1
  9. package/dist/client/gallery.d.ts +3 -5
  10. package/dist/client/gallery.js +16 -20
  11. package/dist/client/gallery.js.map +1 -1
  12. package/dist/client/generated/error-codes.d.ts +1 -1
  13. package/dist/client/generated/error-codes.js +8 -0
  14. package/dist/client/generated/error-codes.js.map +1 -1
  15. package/dist/client/generated/routes.d.ts +15 -2
  16. package/dist/client/generated/routes.js +27 -2
  17. package/dist/client/generated/routes.js.map +1 -1
  18. package/dist/client/generated/wire.d.ts +32 -11
  19. package/dist/client/generated/wire.js.map +1 -1
  20. package/dist/client/media/windows.js +1 -1
  21. package/dist/client/media/windows.js.map +1 -1
  22. package/dist/client/versions.d.ts +6 -4
  23. package/dist/client/versions.js +24 -26
  24. package/dist/client/versions.js.map +1 -1
  25. package/dist/components/content/card.d.ts +5 -3
  26. package/dist/components/content/card.js +131 -109
  27. package/dist/components/content/card.js.map +1 -1
  28. package/dist/components/content/download-button.d.ts +26 -0
  29. package/dist/components/content/download-button.js +51 -0
  30. package/dist/components/content/download-button.js.map +1 -0
  31. package/dist/components/content/grid.d.ts +8 -1
  32. package/dist/components/content/grid.js +125 -76
  33. package/dist/components/content/grid.js.map +1 -1
  34. package/dist/components/content/index.d.ts +2 -1
  35. package/dist/components/content/listing.d.ts +3 -1
  36. package/dist/components/content/listing.js.map +1 -1
  37. package/dist/components/media-gallery.d.ts +1 -4
  38. package/dist/components/media-gallery.js +19 -34
  39. package/dist/components/media-gallery.js.map +1 -1
  40. package/dist/components/social/comments.js +1 -1
  41. package/dist/components/social/comments.js.map +1 -1
  42. package/dist/components/social/engagement.d.ts +2 -1
  43. package/dist/components/social/engagement.js +5 -3
  44. package/dist/components/social/engagement.js.map +1 -1
  45. package/dist/index.js +10 -9
  46. package/dist/locales/de.js +13 -2
  47. package/dist/locales/de.js.map +1 -1
  48. package/dist/locales/discovery/de.js +3 -0
  49. package/dist/locales/discovery/de.js.map +1 -1
  50. package/dist/locales/discovery/en.d.ts +3 -0
  51. package/dist/locales/discovery/en.js +3 -0
  52. package/dist/locales/discovery/en.js.map +1 -1
  53. package/dist/locales/discovery/es.js +3 -0
  54. package/dist/locales/discovery/es.js.map +1 -1
  55. package/dist/locales/discovery/ja.js +3 -0
  56. package/dist/locales/discovery/ja.js.map +1 -1
  57. package/dist/locales/discovery/ko.js +3 -0
  58. package/dist/locales/discovery/ko.js.map +1 -1
  59. package/dist/locales/discovery/zh.js +3 -0
  60. package/dist/locales/discovery/zh.js.map +1 -1
  61. package/dist/locales/en.d.ts +16 -2
  62. package/dist/locales/en.js +13 -2
  63. package/dist/locales/en.js.map +1 -1
  64. package/dist/locales/es.js +13 -2
  65. package/dist/locales/es.js.map +1 -1
  66. package/dist/locales/ja.js +13 -2
  67. package/dist/locales/ja.js.map +1 -1
  68. package/dist/locales/ko.js +13 -2
  69. package/dist/locales/ko.js.map +1 -1
  70. package/dist/locales/zh.js +13 -2
  71. package/dist/locales/zh.js.map +1 -1
  72. package/dist/react/comments.d.ts +1 -1
  73. package/dist/react/comments.js +3 -2
  74. package/dist/react/comments.js.map +1 -1
  75. package/dist/react/context.d.ts +1 -1
  76. package/dist/react/context.js.map +1 -1
  77. package/dist/react/discovery.d.ts +11 -0
  78. package/dist/react/discovery.js +49 -30
  79. package/dist/react/discovery.js.map +1 -1
  80. package/dist/react/engagement.d.ts +6 -2
  81. package/dist/react/engagement.js +2 -6
  82. package/dist/react/engagement.js.map +1 -1
  83. package/dist/react/index.d.ts +2 -2
  84. package/dist/react/versions.d.ts +25 -5
  85. package/dist/react/versions.js +58 -37
  86. package/dist/react/versions.js.map +1 -1
  87. package/dist/react.js +9 -9
  88. package/dist/styles.css +1 -1
  89. package/package.json +1 -1
package/README.md CHANGED
@@ -281,6 +281,20 @@ slot crops at its image's aspect, else its public preset's
281
281
  (`GET /media/presets`), so an item without an image yet still crops a 3:1
282
282
  cover at 3:1.
283
283
 
284
+ ### Card lists: decorations and locked cards
285
+
286
+ Every card list (`ContentGrid`, `ContentListing`, `ContentRail`,
287
+ `SearchResults`, `ContentHistory`) and `ContentCard` take `decorate(card)`:
288
+ the host's badges, drawn over the cover (place them yourself; clicks reach
289
+ the card, and click signals stay intact). Cards carry what badges need:
290
+ `access.level` and `access.tiers` (the effective rule, the same for every
291
+ reader), `version.has_audio`, `viewer.favorited` and `groups`.
292
+
293
+ `ContentGrid` and `ContentListing` take `locked`: `show` (default) marks the
294
+ cards the reader doesn't open, `hide` leaves them out, and `collapse` lists
295
+ the open ones and puts the rest behind a counted toggle that loads them on
296
+ opening (`useLockedCount` counts them).
297
+
284
298
  ### MediaFolderEditor
285
299
 
286
300
  ```tsx
@@ -325,8 +339,9 @@ the starting shape and edit.
325
339
  `<MediaGallery item={post} prefix="low-res/" renderLocked={unlock} />` reads
326
340
  through the client: the read (refreshed, reloaded after commits), HLS bases,
327
341
  playlist auth and the grant refresh. Passing `read`, `hlsBase`, `xhrSetup`
328
- and `refresh` yourself still works. A locked item's public teaser carries
329
- the locked count and `renderLocked`, so the unlock is on the first slide;
342
+ and `refresh` yourself still works. A locked read lists no files, so the
343
+ lock is uncounted; its public teaser carries `renderLocked`, so the unlock is
344
+ on the first slide;
330
345
  the stage is never taller than `maxHeight` (default `80svh`).
331
346
 
332
347
  ### VideoPosterPicker frames from another video
@@ -411,7 +426,10 @@ free-text answer and its groups; `results="always"` shows results before
411
426
  voting). Signed-out visitors comment under a name, react and vote where the
412
427
  server allows it (`content.Options.Anonymous`, read from the comment standing
413
428
  and `client.config()`); elsewhere they are asked to sign in (`onSignIn`, the
414
- provider's or their own) or, without one, told to.
429
+ provider's or their own) or, without one, told to. On locked content they
430
+ follow its kind's locked view (`kinds.Kind.Locked`): hidden comments show an
431
+ unlock note without a count, hidden reactions render nothing, a hidden
432
+ favorite count is left out, and cards drop hidden counts, never showing 0.
415
433
 
416
434
  Staff building blocks: `CommentBans` (paged) with `CommentBanDialog`, and
417
435
  `PollEditor` (deleting asks for a typed confirmation). The complete staff screens are the admin flows below.
@@ -59,11 +59,13 @@ export type ContentChange =
59
59
  type: "reaction.changed";
60
60
  target: Target;
61
61
  counts: ReactionCounts;
62
- } | {
62
+ }
63
+ /** count is absent where the target's kind's locked view hides it. */
64
+ | {
63
65
  type: "favorite.changed";
64
66
  target: Target;
65
67
  favorited: boolean;
66
- count: number;
68
+ count?: number;
67
69
  }
68
70
  /** Created, updated, voted or answered: the poll as the server now answers it. */
69
71
  | {
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../src/client/content/types.ts"],"sourcesContent":["import type { CommentBan, CommentStanding, Comment, Content, Poll, ReactionCounts, ResolveInput } from \"../generated/wire.js\";\n\n/** What an interaction targets: a content item (optionally one of its versions) or a group. */\nexport interface Target {\n type: \"contents\" | \"groups\";\n id: string;\n /** One version of a content item. */\n version?: string;\n}\n\n/** The same target. */\nexport function sameTarget(a?: Target, b?: Target): boolean {\n return !!a && !!b && a.type === b.type && a.id === b.id && (a.version ?? \"\") === (b.version ?? \"\");\n}\n\n/** A reaction: like 1, dislike -1, none 0. */\nexport type Reaction = -1 | 0 | 1;\n\n/** The route verb that sets a like or dislike; clearing is DELETE …/reaction. */\nexport function reactionVerb(value: -1 | 1): \"like\" | \"dislike\" {\n return value === 1 ? \"like\" : \"dislike\";\n}\n\n/** Comment order: newest (the default), likes (most liked) or best (Wilson lower bound). */\nexport type Sort = \"newest\" | \"likes\" | \"best\";\n\n/** A ban route family: \"owner\" bans from the caller's own content, \"global\" from the whole site. */\nexport type BanScope = CommentStanding[\"ban_scopes\"][number];\n\n/** What the review queue holds: comments and content items. */\nexport type HeldKind = \"comment\" | \"content\";\n\n/** The moderation rules: banned words or IP bans. */\nexport type RuleSet = \"words\" | \"ips\";\n\n/** A reviewer's decision on a held item. */\nexport type Decision = Exclude<ResolveInput[\"decision\"], \"review\">;\n\n/** A content-module mutation made through the client. */\nexport type ContentChange =\n /** Created or edited: the item as the server now answers it. */\n | { type: \"content.saved\"; content: Content }\n /** A version changed: refetch the item. */\n | { type: \"content.changed\"; id: string }\n | { type: \"content.deleted\"; id: string }\n /** target is the commented item. */\n | { type: \"comment.created\"; target: Target; comment: Comment }\n | { type: \"comment.updated\"; comment: Comment }\n | { type: \"comment.deleted\"; id: string }\n | { type: \"comment.restored\"; id: string }\n | { type: \"comment.reacted\"; id: string; counts: ReactionCounts }\n | { type: \"reaction.changed\"; target: Target; counts: ReactionCounts }\n | { type: \"favorite.changed\"; target: Target; favorited: boolean; count: number }\n /** Created, updated, voted or answered: the poll as the server now answers it. */\n | { type: \"poll.created\"; poll: Poll }\n | { type: \"poll.updated\"; poll: Poll }\n /** An option or image changed: refetch the poll. */\n | { type: \"poll.changed\"; id: string }\n | { type: \"poll.deleted\"; id: string }\n | { type: \"ban.saved\"; scope: BanScope; ban: CommentBan }\n | { type: \"ban.lifted\"; scope: BanScope; user: string }\n | { type: \"moderation.resolved\"; kind: HeldKind; id: string; decision: Decision }\n /** Banned words or IP bans changed. */\n | { type: \"rules.changed\"; rules: RuleSet }\n /** A version's reader metadata changed: its reader plan reads again. */\n | { type: \"reader.changed\"; content: string; version: string }\n | { type: \"groups.changed\" }\n /** The reader's content filters changed: every list reads again. */\n | { type: \"contentFilters.changed\" }\n | { type: \"history.changed\" }\n /** A search ran or recent searches were cleared. */\n | { type: \"searches.changed\" }\n /** The reader's keys changed (a join, a purchase the host reports): reads of access re-run. */\n | { type: \"access.changed\" };\n"],"mappings":";AAWA,SAAgB,EAAW,GAAY,GAAqB;CAC1D,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,SAAS,EAAE,WAAW;AACjG;AAMA,SAAgB,EAAa,GAAmC;CAC9D,OAAO,MAAU,IAAI,SAAS;AAChC"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../src/client/content/types.ts"],"sourcesContent":["import type { CommentBan, CommentStanding, Comment, Content, Poll, ReactionCounts, ResolveInput } from \"../generated/wire.js\";\n\n/** What an interaction targets: a content item (optionally one of its versions) or a group. */\nexport interface Target {\n type: \"contents\" | \"groups\";\n id: string;\n /** One version of a content item. */\n version?: string;\n}\n\n/** The same target. */\nexport function sameTarget(a?: Target, b?: Target): boolean {\n return !!a && !!b && a.type === b.type && a.id === b.id && (a.version ?? \"\") === (b.version ?? \"\");\n}\n\n/** A reaction: like 1, dislike -1, none 0. */\nexport type Reaction = -1 | 0 | 1;\n\n/** The route verb that sets a like or dislike; clearing is DELETE …/reaction. */\nexport function reactionVerb(value: -1 | 1): \"like\" | \"dislike\" {\n return value === 1 ? \"like\" : \"dislike\";\n}\n\n/** Comment order: newest (the default), likes (most liked) or best (Wilson lower bound). */\nexport type Sort = \"newest\" | \"likes\" | \"best\";\n\n/** A ban route family: \"owner\" bans from the caller's own content, \"global\" from the whole site. */\nexport type BanScope = CommentStanding[\"ban_scopes\"][number];\n\n/** What the review queue holds: comments and content items. */\nexport type HeldKind = \"comment\" | \"content\";\n\n/** The moderation rules: banned words or IP bans. */\nexport type RuleSet = \"words\" | \"ips\";\n\n/** A reviewer's decision on a held item. */\nexport type Decision = Exclude<ResolveInput[\"decision\"], \"review\">;\n\n/** A content-module mutation made through the client. */\nexport type ContentChange =\n /** Created or edited: the item as the server now answers it. */\n | { type: \"content.saved\"; content: Content }\n /** A version changed: refetch the item. */\n | { type: \"content.changed\"; id: string }\n | { type: \"content.deleted\"; id: string }\n /** target is the commented item. */\n | { type: \"comment.created\"; target: Target; comment: Comment }\n | { type: \"comment.updated\"; comment: Comment }\n | { type: \"comment.deleted\"; id: string }\n | { type: \"comment.restored\"; id: string }\n | { type: \"comment.reacted\"; id: string; counts: ReactionCounts }\n | { type: \"reaction.changed\"; target: Target; counts: ReactionCounts }\n /** count is absent where the target's kind's locked view hides it. */\n | { type: \"favorite.changed\"; target: Target; favorited: boolean; count?: number }\n /** Created, updated, voted or answered: the poll as the server now answers it. */\n | { type: \"poll.created\"; poll: Poll }\n | { type: \"poll.updated\"; poll: Poll }\n /** An option or image changed: refetch the poll. */\n | { type: \"poll.changed\"; id: string }\n | { type: \"poll.deleted\"; id: string }\n | { type: \"ban.saved\"; scope: BanScope; ban: CommentBan }\n | { type: \"ban.lifted\"; scope: BanScope; user: string }\n | { type: \"moderation.resolved\"; kind: HeldKind; id: string; decision: Decision }\n /** Banned words or IP bans changed. */\n | { type: \"rules.changed\"; rules: RuleSet }\n /** A version's reader metadata changed: its reader plan reads again. */\n | { type: \"reader.changed\"; content: string; version: string }\n | { type: \"groups.changed\" }\n /** The reader's content filters changed: every list reads again. */\n | { type: \"contentFilters.changed\" }\n | { type: \"history.changed\" }\n /** A search ran or recent searches were cleared. */\n | { type: \"searches.changed\" }\n /** The reader's keys changed (a join, a purchase the host reports): reads of access re-run. */\n | { type: \"access.changed\" };\n"],"mappings":";AAWA,SAAgB,EAAW,GAAY,GAAqB;CAC1D,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,SAAS,EAAE,WAAW;AACjG;AAMA,SAAgB,EAAa,GAAmC;CAC9D,OAAO,MAAU,IAAI,SAAS;AAChC"}
@@ -4,6 +4,9 @@ import { readerSession as t } from "./beacons.js";
4
4
  var n = /* @__PURE__ */ new Set([
5
5
  "groups",
6
6
  "exclude_groups",
7
+ "creators",
8
+ "access",
9
+ "tiers",
7
10
  "traits",
8
11
  "card_groups",
9
12
  "group_kinds"
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","names":[],"sources":["../../src/client/discovery.ts"],"sourcesContent":["import type {\n ContentFilters,\n ContentFiltersUpdate,\n ContentPage,\n ExposureInput,\n Exposures,\n RecentSearches,\n ReleaseDates,\n SignalInput,\n Signals,\n Typeahead,\n} from \"./generated/wire.js\";\nimport { readerSession } from \"./beacons.js\";\nimport type { Http } from \"./http.js\";\nimport { call } from \"./route.js\";\n\n/** A popularity period. */\nexport type Period = \"7d\" | \"30d\" | \"90d\" | \"365d\" | \"all\";\n/** A list's order; position orders by the first of `groups` (an ordered group). */\nexport type ListSort = \"newest\" | \"oldest\" | \"title\" | \"popular\" | `popular:${Period}` | \"top_rated\" | `top_rated:${Period}` | \"most_favorited\" | \"most_viewed\" | \"position\" | \"random\";\n/** How locked contents list: marked (lock, the default) or left out (hide). */\nexport type AccessMode = \"lock\" | \"hide\";\n\n/** Which contents a list holds. */\nexport interface ContentFilterQuery {\n kind?: string;\n /** Group ids every listed version is in. */\n groups?: readonly string[];\n /** Group ids no listed version is in. */\n exclude_groups?: readonly string[];\n /** Only contents this group owns: a channel's feed. */\n owner_group?: string;\n /** User ids: only contents one of them is credited on. */\n creators?: readonly string[];\n /** public (anyone opens it now) or the level it stays locked at. */\n access?: readonly (\"public\" | \"tier\" | \"group\" | \"ppv\" | \"group_ppv\")[];\n /** Only contents locked behind one of these tiers. */\n tiers?: readonly string[];\n /** Version traits every listed version has. */\n traits?: readonly string[];\n /** \"name:value\" of the kind's filterable attributes. */\n attribute?: readonly string[];\n /** Published on this day (YYYY-MM-DD) or month (YYYY-MM). */\n released_on?: string;\n /** IANA time zone of released_on and release dates; UTC by default. */\n tz?: string;\n /** The signed-in reader's unseen, in-progress or completed contents. */\n view_status?: \"unseen\" | \"in_progress\" | \"completed\";\n}\n\n/** How a list reads to its reader. */\nexport interface ReaderQuery {\n /** Only content with a live version in this language. */\n language?: string;\n /** Titles, names and paths in this language, and each card's preferred version's; language by default. */\n display_language?: string;\n mode?: AccessMode;\n}\n\n/** The group kinds cards name; every kind with a page by default. */\nexport interface CardQuery {\n card_groups?: readonly string[];\n}\n\nexport interface PageQuery {\n cursor?: string;\n limit?: number;\n}\n\nexport type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & { sort?: ListSort };\nexport type SearchQuery = ContentFilterQuery &\n ReaderQuery &\n CardQuery &\n PageQuery & {\n q: string;\n sort?: \"relevance\" | ListSort;\n personalize?: boolean;\n /** A signed-out reader's session, which keeps its recent searches; default readerSession(). */\n session?: string;\n };\nexport interface RecentSearchesQuery {\n /** A signed-out reader's session; default readerSession(). */\n session?: string;\n /** 10 by default, at most 100. */\n limit?: number;\n}\nexport type RailQuery = ContentFilterQuery & ReaderQuery & CardQuery & { limit?: number };\nexport type ReleaseDatesQuery = ContentFilterQuery & ReaderQuery & PageQuery & { by?: \"month\" | \"day\" };\nexport type HistoryQuery = ReaderQuery & CardQuery & PageQuery & { kind?: string; status?: \"seen\" | \"in_progress\" | \"completed\" };\nexport interface TypeaheadQuery extends ReaderQuery {\n q: string;\n /** Only contents of this kind. */\n kind?: string;\n /** The group kinds suggested; every kind with a page by default. */\n group_kinds?: readonly string[];\n /** Per section: 8 by default, at most 20. */\n limit?: number;\n}\n\n// Comma-joined list parameters; `attribute` repeats instead.\nconst LISTS = new Set([\"groups\", \"exclude_groups\", \"traits\", \"card_groups\", \"group_kinds\"]);\n\n/** A query with its list parameters comma-joined, as discovery routes read them. */\nexport function query(q: object): Record<string, string | number | boolean | readonly string[] | undefined> {\n const out: Record<string, string | number | boolean | readonly string[] | undefined> = {};\n for (const [k, v] of Object.entries(q)) out[k] = LISTS.has(k) && Array.isArray(v) ? (v.length ? v.join(\",\") : undefined) : v;\n return out;\n}\n\n/**\n * Discovery: lists, search, typeahead, similar and recommended contents,\n * release dates, the reader's content filters and history, signals and exposures.\n */\nexport class DiscoveryClient {\n constructor(private readonly http: Http) {}\n\n /** Contents as cards, one per content item with the version that fits the reader. */\n list(q: ContentQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents\", { query: query(q) as never, signal });\n }\n /** A first page is kept among the reader's recent searches. */\n async search(q: SearchQuery, signal?: AbortSignal): Promise<ContentPage> {\n const emit = q.cursor ? null : this.http.captureChanges();\n const out: ContentPage = await call(this.http, \"GET\", \"/search\", { query: query({ session: readerSession(), ...q }) as never, signal });\n emit?.({ type: \"searches.changed\" });\n return out;\n }\n /** The reader's recent searches, newest first, since they last cleared them. */\n recentSearches(q: RecentSearchesQuery = {}, signal?: AbortSignal): Promise<RecentSearches> {\n return call(this.http, \"GET\", \"/me/searches\", { query: { session: readerSession(), ...q }, signal });\n }\n /** Clears one recent search, or all of them. */\n async clearSearches(o: { query?: string; session?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/searches\", { query: { session: readerSession(), ...o } });\n emit({ type: \"searches.changed\" });\n }\n /** Contents and groups while the reader types. */\n typeahead(q: TypeaheadQuery, signal?: AbortSignal): Promise<Typeahead> {\n return call(this.http, \"GET\", \"/typeahead\", { query: query(q) as never, signal });\n }\n /** Contents like one, of its kind unless q.kind names another. */\n similar(id: string, q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents/{id}/similar\", { params: { id }, query: query(q) as never, signal });\n }\n /** For the signed-in reader. */\n recommendations(q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/recommendations\", { query: query(q) as never, signal });\n }\n /** Per month or day, newest first: how many contents the reader's listing shows released then, and how many are free. */\n releaseDates(q: ReleaseDatesQuery = {}, signal?: AbortSignal): Promise<ReleaseDates> {\n return call(this.http, \"GET\", \"/release-dates\", { query: query(q) as never, signal });\n }\n contentFilters(language?: string, signal?: AbortSignal): Promise<ContentFilters> {\n return call(this.http, \"GET\", \"/me/content-filters\", { query: { language }, signal });\n }\n /** Replaces the reader's filter settings; every read applies them. */\n async setContentFilters(update: ContentFiltersUpdate, language?: string): Promise<ContentFilters> {\n const emit = this.http.captureChanges();\n const out: ContentFilters = await call(this.http, \"PUT\", \"/me/content-filters\", { query: { language }, body: update });\n emit({ type: \"contentFilters.changed\" });\n return out;\n }\n /** What the reader engaged with, most recent first. */\n history(q: HistoryQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/me/history\", { query: query(q) as never, signal });\n }\n /** Erases the reader's history: of one content item, one kind or everything. */\n async forgetHistory(o: { kind?: string; content_id?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/history\", { query: o });\n emit({ type: \"history.changed\" });\n }\n /** Views and clicks; signed out, of the session named. */\n signals(signals: SignalInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Signals = { session, signals };\n return call(this.http, \"POST\", \"/signals\", { body, keepalive: o.keepalive });\n }\n /** Result lists served, rendered or shown, for click attribution. */\n exposures(exposures: ExposureInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Exposures = { session, exposures };\n return call(this.http, \"POST\", \"/exposures\", { body, keepalive: o.keepalive });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"discovery.js","names":[],"sources":["../../src/client/discovery.ts"],"sourcesContent":["import type {\n ContentFilters,\n ContentFiltersUpdate,\n ContentPage,\n ExposureInput,\n Exposures,\n RecentSearches,\n ReleaseDates,\n SignalInput,\n Signals,\n Typeahead,\n} from \"./generated/wire.js\";\nimport { readerSession } from \"./beacons.js\";\nimport type { Http } from \"./http.js\";\nimport { call } from \"./route.js\";\n\n/** A popularity period. */\nexport type Period = \"7d\" | \"30d\" | \"90d\" | \"365d\" | \"all\";\n/** A list's order; position orders by the first of `groups` (an ordered group). */\nexport type ListSort = \"newest\" | \"oldest\" | \"title\" | \"popular\" | `popular:${Period}` | \"top_rated\" | `top_rated:${Period}` | \"most_favorited\" | \"most_viewed\" | \"position\" | \"random\";\n/** How locked contents list: marked (lock, the default) or left out (hide). */\nexport type AccessMode = \"lock\" | \"hide\";\n\n/** Which contents a list holds. */\nexport interface ContentFilterQuery {\n kind?: string;\n /** Group ids every listed version is in. */\n groups?: readonly string[];\n /** Group ids no listed version is in. */\n exclude_groups?: readonly string[];\n /** Only contents this group owns: a channel's feed. */\n owner_group?: string;\n /** User ids: only contents one of them is credited on. */\n creators?: readonly string[];\n /** public (anyone opens it now) or the level it stays locked at. */\n access?: readonly (\"public\" | \"tier\" | \"group\" | \"ppv\" | \"group_ppv\")[];\n /** Only contents locked behind one of these tiers. */\n tiers?: readonly string[];\n /** Version traits every listed version has. */\n traits?: readonly string[];\n /** \"name:value\" of the kind's filterable attributes. */\n attribute?: readonly string[];\n /** Published on this day (YYYY-MM-DD) or month (YYYY-MM). */\n released_on?: string;\n /** IANA time zone of released_on and release dates; UTC by default. */\n tz?: string;\n /** The signed-in reader's unseen, in-progress or completed contents. */\n view_status?: \"unseen\" | \"in_progress\" | \"completed\";\n}\n\n/** How a list reads to its reader. */\nexport interface ReaderQuery {\n /** Only content with a live version in this language. */\n language?: string;\n /** Titles, names and paths in this language, and each card's preferred version's; language by default. */\n display_language?: string;\n mode?: AccessMode;\n}\n\n/** The group kinds cards name; every kind with a page by default. */\nexport interface CardQuery {\n card_groups?: readonly string[];\n}\n\nexport interface PageQuery {\n cursor?: string;\n limit?: number;\n}\n\nexport type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & { sort?: ListSort };\nexport type SearchQuery = ContentFilterQuery &\n ReaderQuery &\n CardQuery &\n PageQuery & {\n q: string;\n sort?: \"relevance\" | ListSort;\n personalize?: boolean;\n /** A signed-out reader's session, which keeps its recent searches; default readerSession(). */\n session?: string;\n };\nexport interface RecentSearchesQuery {\n /** A signed-out reader's session; default readerSession(). */\n session?: string;\n /** 10 by default, at most 100. */\n limit?: number;\n}\nexport type RailQuery = ContentFilterQuery & ReaderQuery & CardQuery & { limit?: number };\nexport type ReleaseDatesQuery = ContentFilterQuery & ReaderQuery & PageQuery & { by?: \"month\" | \"day\" };\nexport type HistoryQuery = ReaderQuery & CardQuery & PageQuery & { kind?: string; status?: \"seen\" | \"in_progress\" | \"completed\" };\nexport interface TypeaheadQuery extends ReaderQuery {\n q: string;\n /** Only contents of this kind. */\n kind?: string;\n /** The group kinds suggested; every kind with a page by default. */\n group_kinds?: readonly string[];\n /** Per section: 8 by default, at most 20. */\n limit?: number;\n}\n\n// Comma-joined list parameters; `attribute` repeats instead.\nconst LISTS = new Set([\"groups\", \"exclude_groups\", \"creators\", \"access\", \"tiers\", \"traits\", \"card_groups\", \"group_kinds\"]);\n\n/** A query with its list parameters comma-joined, as discovery routes read them. */\nexport function query(q: object): Record<string, string | number | boolean | readonly string[] | undefined> {\n const out: Record<string, string | number | boolean | readonly string[] | undefined> = {};\n for (const [k, v] of Object.entries(q)) out[k] = LISTS.has(k) && Array.isArray(v) ? (v.length ? v.join(\",\") : undefined) : v;\n return out;\n}\n\n/**\n * Discovery: lists, search, typeahead, similar and recommended contents,\n * release dates, the reader's content filters and history, signals and exposures.\n */\nexport class DiscoveryClient {\n constructor(private readonly http: Http) {}\n\n /** Contents as cards, one per content item with the version that fits the reader. */\n list(q: ContentQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents\", { query: query(q) as never, signal });\n }\n /** A first page is kept among the reader's recent searches. */\n async search(q: SearchQuery, signal?: AbortSignal): Promise<ContentPage> {\n const emit = q.cursor ? null : this.http.captureChanges();\n const out: ContentPage = await call(this.http, \"GET\", \"/search\", { query: query({ session: readerSession(), ...q }) as never, signal });\n emit?.({ type: \"searches.changed\" });\n return out;\n }\n /** The reader's recent searches, newest first, since they last cleared them. */\n recentSearches(q: RecentSearchesQuery = {}, signal?: AbortSignal): Promise<RecentSearches> {\n return call(this.http, \"GET\", \"/me/searches\", { query: { session: readerSession(), ...q }, signal });\n }\n /** Clears one recent search, or all of them. */\n async clearSearches(o: { query?: string; session?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/searches\", { query: { session: readerSession(), ...o } });\n emit({ type: \"searches.changed\" });\n }\n /** Contents and groups while the reader types. */\n typeahead(q: TypeaheadQuery, signal?: AbortSignal): Promise<Typeahead> {\n return call(this.http, \"GET\", \"/typeahead\", { query: query(q) as never, signal });\n }\n /** Contents like one, of its kind unless q.kind names another. */\n similar(id: string, q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents/{id}/similar\", { params: { id }, query: query(q) as never, signal });\n }\n /** For the signed-in reader. */\n recommendations(q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/recommendations\", { query: query(q) as never, signal });\n }\n /** Per month or day, newest first: how many contents the reader's listing shows released then, and how many are free. */\n releaseDates(q: ReleaseDatesQuery = {}, signal?: AbortSignal): Promise<ReleaseDates> {\n return call(this.http, \"GET\", \"/release-dates\", { query: query(q) as never, signal });\n }\n contentFilters(language?: string, signal?: AbortSignal): Promise<ContentFilters> {\n return call(this.http, \"GET\", \"/me/content-filters\", { query: { language }, signal });\n }\n /** Replaces the reader's filter settings; every read applies them. */\n async setContentFilters(update: ContentFiltersUpdate, language?: string): Promise<ContentFilters> {\n const emit = this.http.captureChanges();\n const out: ContentFilters = await call(this.http, \"PUT\", \"/me/content-filters\", { query: { language }, body: update });\n emit({ type: \"contentFilters.changed\" });\n return out;\n }\n /** What the reader engaged with, most recent first. */\n history(q: HistoryQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/me/history\", { query: query(q) as never, signal });\n }\n /** Erases the reader's history: of one content item, one kind or everything. */\n async forgetHistory(o: { kind?: string; content_id?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/history\", { query: o });\n emit({ type: \"history.changed\" });\n }\n /** Views and clicks; signed out, of the session named. */\n signals(signals: SignalInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Signals = { session, signals };\n return call(this.http, \"POST\", \"/signals\", { body, keepalive: o.keepalive });\n }\n /** Result lists served, rendered or shown, for click attribution. */\n exposures(exposures: ExposureInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Exposures = { session, exposures };\n return call(this.http, \"POST\", \"/exposures\", { body, keepalive: o.keepalive });\n }\n}\n"],"mappings":"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"}
@@ -17,6 +17,8 @@ export interface ContentKitErrorInit extends ErrorOptions {
17
17
  blobs?: string[];
18
18
  ban?: BanNotice;
19
19
  action?: string;
20
+ reason?: string;
21
+ availableAt?: string;
20
22
  }
21
23
  export declare class ContentKitError extends Error {
22
24
  readonly code: ContentKitErrorCode;
@@ -28,6 +30,10 @@ export declare class ContentKitError extends Error {
28
30
  readonly blobs?: string[];
29
31
  readonly ban?: BanNotice;
30
32
  readonly action?: string;
33
+ /** A download_unavailable's code of the host's policy ("release_window"). */
34
+ readonly reason?: string;
35
+ /** A download_unavailable's opening time (RFC 3339), when a matter of time. */
36
+ readonly availableAt?: string;
31
37
  constructor(code: ContentKitErrorCode, message: string, init?: ContentKitErrorInit);
32
38
  /**
33
39
  * The request broke a rule the message states (a 4xx or a typed refusal),
@@ -16,8 +16,10 @@ var t = [
16
16
  blobs;
17
17
  ban;
18
18
  action;
19
+ reason;
20
+ availableAt;
19
21
  constructor(e, t, n = {}) {
20
- super(t, n.cause === void 0 ? void 0 : { cause: n.cause }), this.code = e, this.status = n.status ?? 0, this.retryAfter = n.retryAfter, this.details = n.details, this.blobs = n.blobs, this.ban = n.ban, this.action = n.action;
22
+ super(t, n.cause === void 0 ? void 0 : { cause: n.cause }), this.code = e, this.status = n.status ?? 0, this.retryAfter = n.retryAfter, this.details = n.details, this.blobs = n.blobs, this.ban = n.ban, this.action = n.action, this.reason = n.reason, this.availableAt = n.availableAt;
21
23
  }
22
24
  get refusal() {
23
25
  return this.status >= 400 && this.status < 500 && this.code !== "storage" || this.code.startsWith("image_") || this.code === "decode";
@@ -68,7 +70,9 @@ async function u(e) {
68
70
  blobs: t.blobs,
69
71
  details: t.details,
70
72
  ban: t.ban,
71
- action: t.action
73
+ action: t.action,
74
+ reason: t.reason,
75
+ availableAt: t.available_at
72
76
  });
73
77
  }
74
78
  function d(e) {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/client/errors.ts"],"sourcesContent":["import { CONTENTKIT_ERROR_CODES, type ErrorCode } from \"./generated/error-codes.js\";\nimport type { BanNotice, ErrorDetails, ErrorReply, Failure } from \"./generated/wire.js\";\n\n/**\n * Codes raised in the browser: network (no response), storage (the bucket\n * refused a PUT), aborted (the caller's signal), resume_mismatch (saved state\n * is not for this file), decode (not readable as an image) and render_timeout\n * (still processing after the wait).\n */\nexport const CLIENT_ERROR_CODES = [\"network\", \"storage\", \"aborted\", \"resume_mismatch\", \"decode\", \"render_timeout\"] as const;\n\nexport type ContentKitErrorCode = ErrorCode | (typeof CLIENT_ERROR_CODES)[number];\n\n/** Every code a ContentKitError can carry: the server's registry, then the client's. */\nexport const ERROR_CODES: readonly ContentKitErrorCode[] = [...(Object.keys(CONTENTKIT_ERROR_CODES) as ErrorCode[]), ...CLIENT_ERROR_CODES];\n\nexport interface ContentKitErrorInit extends ErrorOptions {\n status?: number;\n retryAfter?: number;\n details?: ErrorDetails;\n blobs?: string[];\n ban?: BanNotice;\n action?: string;\n}\n\nexport class ContentKitError extends Error {\n override readonly name = \"ContentKitError\";\n /** HTTP status; 0 for client-side codes. */\n readonly status: number;\n readonly retryAfter?: number;\n readonly details?: ErrorDetails;\n readonly blobs?: string[];\n readonly ban?: BanNotice;\n readonly action?: string;\n\n constructor(\n readonly code: ContentKitErrorCode,\n message: string,\n init: ContentKitErrorInit = {},\n ) {\n super(message, init.cause === undefined ? undefined : { cause: init.cause });\n this.status = init.status ?? 0;\n this.retryAfter = init.retryAfter;\n this.details = init.details;\n this.blobs = init.blobs;\n this.ban = init.ban;\n this.action = init.action;\n }\n\n /**\n * The request broke a rule the message states (a 4xx or a typed refusal),\n * as opposed to a fault on the server or the network: show it as is.\n */\n get refusal(): boolean {\n return (this.status >= 400 && this.status < 500 && this.code !== \"storage\") || this.code.startsWith(\"image_\") || this.code === \"decode\";\n }\n\n /** Rate or quota refusals: stop starting new requests until the limit frees. */\n get isLimit(): boolean {\n return this.code === \"rate_limited\" || this.code === \"quota_exceeded\";\n }\n\n /** The commit would exceed the kind's file caps (too_many_files, 409): remove files first. */\n get isCeiling(): boolean {\n return this.code === \"too_many_files\";\n }\n\n /** Refusals that apply to every upload of this caller, not just this file. */\n get blocksQueue(): boolean {\n return this.isLimit || this.code === \"unauthorized\" || this.code === \"forbidden\";\n }\n\n /** Worth retrying the same request. */\n get transient(): boolean {\n return (\n this.code === \"network\" ||\n this.code === \"internal_error\" ||\n this.code === \"unavailable\" ||\n (this.code === \"storage\" && (this.status >= 500 || this.status === 403 || this.status === 408 || this.status === 429))\n );\n }\n}\n\nexport const isContentKitError = (e: unknown): e is ContentKitError => e instanceof ContentKitError;\n\n/** Anything a call can throw, as a ContentKitError (network when it is not one). */\nexport function toContentKitError(e: unknown): ContentKitError {\n if (e instanceof ContentKitError) return e;\n return new ContentKitError(\"network\", e instanceof Error ? e.message : String(e), { cause: e });\n}\n\nexport function aborted(signal?: AbortSignal): ContentKitError {\n return new ContentKitError(\"aborted\", \"request canceled\", { cause: signal?.reason });\n}\n\nexport function throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) throw aborted(signal);\n}\n\nconst byStatus: Record<number, ContentKitErrorCode> = {\n 401: \"unauthorized\",\n 403: \"forbidden\",\n 404: \"not_found\",\n 409: \"conflict\",\n 410: \"gone\",\n 413: \"too_large\",\n 415: \"type_not_allowed\",\n 429: \"rate_limited\",\n 501: \"not_configured\",\n 503: \"unavailable\",\n};\n\nconst known = new Set<string>(ERROR_CODES);\n\n/** Reads a response that is not 2xx. */\nexport async function readContentKitError(res: Response): Promise<ContentKitError> {\n let body: Partial<ErrorReply> = {};\n try {\n body = (await res.json()) as Partial<ErrorReply>;\n } catch {\n // not JSON (a proxy error page)\n }\n const header = Number(res.headers.get(\"Retry-After\"));\n const retryAfter = body.retry_after ?? (Number.isFinite(header) && header > 0 ? header : undefined);\n const code = body.code && known.has(body.code) ? (body.code as ContentKitErrorCode) : (byStatus[res.status] ?? (res.status >= 500 ? \"internal_error\" : \"invalid_request\"));\n return new ContentKitError(code, body.error ?? `HTTP ${res.status}`, {\n status: res.status,\n retryAfter,\n blobs: body.blobs,\n details: body.details,\n ban: body.ban,\n action: body.action,\n });\n}\n\n/** An upload's recorded processing failure: its typed refusal, else a processing fault. */\nexport function failureError(f: Failure): ContentKitError {\n const code = f.code && known.has(f.code) ? (f.code as ContentKitErrorCode) : \"internal_error\";\n return new ContentKitError(code, f.message, { details: f.details });\n}\n"],"mappings":";;AASA,IAAa,IAAqB;CAAC;CAAW;CAAW;CAAW;CAAmB;CAAU;AAAgB,GAKpG,IAA8C,CAAC,GAAI,OAAO,KAAK,CAAsB,GAAmB,GAAG,CAAkB,GAW7H,IAAb,cAAqC,MAAM;CAW9B;CAVX,OAAyB;CAEzB;CACA;CACA;CACA;CACA;CACA;CAEA,YACE,GACA,GACA,IAA4B,CAAC,GAC7B;EAOA,AANA,MAAM,GAAS,EAAK,UAAU,KAAA,IAAY,KAAA,IAAY,EAAE,OAAO,EAAK,MAAM,CAAC,GAJlE,KAAA,OAAA,GAKT,KAAK,SAAS,EAAK,UAAU,GAC7B,KAAK,aAAa,EAAK,YACvB,KAAK,UAAU,EAAK,SACpB,KAAK,QAAQ,EAAK,OAClB,KAAK,MAAM,EAAK,KAChB,KAAK,SAAS,EAAK;CACrB;CAMA,IAAI,UAAmB;EACrB,OAAQ,KAAK,UAAU,OAAO,KAAK,SAAS,OAAO,KAAK,SAAS,aAAc,KAAK,KAAK,WAAW,QAAQ,KAAK,KAAK,SAAS;CACjI;CAGA,IAAI,UAAmB;EACrB,OAAO,KAAK,SAAS,kBAAkB,KAAK,SAAS;CACvD;CAGA,IAAI,YAAqB;EACvB,OAAO,KAAK,SAAS;CACvB;CAGA,IAAI,cAAuB;EACzB,OAAO,KAAK,WAAW,KAAK,SAAS,kBAAkB,KAAK,SAAS;CACvE;CAGA,IAAI,YAAqB;EACvB,OACE,KAAK,SAAS,aACd,KAAK,SAAS,oBACd,KAAK,SAAS,iBACb,KAAK,SAAS,cAAc,KAAK,UAAU,OAAO,KAAK,WAAW,OAAO,KAAK,WAAW,OAAO,KAAK,WAAW;CAErH;AACF,GAEa,KAAqB,MAAqC,aAAa;AAGpF,SAAgB,EAAkB,GAA6B;CAE7D,OADI,aAAa,IAAwB,IAClC,IAAI,EAAgB,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC;AAChG;AAEA,SAAgB,EAAQ,GAAuC;CAC7D,OAAO,IAAI,EAAgB,WAAW,oBAAoB,EAAE,OAAO,GAAQ,OAAO,CAAC;AACrF;AAEA,SAAgB,EAAe,GAA4B;CACzD,IAAI,GAAQ,SAAS,MAAM,EAAQ,CAAM;AAC3C;AAEA,IAAM,IAAgD;CACpD,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACP,GAEM,IAAQ,IAAI,IAAY,CAAW;AAGzC,eAAsB,EAAoB,GAAyC;CACjF,IAAI,IAA4B,CAAC;CACjC,IAAI;EACF,IAAQ,MAAM,EAAI,KAAK;CACzB,QAAQ,CAER;CACA,IAAM,IAAS,OAAO,EAAI,QAAQ,IAAI,aAAa,CAAC,GAC9C,IAAa,EAAK,gBAAgB,OAAO,SAAS,CAAM,KAAK,IAAS,IAAI,IAAS,KAAA;CAEzF,OAAO,IAAI,EADE,EAAK,QAAQ,EAAM,IAAI,EAAK,IAAI,IAAK,EAAK,OAAgC,EAAS,EAAI,YAAY,EAAI,UAAU,MAAM,mBAAmB,oBACtH,EAAK,SAAS,QAAQ,EAAI,UAAU;EACnE,QAAQ,EAAI;EACZ;EACA,OAAO,EAAK;EACZ,SAAS,EAAK;EACd,KAAK,EAAK;EACV,QAAQ,EAAK;CACf,CAAC;AACH;AAGA,SAAgB,EAAa,GAA6B;CAExD,OAAO,IAAI,EADE,EAAE,QAAQ,EAAM,IAAI,EAAE,IAAI,IAAK,EAAE,OAA+B,kBAC5C,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,CAAC;AACpE"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/client/errors.ts"],"sourcesContent":["import { CONTENTKIT_ERROR_CODES, type ErrorCode } from \"./generated/error-codes.js\";\nimport type { BanNotice, ErrorDetails, ErrorReply, Failure } from \"./generated/wire.js\";\n\n/**\n * Codes raised in the browser: network (no response), storage (the bucket\n * refused a PUT), aborted (the caller's signal), resume_mismatch (saved state\n * is not for this file), decode (not readable as an image) and render_timeout\n * (still processing after the wait).\n */\nexport const CLIENT_ERROR_CODES = [\"network\", \"storage\", \"aborted\", \"resume_mismatch\", \"decode\", \"render_timeout\"] as const;\n\nexport type ContentKitErrorCode = ErrorCode | (typeof CLIENT_ERROR_CODES)[number];\n\n/** Every code a ContentKitError can carry: the server's registry, then the client's. */\nexport const ERROR_CODES: readonly ContentKitErrorCode[] = [...(Object.keys(CONTENTKIT_ERROR_CODES) as ErrorCode[]), ...CLIENT_ERROR_CODES];\n\nexport interface ContentKitErrorInit extends ErrorOptions {\n status?: number;\n retryAfter?: number;\n details?: ErrorDetails;\n blobs?: string[];\n ban?: BanNotice;\n action?: string;\n reason?: string;\n availableAt?: string;\n}\n\nexport class ContentKitError extends Error {\n override readonly name = \"ContentKitError\";\n /** HTTP status; 0 for client-side codes. */\n readonly status: number;\n readonly retryAfter?: number;\n readonly details?: ErrorDetails;\n readonly blobs?: string[];\n readonly ban?: BanNotice;\n readonly action?: string;\n /** A download_unavailable's code of the host's policy (\"release_window\"). */\n readonly reason?: string;\n /** A download_unavailable's opening time (RFC 3339), when a matter of time. */\n readonly availableAt?: string;\n\n constructor(\n readonly code: ContentKitErrorCode,\n message: string,\n init: ContentKitErrorInit = {},\n ) {\n super(message, init.cause === undefined ? undefined : { cause: init.cause });\n this.status = init.status ?? 0;\n this.retryAfter = init.retryAfter;\n this.details = init.details;\n this.blobs = init.blobs;\n this.ban = init.ban;\n this.action = init.action;\n this.reason = init.reason;\n this.availableAt = init.availableAt;\n }\n\n /**\n * The request broke a rule the message states (a 4xx or a typed refusal),\n * as opposed to a fault on the server or the network: show it as is.\n */\n get refusal(): boolean {\n return (this.status >= 400 && this.status < 500 && this.code !== \"storage\") || this.code.startsWith(\"image_\") || this.code === \"decode\";\n }\n\n /** Rate or quota refusals: stop starting new requests until the limit frees. */\n get isLimit(): boolean {\n return this.code === \"rate_limited\" || this.code === \"quota_exceeded\";\n }\n\n /** The commit would exceed the kind's file caps (too_many_files, 409): remove files first. */\n get isCeiling(): boolean {\n return this.code === \"too_many_files\";\n }\n\n /** Refusals that apply to every upload of this caller, not just this file. */\n get blocksQueue(): boolean {\n return this.isLimit || this.code === \"unauthorized\" || this.code === \"forbidden\";\n }\n\n /** Worth retrying the same request. */\n get transient(): boolean {\n return (\n this.code === \"network\" ||\n this.code === \"internal_error\" ||\n this.code === \"unavailable\" ||\n (this.code === \"storage\" && (this.status >= 500 || this.status === 403 || this.status === 408 || this.status === 429))\n );\n }\n}\n\nexport const isContentKitError = (e: unknown): e is ContentKitError => e instanceof ContentKitError;\n\n/** Anything a call can throw, as a ContentKitError (network when it is not one). */\nexport function toContentKitError(e: unknown): ContentKitError {\n if (e instanceof ContentKitError) return e;\n return new ContentKitError(\"network\", e instanceof Error ? e.message : String(e), { cause: e });\n}\n\nexport function aborted(signal?: AbortSignal): ContentKitError {\n return new ContentKitError(\"aborted\", \"request canceled\", { cause: signal?.reason });\n}\n\nexport function throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) throw aborted(signal);\n}\n\nconst byStatus: Record<number, ContentKitErrorCode> = {\n 401: \"unauthorized\",\n 403: \"forbidden\",\n 404: \"not_found\",\n 409: \"conflict\",\n 410: \"gone\",\n 413: \"too_large\",\n 415: \"type_not_allowed\",\n 429: \"rate_limited\",\n 501: \"not_configured\",\n 503: \"unavailable\",\n};\n\nconst known = new Set<string>(ERROR_CODES);\n\n/** Reads a response that is not 2xx. */\nexport async function readContentKitError(res: Response): Promise<ContentKitError> {\n let body: Partial<ErrorReply> = {};\n try {\n body = (await res.json()) as Partial<ErrorReply>;\n } catch {\n // not JSON (a proxy error page)\n }\n const header = Number(res.headers.get(\"Retry-After\"));\n const retryAfter = body.retry_after ?? (Number.isFinite(header) && header > 0 ? header : undefined);\n const code = body.code && known.has(body.code) ? (body.code as ContentKitErrorCode) : (byStatus[res.status] ?? (res.status >= 500 ? \"internal_error\" : \"invalid_request\"));\n return new ContentKitError(code, body.error ?? `HTTP ${res.status}`, {\n status: res.status,\n retryAfter,\n blobs: body.blobs,\n details: body.details,\n ban: body.ban,\n action: body.action,\n reason: body.reason,\n availableAt: body.available_at,\n });\n}\n\n/** An upload's recorded processing failure: its typed refusal, else a processing fault. */\nexport function failureError(f: Failure): ContentKitError {\n const code = f.code && known.has(f.code) ? (f.code as ContentKitErrorCode) : \"internal_error\";\n return new ContentKitError(code, f.message, { details: f.details });\n}\n"],"mappings":";;AASA,IAAa,IAAqB;CAAC;CAAW;CAAW;CAAW;CAAmB;CAAU;AAAgB,GAKpG,IAA8C,CAAC,GAAI,OAAO,KAAK,CAAsB,GAAmB,GAAG,CAAkB,GAa7H,IAAb,cAAqC,MAAM;CAe9B;CAdX,OAAyB;CAEzB;CACA;CACA;CACA;CACA;CACA;CAEA;CAEA;CAEA,YACE,GACA,GACA,IAA4B,CAAC,GAC7B;EASA,AARA,MAAM,GAAS,EAAK,UAAU,KAAA,IAAY,KAAA,IAAY,EAAE,OAAO,EAAK,MAAM,CAAC,GAJlE,KAAA,OAAA,GAKT,KAAK,SAAS,EAAK,UAAU,GAC7B,KAAK,aAAa,EAAK,YACvB,KAAK,UAAU,EAAK,SACpB,KAAK,QAAQ,EAAK,OAClB,KAAK,MAAM,EAAK,KAChB,KAAK,SAAS,EAAK,QACnB,KAAK,SAAS,EAAK,QACnB,KAAK,cAAc,EAAK;CAC1B;CAMA,IAAI,UAAmB;EACrB,OAAQ,KAAK,UAAU,OAAO,KAAK,SAAS,OAAO,KAAK,SAAS,aAAc,KAAK,KAAK,WAAW,QAAQ,KAAK,KAAK,SAAS;CACjI;CAGA,IAAI,UAAmB;EACrB,OAAO,KAAK,SAAS,kBAAkB,KAAK,SAAS;CACvD;CAGA,IAAI,YAAqB;EACvB,OAAO,KAAK,SAAS;CACvB;CAGA,IAAI,cAAuB;EACzB,OAAO,KAAK,WAAW,KAAK,SAAS,kBAAkB,KAAK,SAAS;CACvE;CAGA,IAAI,YAAqB;EACvB,OACE,KAAK,SAAS,aACd,KAAK,SAAS,oBACd,KAAK,SAAS,iBACb,KAAK,SAAS,cAAc,KAAK,UAAU,OAAO,KAAK,WAAW,OAAO,KAAK,WAAW,OAAO,KAAK,WAAW;CAErH;AACF,GAEa,KAAqB,MAAqC,aAAa;AAGpF,SAAgB,EAAkB,GAA6B;CAE7D,OADI,aAAa,IAAwB,IAClC,IAAI,EAAgB,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC;AAChG;AAEA,SAAgB,EAAQ,GAAuC;CAC7D,OAAO,IAAI,EAAgB,WAAW,oBAAoB,EAAE,OAAO,GAAQ,OAAO,CAAC;AACrF;AAEA,SAAgB,EAAe,GAA4B;CACzD,IAAI,GAAQ,SAAS,MAAM,EAAQ,CAAM;AAC3C;AAEA,IAAM,IAAgD;CACpD,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACP,GAEM,IAAQ,IAAI,IAAY,CAAW;AAGzC,eAAsB,EAAoB,GAAyC;CACjF,IAAI,IAA4B,CAAC;CACjC,IAAI;EACF,IAAQ,MAAM,EAAI,KAAK;CACzB,QAAQ,CAER;CACA,IAAM,IAAS,OAAO,EAAI,QAAQ,IAAI,aAAa,CAAC,GAC9C,IAAa,EAAK,gBAAgB,OAAO,SAAS,CAAM,KAAK,IAAS,IAAI,IAAS,KAAA;CAEzF,OAAO,IAAI,EADE,EAAK,QAAQ,EAAM,IAAI,EAAK,IAAI,IAAK,EAAK,OAAgC,EAAS,EAAI,YAAY,EAAI,UAAU,MAAM,mBAAmB,oBACtH,EAAK,SAAS,QAAQ,EAAI,UAAU;EACnE,QAAQ,EAAI;EACZ;EACA,OAAO,EAAK;EACZ,SAAS,EAAK;EACd,KAAK,EAAK;EACV,QAAQ,EAAK;EACb,QAAQ,EAAK;EACb,aAAa,EAAK;CACpB,CAAC;AACH;AAGA,SAAgB,EAAa,GAA6B;CAExD,OAAO,IAAI,EADE,EAAE,QAAQ,EAAM,IAAI,EAAE,IAAI,IAAK,EAAE,OAA+B,kBAC5C,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,CAAC;AACpE"}
@@ -10,12 +10,10 @@ export interface GalleryMediaItem {
10
10
  /** Width / height from the read API; a default when unknown. */
11
11
  aspect: number;
12
12
  }
13
- /** What a viewer without access is missing; `backdrop` is the public preview drawn behind it. */
13
+ /** What a viewer without access is missing, uncounted; `backdrop` is the public preview drawn behind it. */
14
14
  export interface GalleryLockedItem {
15
15
  kind: "locked";
16
16
  key: string;
17
- count: number;
18
- videos: number;
19
17
  backdrop?: string;
20
18
  aspect: number;
21
19
  }
@@ -31,8 +29,8 @@ export declare const isSubtitleType: (type?: string) => boolean;
31
29
  * The read result as gallery items in manifest order. With access: each
32
30
  * image, each playable HLS ladder (a folder in `hls`: a video, or an
33
31
  * audio-only one) and each other audio file. Without: the item's public
34
- * previews (the read's `previews`), then one locked item for the rest; an
35
- * item's private files are all or nothing, so locked files never carry URLs.
32
+ * previews (the read's `previews`), then one locked item when the read says
33
+ * it holds locked files; a locked read lists none, so nothing counts them.
36
34
  * Other files (plain videos, subtitles, zips) are not items; scope the read
37
35
  * with a prefix to choose.
38
36
  */
@@ -8,10 +8,9 @@ var e = (e) => !!e?.startsWith("video/"), t = (e) => !!e?.startsWith("audio/"),
8
8
  function i(e, t) {
9
9
  return e?.w && e.h ? e.w / e.h : t;
10
10
  }
11
- var a = (e) => e.slice(0, e.lastIndexOf("/") + 1);
12
- function o(r) {
11
+ function a(r) {
13
12
  if (!r) return [];
14
- let o = r.access === "full", s = r.hls ?? [], c = (e) => s.find((t) => e.path.startsWith(t)), l = (e) => a(e.path) || e.path, u = r.files.filter((n) => n.locked && (e(n.type) || t(n.type))), d = new Set(u.map(l)), f = r.files.filter((e) => e.locked && n(e.type) && !d.has(a(e.path))), p = new Set(u.filter((t) => e(t.type)).map(l)).size, m = o ? [] : r.previews ?? [], h = Math.max(f.length - m.length, 0) + d.size, g = m.map((e, t) => ({
13
+ let a = r.access === "full", o = r.hls ?? [], s = (e) => o.find((t) => e.path.startsWith(t)), c = a ? [] : r.previews ?? [], l = c.map((e, t) => ({
15
14
  kind: "image",
16
15
  key: `preview/${t + 1}`,
17
16
  file: {
@@ -20,17 +19,16 @@ function o(r) {
20
19
  url: e
21
20
  },
22
21
  aspect: 1
23
- })), _ = /* @__PURE__ */ new Set();
22
+ })), u = /* @__PURE__ */ new Set();
24
23
  for (let a of r.files) {
25
- if (a.locked) continue;
26
- let o = c(a);
24
+ let o = s(a);
27
25
  if (o) {
28
- if (_.has(o)) continue;
29
- _.add(o);
26
+ if (u.has(o)) continue;
27
+ u.add(o);
30
28
  let n = r.files.filter((e) => e.path.startsWith(o)), a = n.filter((t) => e(t.type)).reduce((e, t) => (t.w ?? 0) > (e?.w ?? -1) ? t : e, void 0);
31
29
  if (a) {
32
30
  let e = a.dur ?? n.find((e) => e.dur)?.dur;
33
- g.push({
31
+ l.push({
34
32
  kind: "video",
35
33
  key: o,
36
34
  dir: o,
@@ -42,7 +40,7 @@ function o(r) {
42
40
  });
43
41
  } else {
44
42
  let e = n.find((e) => e.path.endsWith(".m4a")) ?? n.find((e) => t(e.type));
45
- e && g.push({
43
+ e && l.push({
46
44
  kind: "audio",
47
45
  key: o,
48
46
  dir: o,
@@ -52,35 +50,33 @@ function o(r) {
52
50
  }
53
51
  continue;
54
52
  }
55
- n(a.type) ? g.push({
53
+ n(a.type) ? l.push({
56
54
  kind: "image",
57
55
  key: a.path,
58
56
  file: a,
59
57
  aspect: i(a, 1)
60
- }) : t(a.type) && g.push({
58
+ }) : t(a.type) && l.push({
61
59
  kind: "audio",
62
60
  key: a.path,
63
61
  file: a,
64
62
  aspect: 3
65
63
  });
66
64
  }
67
- return h > 0 && g.push({
65
+ return !a && r.locked && l.push({
68
66
  kind: "locked",
69
67
  key: "locked",
70
- count: h,
71
- videos: p,
72
- backdrop: m.at(-1),
68
+ backdrop: c.at(-1),
73
69
  aspect: 1
74
- }), g;
70
+ }), l;
75
71
  }
76
- function s(e, t = 0) {
72
+ function o(e, t = 0) {
77
73
  return e[t]?.aspect ?? 1;
78
74
  }
79
- function c(e) {
75
+ function s(e) {
80
76
  let t = Math.max(0, Math.round(e)), n = Math.floor(t / 3600), r = Math.floor(t % 3600 / 60), i = String(t % 60).padStart(2, "0");
81
77
  return n ? `${n}:${String(r).padStart(2, "0")}:${i}` : `${r}:${i}`;
82
78
  }
83
79
  //#endregion
84
- export { c as formatDuration, o as galleryItems, t as isAudioType, n as isImageType, r as isSubtitleType, e as isVideoType, s as stageAspect };
80
+ export { s as formatDuration, a as galleryItems, t as isAudioType, n as isImageType, r as isSubtitleType, e as isVideoType, o as stageAspect };
85
81
 
86
82
  //# sourceMappingURL=gallery.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gallery.js","names":[],"sources":["../../src/client/gallery.ts"],"sourcesContent":["import type { FileInfo, ReadResult } from \"./generated/wire.js\";\n\nexport type GalleryView = \"carousel\" | \"grid\";\n\nexport interface GalleryMediaItem {\n kind: \"image\" | \"video\" | \"audio\";\n key: string;\n /** An image; a ladder's widest video track (its size and duration) or its audio file. */\n file: FileInfo;\n /** A video or audio ladder's HLS folder from the read's `hls`, e.g. \"hls/\". */\n dir?: string;\n /** Width / height from the read API; a default when unknown. */\n aspect: number;\n}\n\n/** What a viewer without access is missing; `backdrop` is the public preview drawn behind it. */\nexport interface GalleryLockedItem {\n kind: \"locked\";\n key: string;\n count: number;\n videos: number;\n backdrop?: string;\n aspect: number;\n}\n\nexport type GalleryItem = GalleryMediaItem | GalleryLockedItem;\n\nexport const isVideoType = (type?: string) => !!type?.startsWith(\"video/\");\nexport const isAudioType = (type?: string) => !!type?.startsWith(\"audio/\");\nexport const isImageType = (type?: string) => !!type?.startsWith(\"image/\");\n\n/** The stage aspect of an audio slide. */\nexport const AUDIO_ASPECT = 3;\n\n/** Subtitle files are a video's text tracks, not gallery items. */\nexport const isSubtitleType = (type?: string) =>\n [\"text/vtt\", \"application/x-subrip\", \"text/x-ssa\", \"text/x-ass\"].includes(type ?? \"\");\n\nfunction aspectOf(f: FileInfo | undefined, fallback: number) {\n return f?.w && f.h ? f.w / f.h : fallback;\n}\n\nconst parent = (path: string) => path.slice(0, path.lastIndexOf(\"/\") + 1);\n\n/**\n * The read result as gallery items in manifest order. With access: each\n * image, each playable HLS ladder (a folder in `hls`: a video, or an\n * audio-only one) and each other audio file. Without: the item's public\n * previews (the read's `previews`), then one locked item for the rest; an\n * item's private files are all or nothing, so locked files never carry URLs.\n * Other files (plain videos, subtitles, zips) are not items; scope the read\n * with a prefix to choose.\n */\nexport function galleryItems(read: ReadResult | null | undefined): GalleryItem[] {\n if (!read) return [];\n const full = read.access === \"full\";\n const dirs = read.hls ?? [];\n const dirOf = (f: FileInfo) => dirs.find((d) => f.path.startsWith(d));\n // A locked ladder lists its tracks (and sprite): count each folder once.\n const unit = (f: FileInfo) => parent(f.path) || f.path;\n const lockedMedia = read.files.filter((f) => f.locked && (isVideoType(f.type) || isAudioType(f.type)));\n const units = new Set(lockedMedia.map(unit));\n const lockedImages = read.files.filter((f) => f.locked && isImageType(f.type) && !units.has(parent(f.path)));\n const lockedVideos = new Set(lockedMedia.filter((f) => isVideoType(f.type)).map(unit)).size;\n const previews = full ? [] : (read.previews ?? []);\n // Each preview shows one of the locked images.\n const lockedCount = Math.max(lockedImages.length - previews.length, 0) + units.size;\n const items: GalleryItem[] = previews.map((url, i) => ({ kind: \"image\", key: `preview/${i + 1}`, file: { path: `preview/${i + 1}`, type: \"image/webp\", url }, aspect: 1 }));\n const seen = new Set<string>();\n for (const f of read.files) {\n if (f.locked) continue;\n const dir = dirOf(f);\n if (dir) {\n if (seen.has(dir)) continue;\n seen.add(dir);\n const files = read.files.filter((x) => x.path.startsWith(dir));\n const video = files.filter((x) => isVideoType(x.type)).reduce<FileInfo | undefined>((a, x) => ((x.w ?? 0) > (a?.w ?? -1) ? x : a), undefined);\n if (video) {\n const dur = video.dur ?? files.find((x) => x.dur)?.dur;\n items.push({ kind: \"video\", key: dir, dir, file: { ...video, dur }, aspect: aspectOf(video, 16 / 9) });\n } else {\n const audio = files.find((x) => x.path.endsWith(\".m4a\")) ?? files.find((x) => isAudioType(x.type));\n if (audio) items.push({ kind: \"audio\", key: dir, dir, file: audio, aspect: AUDIO_ASPECT });\n }\n continue;\n }\n if (isImageType(f.type)) items.push({ kind: \"image\", key: f.path, file: f, aspect: aspectOf(f, 1) });\n else if (isAudioType(f.type)) items.push({ kind: \"audio\", key: f.path, file: f, aspect: AUDIO_ASPECT });\n }\n if (lockedCount > 0)\n items.push({ kind: \"locked\", key: \"locked\", count: lockedCount, videos: lockedVideos, backdrop: previews.at(-1), aspect: 1 });\n return items;\n}\n\n/** The carousel stage's width / height: the current item's native aspect. */\nexport function stageAspect(items: readonly GalleryItem[], index = 0): number {\n return items[index]?.aspect ?? 1;\n}\n\n/** \"0:42\", \"12:05\", \"1:02:09\". */\nexport function formatDuration(seconds: number): string {\n const s = Math.max(0, Math.round(seconds));\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n const ss = String(s % 60).padStart(2, \"0\");\n return h ? `${h}:${String(m).padStart(2, \"0\")}:${ss}` : `${m}:${ss}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"gallery.js","names":[],"sources":["../../src/client/gallery.ts"],"sourcesContent":["import type { FileInfo, ReadResult } from \"./generated/wire.js\";\n\nexport type GalleryView = \"carousel\" | \"grid\";\n\nexport interface GalleryMediaItem {\n kind: \"image\" | \"video\" | \"audio\";\n key: string;\n /** An image; a ladder's widest video track (its size and duration) or its audio file. */\n file: FileInfo;\n /** A video or audio ladder's HLS folder from the read's `hls`, e.g. \"hls/\". */\n dir?: string;\n /** Width / height from the read API; a default when unknown. */\n aspect: number;\n}\n\n/** What a viewer without access is missing, uncounted; `backdrop` is the public preview drawn behind it. */\nexport interface GalleryLockedItem {\n kind: \"locked\";\n key: string;\n backdrop?: string;\n aspect: number;\n}\n\nexport type GalleryItem = GalleryMediaItem | GalleryLockedItem;\n\nexport const isVideoType = (type?: string) => !!type?.startsWith(\"video/\");\nexport const isAudioType = (type?: string) => !!type?.startsWith(\"audio/\");\nexport const isImageType = (type?: string) => !!type?.startsWith(\"image/\");\n\n/** The stage aspect of an audio slide. */\nexport const AUDIO_ASPECT = 3;\n\n/** Subtitle files are a video's text tracks, not gallery items. */\nexport const isSubtitleType = (type?: string) =>\n [\"text/vtt\", \"application/x-subrip\", \"text/x-ssa\", \"text/x-ass\"].includes(type ?? \"\");\n\nfunction aspectOf(f: FileInfo | undefined, fallback: number) {\n return f?.w && f.h ? f.w / f.h : fallback;\n}\n\n/**\n * The read result as gallery items in manifest order. With access: each\n * image, each playable HLS ladder (a folder in `hls`: a video, or an\n * audio-only one) and each other audio file. Without: the item's public\n * previews (the read's `previews`), then one locked item when the read says\n * it holds locked files; a locked read lists none, so nothing counts them.\n * Other files (plain videos, subtitles, zips) are not items; scope the read\n * with a prefix to choose.\n */\nexport function galleryItems(read: ReadResult | null | undefined): GalleryItem[] {\n if (!read) return [];\n const full = read.access === \"full\";\n const dirs = read.hls ?? [];\n const dirOf = (f: FileInfo) => dirs.find((d) => f.path.startsWith(d));\n const previews = full ? [] : (read.previews ?? []);\n const items: GalleryItem[] = previews.map((url, i) => ({ kind: \"image\", key: `preview/${i + 1}`, file: { path: `preview/${i + 1}`, type: \"image/webp\", url }, aspect: 1 }));\n const seen = new Set<string>();\n for (const f of read.files) {\n const dir = dirOf(f);\n if (dir) {\n if (seen.has(dir)) continue;\n seen.add(dir);\n const files = read.files.filter((x) => x.path.startsWith(dir));\n const video = files.filter((x) => isVideoType(x.type)).reduce<FileInfo | undefined>((a, x) => ((x.w ?? 0) > (a?.w ?? -1) ? x : a), undefined);\n if (video) {\n const dur = video.dur ?? files.find((x) => x.dur)?.dur;\n items.push({ kind: \"video\", key: dir, dir, file: { ...video, dur }, aspect: aspectOf(video, 16 / 9) });\n } else {\n const audio = files.find((x) => x.path.endsWith(\".m4a\")) ?? files.find((x) => isAudioType(x.type));\n if (audio) items.push({ kind: \"audio\", key: dir, dir, file: audio, aspect: AUDIO_ASPECT });\n }\n continue;\n }\n if (isImageType(f.type)) items.push({ kind: \"image\", key: f.path, file: f, aspect: aspectOf(f, 1) });\n else if (isAudioType(f.type)) items.push({ kind: \"audio\", key: f.path, file: f, aspect: AUDIO_ASPECT });\n }\n if (!full && read.locked) items.push({ kind: \"locked\", key: \"locked\", backdrop: previews.at(-1), aspect: 1 });\n return items;\n}\n\n/** The carousel stage's width / height: the current item's native aspect. */\nexport function stageAspect(items: readonly GalleryItem[], index = 0): number {\n return items[index]?.aspect ?? 1;\n}\n\n/** \"0:42\", \"12:05\", \"1:02:09\". */\nexport function formatDuration(seconds: number): string {\n const s = Math.max(0, Math.round(seconds));\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n const ss = String(s % 60).padStart(2, \"0\");\n return h ? `${h}:${String(m).padStart(2, \"0\")}:${ss}` : `${m}:${ss}`;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- export type ErrorCode = "animation_not_allowed" | "animation_too_long" | "animation_unsupported" | "checksum_mismatch" | "comment_banned" | "comment_too_long" | "comments_closed" | "comments_locked" | "conflict" | "forbidden" | "gone" | "image_too_large" | "image_too_small" | "image_unreadable" | "incomplete" | "internal_error" | "invalid_request" | "limit_reached" | "moderation_rejected" | "not_configured" | "not_found" | "not_joinable" | "not_uploaded" | "quota_exceeded" | "rate_limited" | "tenant_mismatch" | "too_large" | "too_many_files" | "type_not_allowed" | "unauthorized" | "unavailable" | "upgrade_required" | "video_aspect_unsupported" | "video_over_budget" | "video_too_large" | "video_too_long";
1
+ export type ErrorCode = "animation_not_allowed" | "animation_too_long" | "animation_unsupported" | "checksum_mismatch" | "comment_banned" | "comment_too_long" | "comments_closed" | "comments_locked" | "conflict" | "download_unavailable" | "forbidden" | "gone" | "image_too_large" | "image_too_small" | "image_unreadable" | "incomplete" | "internal_error" | "invalid_request" | "limit_reached" | "moderation_rejected" | "no_download" | "not_configured" | "not_found" | "not_joinable" | "not_uploaded" | "quota_exceeded" | "rate_limited" | "tenant_mismatch" | "too_large" | "too_many_files" | "type_not_allowed" | "unauthorized" | "unavailable" | "upgrade_required" | "video_aspect_unsupported" | "video_over_budget" | "video_too_large" | "video_too_long";
2
2
  export declare const CONTENTKIT_ERROR_CODES: Readonly<Record<ErrorCode, {
3
3
  status: number;
4
4
  meaning: string;
@@ -36,6 +36,10 @@ var e = {
36
36
  status: 409,
37
37
  meaning: "A concurrent change, a path or slug already taken, or an operation id already used for another commit."
38
38
  },
39
+ download_unavailable: {
40
+ status: 403,
41
+ meaning: "The host's download policy refuses this download now: `reason` is its code, `available_at` when it opens if a matter of time. Nothing was charged."
42
+ },
39
43
  forbidden: {
40
44
  status: 403,
41
45
  meaning: "The actor may not do this."
@@ -76,6 +80,10 @@ var e = {
76
80
  status: 422,
77
81
  meaning: "The moderator refused the text; `error` says why."
78
82
  },
83
+ no_download: {
84
+ status: 409,
85
+ meaning: "The version has no file its kind's download serves: nothing uploaded, or its upload failed or was dropped."
86
+ },
79
87
  not_configured: {
80
88
  status: 501,
81
89
  meaning: "The capability needs a port the host did not configure; retrying does not help."
@@ -1 +1 @@
1
- {"version":3,"file":"error-codes.js","names":[],"sources":["../../../src/client/generated/error-codes.ts"],"sourcesContent":["// Generated by `go generate ./internal/contract` from ContentKit's error-code registry. Do not edit.\n\nexport type ErrorCode =\n | \"animation_not_allowed\"\n | \"animation_too_long\"\n | \"animation_unsupported\"\n | \"checksum_mismatch\"\n | \"comment_banned\"\n | \"comment_too_long\"\n | \"comments_closed\"\n | \"comments_locked\"\n | \"conflict\"\n | \"forbidden\"\n | \"gone\"\n | \"image_too_large\"\n | \"image_too_small\"\n | \"image_unreadable\"\n | \"incomplete\"\n | \"internal_error\"\n | \"invalid_request\"\n | \"limit_reached\"\n | \"moderation_rejected\"\n | \"not_configured\"\n | \"not_found\"\n | \"not_joinable\"\n | \"not_uploaded\"\n | \"quota_exceeded\"\n | \"rate_limited\"\n | \"tenant_mismatch\"\n | \"too_large\"\n | \"too_many_files\"\n | \"type_not_allowed\"\n | \"unauthorized\"\n | \"unavailable\"\n | \"upgrade_required\"\n | \"video_aspect_unsupported\"\n | \"video_over_budget\"\n | \"video_too_large\"\n | \"video_too_long\"\n;\n\n// Each code's HTTP status and meaning. A message on the wire is for people;\n// classify by code.\nexport const CONTENTKIT_ERROR_CODES: Readonly<Record<ErrorCode, { status: number; meaning: string }>> = {\n animation_not_allowed: { status: 422, meaning: \"An animated image where the upload path refuses animation.\" },\n animation_too_long: { status: 422, meaning: \"The animation has more frames or seconds than the processor allows.\" },\n animation_unsupported: { status: 415, meaning: \"An AVIF or HEIF image sequence, which is decoded as one frame.\" },\n checksum_mismatch: { status: 422, meaning: \"The stored bytes differ from the declared SHA-256.\" },\n comment_banned: { status: 403, meaning: \"The actor is banned from commenting on this target; `ban` says the scope, the reason and when it ends.\" },\n comment_too_long: { status: 422, meaning: \"The comment is longer than the site allows; `details.max` is the limit in characters.\" },\n comments_closed: { status: 403, meaning: \"The target's comments are closed or hidden: existing ones may show, new ones and edits are refused.\" },\n comments_locked: { status: 403, meaning: \"The actor can't open the content (or the comment's version), so its comments take no writes from them; reads withhold them.\" },\n conflict: { status: 409, meaning: \"A concurrent change, a path or slug already taken, or an operation id already used for another commit.\" },\n forbidden: { status: 403, meaning: \"The actor may not do this.\" },\n gone: { status: 410, meaning: \"The content was removed.\" },\n image_too_large: { status: 413, meaning: \"The image has more pixels or frames than the processor decodes.\" },\n image_too_small: { status: 422, meaning: \"The edited image is narrower than its public preset's minimum width.\" },\n image_unreadable: { status: 422, meaning: \"The file is not a decodable image of its declared type.\" },\n incomplete: { status: 409, meaning: \"A multipart upload is missing parts.\" },\n internal_error: { status: 500, meaning: \"A server fault; the cause is in the server log.\" },\n invalid_request: { status: 400, meaning: \"The request is malformed: its body, a query parameter or a path value.\" },\n limit_reached: { status: 409, meaning: \"The owner already has as many groups of the kind as the site allows (max_per_owner).\" },\n moderation_rejected: { status: 422, meaning: \"The moderator refused the text; `error` says why.\" },\n not_configured: { status: 501, meaning: \"The capability needs a port the host did not configure; retrying does not help.\" },\n not_found: { status: 404, meaning: \"Nothing by that name, or nothing the actor may see.\" },\n not_joinable: { status: 409, meaning: \"The group has no free offer to join: it sells its membership, or nothing.\" },\n not_uploaded: { status: 409, meaning: \"A commit names an upload that has not landed; `blobs` lists the ones to upload again.\" },\n quota_exceeded: { status: 413, meaning: \"The uploader's storage quota is used up.\" },\n rate_limited: { status: 429, meaning: \"Too many requests; retry after `retry_after` seconds (also the Retry-After header).\" },\n tenant_mismatch: { status: 500, meaning: \"A host port answered with another tenant's data: a configuration fault.\" },\n too_large: { status: 413, meaning: \"The file is larger than its upload path allows; `details` has `size` and `max_bytes`.\" },\n too_many_files: { status: 409, meaning: \"The commit would put more files in an upload path, or the item, than allowed; `details.max` is the limit.\" },\n type_not_allowed: { status: 415, meaning: \"The file type is not allowed in the upload path; `details.allowed` lists the types.\" },\n unauthorized: { status: 401, meaning: \"The request needs a valid credential: it has none where the route needs one, or one that is expired, revoked or invalid. WWW-Authenticate says which.\" },\n unavailable: { status: 503, meaning: \"The host's auth, media storage or signal plane cannot be reached, or a commit's recovery is pending; retry.\" },\n upgrade_required: { status: 503, meaning: \"The media item was stored before v0.68 and is served once the host's media upgrade (contentkit_media_upgrade) has converted it.\" },\n video_aspect_unsupported: { status: 422, meaning: \"The video's display aspect is outside what its upload path accepts; `details` has `min_aspect` and `max_aspect` (width/height).\" },\n video_over_budget: { status: 422, meaning: \"The planned encode costs more than its upload path allows, or the source averages under a frame a second.\" },\n video_too_large: { status: 422, meaning: \"The video's frames are larger than its upload path allows.\" },\n video_too_long: { status: 422, meaning: \"The video or audio runs longer than its upload path allows.\" },\n};\n"],"mappings":";AA2CA,IAAa,IAA2F;CACtG,uBAAuB;EAAE,QAAQ;EAAK,SAAS;CAA6D;CAC5G,oBAAoB;EAAE,QAAQ;EAAK,SAAS;CAAsE;CAClH,uBAAuB;EAAE,QAAQ;EAAK,SAAS;CAAiE;CAChH,mBAAmB;EAAE,QAAQ;EAAK,SAAS;CAAqD;CAChG,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAAyG;CACjJ,kBAAkB;EAAE,QAAQ;EAAK,SAAS;CAAwF;CAClI,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAAsG;CAC/I,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAA8H;CACvK,UAAU;EAAE,QAAQ;EAAK,SAAS;CAAyG;CAC3I,WAAW;EAAE,QAAQ;EAAK,SAAS;CAA6B;CAChE,MAAM;EAAE,QAAQ;EAAK,SAAS;CAA2B;CACzD,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAAkE;CAC3G,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAAuE;CAChH,kBAAkB;EAAE,QAAQ;EAAK,SAAS;CAA0D;CACpG,YAAY;EAAE,QAAQ;EAAK,SAAS;CAAuC;CAC3E,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAAkD;CAC1F,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAAyE;CAClH,eAAe;EAAE,QAAQ;EAAK,SAAS;CAAuF;CAC9H,qBAAqB;EAAE,QAAQ;EAAK,SAAS;CAAoD;CACjG,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAAkF;CAC1H,WAAW;EAAE,QAAQ;EAAK,SAAS;CAAsD;CACzF,cAAc;EAAE,QAAQ;EAAK,SAAS;CAA4E;CAClH,cAAc;EAAE,QAAQ;EAAK,SAAS;CAAwF;CAC9H,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAA2C;CACnF,cAAc;EAAE,QAAQ;EAAK,SAAS;CAAsF;CAC5H,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAA0E;CACnH,WAAW;EAAE,QAAQ;EAAK,SAAS;CAAwF;CAC3H,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAA4G;CACpJ,kBAAkB;EAAE,QAAQ;EAAK,SAAS;CAAsF;CAChI,cAAc;EAAE,QAAQ;EAAK,SAAS;CAAwJ;CAC9L,aAAa;EAAE,QAAQ;EAAK,SAAS;CAA8G;CACnJ,kBAAkB;EAAE,QAAQ;EAAK,SAAS;CAAkI;CAC5K,0BAA0B;EAAE,QAAQ;EAAK,SAAS;CAAkI;CACpL,mBAAmB;EAAE,QAAQ;EAAK,SAAS;CAA4G;CACvJ,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAA6D;CACtG,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAA8D;AACxG"}
1
+ {"version":3,"file":"error-codes.js","names":[],"sources":["../../../src/client/generated/error-codes.ts"],"sourcesContent":["// Generated by `go generate ./internal/contract` from ContentKit's error-code registry. Do not edit.\n\nexport type ErrorCode =\n | \"animation_not_allowed\"\n | \"animation_too_long\"\n | \"animation_unsupported\"\n | \"checksum_mismatch\"\n | \"comment_banned\"\n | \"comment_too_long\"\n | \"comments_closed\"\n | \"comments_locked\"\n | \"conflict\"\n | \"download_unavailable\"\n | \"forbidden\"\n | \"gone\"\n | \"image_too_large\"\n | \"image_too_small\"\n | \"image_unreadable\"\n | \"incomplete\"\n | \"internal_error\"\n | \"invalid_request\"\n | \"limit_reached\"\n | \"moderation_rejected\"\n | \"no_download\"\n | \"not_configured\"\n | \"not_found\"\n | \"not_joinable\"\n | \"not_uploaded\"\n | \"quota_exceeded\"\n | \"rate_limited\"\n | \"tenant_mismatch\"\n | \"too_large\"\n | \"too_many_files\"\n | \"type_not_allowed\"\n | \"unauthorized\"\n | \"unavailable\"\n | \"upgrade_required\"\n | \"video_aspect_unsupported\"\n | \"video_over_budget\"\n | \"video_too_large\"\n | \"video_too_long\"\n;\n\n// Each code's HTTP status and meaning. A message on the wire is for people;\n// classify by code.\nexport const CONTENTKIT_ERROR_CODES: Readonly<Record<ErrorCode, { status: number; meaning: string }>> = {\n animation_not_allowed: { status: 422, meaning: \"An animated image where the upload path refuses animation.\" },\n animation_too_long: { status: 422, meaning: \"The animation has more frames or seconds than the processor allows.\" },\n animation_unsupported: { status: 415, meaning: \"An AVIF or HEIF image sequence, which is decoded as one frame.\" },\n checksum_mismatch: { status: 422, meaning: \"The stored bytes differ from the declared SHA-256.\" },\n comment_banned: { status: 403, meaning: \"The actor is banned from commenting on this target; `ban` says the scope, the reason and when it ends.\" },\n comment_too_long: { status: 422, meaning: \"The comment is longer than the site allows; `details.max` is the limit in characters.\" },\n comments_closed: { status: 403, meaning: \"The target's comments are closed or hidden: existing ones may show, new ones and edits are refused.\" },\n comments_locked: { status: 403, meaning: \"The actor can't open the content (or the comment's version), so its comments take no writes from them; reads withhold them.\" },\n conflict: { status: 409, meaning: \"A concurrent change, a path or slug already taken, or an operation id already used for another commit.\" },\n download_unavailable: { status: 403, meaning: \"The host's download policy refuses this download now: `reason` is its code, `available_at` when it opens if a matter of time. Nothing was charged.\" },\n forbidden: { status: 403, meaning: \"The actor may not do this.\" },\n gone: { status: 410, meaning: \"The content was removed.\" },\n image_too_large: { status: 413, meaning: \"The image has more pixels or frames than the processor decodes.\" },\n image_too_small: { status: 422, meaning: \"The edited image is narrower than its public preset's minimum width.\" },\n image_unreadable: { status: 422, meaning: \"The file is not a decodable image of its declared type.\" },\n incomplete: { status: 409, meaning: \"A multipart upload is missing parts.\" },\n internal_error: { status: 500, meaning: \"A server fault; the cause is in the server log.\" },\n invalid_request: { status: 400, meaning: \"The request is malformed: its body, a query parameter or a path value.\" },\n limit_reached: { status: 409, meaning: \"The owner already has as many groups of the kind as the site allows (max_per_owner).\" },\n moderation_rejected: { status: 422, meaning: \"The moderator refused the text; `error` says why.\" },\n no_download: { status: 409, meaning: \"The version has no file its kind's download serves: nothing uploaded, or its upload failed or was dropped.\" },\n not_configured: { status: 501, meaning: \"The capability needs a port the host did not configure; retrying does not help.\" },\n not_found: { status: 404, meaning: \"Nothing by that name, or nothing the actor may see.\" },\n not_joinable: { status: 409, meaning: \"The group has no free offer to join: it sells its membership, or nothing.\" },\n not_uploaded: { status: 409, meaning: \"A commit names an upload that has not landed; `blobs` lists the ones to upload again.\" },\n quota_exceeded: { status: 413, meaning: \"The uploader's storage quota is used up.\" },\n rate_limited: { status: 429, meaning: \"Too many requests; retry after `retry_after` seconds (also the Retry-After header).\" },\n tenant_mismatch: { status: 500, meaning: \"A host port answered with another tenant's data: a configuration fault.\" },\n too_large: { status: 413, meaning: \"The file is larger than its upload path allows; `details` has `size` and `max_bytes`.\" },\n too_many_files: { status: 409, meaning: \"The commit would put more files in an upload path, or the item, than allowed; `details.max` is the limit.\" },\n type_not_allowed: { status: 415, meaning: \"The file type is not allowed in the upload path; `details.allowed` lists the types.\" },\n unauthorized: { status: 401, meaning: \"The request needs a valid credential: it has none where the route needs one, or one that is expired, revoked or invalid. WWW-Authenticate says which.\" },\n unavailable: { status: 503, meaning: \"The host's auth, media storage or signal plane cannot be reached, or a commit's recovery is pending; retry.\" },\n upgrade_required: { status: 503, meaning: \"The media item was stored before v0.68 and is served once the host's media upgrade (contentkit_media_upgrade) has converted it.\" },\n video_aspect_unsupported: { status: 422, meaning: \"The video's display aspect is outside what its upload path accepts; `details` has `min_aspect` and `max_aspect` (width/height).\" },\n video_over_budget: { status: 422, meaning: \"The planned encode costs more than its upload path allows, or the source averages under a frame a second.\" },\n video_too_large: { status: 422, meaning: \"The video's frames are larger than its upload path allows.\" },\n video_too_long: { status: 422, meaning: \"The video or audio runs longer than its upload path allows.\" },\n};\n"],"mappings":";AA6CA,IAAa,IAA2F;CACtG,uBAAuB;EAAE,QAAQ;EAAK,SAAS;CAA6D;CAC5G,oBAAoB;EAAE,QAAQ;EAAK,SAAS;CAAsE;CAClH,uBAAuB;EAAE,QAAQ;EAAK,SAAS;CAAiE;CAChH,mBAAmB;EAAE,QAAQ;EAAK,SAAS;CAAqD;CAChG,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAAyG;CACjJ,kBAAkB;EAAE,QAAQ;EAAK,SAAS;CAAwF;CAClI,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAAsG;CAC/I,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAA8H;CACvK,UAAU;EAAE,QAAQ;EAAK,SAAS;CAAyG;CAC3I,sBAAsB;EAAE,QAAQ;EAAK,SAAS;CAAqJ;CACnM,WAAW;EAAE,QAAQ;EAAK,SAAS;CAA6B;CAChE,MAAM;EAAE,QAAQ;EAAK,SAAS;CAA2B;CACzD,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAAkE;CAC3G,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAAuE;CAChH,kBAAkB;EAAE,QAAQ;EAAK,SAAS;CAA0D;CACpG,YAAY;EAAE,QAAQ;EAAK,SAAS;CAAuC;CAC3E,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAAkD;CAC1F,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAAyE;CAClH,eAAe;EAAE,QAAQ;EAAK,SAAS;CAAuF;CAC9H,qBAAqB;EAAE,QAAQ;EAAK,SAAS;CAAoD;CACjG,aAAa;EAAE,QAAQ;EAAK,SAAS;CAA6G;CAClJ,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAAkF;CAC1H,WAAW;EAAE,QAAQ;EAAK,SAAS;CAAsD;CACzF,cAAc;EAAE,QAAQ;EAAK,SAAS;CAA4E;CAClH,cAAc;EAAE,QAAQ;EAAK,SAAS;CAAwF;CAC9H,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAA2C;CACnF,cAAc;EAAE,QAAQ;EAAK,SAAS;CAAsF;CAC5H,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAA0E;CACnH,WAAW;EAAE,QAAQ;EAAK,SAAS;CAAwF;CAC3H,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAA4G;CACpJ,kBAAkB;EAAE,QAAQ;EAAK,SAAS;CAAsF;CAChI,cAAc;EAAE,QAAQ;EAAK,SAAS;CAAwJ;CAC9L,aAAa;EAAE,QAAQ;EAAK,SAAS;CAA8G;CACnJ,kBAAkB;EAAE,QAAQ;EAAK,SAAS;CAAkI;CAC5K,0BAA0B;EAAE,QAAQ;EAAK,SAAS;CAAkI;CACpL,mBAAmB;EAAE,QAAQ;EAAK,SAAS;CAA4G;CACvJ,iBAAiB;EAAE,QAAQ;EAAK,SAAS;CAA6D;CACtG,gBAAgB;EAAE,QAAQ;EAAK,SAAS;CAA8D;AACxG"}
@@ -1665,10 +1665,23 @@ export declare const CONTENTKIT_ROUTES: readonly [{
1665
1665
  readonly query: readonly [];
1666
1666
  readonly request: null;
1667
1667
  readonly responses: {
1668
- readonly 200: "application/zip";
1668
+ readonly 200: "FileDownload";
1669
1669
  readonly 202: "Pending";
1670
1670
  };
1671
- readonly errors: readonly ["forbidden", "rate_limited"];
1671
+ readonly errors: readonly ["forbidden", "download_unavailable", "rate_limited", "no_download"];
1672
+ readonly errorSets: readonly ["module:versions", "auth", "request"];
1673
+ }, {
1674
+ readonly method: "GET";
1675
+ readonly module: "versions";
1676
+ readonly path: "/contents/{id}/versions/{version}/archive";
1677
+ readonly auth: "public";
1678
+ readonly permission: null;
1679
+ readonly query: readonly ["for", "mark", "ticket"];
1680
+ readonly request: null;
1681
+ readonly responses: {
1682
+ readonly 200: "application/zip";
1683
+ };
1684
+ readonly errors: readonly [];
1672
1685
  readonly errorSets: readonly ["module:versions", "auth", "request"];
1673
1686
  }, {
1674
1687
  readonly method: "PUT";
@@ -2301,10 +2301,35 @@ var e = {
2301
2301
  query: [],
2302
2302
  request: null,
2303
2303
  responses: {
2304
- 200: "application/zip",
2304
+ 200: "FileDownload",
2305
2305
  202: "Pending"
2306
2306
  },
2307
- errors: ["forbidden", "rate_limited"],
2307
+ errors: [
2308
+ "forbidden",
2309
+ "download_unavailable",
2310
+ "rate_limited",
2311
+ "no_download"
2312
+ ],
2313
+ errorSets: [
2314
+ "module:versions",
2315
+ "auth",
2316
+ "request"
2317
+ ]
2318
+ },
2319
+ {
2320
+ method: "GET",
2321
+ module: "versions",
2322
+ path: "/contents/{id}/versions/{version}/archive",
2323
+ auth: "public",
2324
+ permission: null,
2325
+ query: [
2326
+ "for",
2327
+ "mark",
2328
+ "ticket"
2329
+ ],
2330
+ request: null,
2331
+ responses: { 200: "application/zip" },
2332
+ errors: [],
2308
2333
  errorSets: [
2309
2334
  "module:versions",
2310
2335
  "auth",