@buildinternet/releases-api-types 0.31.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.31.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
@@ -748,7 +748,16 @@ export interface ReleaseDetail {
748
748
  sourceName: string;
749
749
  sourceSlug: string;
750
750
  sourceType: string;
751
- org: { slug: string; name: string } | null;
751
+ /**
752
+ * Owning org. `avatarUrl` is the resolved `organizations.avatar_url`
753
+ * (nullable; `.optional()` for mid-deploy / pinned-worker tolerance).
754
+ */
755
+ org: { slug: string; name: string; avatarUrl?: string | null } | null;
756
+ /**
757
+ * Owning product, when the source is grouped under one (`sources.product_id`).
758
+ * `null` when ungrouped; `.optional()` so older servers that omit it still type.
759
+ */
760
+ product?: { slug: string; name: string } | null;
752
761
  /** Release type. See {@link ReleaseType}. */
753
762
  type?: ReleaseType;
754
763
  /** Per-category item counts from the AI release-content pass. See {@link ReleaseComposition}. */
@@ -786,6 +795,81 @@ export type ReleaseStreamMessage = z.infer<typeof ReleaseStreamMessageSchema>;
786
795
 
787
796
  export type ReleaseSummaryItem = z.infer<typeof ReleaseSummaryItemSchema>;
788
797
 
798
+ // ── Follows ──
799
+
800
+ /** What a user can follow. */
801
+ export type FollowTarget = "org" | "product";
802
+
803
+ /** A user's follow, enriched for rendering (returned by GET /v1/me/follows). */
804
+ export interface Follow {
805
+ targetType: FollowTarget;
806
+ targetId: string;
807
+ name: string;
808
+ slug: string;
809
+ avatarUrl: string | null;
810
+ /** Owning org slug for product follows (null for org follows). */
811
+ orgSlug: string | null;
812
+ createdAt: string;
813
+ }
814
+
815
+ /** GET /v1/me/follows response. */
816
+ export interface FollowsListResponse {
817
+ follows: Follow[];
818
+ }
819
+
820
+ /** POST /v1/me/follows request body. */
821
+ export interface FollowRequest {
822
+ targetType: FollowTarget;
823
+ targetId: string;
824
+ }
825
+
826
+ /** POST/DELETE /v1/me/follows response. */
827
+ export interface FollowMutationResponse {
828
+ success: true;
829
+ following: boolean;
830
+ }
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
+
852
+ /**
853
+ * GET /v1/me/feed response — the personalized release feed. Reuses the same item
854
+ * shape as /v1/releases/latest (ReleaseLatestItem) inside the standard list envelope.
855
+ */
856
+ export type PersonalizedFeedResponse = ListResponse<ReleaseLatestItem>;
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
+
789
873
  // ── Search ──
790
874
 
791
875
  export type SearchOrgHit = z.infer<typeof SearchOrgHitSchema>;
@@ -796,6 +880,8 @@ export type SearchSourceHit = z.infer<typeof SearchSourceHitSchema>;
796
880
  export interface RawSourceHit extends SearchSourceHit {
797
881
  productName?: string;
798
882
  productCategory?: string;
883
+ /** Owning org's avatar URL — folded onto the catalog hit for the byline avatar. */
884
+ orgAvatarUrl?: string | null;
799
885
  /** Entity taxonomy kind from the `sources.kind` column. */
800
886
  entityKind?: string | null;
801
887
  }
@@ -821,6 +907,7 @@ export function foldSourcesIntoCatalog(
821
907
  name: s.productName ?? s.name,
822
908
  orgSlug: s.orgSlug,
823
909
  orgName: s.orgName,
910
+ orgAvatarUrl: s.orgAvatarUrl ?? null,
824
911
  category: s.productCategory ?? null,
825
912
  entryType: "product",
826
913
  kind: undefined,
@@ -832,6 +919,7 @@ export function foldSourcesIntoCatalog(
832
919
  name: s.name,
833
920
  orgSlug: s.orgSlug,
834
921
  orgName: s.orgName,
922
+ orgAvatarUrl: s.orgAvatarUrl ?? null,
835
923
  category: null,
836
924
  entryType: "source",
837
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({
@@ -168,6 +171,13 @@ export const ReleasesWithMediaResponseSchema = z.array(ReleaseWithMediaRowSchema
168
171
  export const ReleaseDetailOrgSchema = z.object({
169
172
  slug: z.string(),
170
173
  name: z.string(),
174
+ /**
175
+ * Resolved org avatar URL (the `organizations.avatar_url` column). `null`
176
+ * when the org has no stored avatar. Additive — older servers omit it, so
177
+ * treat `undefined` as `null`. Mirrors the `orgAvatarUrl` field on the
178
+ * related-rail feed.
179
+ */
180
+ avatarUrl: z.string().nullable().optional(),
171
181
  });
172
182
 
173
183
  /**
@@ -199,6 +209,13 @@ export const ReleaseDetailResponseSchema = z.object({
199
209
  sourceSlug: z.string().nullable(),
200
210
  sourceType: z.enum(SOURCE_TYPES).nullable(),
201
211
  org: ReleaseDetailOrgSchema.nullable(),
212
+ /**
213
+ * Owning product, when the release's source is grouped under a product
214
+ * (`sources.product_id`). `null` when the source has no product. Additive —
215
+ * older servers omit this field; treat `undefined` as `null`. Reuses the
216
+ * `{ slug, name }` shape from the latest feed.
217
+ */
218
+ product: ReleaseLatestProductSchema.nullable().optional(),
202
219
  composition: ReleaseCompositionSchema.nullable(),
203
220
  appStore: AppStoreSourceInfoSchema.nullable().optional(),
204
221
  video: VideoSourceInfoSchema.nullable().optional(),
@@ -226,6 +243,12 @@ export const UpdateReleaseBodySchema = z
226
243
  titleShort: z.string().nullable().optional(),
227
244
  // `null` removes `$.composition` from metadata; an object replaces it.
228
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(),
229
252
  })
230
253
  .strict();
231
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) ──