@buildinternet/releases-api-types 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/releases-api-types",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Wire protocol — request/response types for the Releases registry API. Shared by web, workers, and clients.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/api-types.ts CHANGED
@@ -829,12 +829,47 @@ export interface FollowMutationResponse {
829
829
  following: boolean;
830
830
  }
831
831
 
832
+ /**
833
+ * A user's personalized feed token, including the full re-revealable feed URL.
834
+ * The token is reversible (stored recoverably) because the feed serves only
835
+ * public release data and carries no PII — so the URL can be re-displayed and
836
+ * copied on any visit (see #1519 design, decision 6). One per user.
837
+ */
838
+ export interface FeedToken {
839
+ /** Absolute, tokenized Atom URL — e.g. https://api.releases.sh/v1/feed/relf_…_….atom */
840
+ feedUrl: string;
841
+ /** Non-secret public handle (for masked display). */
842
+ lookupId: string;
843
+ createdAt: string;
844
+ lastUsedAt: string | null;
845
+ }
846
+
847
+ /** `GET /v1/me/feed/token` — the token, or null if the user has none yet. */
848
+ export interface FeedTokenResponse {
849
+ token: FeedToken | null;
850
+ }
851
+
832
852
  /**
833
853
  * GET /v1/me/feed response — the personalized release feed. Reuses the same item
834
854
  * shape as /v1/releases/latest (ReleaseLatestItem) inside the standard list envelope.
835
855
  */
836
856
  export type PersonalizedFeedResponse = ListResponse<ReleaseLatestItem>;
837
857
 
858
+ // ── Digest emails ──
859
+
860
+ /** How often a user wants a digest email. `off` = no emails. */
861
+ export type DigestCadence = "off" | "daily" | "weekly";
862
+
863
+ /** GET /v1/me/digest response — the caller's current cadence. */
864
+ export interface DigestPrefsResponse {
865
+ cadence: DigestCadence;
866
+ }
867
+
868
+ /** PUT /v1/me/digest request body. */
869
+ export interface DigestPrefsRequest {
870
+ cadence: DigestCadence;
871
+ }
872
+
838
873
  // ── Search ──
839
874
 
840
875
  export type SearchOrgHit = z.infer<typeof SearchOrgHitSchema>;
@@ -845,6 +880,8 @@ export type SearchSourceHit = z.infer<typeof SearchSourceHitSchema>;
845
880
  export interface RawSourceHit extends SearchSourceHit {
846
881
  productName?: string;
847
882
  productCategory?: string;
883
+ /** Owning org's avatar URL — folded onto the catalog hit for the byline avatar. */
884
+ orgAvatarUrl?: string | null;
848
885
  /** Entity taxonomy kind from the `sources.kind` column. */
849
886
  entityKind?: string | null;
850
887
  }
@@ -870,6 +907,7 @@ export function foldSourcesIntoCatalog(
870
907
  name: s.productName ?? s.name,
871
908
  orgSlug: s.orgSlug,
872
909
  orgName: s.orgName,
910
+ orgAvatarUrl: s.orgAvatarUrl ?? null,
873
911
  category: s.productCategory ?? null,
874
912
  entryType: "product",
875
913
  kind: undefined,
@@ -881,6 +919,7 @@ export function foldSourcesIntoCatalog(
881
919
  name: s.name,
882
920
  orgSlug: s.orgSlug,
883
921
  orgName: s.orgName,
922
+ orgAvatarUrl: s.orgAvatarUrl ?? null,
884
923
  category: null,
885
924
  entryType: "source",
886
925
  kind: (s.entityKind as SearchCatalogHit["kind"]) ?? undefined,
@@ -26,6 +26,9 @@ export const ReleaseLatestSourceSchema = z.object({
26
26
  name: z.string(),
27
27
  type: z.enum(SOURCE_TYPES),
28
28
  orgSlug: z.string().nullable(),
29
+ /** Owning org's display name (e.g. "Cloudflare"), distinct from the source
30
+ * name. Null when the source has no org. Additive — older responses omit it. */
31
+ orgName: z.string().nullable().optional(),
29
32
  });
30
33
 
31
34
  export const ReleaseLatestProductSchema = z.object({
@@ -240,6 +243,12 @@ export const UpdateReleaseBodySchema = z
240
243
  titleShort: z.string().nullable().optional(),
241
244
  // `null` removes `$.composition` from metadata; an object replaces it.
242
245
  composition: ReleaseCompositionSchema.nullable().optional(),
246
+ // Replace the release's stored `media[]` wholesale (curator manual edit).
247
+ // Items whose `url` is not already an R2/`media.releases.sh` origin are run
248
+ // through the ingest R2 mirror (`processMediaForR2`) so posters/images land
249
+ // in `released-media` and get `r2Key`/`r2Url` stamped — same path as ingest.
250
+ // Reuses the shared `MediaItemSchema` (type/url/alt/r2Url/r2Key/linkUrl).
251
+ media: z.array(MediaItemSchema).optional(),
243
252
  })
244
253
  .strict();
245
254
 
@@ -40,6 +40,9 @@ export const SearchCatalogHitSchema = z.object({
40
40
  name: z.string(),
41
41
  orgSlug: z.string().nullable(),
42
42
  orgName: z.string().nullable(),
43
+ /** Owning org's avatar URL, for a small inline byline avatar. Products have
44
+ * no avatar of their own, so the web row anchors on the org's instead. */
45
+ orgAvatarUrl: z.string().nullable().optional(),
43
46
  // Loose `z.string().nullable()` rather than the canonical `CategorySchema`
44
47
  // enum because `foldSourcesIntoCatalog` and the catalog-search handlers
45
48
  // pass `s.productCategory` / `o.category` through without validating
@@ -9,7 +9,21 @@ export const MediaItemSchema = z.object({
9
9
  type: z.enum(["image", "video", "gif"]),
10
10
  url: z.string(),
11
11
  alt: z.string().optional(),
12
+ // R2 object key (`releases/<hash>.<ext>`) for media mirrored into the
13
+ // `released-media` bucket; read paths resolve it to a same-origin `r2Url`
14
+ // (`resolveR2Url`). Stamped by `processMediaForR2` at ingest / manual edit and
15
+ // part of the stored `media[]` JSON — surfaced on the wire so a curator round-
16
+ // tripping `media` through `PATCH /v1/releases/:id` can preserve an already-
17
+ // mirrored item without forcing a re-fetch.
18
+ r2Key: z.string().optional(),
12
19
  r2Url: z.string().optional(),
20
+ // For `type: "video"` items promoted from a hosted-video link found inline in
21
+ // a release body (Wistia/Loom/Vimeo/YouTube), `url` holds the poster/thumbnail
22
+ // (mirrored to R2 like any image) and `linkUrl` holds the human watch URL the
23
+ // play-card links out to. Absent for ordinary image/gif media. See
24
+ // `detectInlineVideos` in `@releases/rendering/video-embed` and web's
25
+ // release-content video card.
26
+ linkUrl: z.string().optional(),
13
27
  });
14
28
 
15
29
  /**
@@ -475,13 +475,27 @@ export const CreateSourceSummaryResponseSchema = z.object({
475
475
  // ── Source fetch trigger (POST /sources/:slug/fetch) ──
476
476
 
477
477
  /**
478
- * Response shape for `POST /v1/sources/:slug/fetch`. Two branches:
478
+ * Response shape for `POST /v1/sources/:slug/fetch`. Three branches:
479
479
  * - Feed/GitHub/scraped-with-feed sources: `{ fetched: true, releasesInserted, … }`
480
480
  * - Scrape/agent sources (flagged for CLI pickup): `{ queued: true, type: "flagged" }`
481
+ * - Render dry-run probe (`?dryRun=true` on a client-rendered scrape source):
482
+ * `{ renderCheck: true, rendered, candidateCount, sampleUrls, … }`
481
483
  */
482
484
  export const SourceFetchResponseSchema = z.union([
483
485
  z.looseObject({ fetched: z.literal(true) }),
484
486
  z.object({ queued: z.literal(true), type: z.string() }),
487
+ // Render dry-run probe (#1528): renders a client-rendered scrape source's
488
+ // index once and reports candidate links found, without the managed-agent
489
+ // extraction loop.
490
+ z.looseObject({
491
+ renderCheck: z.literal(true),
492
+ status: z.enum(["dry_run", "error"]),
493
+ rendered: z.boolean(),
494
+ candidateCount: z.number().int(),
495
+ sampleUrls: z.array(z.string()),
496
+ durationMs: z.number().int(),
497
+ error: z.string().optional(),
498
+ }),
485
499
  ]);
486
500
 
487
501
  // ── Source content-hash check (POST /sources/:slug/content-hash) ──