@buildinternet/releases-api-types 0.22.0 → 0.23.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.22.0",
3
+ "version": "0.23.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
@@ -10,6 +10,8 @@ export type {
10
10
  SourceDiscovery,
11
11
  SourceFetchPriority,
12
12
  } from "@buildinternet/releases-core/source-enums";
13
+ // Re-export above doesn't create local bindings; import for use in interfaces below.
14
+ import type { SourceType, SourceFetchPriority } from "@buildinternet/releases-core/source-enums";
13
15
  import type {
14
16
  MediaItemSchema,
15
17
  PaginationSchema,
@@ -95,6 +97,7 @@ import type {
95
97
  } from "./schemas/sources.js";
96
98
  import type {
97
99
  ProductRowSchema,
100
+ ProductCreateResponseSchema,
98
101
  ProductListItemSchema,
99
102
  ProductListResponseSchema,
100
103
  ProductDetailSourceSchema,
@@ -109,9 +112,12 @@ import type {
109
112
  ProductTagsListResponseSchema,
110
113
  ProductTagsBodySchema,
111
114
  ProductTagsMutationResponseSchema,
115
+ ProductActivityResponseSchema,
116
+ ProductHeatmapResponseSchema,
112
117
  } from "./schemas/products.js";
113
118
  import type {
114
119
  ReleaseLatestSourceSchema,
120
+ ReleaseLatestProductSchema,
115
121
  ReleaseLatestItemSchema,
116
122
  ReleaseLatestResponseSchema,
117
123
  ReleaseCoverageRowSchema,
@@ -171,6 +177,14 @@ import type {
171
177
  StatsRecentActivitySchema,
172
178
  StatsResponseSchema,
173
179
  } from "./schemas/stats.js";
180
+ import type {
181
+ FeedbackTypeSchema,
182
+ FeedbackStatusSchema,
183
+ FeedbackItemSchema,
184
+ FeedbackListResponseSchema,
185
+ FeedbackUpdateBodySchema,
186
+ FeedbackDeleteResponseSchema,
187
+ } from "./schemas/feedback.js";
174
188
  import type { SitemapSourceSchema, SitemapPayloadSchema } from "./schemas/sitemap.js";
175
189
  import type {
176
190
  RelatedScopeSchema,
@@ -187,6 +201,9 @@ import type {
187
201
  } from "./schemas/related.js";
188
202
  import type {
189
203
  CollectionMemberOrgSchema,
204
+ ProductParentOrgSchema,
205
+ CollectionMemberProductSchema,
206
+ CollectionMemberSchema,
190
207
  CollectionListItemSchema,
191
208
  CollectionListResponseSchema,
192
209
  CollectionDetailSchema,
@@ -304,6 +321,7 @@ export {
304
321
  } from "./schemas/sources.js";
305
322
  export {
306
323
  ProductRowSchema,
324
+ ProductCreateResponseSchema,
307
325
  ProductListItemSchema,
308
326
  ProductListResponseSchema,
309
327
  ProductDetailSourceSchema,
@@ -318,6 +336,8 @@ export {
318
336
  ProductTagsListResponseSchema,
319
337
  ProductTagsBodySchema,
320
338
  ProductTagsMutationResponseSchema,
339
+ ProductActivityResponseSchema,
340
+ ProductHeatmapResponseSchema,
321
341
  } from "./schemas/products.js";
322
342
  export {
323
343
  LookupStatusSchema,
@@ -347,6 +367,7 @@ export {
347
367
  } from "./schemas/taxonomy.js";
348
368
  export {
349
369
  ReleaseLatestSourceSchema,
370
+ ReleaseLatestProductSchema,
350
371
  ReleaseLatestItemSchema,
351
372
  ReleaseLatestResponseSchema,
352
373
  ReleaseCoverageRowSchema,
@@ -381,6 +402,14 @@ export {
381
402
  StatsRecentActivitySchema,
382
403
  StatsResponseSchema,
383
404
  } from "./schemas/stats.js";
405
+ export {
406
+ FeedbackTypeSchema,
407
+ FeedbackStatusSchema,
408
+ FeedbackItemSchema,
409
+ FeedbackListResponseSchema,
410
+ FeedbackUpdateBodySchema,
411
+ FeedbackDeleteResponseSchema,
412
+ } from "./schemas/feedback.js";
384
413
  export { SitemapSourceSchema, SitemapPayloadSchema } from "./schemas/sitemap.js";
385
414
  export {
386
415
  RelatedScopeSchema,
@@ -397,6 +426,9 @@ export {
397
426
  } from "./schemas/related.js";
398
427
  export {
399
428
  CollectionMemberOrgSchema,
429
+ ProductParentOrgSchema,
430
+ CollectionMemberProductSchema,
431
+ CollectionMemberSchema,
400
432
  CollectionListItemSchema,
401
433
  CollectionListResponseSchema,
402
434
  CollectionDetailSchema,
@@ -426,6 +458,8 @@ export {
426
458
  UpdatePlaybookNotesBodySchema,
427
459
  UpdatePlaybookNotesResponseSchema,
428
460
  } from "./schemas/overviews.js";
461
+ export { ResolveResponseSchema } from "./schemas/resolve.js";
462
+ export type { ResolveResponse } from "./schemas/resolve.js";
429
463
 
430
464
  // ── Media ──
431
465
 
@@ -566,6 +600,61 @@ export interface OrgsRollupResponse extends ListResponse<OrgsRollupRow> {
566
600
  };
567
601
  }
568
602
 
603
+ // ── Admin telemetry: stuck sources ──
604
+
605
+ /**
606
+ * A source whose recent fetch history is all errors with no reachability — a
607
+ * candidate for pausing. "Stuck" means: within the last `window` non-`dry_run`
608
+ * fetch attempts, every one was an `error` (zero `success`/`no_change`) and
609
+ * there were at least `minAttempts` of them.
610
+ *
611
+ * This keys off the `fetch_log` error streak rather than
612
+ * `sources.consecutive_errors`, because the scrape/agent fetch path never bumps
613
+ * that column (a source can fail for days with `consecutive_errors = 0`).
614
+ */
615
+ export interface StuckSource {
616
+ sourceId: string;
617
+ sourceSlug: string;
618
+ name: string;
619
+ type: SourceType;
620
+ url: string;
621
+ kind: string | null;
622
+ orgSlug: string | null;
623
+ orgName: string | null;
624
+ /** Current fetch tier — `paused` rows only appear when `includePaused`. */
625
+ fetchPriority: SourceFetchPriority;
626
+ /** True when this is the org's primary changelog (Firebase's was). */
627
+ isPrimary: boolean;
628
+ isHidden: boolean;
629
+ /** Non-`dry_run` attempts examined in the window (all errors when stuck). */
630
+ recentAttempts: number;
631
+ /** Errors among the examined attempts (equals `recentAttempts` when stuck). */
632
+ recentErrors: number;
633
+ /** Most-recent fetch attempt timestamp (ISO), or null. */
634
+ lastAttemptAt: string | null;
635
+ /** Most-recent error message. */
636
+ lastError: string | null;
637
+ /** Most-recent error category, when the fetcher classified it. */
638
+ lastErrorCategory: string | null;
639
+ /** Last time the source was reachable (`success`/`no_change`), ISO; null = never. */
640
+ lastSuccessAt: string | null;
641
+ /** `sources.last_fetched_at` — null when the source has never fetched. */
642
+ lastFetchedAt: string | null;
643
+ /** When the source row was created (ISO) — proxy for how long it's failed. */
644
+ sourceCreatedAt: string | null;
645
+ }
646
+
647
+ export interface StuckSourcesResponse extends ListResponse<StuckSource> {
648
+ meta: {
649
+ /** Recent non-`dry_run` attempts examined per source. */
650
+ window: number;
651
+ /** Minimum attempts in the window required to flag a source. */
652
+ minAttempts: number;
653
+ /** Whether already-paused sources were included. */
654
+ includePaused: boolean;
655
+ };
656
+ }
657
+
569
658
  // ── Releases ──
570
659
 
571
660
  /**
@@ -607,6 +696,8 @@ export interface ReleaseDetail {
607
696
  type?: ReleaseType;
608
697
  /** Per-category item counts from the AI release-content pass. See {@link ReleaseComposition}. */
609
698
  composition?: ReleaseComposition | null;
699
+ /** App Store platform + icon, present only when `sourceType === "appstore"`. */
700
+ appStore?: { platform: "ios" | "macos"; iconUrl: string | null } | null;
610
701
  }
611
702
 
612
703
  export type ReleaseCoverageRow = z.infer<typeof ReleaseCoverageRowSchema>;
@@ -616,6 +707,7 @@ export type LinkReleaseCoverageResponse = z.infer<typeof LinkReleaseCoverageResp
616
707
  export type UnlinkReleaseCoverageResponse = z.infer<typeof UnlinkReleaseCoverageResponseSchema>;
617
708
 
618
709
  export type ReleaseLatestSource = z.infer<typeof ReleaseLatestSourceSchema>;
710
+ export type ReleaseLatestProduct = z.infer<typeof ReleaseLatestProductSchema>;
619
711
  export type ReleaseLatestItem = z.infer<typeof ReleaseLatestItemSchema>;
620
712
  export type ReleaseLatestResponse = z.infer<typeof ReleaseLatestResponseSchema>;
621
713
 
@@ -861,7 +953,7 @@ export type SourceActivity = SourceActivityResponse;
861
953
 
862
954
  // ── Org Activity (Zod-derived aliases) ──
863
955
  // The old hand-written OrgActivitySource / OrgActivity / OrgSparklines /
864
- // OrgHeatmap / OrgReleaseItem / OrgReleasesResponse interfaces have been
956
+ // OrgHeatmap / OrgReleaseItem interfaces have been
865
957
  // replaced by Zod-derived types in the Organizations section above. Keeping
866
958
  // the interface names as deprecated aliases so callsites don't need an
867
959
  // immediate churn.
@@ -875,9 +967,6 @@ export type OrgSparklines = OrgSparklinesResponse;
875
967
  /** @deprecated Use OrgHeatmapResponse */
876
968
  export type OrgHeatmap = OrgHeatmapResponse;
877
969
 
878
- /** @deprecated Use OrgReleasesFeedResponse */
879
- export type OrgReleasesResponse = OrgReleasesFeedResponse;
880
-
881
970
  // ── Org Sparklines (per-source/product breakdown) ──
882
971
  // (covered above)
883
972
 
@@ -905,6 +994,7 @@ export interface SourceReleasesResponse {
905
994
  // `Category` lives in @buildinternet/releases-core/categories — import it from
906
995
  // there. CategorySchema is re-exported here for OpenAPI / Zod consumers only.
907
996
  export type ProductRow = z.infer<typeof ProductRowSchema>;
997
+ export type ProductCreateResponse = z.infer<typeof ProductCreateResponseSchema>;
908
998
  export type ProductListItem = z.infer<typeof ProductListItemSchema>;
909
999
  export type ProductListResponse = z.infer<typeof ProductListResponseSchema>;
910
1000
  export type ProductDetailSource = z.infer<typeof ProductDetailSourceSchema>;
@@ -919,6 +1009,8 @@ export type ProductDeleteResponse = z.infer<typeof ProductDeleteResponseSchema>;
919
1009
  export type ProductTagsListResponse = z.infer<typeof ProductTagsListResponseSchema>;
920
1010
  export type ProductTagsBody = z.infer<typeof ProductTagsBodySchema>;
921
1011
  export type ProductTagsMutationResponse = z.infer<typeof ProductTagsMutationResponseSchema>;
1012
+ export type ProductActivityResponse = z.infer<typeof ProductActivityResponseSchema>;
1013
+ export type ProductHeatmapResponse = z.infer<typeof ProductHeatmapResponseSchema>;
922
1014
 
923
1015
  // ── Taxonomy (categories + tags) ──
924
1016
 
@@ -947,6 +1039,9 @@ export type CategoryReleaseItem = CollectionReleaseItem;
947
1039
  // /collections/frontier-ai-labs). Independent of the fixed `category` taxonomy.
948
1040
 
949
1041
  export type CollectionMemberOrg = z.infer<typeof CollectionMemberOrgSchema>;
1042
+ export type ProductParentOrg = z.infer<typeof ProductParentOrgSchema>;
1043
+ export type CollectionMemberProduct = z.infer<typeof CollectionMemberProductSchema>;
1044
+ export type CollectionMember = z.infer<typeof CollectionMemberSchema>;
950
1045
  export type CollectionListItem = z.infer<typeof CollectionListItemSchema>;
951
1046
  export type CollectionListResponse = z.infer<typeof CollectionListResponseSchema>;
952
1047
  export type CollectionDetail = z.infer<typeof CollectionDetailSchema>;
@@ -1014,6 +1109,12 @@ export interface LatestRelease {
1014
1109
  */
1015
1110
  contentChars?: number | null;
1016
1111
  contentTokens?: number | null;
1112
+ /**
1113
+ * Owning product, when the release's source is grouped under a product.
1114
+ * `null` / `undefined` when the source has no `product_id`. Additive — older
1115
+ * API responses omit this field; treat `undefined` as `null`. #1217.
1116
+ */
1117
+ product?: { slug: string; name: string } | null;
1017
1118
  }
1018
1119
 
1019
1120
  // ── Stats ──
@@ -1034,6 +1135,14 @@ export type StatsResponse = z.infer<typeof StatsResponseSchema>;
1034
1135
  */
1035
1136
  export type StatsSummary = Omit<StatsResponse, "orgs" | "sources" | "releases" | "products">;
1036
1137
 
1138
+ // ── Feedback ──
1139
+ export type FeedbackType = z.infer<typeof FeedbackTypeSchema>;
1140
+ export type FeedbackStatus = z.infer<typeof FeedbackStatusSchema>;
1141
+ export type FeedbackItem = z.infer<typeof FeedbackItemSchema>;
1142
+ export type FeedbackListResponse = z.infer<typeof FeedbackListResponseSchema>;
1143
+ export type FeedbackUpdateBody = z.infer<typeof FeedbackUpdateBodySchema>;
1144
+ export type FeedbackDeleteResponse = z.infer<typeof FeedbackDeleteResponseSchema>;
1145
+
1037
1146
  // ── Fetch log ──
1038
1147
 
1039
1148
  export interface FetchLogEntry {
@@ -1236,3 +1345,20 @@ export interface EvaluationResult {
1236
1345
  provider?: string;
1237
1346
  notes?: string;
1238
1347
  }
1348
+
1349
+ /**
1350
+ * Identity returned by `GET /v1/tokens/me` — the caller introspecting its own
1351
+ * credential. `kind: "root"` is the static break-glass key (synthetic identity,
1352
+ * no DB row); `kind: "token"` is a DB-backed `relk_` token.
1353
+ */
1354
+ export interface TokenIdentity {
1355
+ kind: "root" | "token";
1356
+ /** Display label; "root" for the static key, "local-dev" when no secret is bound. */
1357
+ name: string;
1358
+ /** e.g. ["read","write"] or ["*"] for root. */
1359
+ scopes: string[];
1360
+ principalType: "internal" | "agent" | "user";
1361
+ principalId?: string | null;
1362
+ expiresAt?: string | null;
1363
+ lastUsedAt?: string | null;
1364
+ }
@@ -17,30 +17,81 @@ export const CollectionMemberOrgSchema = z.object({
17
17
  });
18
18
 
19
19
  /**
20
- * Row on `GET /v1/collections`. `previewMembers` is the first few visible
21
- * members (capped at 3) so the list page can render inline avatars without a
22
- * second round trip.
20
+ * Org context attached to a product member so the chip can render the parent
21
+ * org's avatar / handle. Subset of `CollectionMemberOrg` — description is
22
+ * intentionally omitted because the byline comes from the product itself.
23
+ */
24
+ export const ProductParentOrgSchema = z.object({
25
+ slug: z.string(),
26
+ name: z.string(),
27
+ domain: z.string().nullable(),
28
+ avatarUrl: z.string().nullable(),
29
+ githubHandle: z.string().nullable(),
30
+ });
31
+
32
+ /**
33
+ * One product as it appears inside a collection. Carries the parent org block
34
+ * so the chip can render the org's avatar (products don't have their own).
35
+ */
36
+ export const CollectionMemberProductSchema = z.object({
37
+ slug: z.string(),
38
+ name: z.string(),
39
+ description: z.string().nullable(),
40
+ org: ProductParentOrgSchema,
41
+ });
42
+
43
+ /**
44
+ * Mixed-kind member entry. `kind` discriminates: `org` carries the org block at
45
+ * the top level; `product` carries the product fields plus a `org` parent block.
46
+ * Used by `GET /v1/collections/:slug` (`members` field) and the new
47
+ * `previewMembers` on `GET /v1/collections`.
48
+ */
49
+ export const CollectionMemberSchema = z.discriminatedUnion("kind", [
50
+ CollectionMemberOrgSchema.extend({ kind: z.literal("org") }),
51
+ CollectionMemberProductSchema.extend({ kind: z.literal("product") }),
52
+ ]);
53
+
54
+ /**
55
+ * Row on `GET /v1/collections`. `previewMembers` (the new mixed-kind preview)
56
+ * is the first few visible members so the list page can render inline avatars
57
+ * without a second round trip. `previewOrgs` is the legacy org-only preview,
58
+ * retained for back-compat — clients should migrate to `previewMembers`.
23
59
  */
24
60
  export const CollectionListItemSchema = z.object({
25
61
  slug: z.string(),
26
62
  name: z.string(),
27
63
  description: z.string().nullable(),
28
64
  memberCount: z.number().int().min(0),
29
- previewMembers: z.array(CollectionMemberOrgSchema).optional(),
65
+ /** Whether the collection is promoted on the homepage. Filterable via `?featured=1`. */
66
+ isFeatured: z.boolean(),
67
+ /** Mixed-kind preview (org + product). Cap matches `previewOrgs`. */
68
+ previewMembers: z.array(CollectionMemberSchema).optional(),
69
+ /** Legacy org-only preview. Populated with the org-kind subset of `previewMembers`. */
70
+ previewOrgs: z.array(CollectionMemberOrgSchema).optional(),
30
71
  });
31
72
 
32
73
  export const CollectionListResponseSchema = z.array(CollectionListItemSchema);
33
74
 
34
- /** Returned by `GET /v1/collections/:slug`. */
75
+ /**
76
+ * Returned by `GET /v1/collections/:slug`.
77
+ *
78
+ * `members` is the mixed-kind canonical list. `orgs` is the legacy org-only
79
+ * field, populated with the org-kind subset of `members` for back-compat —
80
+ * new clients should consume `members`.
81
+ */
35
82
  export const CollectionDetailSchema = z.object({
36
83
  slug: z.string(),
37
84
  name: z.string(),
38
85
  description: z.string().nullable(),
86
+ /** Whether the collection is promoted on the homepage. */
87
+ isFeatured: z.boolean(),
88
+ members: z.array(CollectionMemberSchema),
89
+ /** @deprecated Use `members`. Org-only subset, kept for back-compat. */
39
90
  orgs: z.array(CollectionMemberOrgSchema),
40
91
  });
41
92
 
42
93
  /**
43
- * Cross-org release feed row on `GET /v1/collections/:slug/releases`. Same
94
+ * Cross-member release feed row on `GET /v1/collections/:slug/releases`. Same
44
95
  * shape as `OrgReleaseItem` plus the origin `org` block (and optional
45
96
  * `product`) so the web's release card can render it identically. `source.type`
46
97
  * stays as `z.string()` because the SQL passes the column through unvalidated —
@@ -79,6 +130,7 @@ export const CollectionRowSchema = z.object({
79
130
  slug: z.string(),
80
131
  name: z.string(),
81
132
  description: z.string().nullable(),
133
+ isFeatured: z.boolean(),
82
134
  createdAt: z.string(),
83
135
  updatedAt: z.string(),
84
136
  });
@@ -99,15 +151,26 @@ export const UpdateCollectionRequestSchema = z.object({
99
151
  slug: z.string().optional(),
100
152
  name: z.string().min(1).max(200).optional(),
101
153
  description: z.string().max(2000).nullable().optional(),
154
+ /** Promote / demote the collection on the homepage. */
155
+ isFeatured: z.boolean().optional(),
102
156
  });
103
157
 
104
158
  /**
105
- * One member entry accepted by the member-write endpoints. Either `orgId`
106
- * (`org_…`) or `orgSlug` is required; when both are given, `orgId` wins.
159
+ * One member entry accepted by the member-write endpoints.
160
+ *
161
+ * Exactly one of `orgId` / `orgSlug` / `productId` / `productSlug` must be
162
+ * present. When both an id and a slug are supplied for the same kind, the id
163
+ * wins. Mixing kinds (e.g. `orgId` + `productSlug`) is rejected by the handler.
164
+ *
165
+ * Product slug is resolved within an org — supply either `productId`
166
+ * (`prod_…`, globally unique) or pair `productSlug` with `orgSlug`/`orgId`.
167
+ * `productSlug` alone is rejected.
107
168
  */
108
169
  export const CollectionMemberInputSchema = z.object({
109
170
  orgId: z.string().optional(),
110
171
  orgSlug: z.string().optional(),
172
+ productId: z.string().optional(),
173
+ productSlug: z.string().optional(),
111
174
  /** Authoring position (default 0). For PUT, omit to use the array index. */
112
175
  position: z.number().int().optional(),
113
176
  });
@@ -115,14 +178,25 @@ export const CollectionMemberInputSchema = z.object({
115
178
  /** Body for `POST /v1/collections/:slug/members`. */
116
179
  export const AddCollectionMemberRequestSchema = CollectionMemberInputSchema;
117
180
 
118
- /** Body for `PUT /v1/collections/:slug/members`. Replaces the full membership atomically. */
181
+ /**
182
+ * Body for `PUT /v1/collections/:slug/members`. Replaces the full membership
183
+ * atomically. The field is still named `orgs` for back-compat — it accepts
184
+ * any `CollectionMemberInput`, org or product.
185
+ */
119
186
  export const ReplaceCollectionMembersRequestSchema = z.object({
120
187
  orgs: z.array(CollectionMemberInputSchema),
121
188
  });
122
189
 
123
- /** Resolved member returned by `PUT /v1/collections/:slug/members`. */
190
+ /**
191
+ * Resolved member returned by `PUT /v1/collections/:slug/members` and `POST
192
+ * /v1/collections/:slug/members`. `kind` discriminates which id field is
193
+ * populated. `orgId` is also set on legacy callers that only used org
194
+ * membership — for clean dispatch, dispatch on `kind`.
195
+ */
124
196
  export const ResolvedCollectionMemberSchema = z.object({
125
- orgId: z.string(),
197
+ kind: z.enum(["org", "product"]),
198
+ orgId: z.string().optional(),
199
+ productId: z.string().optional(),
126
200
  position: z.number().int(),
127
201
  });
128
202
 
@@ -132,9 +206,15 @@ export const ReplaceCollectionMembersResponseSchema = z.object({
132
206
  members: z.array(ResolvedCollectionMemberSchema),
133
207
  });
134
208
 
135
- /** Response from `POST /v1/collections/:slug/members`. */
209
+ /**
210
+ * Response from `POST /v1/collections/:slug/members`. `orgId` stays populated
211
+ * for org-kind members so existing CLI callers don't break; `kind` +
212
+ * `productId` are the canonical fields for new code.
213
+ */
136
214
  export const AddCollectionMemberResponseSchema = z.object({
137
215
  collectionSlug: z.string(),
138
- orgId: z.string(),
216
+ kind: z.enum(["org", "product"]),
217
+ orgId: z.string().optional(),
218
+ productId: z.string().optional(),
139
219
  position: z.number().int(),
140
220
  });
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { FEEDBACK_TYPES, FEEDBACK_STATUSES } from "@buildinternet/releases-core/schema";
3
+
4
+ /** Feedback category — mirrors `FEEDBACK_TYPES` in `@buildinternet/releases-core/schema`. */
5
+ export const FeedbackTypeSchema = z.enum(FEEDBACK_TYPES);
6
+
7
+ /** Triage lifecycle — mirrors `FEEDBACK_STATUSES` in `@buildinternet/releases-core/schema`. */
8
+ export const FeedbackStatusSchema = z.enum(FEEDBACK_STATUSES);
9
+
10
+ /**
11
+ * One submitted feedback row, as returned by `GET /v1/admin/feedback` and by
12
+ * the triage write-path (`PATCH /v1/feedback/:id` echoes the updated row).
13
+ * `archived` soft-removes the row from the default admin read view and is
14
+ * orthogonal to triage `status` — a row can be `closed` and visible, or `new`
15
+ * and archived. `createdAt` is epoch milliseconds.
16
+ */
17
+ export const FeedbackItemSchema = z.object({
18
+ id: z.string(),
19
+ createdAt: z.number(),
20
+ message: z.string(),
21
+ contact: z.string().nullable(),
22
+ type: FeedbackTypeSchema,
23
+ status: FeedbackStatusSchema,
24
+ archived: z.boolean(),
25
+ cliVersion: z.string().nullable(),
26
+ clientKind: z.string(),
27
+ anonId: z.string().nullable(),
28
+ os: z.string().nullable(),
29
+ arch: z.string().nullable(),
30
+ runtime: z.string().nullable(),
31
+ surface: z.string(),
32
+ });
33
+
34
+ /**
35
+ * `GET /v1/admin/feedback` — cursor-paginated, newest-first. Archived rows are
36
+ * hidden unless the request passes `?includeArchived=true`.
37
+ */
38
+ export const FeedbackListResponseSchema = z.object({
39
+ items: z.array(FeedbackItemSchema),
40
+ nextCursor: z.string().nullable(),
41
+ });
42
+
43
+ /**
44
+ * `PATCH /v1/feedback/:id` body — partial triage update. At least one field
45
+ * must be present; the response echoes the updated `FeedbackItem`.
46
+ */
47
+ export const FeedbackUpdateBodySchema = z.object({
48
+ status: FeedbackStatusSchema.optional(),
49
+ archived: z.boolean().optional(),
50
+ });
51
+
52
+ /** `DELETE /v1/feedback/:id` response — hard delete of a single row. */
53
+ export const FeedbackDeleteResponseSchema = z.object({
54
+ deleted: z.literal(true),
55
+ id: z.string(),
56
+ });
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
3
3
  import {
4
+ AppStoreSourceInfoSchema,
4
5
  CategorySchema,
5
6
  ListResponseSchema,
6
7
  OverviewPageItemSchema,
@@ -103,6 +104,10 @@ export const UpdateOrgBodySchema = z.object({
103
104
  aliases: z.array(z.string()).optional(),
104
105
  /** Admin-only: pause/unpause all ingest for this org without touching sources (#1057). */
105
106
  fetchPaused: z.boolean().optional(),
107
+ /** Admin-only: hide the org from the homepage ticker + /v1/orgs directory. Stays reachable via detail, search, sitemap. */
108
+ isHidden: z.boolean().optional(),
109
+ /** Admin-only: opt the org into automatic AI content — org overviews AND per-release summaries (single backend flag `auto_generate_content`). */
110
+ autoGenerateContent: z.boolean().optional(),
106
111
  });
107
112
 
108
113
  // Org detail's products query selects a strict subset of `ProductListItem` —
@@ -115,6 +120,9 @@ const OrgDetailProductSchema = ProductListItemSchema.pick({
115
120
  url: true,
116
121
  description: true,
117
122
  sourceCount: true,
123
+ kind: true,
124
+ }).extend({
125
+ releaseCount: z.number().int().min(0),
118
126
  });
119
127
 
120
128
  const OrgDetailPlaybookSchema = z.object({
@@ -328,7 +336,18 @@ export const OrgSparklinesResponseSchema = z.object({
328
336
  * with a `source` block identifying the originating source within the org.
329
337
  */
330
338
  export const OrgReleaseItemSchema = ReleaseItemSchema.extend({
331
- source: z.object({ slug: z.string(), name: z.string(), type: z.string() }),
339
+ source: z.object({
340
+ slug: z.string(),
341
+ name: z.string(),
342
+ type: z.string(),
343
+ appStore: AppStoreSourceInfoSchema.optional(),
344
+ }),
345
+ /**
346
+ * Owning product, when the release's source is grouped under a product.
347
+ * `null` when the source has no `product_id`. Additive — older API responses
348
+ * omit this field; treat `undefined` as `null`. #1217.
349
+ */
350
+ product: z.object({ slug: z.string(), name: z.string() }).nullable().optional(),
332
351
  });
333
352
 
334
353
  /** Cursor pagination shape for the org releases feed. */
@@ -394,6 +413,14 @@ export const OrgDetailSchema = z.object({
394
413
  description: z.string().nullable().optional(),
395
414
  category: CategorySchema.nullable().optional(),
396
415
  avatarUrl: z.string().nullable(),
416
+ /** Admin display flag: true when the org is hidden from listings (homepage + /v1/orgs). Optional on the wire for older workers mid-deploy. */
417
+ isHidden: z.boolean().optional(),
418
+ /** Admin display flag: org is opted into automatic overviews + per-release summaries. Optional on the wire for older workers mid-deploy. */
419
+ autoGenerateContent: z.boolean().optional(),
420
+ /** Admin display flag: all ingest paused for this org. */
421
+ fetchPaused: z.boolean().optional(),
422
+ /** How the org row was created. `on_demand` orgs are excluded from overview generation regardless of `autoGenerateContent`. */
423
+ discovery: z.enum(["curated", "agent", "on_demand"]).optional(),
397
424
  tags: z.array(z.string()).optional(),
398
425
  sourceCount: z.number().int().min(0),
399
426
  releaseCount: z.number().int().min(0),
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
3
- import { CategorySchema, ListResponseSchema } from "./shared.js";
4
- import { SourceTypeSchema } from "./sources.js";
3
+ import { CategorySchema, ListResponseSchema, ReleaseItemSchema } from "./shared.js";
4
+ import { SourceTypeSchema, SourceFeedPaginationSchema } from "./sources.js";
5
5
 
6
6
  /**
7
7
  * Raw `products` table row, returned by `POST /v1/products` and
@@ -21,11 +21,19 @@ export const ProductRowSchema = z.object({
21
21
  description: z.string().nullable(),
22
22
  category: CategorySchema.nullable(),
23
23
  kind: z.enum(KIND_VALUES).nullable().optional(),
24
+ avatarUrl: z.string().nullable().optional(),
24
25
  createdAt: z.string(),
25
26
  embeddedAt: z.string().nullable(),
26
27
  deletedAt: z.string().nullable(),
27
28
  });
28
29
 
30
+ /** 201 body of POST /v1/products — ProductRow plus an optional non-blocking
31
+ * warning emitted when the new slug shadows an existing same-org source (#1190). */
32
+ export const ProductCreateResponseSchema = ProductRowSchema.extend({
33
+ warning: z.string().optional(),
34
+ });
35
+ export type ProductCreateResponse = z.infer<typeof ProductCreateResponseSchema>;
36
+
29
37
  /**
30
38
  * Per-product row returned by `GET /v1/products`. Adds `sourceCount` to
31
39
  * the row shape but omits the internal `embeddedAt` / `deletedAt` columns
@@ -57,16 +65,27 @@ export const ProductDetailSourceSchema = z.object({
57
65
  name: z.string(),
58
66
  type: SourceTypeSchema,
59
67
  url: z.string(),
68
+ metadata: z.string().nullable().optional(),
69
+ kind: z.enum(KIND_VALUES).nullable().optional(),
60
70
  });
61
71
 
62
72
  /**
63
73
  * Returned by `GET /v1/products/:identifier` (and the org-scoped twin).
64
74
  * Spreads the raw product row and adds `sources`, `tags`, `aliases`.
75
+ *
76
+ * `releases` and `pagination` are populated only by the web format adapter
77
+ * (the `.json` / `.md` product route — see PR #1207), NOT by the bare product
78
+ * detail endpoint. They are optional so bare endpoint consumers see a valid
79
+ * `ProductDetail` without the embedded feed.
65
80
  */
66
81
  export const ProductDetailSchema = ProductRowSchema.extend({
67
82
  sources: z.array(ProductDetailSourceSchema),
68
83
  tags: z.array(z.string()),
69
84
  aliases: z.array(z.string()),
85
+ /** Embedded release feed — set by the web format adapter (PR #1207), absent on bare product detail. */
86
+ releases: z.array(ReleaseItemSchema).optional(),
87
+ /** Cursor pagination for the embedded release feed — set by the web format adapter (PR #1207), absent on bare product detail. */
88
+ pagination: SourceFeedPaginationSchema.optional(),
70
89
  });
71
90
 
72
91
  /**
@@ -102,6 +121,7 @@ export const UpdateProductBodySchema = z.object({
102
121
  tags: z.array(z.string()).optional(),
103
122
  aliases: z.array(z.string()).optional(),
104
123
  kind: z.enum(KIND_VALUES).nullable().optional(),
124
+ avatarUrl: z.string().nullable().optional(),
105
125
  });
106
126
 
107
127
  /**
@@ -177,3 +197,49 @@ export const ProductTagsBodySchema = z.object({
177
197
  export const ProductTagsMutationResponseSchema = z.object({
178
198
  ok: z.literal(true),
179
199
  });
200
+
201
+ // ── Product activity (/products/:slug/activity, /orgs/:orgSlug/products/:productSlug/activity) ──
202
+
203
+ const ProductActivityWeeklyBucketSchema = z.object({
204
+ weekStart: z.string(),
205
+ count: z.number().int().min(0),
206
+ earliestVersion: z.string().nullable(),
207
+ latestVersion: z.string().nullable(),
208
+ });
209
+
210
+ const ProductActivitySourceSchema = z.object({
211
+ slug: z.string(),
212
+ name: z.string(),
213
+ releaseCount: z.number().int().min(0),
214
+ avgReleasesPerWeek: z.number(),
215
+ earliestVersion: z.string().nullable(),
216
+ latestVersion: z.string().nullable(),
217
+ latestDate: z.string().nullable(),
218
+ weeklyBuckets: z.array(ProductActivityWeeklyBucketSchema),
219
+ });
220
+
221
+ /**
222
+ * Response shape for `GET /v1/products/:slug/activity` and the org-scoped twin.
223
+ * Returns per-source weekly release buckets plus an aggregate rollup — used for
224
+ * timeline visualization on the product detail page.
225
+ */
226
+ export const ProductActivityResponseSchema = z.object({
227
+ product: z.object({ slug: z.string(), name: z.string() }),
228
+ range: z.object({ from: z.string(), to: z.string() }),
229
+ sources: z.array(ProductActivitySourceSchema),
230
+ aggregateWeekly: z.array(z.object({ weekStart: z.string(), count: z.number().int().min(0) })),
231
+ });
232
+
233
+ // ── Product heatmap (/products/:slug/heatmap, /orgs/:orgSlug/products/:productSlug/heatmap) ──
234
+
235
+ /**
236
+ * Response shape for `GET /v1/products/:slug/heatmap` and the org-scoped twin.
237
+ * Returns daily release counts for the trailing 365 days — used for
238
+ * contribution-graph visualization on the product detail page.
239
+ */
240
+ export const ProductHeatmapResponseSchema = z.object({
241
+ product: z.object({ slug: z.string(), name: z.string() }),
242
+ range: z.object({ from: z.string(), to: z.string() }),
243
+ dailyCounts: z.array(z.object({ date: z.string(), count: z.number().int().min(0) })),
244
+ total: z.number().int().min(0),
245
+ });
@@ -17,8 +17,12 @@ export const RelatedReleaseSourceSchema = z.object({
17
17
  id: z.string(),
18
18
  slug: z.string(),
19
19
  name: z.string(),
20
+ /** Parent product name, when the source belongs to one — preferred over the
21
+ * bare source/feed name for display. Null for sources with no product. */
22
+ productName: z.string().nullable(),
20
23
  orgSlug: z.string().nullable(),
21
24
  orgName: z.string().nullable(),
25
+ orgAvatarUrl: z.string().nullable(),
22
26
  });
23
27
 
24
28
  /**
@@ -1,6 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { SOURCE_TYPES } from "@buildinternet/releases-core/source-enums";
3
- import { MediaItemSchema, ReleaseCompositionSchema, ReleaseTypeSchema } from "./shared.js";
3
+ import {
4
+ AppStoreSourceInfoSchema,
5
+ MediaItemSchema,
6
+ ReleaseCompositionSchema,
7
+ ReleaseTypeSchema,
8
+ } from "./shared.js";
4
9
 
5
10
  // ReleaseCompositionSchema lives in ./shared.js (single source of truth) — both
6
11
  // this file and ReleaseItemSchema reference it from there.
@@ -22,6 +27,11 @@ export const ReleaseLatestSourceSchema = z.object({
22
27
  orgSlug: z.string().nullable(),
23
28
  });
24
29
 
30
+ export const ReleaseLatestProductSchema = z.object({
31
+ slug: z.string(),
32
+ name: z.string(),
33
+ });
34
+
25
35
  export const ReleaseLatestItemSchema = z.object({
26
36
  id: z.string(),
27
37
  version: z.string().nullable(),
@@ -34,6 +44,12 @@ export const ReleaseLatestItemSchema = z.object({
34
44
  url: z.string().nullable(),
35
45
  media: z.array(MediaItemSchema),
36
46
  source: ReleaseLatestSourceSchema,
47
+ /**
48
+ * Owning product, when the release's source is grouped under a product.
49
+ * `null` when the source has no `product_id`. Additive — older API responses
50
+ * omit this field; treat `undefined` as `null`. #1217.
51
+ */
52
+ product: ReleaseLatestProductSchema.nullable().optional(),
37
53
  coverageCount: z.number().int().min(0).optional(),
38
54
  // Cached release-body size hint — see {@link ReleaseItemSchema} for the
39
55
  // same fields on the org / collection feeds. #958.
@@ -160,6 +176,7 @@ export const ReleaseDetailResponseSchema = z.object({
160
176
  sourceType: z.enum(SOURCE_TYPES).nullable(),
161
177
  org: ReleaseDetailOrgSchema.nullable(),
162
178
  composition: ReleaseCompositionSchema.nullable(),
179
+ appStore: AppStoreSourceInfoSchema.nullable().optional(),
163
180
  });
164
181
 
165
182
  /**
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { ProductDetailSchema } from "./products.js";
3
+ import { SourceDetailSchema } from "./sources.js";
4
+
5
+ /**
6
+ * Response of `GET /v1/orgs/:org/resolve/:slug` (#1190). Product-first: when a
7
+ * product and a source share a slug in an org, the product variant is returned.
8
+ * A 404 (ErrorResponse) is returned when neither matches.
9
+ */
10
+ export const ResolveResponseSchema = z.discriminatedUnion("kind", [
11
+ z.object({ kind: z.literal("product"), product: ProductDetailSchema }),
12
+ z.object({ kind: z.literal("source"), source: SourceDetailSchema }),
13
+ ]);
14
+
15
+ export type ResolveResponse = z.infer<typeof ResolveResponseSchema>;
@@ -68,6 +68,9 @@ export const SearchReleaseHitSchema = z.object({
68
68
  sourceType: z.string().optional(),
69
69
  orgSlug: z.string().nullable(),
70
70
  orgName: z.string().nullable().optional(),
71
+ /** Owning product slug — present when the source belongs to a product. Lets
72
+ * the web byline link to the product page instead of the source. */
73
+ productSlug: z.string().nullable().optional(),
71
74
  version: z.string().nullable(),
72
75
  title: z.string(),
73
76
  summary: z.string(),
@@ -113,8 +116,9 @@ export const SearchCollectionHitSchema = z.object({
113
116
  /**
114
117
  * Heading-aware CHANGELOG.md slice returned by hybrid / semantic search.
115
118
  * Clients can deep-link to
116
- * `/source/<sourceSlug>?tab=changelog&offset=<offset>` to read the
117
- * surrounding file content.
119
+ * `/<orgSlug>/<sourceSlug>/changelog?offset=<offset>#chunk` to read the
120
+ * surrounding file content (the web `chunkDeepLink` builder; falls back to
121
+ * the `/source/<sourceSlug>` redirect shim when `orgSlug` is null).
118
122
  */
119
123
  export const SearchChunkHitSchema = z.object({
120
124
  sourceSlug: z.string(),
@@ -11,6 +11,17 @@ export const MediaItemSchema = z.object({
11
11
  r2Url: z.string().optional(),
12
12
  });
13
13
 
14
+ /**
15
+ * App Store platform + icon for a `type: "appstore"` source. Threaded onto
16
+ * read surfaces so the UI can render the compact app-update row (app icon +
17
+ * "Available for iOS/macOS") instead of the standard version/notes/thumbnail
18
+ * layout. Sourced from `source.metadata.appStore` server-side. #mobile-appstore-feed-row
19
+ */
20
+ export const AppStoreSourceInfoSchema = z.object({
21
+ platform: z.enum(["ios", "macos"]),
22
+ iconUrl: z.string().nullable(),
23
+ });
24
+
14
25
  export const PaginationSchema = z.object({
15
26
  page: z.number().int().min(1),
16
27
  pageSize: z.number().int().min(1),
@@ -6,6 +6,12 @@ import { z } from "zod";
6
6
  * fields, but the current handler always populates them (#875).
7
7
  */
8
8
  export const SitemapSourceSchema = z.object({
9
+ /**
10
+ * Source id. `.optional()` for backwards compatibility — added #1190 so the
11
+ * web can route shadowed sources to `/sources/:id`; older/cached responses
12
+ * may omit it and the web degrades to the bare URL.
13
+ */
14
+ id: z.string().optional(),
9
15
  orgSlug: z.string(),
10
16
  slug: z.string(),
11
17
  latestDate: z.string().nullable(),
@@ -459,12 +459,25 @@ export const SourceMetadataResponseSchema = z.object({
459
459
 
460
460
  // ── Changelog probe (POST /sources/:slug/changelog/probe) ──
461
461
 
462
+ /**
463
+ * A discovered CHANGELOG path returned by the probe. Mirrors
464
+ * `DiscoveredChangelogPath` from `@releases/adapters/github-discovery`:
465
+ * `origin` tags where the path came from (repo root, a workspace package, or
466
+ * an explicit `metadata.changelogPaths` override) and `exists` is true when a
467
+ * directory listing confirmed the file on HEAD.
468
+ */
469
+ export const DiscoveredChangelogPathSchema = z.object({
470
+ path: z.string(),
471
+ origin: z.enum(["root", "workspace", "override"]),
472
+ exists: z.boolean(),
473
+ });
474
+
462
475
  /** Response for `POST /v1/sources/:slug/changelog/probe`. */
463
476
  export const ChangelogProbeResponseSchema = z.object({
464
477
  sourceId: z.string(),
465
478
  sourceSlug: z.string(),
466
479
  url: z.string(),
467
- paths: z.array(z.string()),
480
+ paths: z.array(DiscoveredChangelogPathSchema),
468
481
  });
469
482
 
470
483
  // ── Delete source (DELETE /sources/:slug) ──