@openrails/contentkit-ui 0.83.0 → 0.85.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 (124) hide show
  1. package/README.md +11 -3
  2. package/dist/client/beacons.d.ts +8 -5
  3. package/dist/client/beacons.js +15 -7
  4. package/dist/client/beacons.js.map +1 -1
  5. package/dist/client/content/moderation.d.ts +14 -2
  6. package/dist/client/content/moderation.js +34 -0
  7. package/dist/client/content/moderation.js.map +1 -1
  8. package/dist/client/content/types.d.ts +13 -0
  9. package/dist/client/content/types.js.map +1 -1
  10. package/dist/client/discovery.d.ts +6 -0
  11. package/dist/client/discovery.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 +77 -12
  16. package/dist/client/generated/routes.js +106 -1
  17. package/dist/client/generated/routes.js.map +1 -1
  18. package/dist/client/generated/wire.d.ts +52 -0
  19. package/dist/client/generated/wire.js.map +1 -1
  20. package/dist/client/index.d.ts +2 -1
  21. package/dist/client/reader-meta.d.ts +27 -0
  22. package/dist/client/reader-meta.js +99 -0
  23. package/dist/client/reader-meta.js.map +1 -0
  24. package/dist/client/versions.d.ts +1 -1
  25. package/dist/client/versions.js +7 -2
  26. package/dist/client/versions.js.map +1 -1
  27. package/dist/client.js +2 -1
  28. package/dist/components/admin/reader-manifest.d.ts +18 -0
  29. package/dist/components/admin/reader-manifest.js +490 -0
  30. package/dist/components/admin/reader-manifest.js.map +1 -0
  31. package/dist/components/content/card.js +1 -1
  32. package/dist/components/content/card.js.map +1 -1
  33. package/dist/components/content/editors.d.ts +2 -1
  34. package/dist/components/content/editors.js +291 -256
  35. package/dist/components/content/editors.js.map +1 -1
  36. package/dist/components/groups/banner.d.ts +1 -1
  37. package/dist/components/groups/banner.js +1 -1
  38. package/dist/components/groups/banner.js.map +1 -1
  39. package/dist/components/groups/editor.js +3 -3
  40. package/dist/components/groups/editor.js.map +1 -1
  41. package/dist/components/groups/parts.d.ts +2 -2
  42. package/dist/components/groups/parts.js +71 -72
  43. package/dist/components/groups/parts.js.map +1 -1
  44. package/dist/components/social/comments.js +59 -45
  45. package/dist/components/social/comments.js.map +1 -1
  46. package/dist/components/social/index.d.ts +1 -0
  47. package/dist/components/social/moderation.d.ts +5 -4
  48. package/dist/components/social/moderation.js +143 -130
  49. package/dist/components/social/moderation.js.map +1 -1
  50. package/dist/components/social/parts.js +22 -14
  51. package/dist/components/social/parts.js.map +1 -1
  52. package/dist/components/social/rules.d.ts +23 -0
  53. package/dist/components/social/rules.js +261 -0
  54. package/dist/components/social/rules.js.map +1 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +26 -24
  57. package/dist/locales/admin/de.js +78 -0
  58. package/dist/locales/admin/de.js.map +1 -1
  59. package/dist/locales/admin/en.d.ts +78 -0
  60. package/dist/locales/admin/en.js +78 -0
  61. package/dist/locales/admin/en.js.map +1 -1
  62. package/dist/locales/admin/es.js +78 -0
  63. package/dist/locales/admin/es.js.map +1 -1
  64. package/dist/locales/admin/ja.js +78 -0
  65. package/dist/locales/admin/ja.js.map +1 -1
  66. package/dist/locales/admin/ko.js +78 -0
  67. package/dist/locales/admin/ko.js.map +1 -1
  68. package/dist/locales/admin/zh.js +78 -0
  69. package/dist/locales/admin/zh.js.map +1 -1
  70. package/dist/locales/de.js +3 -1
  71. package/dist/locales/de.js.map +1 -1
  72. package/dist/locales/discovery/de.js +1 -0
  73. package/dist/locales/discovery/de.js.map +1 -1
  74. package/dist/locales/discovery/en.d.ts +1 -0
  75. package/dist/locales/discovery/en.js +1 -0
  76. package/dist/locales/discovery/en.js.map +1 -1
  77. package/dist/locales/discovery/es.js +1 -0
  78. package/dist/locales/discovery/es.js.map +1 -1
  79. package/dist/locales/discovery/ja.js +1 -0
  80. package/dist/locales/discovery/ja.js.map +1 -1
  81. package/dist/locales/discovery/ko.js +1 -0
  82. package/dist/locales/discovery/ko.js.map +1 -1
  83. package/dist/locales/discovery/zh.js +1 -0
  84. package/dist/locales/discovery/zh.js.map +1 -1
  85. package/dist/locales/en.d.ts +84 -0
  86. package/dist/locales/en.js +3 -1
  87. package/dist/locales/en.js.map +1 -1
  88. package/dist/locales/es.js +3 -1
  89. package/dist/locales/es.js.map +1 -1
  90. package/dist/locales/ja.js +3 -1
  91. package/dist/locales/ja.js.map +1 -1
  92. package/dist/locales/ko.js +3 -1
  93. package/dist/locales/ko.js.map +1 -1
  94. package/dist/locales/social/de.js +3 -0
  95. package/dist/locales/social/de.js.map +1 -1
  96. package/dist/locales/social/en.d.ts +3 -0
  97. package/dist/locales/social/en.js +3 -0
  98. package/dist/locales/social/en.js.map +1 -1
  99. package/dist/locales/social/es.js +3 -0
  100. package/dist/locales/social/es.js.map +1 -1
  101. package/dist/locales/social/ja.js +3 -0
  102. package/dist/locales/social/ja.js.map +1 -1
  103. package/dist/locales/social/ko.js +3 -0
  104. package/dist/locales/social/ko.js.map +1 -1
  105. package/dist/locales/social/zh.js +3 -0
  106. package/dist/locales/social/zh.js.map +1 -1
  107. package/dist/locales/zh.js +3 -1
  108. package/dist/locales/zh.js.map +1 -1
  109. package/dist/react/admin.d.ts +1 -1
  110. package/dist/react/admin.js +2 -0
  111. package/dist/react/admin.js.map +1 -1
  112. package/dist/react/comments.d.ts +29 -1
  113. package/dist/react/comments.js +30 -2
  114. package/dist/react/comments.js.map +1 -1
  115. package/dist/react/context.d.ts +1 -1
  116. package/dist/react/context.js.map +1 -1
  117. package/dist/react/discovery.js +1 -1
  118. package/dist/react/discovery.js.map +1 -1
  119. package/dist/react/index.d.ts +1 -1
  120. package/dist/react/resources.js +6 -0
  121. package/dist/react/resources.js.map +1 -1
  122. package/dist/react.js +14 -14
  123. package/dist/styles.css +1 -1
  124. package/package.json +1 -1
package/README.md CHANGED
@@ -477,9 +477,11 @@ const nav = useMemo(() => ({ pathname: location.pathname, search: location.searc
477
477
 
478
478
  - **`CommentModeration`**: every comment (deleted, held and rejected ones with
479
479
  their text) by content kind or author, to delete, restore or ban its author;
480
- the review queue; the site's bans. Sections at `basePath`, `/queue`,
481
- `/bans`; `?kind`, `?author`, `?page`. Props: `contentKinds`, `itemHref`,
482
- `userHref`, `sections`, `pageSize`. `useCommentModerationAccess()`.
480
+ the review queue; the site's bans; banned words (mask or refuse) and IP
481
+ bans. Sections at `basePath`, `/queue`, `/bans`, `/words`, `/ips`; `?kind`,
482
+ `?author`, `?page`. Props: `contentKinds`, `itemHref`, `userHref`,
483
+ `sections`, `pageSize`. `useCommentModerationAccess()`. The rule sections
484
+ alone: `BannedWords`, `IPBans` (`useBannedWords`, `useIPBans`).
483
485
 
484
486
  ```tsx
485
487
  <CommentModeration client={contentkit} basePath="/admin/comments" navigation={nav} contentKinds={["video", "blog"]} />
@@ -512,6 +514,12 @@ const nav = useMemo(() => ({ pathname: location.pathname, search: location.searc
512
514
  emphasis, lists, quotes, links and images picked, pasted or dropped. Its
513
515
  HTML keeps to that schema, so bodies stay a small, predictable subset.
514
516
 
517
+ - **`ReaderManifestEditor`**: a reader version's manifest over its published
518
+ pages and audio: scroll anchors, audio cues (matched to pages or anchors by
519
+ file name in one step), background tracks and auto-advance. Page order is
520
+ the version's media order. `ContentManager` shows it under each version of
521
+ a `reader` kind. Props: `content`, `version`.
522
+
515
523
  - **`MediaItemPanel`**: one video item for its editors: encode state and
516
524
  progress, failures with their reason, replace the source, subtitle
517
525
  sidecars (language, label, forced), the poster from a frame or an image,
@@ -1,4 +1,4 @@
1
- import { DiscoveryClient } from './discovery.js';
1
+ import { ContentKitClient } from './client.js';
2
2
  import { ExposureInput } from './generated/wire.js';
3
3
  export interface BeaconOptions {
4
4
  /** A signed-out visitor's session; default one per browser tab. */
@@ -26,18 +26,19 @@ export declare const eventId: () => string;
26
26
  export declare function readerSession(): string;
27
27
  /**
28
28
  * Batched, best-effort signals and exposures: gathered for `delay`, sent in
29
- * one request each, flushed with keepalive when the page hides. A site
30
- * without the signal plane (not_configured) turns them off.
29
+ * one request each, flushed with keepalive when the page hides. A site whose
30
+ * content model says it records no signals gets none.
31
31
  */
32
32
  export declare class Beacons {
33
- private readonly discovery;
33
+ private readonly client;
34
34
  private signals;
35
35
  private exposures;
36
36
  private timer;
37
37
  private off;
38
+ private recorded;
38
39
  private readonly session;
39
40
  private readonly delay;
40
- constructor(discovery: DiscoveryClient, o?: BeaconOptions);
41
+ constructor(client: Pick<ContentKitClient, "discovery" | "kinds">, o?: BeaconOptions);
41
42
  /** A click on a listed card. */
42
43
  click(content: string, a?: Attribution & {
43
44
  version_id?: string;
@@ -49,5 +50,7 @@ export declare class Beacons {
49
50
  /** Sends what has gathered now. */
50
51
  flush(keepalive?: boolean): Promise<void>;
51
52
  private signal;
53
+ /** Whether the site records signals, asked once at the first event; an unreadable model sends anyway (a not_configured answer still turns them off). */
54
+ private recording;
52
55
  private schedule;
53
56
  }
@@ -12,15 +12,16 @@ function a() {
12
12
  }
13
13
  }
14
14
  var o = class {
15
- discovery;
15
+ client;
16
16
  signals = [];
17
17
  exposures = [];
18
18
  timer;
19
19
  off = !1;
20
+ recorded;
20
21
  session;
21
22
  delay;
22
23
  constructor(e, t = {}) {
23
- this.discovery = e, this.session = t.session ?? a, this.delay = t.delay ?? 2e3, typeof addEventListener == "function" && (addEventListener("pagehide", () => void this.flush(!0)), addEventListener("visibilitychange", () => {
24
+ this.client = e, this.session = t.session ?? a, this.delay = t.delay ?? 2e3, typeof addEventListener == "function" && (addEventListener("pagehide", () => void this.flush(!0)), addEventListener("visibilitychange", () => {
24
25
  typeof document < "u" && document.visibilityState === "hidden" && this.flush(!0);
25
26
  }));
26
27
  }
@@ -52,17 +53,24 @@ var o = class {
52
53
  async flush(n = !1) {
53
54
  clearTimeout(this.timer), this.timer = void 0;
54
55
  let r = this.signals.splice(0), i = this.exposures.splice(0);
55
- if (this.off) return;
56
- let a = this.session(), o = [];
57
- for (let e = 0; e < r.length; e += t) o.push(this.discovery.signals(r.slice(e, e + t), a, { keepalive: n }));
58
- for (let e = 0; e < i.length; e += t) o.push(this.discovery.exposures(i.slice(e, e + t), a, { keepalive: n }));
56
+ if (this.off || !r.length && !i.length) return;
57
+ if (!await this.recording()) {
58
+ this.off = !0;
59
+ return;
60
+ }
61
+ let a = this.session(), o = [], { discovery: s } = this.client;
62
+ for (let e = 0; e < r.length; e += t) o.push(s.signals(r.slice(e, e + t), a, { keepalive: n }));
63
+ for (let e = 0; e < i.length; e += t) o.push(s.exposures(i.slice(e, e + t), a, { keepalive: n }));
59
64
  (await Promise.allSettled(o)).some((t) => t.status === "rejected" && e(t.reason).code === "not_configured") && (this.off = !0);
60
65
  }
61
66
  signal(e) {
62
67
  this.off || (e.type === "view" && (this.signals = this.signals.filter((t) => t.event_id !== e.event_id)), this.signals.push(e), this.schedule());
63
68
  }
69
+ recording() {
70
+ return this.recorded ??= this.client.kinds().then((e) => e.signals, () => !0);
71
+ }
64
72
  schedule() {
65
- if (this.signals.length + this.exposures.length >= t) return void this.flush();
73
+ if (this.recording(), this.signals.length + this.exposures.length >= t) return void this.flush();
66
74
  this.timer ??= setTimeout(() => void this.flush(), this.delay);
67
75
  }
68
76
  };
@@ -1 +1 @@
1
- {"version":3,"file":"beacons.js","names":[],"sources":["../../src/client/beacons.ts"],"sourcesContent":["import type { DiscoveryClient } from \"./discovery.js\";\nimport { toContentKitError } from \"./errors.js\";\nimport type { ExposureInput, SignalInput } from \"./generated/wire.js\";\n\nexport interface BeaconOptions {\n /** A signed-out visitor's session; default one per browser tab. */\n session?: () => string | undefined;\n /** How long events gather before they are sent, ms. Default 2000. */\n delay?: number;\n}\n\n/** Where a click or a view came from: the list render it was shown in. */\nexport interface Attribution {\n render_id?: string;\n surface?: string;\n /** 1-based position in that render. */\n position?: number;\n}\n\nexport interface ViewSession {\n /** Reports the view so far: pages read, seconds watched or percent scrolled; resume is where to pick up. */\n update(o: { progress?: number; duration_s?: number; resume?: string }): void;\n}\n\nconst MAX = 100;\nconst SESSION_KEY = \"contentkit.session\";\nexport const eventId = () => (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`);\n\nlet unstored: string | undefined;\n\n/** A signed-out reader's session: one per browser tab, kept across reloads. */\nexport function readerSession(): string {\n try {\n const kept = sessionStorage.getItem(SESSION_KEY);\n if (kept) return kept;\n const made = eventId();\n sessionStorage.setItem(SESSION_KEY, made);\n return made;\n } catch {\n return (unstored ??= eventId());\n }\n}\n\n/**\n * Batched, best-effort signals and exposures: gathered for `delay`, sent in\n * one request each, flushed with keepalive when the page hides. A site\n * without the signal plane (not_configured) turns them off.\n */\nexport class Beacons {\n private signals: SignalInput[] = [];\n private exposures: ExposureInput[] = [];\n private timer: ReturnType<typeof setTimeout> | undefined;\n private off = false;\n private readonly session: () => string | undefined;\n private readonly delay: number;\n\n constructor(\n private readonly discovery: DiscoveryClient,\n o: BeaconOptions = {},\n ) {\n this.session = o.session ?? readerSession;\n this.delay = o.delay ?? 2000;\n if (typeof addEventListener === \"function\") {\n addEventListener(\"pagehide\", () => void this.flush(true));\n addEventListener(\"visibilitychange\", () => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\") void this.flush(true);\n });\n }\n }\n\n /** A click on a listed card. */\n click(content: string, a: Attribution & { version_id?: string } = {}): void {\n this.signal({ content_id: content, type: \"click\", event_id: eventId(), occurred_at: new Date().toISOString(), ...a });\n }\n\n /** A consumption session of a content (or one version): each update a revision of one event. */\n view(content: string, version?: string, a: Attribution = {}): ViewSession {\n const event = eventId();\n let revision = 0;\n return {\n update: (o) => this.signal({ content_id: content, version_id: version, type: \"view\", event_id: event, revision: ++revision, occurred_at: new Date().toISOString(), ...a, ...o }),\n };\n }\n\n /** A result list served, rendered or shown. */\n exposure(e: ExposureInput): void {\n if (this.off || !e.shown.length) return;\n this.exposures.push(e);\n this.schedule();\n }\n\n /** Sends what has gathered now. */\n async flush(keepalive = false): Promise<void> {\n clearTimeout(this.timer);\n this.timer = undefined;\n const signals = this.signals.splice(0);\n const exposures = this.exposures.splice(0);\n if (this.off) return;\n const session = this.session();\n const sends: Promise<void>[] = [];\n for (let i = 0; i < signals.length; i += MAX) sends.push(this.discovery.signals(signals.slice(i, i + MAX), session, { keepalive }));\n for (let i = 0; i < exposures.length; i += MAX) sends.push(this.discovery.exposures(exposures.slice(i, i + MAX), session, { keepalive }));\n const done = await Promise.allSettled(sends);\n if (done.some((r) => r.status === \"rejected\" && toContentKitError(r.reason).code === \"not_configured\")) this.off = true;\n }\n\n private signal(s: SignalInput): void {\n if (this.off) return;\n // A view's later revision replaces its earlier one still waiting.\n if (s.type === \"view\") this.signals = this.signals.filter((x) => x.event_id !== s.event_id);\n this.signals.push(s);\n this.schedule();\n }\n\n private schedule(): void {\n if (this.signals.length + this.exposures.length >= MAX) return void this.flush();\n this.timer ??= setTimeout(() => void this.flush(), this.delay);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"beacons.js","names":[],"sources":["../../src/client/beacons.ts"],"sourcesContent":["import type { ContentKitClient } from \"./client.js\";\nimport { toContentKitError } from \"./errors.js\";\nimport type { ExposureInput, SignalInput } from \"./generated/wire.js\";\n\nexport interface BeaconOptions {\n /** A signed-out visitor's session; default one per browser tab. */\n session?: () => string | undefined;\n /** How long events gather before they are sent, ms. Default 2000. */\n delay?: number;\n}\n\n/** Where a click or a view came from: the list render it was shown in. */\nexport interface Attribution {\n render_id?: string;\n surface?: string;\n /** 1-based position in that render. */\n position?: number;\n}\n\nexport interface ViewSession {\n /** Reports the view so far: pages read, seconds watched or percent scrolled; resume is where to pick up. */\n update(o: { progress?: number; duration_s?: number; resume?: string }): void;\n}\n\nconst MAX = 100;\nconst SESSION_KEY = \"contentkit.session\";\nexport const eventId = () => (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`);\n\nlet unstored: string | undefined;\n\n/** A signed-out reader's session: one per browser tab, kept across reloads. */\nexport function readerSession(): string {\n try {\n const kept = sessionStorage.getItem(SESSION_KEY);\n if (kept) return kept;\n const made = eventId();\n sessionStorage.setItem(SESSION_KEY, made);\n return made;\n } catch {\n return (unstored ??= eventId());\n }\n}\n\n/**\n * Batched, best-effort signals and exposures: gathered for `delay`, sent in\n * one request each, flushed with keepalive when the page hides. A site whose\n * content model says it records no signals gets none.\n */\nexport class Beacons {\n private signals: SignalInput[] = [];\n private exposures: ExposureInput[] = [];\n private timer: ReturnType<typeof setTimeout> | undefined;\n private off = false;\n private recorded: Promise<boolean> | undefined;\n private readonly session: () => string | undefined;\n private readonly delay: number;\n\n constructor(\n private readonly client: Pick<ContentKitClient, \"discovery\" | \"kinds\">,\n o: BeaconOptions = {},\n ) {\n this.session = o.session ?? readerSession;\n this.delay = o.delay ?? 2000;\n if (typeof addEventListener === \"function\") {\n addEventListener(\"pagehide\", () => void this.flush(true));\n addEventListener(\"visibilitychange\", () => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\") void this.flush(true);\n });\n }\n }\n\n /** A click on a listed card. */\n click(content: string, a: Attribution & { version_id?: string } = {}): void {\n this.signal({ content_id: content, type: \"click\", event_id: eventId(), occurred_at: new Date().toISOString(), ...a });\n }\n\n /** A consumption session of a content (or one version): each update a revision of one event. */\n view(content: string, version?: string, a: Attribution = {}): ViewSession {\n const event = eventId();\n let revision = 0;\n return {\n update: (o) => this.signal({ content_id: content, version_id: version, type: \"view\", event_id: event, revision: ++revision, occurred_at: new Date().toISOString(), ...a, ...o }),\n };\n }\n\n /** A result list served, rendered or shown. */\n exposure(e: ExposureInput): void {\n if (this.off || !e.shown.length) return;\n this.exposures.push(e);\n this.schedule();\n }\n\n /** Sends what has gathered now. */\n async flush(keepalive = false): Promise<void> {\n clearTimeout(this.timer);\n this.timer = undefined;\n const signals = this.signals.splice(0);\n const exposures = this.exposures.splice(0);\n if (this.off || (!signals.length && !exposures.length)) return;\n if (!(await this.recording())) {\n this.off = true;\n return;\n }\n const session = this.session();\n const sends: Promise<void>[] = [];\n const { discovery } = this.client;\n for (let i = 0; i < signals.length; i += MAX) sends.push(discovery.signals(signals.slice(i, i + MAX), session, { keepalive }));\n for (let i = 0; i < exposures.length; i += MAX) sends.push(discovery.exposures(exposures.slice(i, i + MAX), session, { keepalive }));\n const done = await Promise.allSettled(sends);\n if (done.some((r) => r.status === \"rejected\" && toContentKitError(r.reason).code === \"not_configured\")) this.off = true;\n }\n\n private signal(s: SignalInput): void {\n if (this.off) return;\n // A view's later revision replaces its earlier one still waiting.\n if (s.type === \"view\") this.signals = this.signals.filter((x) => x.event_id !== s.event_id);\n this.signals.push(s);\n this.schedule();\n }\n\n /** Whether the site records signals, asked once at the first event; an unreadable model sends anyway (a not_configured answer still turns them off). */\n private recording(): Promise<boolean> {\n return (this.recorded ??= this.client.kinds().then(\n (m) => m.signals,\n () => true,\n ));\n }\n\n private schedule(): void {\n void this.recording();\n if (this.signals.length + this.exposures.length >= MAX) return void this.flush();\n this.timer ??= setTimeout(() => void this.flush(), this.delay);\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { HeldItem, HeldPage, ReviewOutcome } from '../generated/wire.js';
1
+ import { BannedWord, BannedWordInput, HeldItem, HeldPage, IPBan, IPBanInput, ReviewOutcome } from '../generated/wire.js';
2
2
  import { Http } from '../http.js';
3
3
  import { Decision, HeldKind } from './types.js';
4
4
  export interface HeldQuery {
@@ -7,7 +7,7 @@ export interface HeldQuery {
7
7
  cursor?: string;
8
8
  limit?: number;
9
9
  }
10
- /** The review queue of held comments and content (ModerationReview). */
10
+ /** The review queue of held comments and content (ModerationReview), and the rules every comment and answer meets (root:comments:moderate). */
11
11
  export declare class ModerationClient {
12
12
  private readonly http;
13
13
  constructor(http: Http);
@@ -15,4 +15,16 @@ export declare class ModerationClient {
15
15
  held(q: HeldQuery, signal?: AbortSignal): Promise<HeldPage>;
16
16
  /** Approves (publishes) or rejects a held item at the revision the queue listed; conflict when it was edited since. */
17
17
  resolve(item: Pick<HeldItem, "kind" | "id" | "revision">, decision: Decision, reason?: string): Promise<ReviewOutcome>;
18
+ /** The banned words, alphabetically, and what each does: mask or reject. */
19
+ bannedWords(signal?: AbortSignal): Promise<BannedWord[]>;
20
+ /** Bans words, or changes what banned ones do (mask by default); the list after. */
21
+ banWords(words: BannedWordInput[]): Promise<BannedWord[]>;
22
+ /** Unbans words; idempotent. */
23
+ unbanWords(words: string[]): Promise<void>;
24
+ /** The IP bans, newest first, expired ones included. */
25
+ ipBans(signal?: AbortSignal): Promise<IPBan[]>;
26
+ /** Bans addresses or networks (CIDR) from commenting and answering, or replaces their bans; the list after. */
27
+ banIPs(bans: IPBanInput[]): Promise<IPBan[]>;
28
+ /** Lifts the bans of addresses or networks; idempotent. */
29
+ liftIPBans(networks: string[]): Promise<void>;
18
30
  }
@@ -30,6 +30,40 @@ var t = class {
30
30
  decision: n
31
31
  }), o;
32
32
  }
33
+ bannedWords(t) {
34
+ return e(this.http, "GET", "/moderation/banned-words", { signal: t });
35
+ }
36
+ async banWords(t) {
37
+ let n = this.http.captureChanges(), r = await e(this.http, "POST", "/moderation/banned-words", { body: t });
38
+ return n({
39
+ type: "rules.changed",
40
+ rules: "words"
41
+ }), r;
42
+ }
43
+ async unbanWords(t) {
44
+ let n = this.http.captureChanges();
45
+ await e(this.http, "DELETE", "/moderation/banned-words", { body: t }), n({
46
+ type: "rules.changed",
47
+ rules: "words"
48
+ });
49
+ }
50
+ ipBans(t) {
51
+ return e(this.http, "GET", "/moderation/ip-bans", { signal: t });
52
+ }
53
+ async banIPs(t) {
54
+ let n = this.http.captureChanges(), r = await e(this.http, "POST", "/moderation/ip-bans", { body: t });
55
+ return n({
56
+ type: "rules.changed",
57
+ rules: "ips"
58
+ }), r;
59
+ }
60
+ async liftIPBans(t) {
61
+ let n = this.http.captureChanges();
62
+ await e(this.http, "DELETE", "/moderation/ip-bans", { body: t }), n({
63
+ type: "rules.changed",
64
+ rules: "ips"
65
+ });
66
+ }
33
67
  };
34
68
  //#endregion
35
69
  export { t as ModerationClient };
@@ -1 +1 @@
1
- {"version":3,"file":"moderation.js","names":[],"sources":["../../../src/client/content/moderation.ts"],"sourcesContent":["import type { HeldItem, HeldPage, ReviewOutcome } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call } from \"../route.js\";\nimport type { Decision, HeldKind } from \"./types.js\";\n\nexport interface HeldQuery {\n kind: HeldKind;\n /** The previous page's next. */\n cursor?: string;\n limit?: number;\n}\n\n/** The review queue of held comments and content (ModerationReview). */\nexport class ModerationClient {\n constructor(private readonly http: Http) {}\n\n /** Held items awaiting review, oldest first. */\n held(q: HeldQuery, signal?: AbortSignal): Promise<HeldPage> {\n return call(this.http, \"GET\", \"/moderation/held\", { query: { ...q }, signal });\n }\n /** Approves (publishes) or rejects a held item at the revision the queue listed; conflict when it was edited since. */\n async resolve(item: Pick<HeldItem, \"kind\" | \"id\" | \"revision\">, decision: Decision, reason?: string): Promise<ReviewOutcome> {\n const emit = this.http.captureChanges();\n const kind = item.kind as HeldKind;\n const out: ReviewOutcome = await call(this.http, \"POST\", \"/moderation/{kind}/{id}/resolve\", {\n params: { kind, id: item.id },\n body: { decision, revision: item.revision, ...(reason ? { reason } : {}) },\n });\n emit({ type: \"moderation.resolved\", kind, id: item.id, decision });\n return out;\n }\n}\n"],"mappings":";;AAaA,IAAa,IAAb,MAA8B;CACC;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,KAAK,GAAc,GAAyC;EAC1D,OAAO,EAAK,KAAK,MAAM,OAAO,oBAAoB;GAAE,OAAO,EAAE,GAAG,EAAE;GAAG;EAAO,CAAC;CAC/E;CAEA,MAAM,QAAQ,GAAkD,GAAoB,GAAyC;EAC3H,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAO,EAAK,MACZ,IAAqB,MAAM,EAAK,KAAK,MAAM,QAAQ,mCAAmC;GAC1F,QAAQ;IAAE;IAAM,IAAI,EAAK;GAAG;GAC5B,MAAM;IAAE;IAAU,UAAU,EAAK;IAAU,GAAI,IAAS,EAAE,UAAO,IAAI,CAAC;GAAG;EAC3E,CAAC;EAED,OADA,EAAK;GAAE,MAAM;GAAuB;GAAM,IAAI,EAAK;GAAI;EAAS,CAAC,GAC1D;CACT;AACF"}
1
+ {"version":3,"file":"moderation.js","names":[],"sources":["../../../src/client/content/moderation.ts"],"sourcesContent":["import type { BannedWord, BannedWordInput, HeldItem, HeldPage, IPBan, IPBanInput, ReviewOutcome } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call } from \"../route.js\";\nimport type { Decision, HeldKind } from \"./types.js\";\n\nexport interface HeldQuery {\n kind: HeldKind;\n /** The previous page's next. */\n cursor?: string;\n limit?: number;\n}\n\n/** The review queue of held comments and content (ModerationReview), and the rules every comment and answer meets (root:comments:moderate). */\nexport class ModerationClient {\n constructor(private readonly http: Http) {}\n\n /** Held items awaiting review, oldest first. */\n held(q: HeldQuery, signal?: AbortSignal): Promise<HeldPage> {\n return call(this.http, \"GET\", \"/moderation/held\", { query: { ...q }, signal });\n }\n /** Approves (publishes) or rejects a held item at the revision the queue listed; conflict when it was edited since. */\n async resolve(item: Pick<HeldItem, \"kind\" | \"id\" | \"revision\">, decision: Decision, reason?: string): Promise<ReviewOutcome> {\n const emit = this.http.captureChanges();\n const kind = item.kind as HeldKind;\n const out: ReviewOutcome = await call(this.http, \"POST\", \"/moderation/{kind}/{id}/resolve\", {\n params: { kind, id: item.id },\n body: { decision, revision: item.revision, ...(reason ? { reason } : {}) },\n });\n emit({ type: \"moderation.resolved\", kind, id: item.id, decision });\n return out;\n }\n\n /** The banned words, alphabetically, and what each does: mask or reject. */\n bannedWords(signal?: AbortSignal): Promise<BannedWord[]> {\n return call(this.http, \"GET\", \"/moderation/banned-words\", { signal });\n }\n /** Bans words, or changes what banned ones do (mask by default); the list after. */\n async banWords(words: BannedWordInput[]): Promise<BannedWord[]> {\n const emit = this.http.captureChanges();\n const out: BannedWord[] = await call(this.http, \"POST\", \"/moderation/banned-words\", { body: words });\n emit({ type: \"rules.changed\", rules: \"words\" });\n return out;\n }\n /** Unbans words; idempotent. */\n async unbanWords(words: string[]): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/moderation/banned-words\", { body: words });\n emit({ type: \"rules.changed\", rules: \"words\" });\n }\n /** The IP bans, newest first, expired ones included. */\n ipBans(signal?: AbortSignal): Promise<IPBan[]> {\n return call(this.http, \"GET\", \"/moderation/ip-bans\", { signal });\n }\n /** Bans addresses or networks (CIDR) from commenting and answering, or replaces their bans; the list after. */\n async banIPs(bans: IPBanInput[]): Promise<IPBan[]> {\n const emit = this.http.captureChanges();\n const out: IPBan[] = await call(this.http, \"POST\", \"/moderation/ip-bans\", { body: bans });\n emit({ type: \"rules.changed\", rules: \"ips\" });\n return out;\n }\n /** Lifts the bans of addresses or networks; idempotent. */\n async liftIPBans(networks: string[]): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/moderation/ip-bans\", { body: networks });\n emit({ type: \"rules.changed\", rules: \"ips\" });\n }\n}\n"],"mappings":";;AAaA,IAAa,IAAb,MAA8B;CACC;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,KAAK,GAAc,GAAyC;EAC1D,OAAO,EAAK,KAAK,MAAM,OAAO,oBAAoB;GAAE,OAAO,EAAE,GAAG,EAAE;GAAG;EAAO,CAAC;CAC/E;CAEA,MAAM,QAAQ,GAAkD,GAAoB,GAAyC;EAC3H,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAO,EAAK,MACZ,IAAqB,MAAM,EAAK,KAAK,MAAM,QAAQ,mCAAmC;GAC1F,QAAQ;IAAE;IAAM,IAAI,EAAK;GAAG;GAC5B,MAAM;IAAE;IAAU,UAAU,EAAK;IAAU,GAAI,IAAS,EAAE,UAAO,IAAI,CAAC;GAAG;EAC3E,CAAC;EAED,OADA,EAAK;GAAE,MAAM;GAAuB;GAAM,IAAI,EAAK;GAAI;EAAS,CAAC,GAC1D;CACT;CAGA,YAAY,GAA6C;EACvD,OAAO,EAAK,KAAK,MAAM,OAAO,4BAA4B,EAAE,UAAO,CAAC;CACtE;CAEA,MAAM,SAAS,GAAiD;EAC9D,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAoB,MAAM,EAAK,KAAK,MAAM,QAAQ,4BAA4B,EAAE,MAAM,EAAM,CAAC;EAEnG,OADA,EAAK;GAAE,MAAM;GAAiB,OAAO;EAAQ,CAAC,GACvC;CACT;CAEA,MAAM,WAAW,GAAgC;EAC/C,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,UAAU,4BAA4B,EAAE,MAAM,EAAM,CAAC,GAC3E,EAAK;GAAE,MAAM;GAAiB,OAAO;EAAQ,CAAC;CAChD;CAEA,OAAO,GAAwC;EAC7C,OAAO,EAAK,KAAK,MAAM,OAAO,uBAAuB,EAAE,UAAO,CAAC;CACjE;CAEA,MAAM,OAAO,GAAsC;EACjD,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAe,MAAM,EAAK,KAAK,MAAM,QAAQ,uBAAuB,EAAE,MAAM,EAAK,CAAC;EAExF,OADA,EAAK;GAAE,MAAM;GAAiB,OAAO;EAAM,CAAC,GACrC;CACT;CAEA,MAAM,WAAW,GAAmC;EAClD,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,UAAU,uBAAuB,EAAE,MAAM,EAAS,CAAC,GACzE,EAAK;GAAE,MAAM;GAAiB,OAAO;EAAM,CAAC;CAC9C;AACF"}
@@ -18,6 +18,8 @@ export type Sort = "newest" | "likes" | "best";
18
18
  export type BanScope = CommentStanding["ban_scopes"][number];
19
19
  /** What the review queue holds: comments and content items. */
20
20
  export type HeldKind = "comment" | "content";
21
+ /** The moderation rules: banned words or IP bans. */
22
+ export type RuleSet = "words" | "ips";
21
23
  /** A reviewer's decision on a held item. */
22
24
  export type Decision = Exclude<ResolveInput["decision"], "review">;
23
25
  /** A content-module mutation made through the client. */
@@ -91,6 +93,17 @@ export type ContentChange =
91
93
  kind: HeldKind;
92
94
  id: string;
93
95
  decision: Decision;
96
+ }
97
+ /** Banned words or IP bans changed. */
98
+ | {
99
+ type: "rules.changed";
100
+ rules: RuleSet;
101
+ }
102
+ /** A version's reader metadata changed: its reader plan reads again. */
103
+ | {
104
+ type: "reader.changed";
105
+ content: string;
106
+ version: string;
94
107
  } | {
95
108
  type: "groups.changed";
96
109
  }
@@ -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/** 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 | { 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 | { 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"}
@@ -15,6 +15,12 @@ export interface ContentFilterQuery {
15
15
  exclude_groups?: readonly string[];
16
16
  /** Only contents this group owns: a channel's feed. */
17
17
  owner_group?: string;
18
+ /** User ids: only contents one of them is credited on. */
19
+ creators?: readonly string[];
20
+ /** public (anyone opens it now) or the level it stays locked at. */
21
+ access?: readonly ("public" | "tier" | "group" | "ppv" | "group_ppv")[];
22
+ /** Only contents locked behind one of these tiers. */
23
+ tiers?: readonly string[];
18
24
  /** Version traits every listed version has. */
19
25
  traits?: readonly string[];
20
26
  /** "name:value" of the kind's filterable attributes. */
@@ -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 /** 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":";;;AA8FA,IAAM,oBAAQ,IAAI,IAAI;CAAC;CAAU;CAAkB;CAAU;CAAe;AAAa,CAAC;AAG1F,SAAgB,EAAM,GAAsF;CAC1G,IAAM,IAAiF,CAAC;CACxF,KAAK,IAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,CAAC,GAAG,EAAI,KAAK,EAAM,IAAI,CAAC,KAAK,MAAM,QAAQ,CAAC,IAAK,EAAE,SAAS,EAAE,KAAK,GAAG,IAAI,KAAA,IAAa;CAC3H,OAAO;AACT;AAMA,IAAa,IAAb,MAA6B;CACE;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,KAAK,IAAkB,CAAC,GAAG,GAA4C;EACrE,OAAO,EAAK,KAAK,MAAM,OAAO,aAAa;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACjF;CAEA,MAAM,OAAO,GAAgB,GAA4C;EACvE,IAAM,IAAO,EAAE,SAAS,OAAO,KAAK,KAAK,eAAe,GAClD,IAAmB,MAAM,EAAK,KAAK,MAAM,OAAO,WAAW;GAAE,OAAO,EAAM;IAAE,SAAS,EAAc;IAAG,GAAG;GAAE,CAAC;GAAY;EAAO,CAAC;EAEtI,OADA,IAAO,EAAE,MAAM,mBAAmB,CAAC,GAC5B;CACT;CAEA,eAAe,IAAyB,CAAC,GAAG,GAA+C;EACzF,OAAO,EAAK,KAAK,MAAM,OAAO,gBAAgB;GAAE,OAAO;IAAE,SAAS,EAAc;IAAG,GAAG;GAAE;GAAG;EAAO,CAAC;CACrG;CAEA,MAAM,cAAc,IAA0C,CAAC,GAAkB;EAC/E,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,UAAU,gBAAgB,EAAE,OAAO;GAAE,SAAS,EAAc;GAAG,GAAG;EAAE,EAAE,CAAC,GAC7F,EAAK,EAAE,MAAM,mBAAmB,CAAC;CACnC;CAEA,UAAU,GAAmB,GAA0C;EACrE,OAAO,EAAK,KAAK,MAAM,OAAO,cAAc;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CAClF;CAEA,QAAQ,GAAY,IAAe,CAAC,GAAG,GAA4C;EACjF,OAAO,EAAK,KAAK,MAAM,OAAO,0BAA0B;GAAE,QAAQ,EAAE,MAAG;GAAG,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CAC9G;CAEA,gBAAgB,IAAe,CAAC,GAAG,GAA4C;EAC7E,OAAO,EAAK,KAAK,MAAM,OAAO,oBAAoB;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACxF;CAEA,aAAa,IAAuB,CAAC,GAAG,GAA6C;EACnF,OAAO,EAAK,KAAK,MAAM,OAAO,kBAAkB;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACtF;CACA,eAAe,GAAmB,GAA+C;EAC/E,OAAO,EAAK,KAAK,MAAM,OAAO,uBAAuB;GAAE,OAAO,EAAE,YAAS;GAAG;EAAO,CAAC;CACtF;CAEA,MAAM,kBAAkB,GAA8B,GAA4C;EAChG,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAsB,MAAM,EAAK,KAAK,MAAM,OAAO,uBAAuB;GAAE,OAAO,EAAE,YAAS;GAAG,MAAM;EAAO,CAAC;EAErH,OADA,EAAK,EAAE,MAAM,yBAAyB,CAAC,GAChC;CACT;CAEA,QAAQ,IAAkB,CAAC,GAAG,GAA4C;EACxE,OAAO,EAAK,KAAK,MAAM,OAAO,eAAe;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACnF;CAEA,MAAM,cAAc,IAA4C,CAAC,GAAkB;EACjF,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,UAAU,eAAe,EAAE,OAAO,EAAE,CAAC,GAC3D,EAAK,EAAE,MAAM,kBAAkB,CAAC;CAClC;CAEA,QAAQ,GAAwB,GAAkB,IAA6B,CAAC,GAAkB;EAChG,IAAM,IAAgB;GAAE;GAAS;EAAQ;EACzC,OAAO,EAAK,KAAK,MAAM,QAAQ,YAAY;GAAE;GAAM,WAAW,EAAE;EAAU,CAAC;CAC7E;CAEA,UAAU,GAA4B,GAAkB,IAA6B,CAAC,GAAkB;EACtG,IAAM,IAAkB;GAAE;GAAS;EAAU;EAC7C,OAAO,EAAK,KAAK,MAAM,QAAQ,cAAc;GAAE;GAAM,WAAW,EAAE;EAAU,CAAC;CAC/E;AACF"}
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,4 +1,4 @@
1
- export type ErrorCode = "animation_not_allowed" | "animation_too_long" | "animation_unsupported" | "checksum_mismatch" | "comment_banned" | "comment_too_long" | "comments_closed" | "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_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" | "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";
2
2
  export declare const CONTENTKIT_ERROR_CODES: Readonly<Record<ErrorCode, {
3
3
  status: number;
4
4
  meaning: string;
@@ -28,6 +28,10 @@ var e = {
28
28
  status: 403,
29
29
  meaning: "The target's comments are closed or hidden: existing ones may show, new ones and edits are refused."
30
30
  },
31
+ comments_locked: {
32
+ status: 403,
33
+ meaning: "The actor can't open the content (or the comment's version), so its comments take no writes from them; reads withhold them."
34
+ },
31
35
  conflict: {
32
36
  status: 409,
33
37
  meaning: "A concurrent change, a path or slug already taken, or an operation id already used for another commit."
@@ -80,6 +84,10 @@ var e = {
80
84
  status: 404,
81
85
  meaning: "Nothing by that name, or nothing the actor may see."
82
86
  },
87
+ not_joinable: {
88
+ status: 409,
89
+ meaning: "The group has no free offer to join: it sells its membership, or nothing."
90
+ },
83
91
  not_uploaded: {
84
92
  status: 409,
85
93
  meaning: "A commit names an upload that has not landed; `blobs` lists the ones to upload again."
@@ -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 | \"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_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 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_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 or media storage 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":";AAyCA,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,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;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;CAAgG;CACrI,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 | \"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 or media storage 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;CAAgG;CACrI,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"}