@buildinternet/releases-api-types 0.33.0 → 0.34.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/releases-api-types",
3
- "version": "0.33.0",
3
+ "version": "0.34.1",
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",
@@ -17,7 +17,7 @@
17
17
  "README.md"
18
18
  ],
19
19
  "dependencies": {
20
- "@buildinternet/releases-core": "^0.23.0",
20
+ "@buildinternet/releases-core": "^0.24.0",
21
21
  "zod": "^4.4.3"
22
22
  },
23
23
  "publishConfig": {
package/src/api-types.ts CHANGED
@@ -13,6 +13,7 @@ export type {
13
13
  // Re-export above doesn't create local bindings; import for use in interfaces below.
14
14
  import type { SourceType, SourceFetchPriority } from "@buildinternet/releases-core/source-enums";
15
15
  import type { ApiScope } from "@buildinternet/releases-core/api-token";
16
+ import type { BreakingLevel } from "@buildinternet/releases-core/breaking";
16
17
  import type {
17
18
  MediaItemSchema,
18
19
  PaginationSchema,
@@ -59,6 +60,12 @@ import type {
59
60
  OrgRecentReleasesResponseSchema,
60
61
  DeleteOrgAccountResponseSchema,
61
62
  } from "./schemas/orgs.js";
63
+ import type {
64
+ UploadAvatarResponseSchema,
65
+ WorkspaceProfileFieldsSchema,
66
+ PatchWorkspaceProfileBodySchema,
67
+ WorkspaceProfileResponseSchema,
68
+ } from "./schemas/account-profile.js";
62
69
  import type {
63
70
  SourceListItemSchema,
64
71
  SourceWithOrgSchema,
@@ -138,9 +145,13 @@ import type {
138
145
  UpdateReleaseBodySchema,
139
146
  ReleasePatchResponseSchema,
140
147
  ReleaseDeleteResponseSchema,
148
+ ReleaseBatchDeleteBodySchema,
149
+ ReleaseBatchDeleteResponseSchema,
141
150
  ReleaseSuppressResponseSchema,
142
151
  ReleaseUnsuppressResponseSchema,
143
152
  ReleaseSuppressBodySchema,
153
+ ReleaseBatchSuppressBodySchema,
154
+ ReleaseBatchSuppressResponseSchema,
144
155
  ReleaseStreamMessageSchema,
145
156
  } from "./schemas/releases.js";
146
157
  import type {
@@ -225,6 +236,8 @@ import type {
225
236
  ResolvedCollectionMemberSchema,
226
237
  ReplaceCollectionMembersResponseSchema,
227
238
  AddCollectionMemberResponseSchema,
239
+ CollectionDailySummarySchema,
240
+ CollectionDailySummariesResponseSchema,
228
241
  } from "./schemas/collections.js";
229
242
  import type {
230
243
  OrgOverviewResponseSchema,
@@ -255,6 +268,7 @@ export {
255
268
  CategorySchema,
256
269
  NoticeSchema,
257
270
  } from "./schemas/shared.js";
271
+ export { SiteNoticeSchema, SiteNoticeResponseSchema } from "./schemas/site-notice.js";
258
272
  export {
259
273
  OrgListItemSchema,
260
274
  OrgListResponseSchema,
@@ -289,6 +303,12 @@ export {
289
303
  OrgRecentReleasesResponseSchema,
290
304
  DeleteOrgAccountResponseSchema,
291
305
  } from "./schemas/orgs.js";
306
+ export {
307
+ UploadAvatarResponseSchema,
308
+ WorkspaceProfileFieldsSchema,
309
+ PatchWorkspaceProfileBodySchema,
310
+ WorkspaceProfileResponseSchema,
311
+ } from "./schemas/account-profile.js";
292
312
  export {
293
313
  ReleasesJsonConfigSchema,
294
314
  ReleasesJsonProductSchema,
@@ -400,9 +420,13 @@ export {
400
420
  UpdateReleaseBodySchema,
401
421
  ReleasePatchResponseSchema,
402
422
  ReleaseDeleteResponseSchema,
423
+ ReleaseBatchDeleteBodySchema,
424
+ ReleaseBatchDeleteResponseSchema,
403
425
  ReleaseSuppressResponseSchema,
404
426
  ReleaseUnsuppressResponseSchema,
405
427
  ReleaseSuppressBodySchema,
428
+ ReleaseBatchSuppressBodySchema,
429
+ ReleaseBatchSuppressResponseSchema,
406
430
  ReleaseStreamMessageSchema,
407
431
  } from "./schemas/releases.js";
408
432
  export {
@@ -462,6 +486,8 @@ export {
462
486
  ResolvedCollectionMemberSchema,
463
487
  ReplaceCollectionMembersResponseSchema,
464
488
  AddCollectionMemberResponseSchema,
489
+ CollectionDailySummarySchema,
490
+ CollectionDailySummariesResponseSchema,
465
491
  } from "./schemas/collections.js";
466
492
  export {
467
493
  OrgOverviewResponseSchema,
@@ -560,6 +586,10 @@ export type CreateOrgBody = z.infer<typeof CreateOrgBodySchema>;
560
586
  export type UpdateOrgBody = z.infer<typeof UpdateOrgBodySchema>;
561
587
  export type SetOrgAvatarBody = z.infer<typeof SetOrgAvatarBodySchema>;
562
588
  export type SetOrgAvatarResponse = z.infer<typeof SetOrgAvatarResponseSchema>;
589
+ export type UploadAvatarResponse = z.infer<typeof UploadAvatarResponseSchema>;
590
+ export type WorkspaceProfileFields = z.infer<typeof WorkspaceProfileFieldsSchema>;
591
+ export type PatchWorkspaceProfileBody = z.infer<typeof PatchWorkspaceProfileBodySchema>;
592
+ export type WorkspaceProfileResponse = z.infer<typeof WorkspaceProfileResponseSchema>;
563
593
  export type OrgDetail = z.infer<typeof OrgDetailSchema>;
564
594
 
565
595
  export type OrgCatalogItem = z.infer<typeof OrgCatalogItemSchema>;
@@ -724,6 +754,44 @@ export type ReleaseType = z.infer<typeof ReleaseTypeSchema>;
724
754
  export type ReleaseItem = z.infer<typeof ReleaseItemSchema>;
725
755
  export type ReleaseComposition = z.infer<typeof ReleaseCompositionSchema>;
726
756
 
757
+ /**
758
+ * One entry in a {@link WhatsChangedResponse} — a release in the requested
759
+ * upgrade range; a slim, release-derived projection. `breaking`/`migrationNotes`
760
+ * come from #1696 (`"unknown"` until classified or for non-dev-facing kinds).
761
+ */
762
+ export interface WhatsChangedEntry {
763
+ version: string | null;
764
+ publishedAt: string | null;
765
+ /** AI-generated title when present, else the raw release title. */
766
+ title: string | null;
767
+ summary: string | null;
768
+ breaking: BreakingLevel;
769
+ migrationNotes: string | null;
770
+ url: string | null;
771
+ }
772
+
773
+ /**
774
+ * Response of `GET /v1/whats-changed` (#1697, upgrade intelligence Phase 1) —
775
+ * the changelog entries in the half-open version range `(from, to]` for a
776
+ * package (`from` exclusive, `to` inclusive). `status: "unknown"` (still
777
+ * HTTP 200 — a valid answer, not an error) when the package can't be resolved
778
+ * read-only from the catalog (e.g. an npm/PyPI name absent until #1345).
779
+ * `entries` are ordered oldest→newest; a range wider than the token budget is
780
+ * truncated (newest entries kept), flagged by `truncated` + `truncatedAtTokens`.
781
+ */
782
+ export interface WhatsChangedResponse {
783
+ status: "resolved" | "unknown";
784
+ package: string;
785
+ ecosystem: "npm" | "pypi" | "github" | null;
786
+ from: string;
787
+ to: string;
788
+ source: { sourceId: string; sourceSlug: string; orgSlug: string } | null;
789
+ entries: WhatsChangedEntry[];
790
+ count: number;
791
+ truncated: boolean;
792
+ truncatedAtTokens?: number;
793
+ }
794
+
727
795
  export interface ReleaseDetail {
728
796
  id: string;
729
797
  sourceId: string;
@@ -741,6 +809,16 @@ export interface ReleaseDetail {
741
809
  titleGenerated?: string | null;
742
810
  /** AI-generated smart-brevity headline (#852, renamed in #860). Same fallback as `titleGenerated`. */
743
811
  titleShort?: string | null;
812
+ /**
813
+ * Machine-readable breaking-change level (#1696). `.optional()` for mid-deploy
814
+ * / pinned-worker tolerance; absent or `"unknown"` means not classified (the
815
+ * fail-open default). Populated live at ingest for developer-facing source
816
+ * kinds; history is `"unknown"` pending a backfill. Route population + the web
817
+ * chip are #1696 follow-ups.
818
+ */
819
+ breaking?: BreakingLevel;
820
+ /** Explicit upgrade/migration steps lifted from the body (#1696); null when the body gives none. */
821
+ migrationNotes?: string | null;
744
822
  url: string | null;
745
823
  media: MediaItem[];
746
824
  publishedAt: string | null;
@@ -751,8 +829,17 @@ export interface ReleaseDetail {
751
829
  /**
752
830
  * Owning org. `avatarUrl` is the resolved `organizations.avatar_url`
753
831
  * (nullable; `.optional()` for mid-deploy / pinned-worker tolerance).
832
+ * `discovery` and `isHidden` are additive — older servers omit them.
754
833
  */
755
- org: { slug: string; name: string; avatarUrl?: string | null } | null;
834
+ org: {
835
+ slug: string;
836
+ name: string;
837
+ avatarUrl?: string | null;
838
+ /** How the org was created. Additive — older servers omit it. */
839
+ discovery?: "curated" | "agent" | "on_demand";
840
+ /** Whether the org is hidden from public listings. Additive — older servers omit it. */
841
+ isHidden?: boolean;
842
+ } | null;
756
843
  /**
757
844
  * Owning product, when the source is grouped under one (`sources.product_id`).
758
845
  * `null` when ungrouped; `.optional()` so older servers that omit it still type.
@@ -766,6 +853,11 @@ export interface ReleaseDetail {
766
853
  appStore?: { platform: "ios" | "macos"; iconUrl: string | null } | null;
767
854
  /** Video provider tag, present only when `sourceType === "video"`. */
768
855
  video?: { provider: "youtube" | "vimeo" | "wistia" } | null;
856
+ /**
857
+ * Whether the release's source is hidden from public listings. Additive —
858
+ * older servers omit it; treat `undefined` as `false`.
859
+ */
860
+ sourceIsHidden?: boolean;
769
861
  }
770
862
 
771
863
  export type ReleaseCoverageSibling = z.infer<typeof ReleaseCoverageSiblingSchema>;
@@ -788,9 +880,13 @@ export type ReleaseDetailResponse = z.infer<typeof ReleaseDetailResponseSchema>;
788
880
  export type UpdateReleaseBody = z.infer<typeof UpdateReleaseBodySchema>;
789
881
  export type ReleasePatchResponse = z.infer<typeof ReleasePatchResponseSchema>;
790
882
  export type ReleaseDeleteResponse = z.infer<typeof ReleaseDeleteResponseSchema>;
883
+ export type ReleaseBatchDeleteBody = z.infer<typeof ReleaseBatchDeleteBodySchema>;
884
+ export type ReleaseBatchDeleteResponse = z.infer<typeof ReleaseBatchDeleteResponseSchema>;
791
885
  export type ReleaseSuppressResponse = z.infer<typeof ReleaseSuppressResponseSchema>;
792
886
  export type ReleaseUnsuppressResponse = z.infer<typeof ReleaseUnsuppressResponseSchema>;
793
887
  export type ReleaseSuppressBody = z.infer<typeof ReleaseSuppressBodySchema>;
888
+ export type ReleaseBatchSuppressBody = z.infer<typeof ReleaseBatchSuppressBodySchema>;
889
+ export type ReleaseBatchSuppressResponse = z.infer<typeof ReleaseBatchSuppressResponseSchema>;
794
890
  export type ReleaseStreamMessage = z.infer<typeof ReleaseStreamMessageSchema>;
795
891
 
796
892
  export type ReleaseSummaryItem = z.infer<typeof ReleaseSummaryItemSchema>;
@@ -849,11 +945,117 @@ export interface FeedTokenResponse {
849
945
  token: FeedToken | null;
850
946
  }
851
947
 
948
+ /** Cursor pagination shape for GET /v1/me/feed (same as other release feeds). */
949
+ export type PersonalizedFeedPagination = OrgFeedPagination;
950
+
852
951
  /**
853
952
  * 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.
953
+ * shape as /v1/releases/latest (ReleaseLatestItem); cursor-paginated like other
954
+ * feed-shaped surfaces.
855
955
  */
856
- export type PersonalizedFeedResponse = ListResponse<ReleaseLatestItem>;
956
+ export interface PersonalizedFeedResponse {
957
+ items: ReleaseLatestItem[];
958
+ pagination: PersonalizedFeedPagination;
959
+ }
960
+
961
+ // ── User webhooks ──
962
+
963
+ /** High-level delivery posture derived from summary columns (no AE query). */
964
+ export type WebhookDeliveryHealth =
965
+ | "never_delivered"
966
+ | "healthy"
967
+ | "degraded"
968
+ | "failing"
969
+ | "paused"
970
+ | "auto_paused";
971
+
972
+ /** Self-serve webhook filter scope. */
973
+ export type UserWebhookScope = "org" | "follows";
974
+
975
+ /** Optional per-event filter on release taxonomy type. */
976
+ export type UserWebhookReleaseTypeFilter = "feature" | "rollup";
977
+
978
+ /** Webhook delivery output format. */
979
+ export type UserWebhookFormat = "json" | "slack";
980
+
981
+ /** A user-owned webhook subscription row (no signing secret). */
982
+ export interface UserWebhookSubscription {
983
+ id: string;
984
+ userId: string;
985
+ scope: UserWebhookScope;
986
+ orgId: string | null;
987
+ url: string;
988
+ sourceId: string | null;
989
+ productId: string | null;
990
+ releaseType: UserWebhookReleaseTypeFilter | null;
991
+ format: UserWebhookFormat;
992
+ enabled: boolean;
993
+ description: string | null;
994
+ secretVersion: number;
995
+ createdAt: string;
996
+ lastSuccessAt: string | null;
997
+ lastErrorAt: string | null;
998
+ lastErrorMsg: string | null;
999
+ consecutiveFailures: number;
1000
+ disabledReason: string | null;
1001
+ failureStreakStartedAt: string | null;
1002
+ }
1003
+
1004
+ /** Delivery health fields returned on user webhook read paths. */
1005
+ export interface UserWebhookDeliveryHealth {
1006
+ deliveryHealth: WebhookDeliveryHealth;
1007
+ deliveryHealthSummary: string;
1008
+ }
1009
+
1010
+ /** List item returned by GET /v1/me/webhooks — includes org/source display fields. */
1011
+ export interface UserWebhookListItem
1012
+ extends Omit<UserWebhookSubscription, "userId">, UserWebhookDeliveryHealth {
1013
+ orgSlug: string | null;
1014
+ orgName: string | null;
1015
+ sourceSlug: string | null;
1016
+ sourceName: string | null;
1017
+ productSlug: string | null;
1018
+ productName: string | null;
1019
+ }
1020
+
1021
+ /** GET /v1/me/webhooks response. */
1022
+ export interface UserWebhookListResponse {
1023
+ subscriptions: UserWebhookListItem[];
1024
+ }
1025
+
1026
+ /** POST /v1/me/webhooks response — signing key shown once at creation (omitted for slack format). */
1027
+ export interface CreateUserWebhookResponse
1028
+ extends UserWebhookSubscription, UserWebhookDeliveryHealth {
1029
+ orgSlug: string | null;
1030
+ orgName: string | null;
1031
+ signingKey?: string;
1032
+ }
1033
+
1034
+ /** POST /v1/me/webhooks/:id/rotate-secret response. */
1035
+ export interface RotateUserWebhookSecretResponse {
1036
+ secretVersion: number;
1037
+ signingKey: string;
1038
+ }
1039
+
1040
+ /** POST /v1/me/webhooks/:id/test response. */
1041
+ export interface TestUserWebhookResponse {
1042
+ enqueued: true;
1043
+ eventId: string;
1044
+ }
1045
+
1046
+ /** One Analytics Engine delivery-attempt row from GET …/webhooks/:id/deliveries. */
1047
+ export interface WebhookDeliveryRow {
1048
+ timestamp?: string;
1049
+ event_id?: string;
1050
+ error_message?: string | null;
1051
+ error_code?: string | null;
1052
+ outcome?: string;
1053
+ /** Delivery format — "json" (signed raw event) or "slack" (Block Kit). Empty on pre-format rows. */
1054
+ format?: UserWebhookFormat | "";
1055
+ http_status?: number;
1056
+ latency_ms?: number;
1057
+ attempt?: number;
1058
+ }
857
1059
 
858
1060
  // ── Digest emails ──
859
1061
 
@@ -1208,6 +1410,10 @@ export type ReplaceCollectionMembersResponse = z.infer<
1208
1410
  typeof ReplaceCollectionMembersResponseSchema
1209
1411
  >;
1210
1412
  export type AddCollectionMemberResponse = z.infer<typeof AddCollectionMemberResponseSchema>;
1413
+ export type CollectionDailySummary = z.infer<typeof CollectionDailySummarySchema>;
1414
+ export type CollectionDailySummariesResponse = z.infer<
1415
+ typeof CollectionDailySummariesResponseSchema
1416
+ >;
1211
1417
 
1212
1418
  // ── Releases (enriched) ──
1213
1419
 
@@ -1242,6 +1448,25 @@ export interface LatestRelease {
1242
1448
  publishedAt: string | null;
1243
1449
  sourceName: string;
1244
1450
  sourceSlug: string;
1451
+ /**
1452
+ * Source type (`github`/`scrape`/`feed`/`agent`/`appstore`/`video`). Lets the
1453
+ * live feed render the source-type icon for a freshly-arrived event. Additive —
1454
+ * older buffered events / pinned workers omit it.
1455
+ */
1456
+ sourceType?: string;
1457
+ /**
1458
+ * Owning-org context, resolved at publish time so the live feed can render an
1459
+ * avatar + org name the instant an event arrives. Nested to match
1460
+ * {@link ReleaseDetail.org} and the build-event input. Additive/nullable:
1461
+ * absent for orphan sources or older buffered events. Avatar fallback chain:
1462
+ * `avatarUrl` → `github.com/<githubHandle>.png` → an initial.
1463
+ */
1464
+ org?: {
1465
+ slug: string;
1466
+ name: string;
1467
+ avatarUrl: string | null;
1468
+ githubHandle: string | null;
1469
+ } | null;
1245
1470
  /** AI-generated summary (#852, renamed in #860). Nullable — most rows unpopulated. */
1246
1471
  summary: string | null;
1247
1472
  /** AI-generated headline (#852, renamed in #860). See {@link ReleaseDetail.titleGenerated}. */
@@ -1526,6 +1751,21 @@ export interface TokenIdentity {
1526
1751
  scopes: string[];
1527
1752
  principalType: "internal" | "agent" | "user";
1528
1753
  principalId?: string | null;
1754
+ /**
1755
+ * Stable internal principal id for consumption telemetry (#1719). Same value the
1756
+ * auth middleware attaches (`relk_` row id, `relu_${keyId}`, …) — never the raw
1757
+ * secret. Returned on self-introspection so MCP can distinguish `relu_` keys.
1758
+ */
1759
+ tokenId?: string | null;
1760
+ /**
1761
+ * Owning user id for a user-scoped principal (`relu_` key) — the Better Auth
1762
+ * `apikey.referenceId`. Lets a consumer bucket all of an account's credentials
1763
+ * together. Null for the static root key and machine (`relk_`) tokens, which
1764
+ * have no owning user. Distinct from `principalId`, which carries an
1765
+ * agent/internal id for machine tokens. Used by the MCP worker to bucket
1766
+ * `relu_` keys per-account on the rate-limit account tier (#1729).
1767
+ */
1768
+ userId?: string | null;
1529
1769
  expiresAt?: string | null;
1530
1770
  lastUsedAt?: string | null;
1531
1771
  }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+
3
+ /** Response shared by user + workspace avatar upload routes. */
4
+ export const UploadAvatarResponseSchema = z.object({
5
+ avatarUrl: z.string(),
6
+ key: z.string(),
7
+ width: z.number(),
8
+ height: z.number(),
9
+ });
10
+
11
+ export const WorkspaceProfileFieldsSchema = z.object({
12
+ websiteUrl: z.string().nullable(),
13
+ changelogUrl: z.string().nullable(),
14
+ githubHandle: z.string().nullable(),
15
+ });
16
+
17
+ /** Body for `PATCH /v1/workspaces/:workspaceId/profile`. */
18
+ export const PatchWorkspaceProfileBodySchema = z.object({
19
+ websiteUrl: z.string().nullable().optional(),
20
+ changelogUrl: z.string().nullable().optional(),
21
+ githubHandle: z.string().nullable().optional(),
22
+ });
23
+
24
+ export const WorkspaceProfileResponseSchema = z.object({
25
+ workspaceId: z.string(),
26
+ logo: z.string().nullable(),
27
+ profile: WorkspaceProfileFieldsSchema,
28
+ });
@@ -85,6 +85,8 @@ export const CollectionDetailSchema = z.object({
85
85
  description: z.string().nullable(),
86
86
  /** Whether the collection is promoted on the homepage. */
87
87
  isFeatured: z.boolean(),
88
+ /** Per-collection enable toggle for the nightly daily-summary generation. */
89
+ dailySummaryEnabled: z.boolean(),
88
90
  members: z.array(CollectionMemberSchema),
89
91
  /** @deprecated Use `members`. Org-only subset, kept for back-compat. */
90
92
  orgs: z.array(CollectionMemberOrgSchema),
@@ -141,6 +143,8 @@ export const CollectionRowSchema = z.object({
141
143
  name: z.string(),
142
144
  description: z.string().nullable(),
143
145
  isFeatured: z.boolean(),
146
+ /** Per-collection enable toggle for the nightly daily-summary generation. */
147
+ dailySummaryEnabled: z.boolean(),
144
148
  createdAt: z.string(),
145
149
  updatedAt: z.string(),
146
150
  });
@@ -163,6 +167,8 @@ export const UpdateCollectionRequestSchema = z.object({
163
167
  description: z.string().max(2000).nullable().optional(),
164
168
  /** Promote / demote the collection on the homepage. */
165
169
  isFeatured: z.boolean().optional(),
170
+ /** Enable or disable the nightly daily-summary generation for this collection. */
171
+ dailySummaryEnabled: z.boolean().optional(),
166
172
  });
167
173
 
168
174
  /**
@@ -228,3 +234,18 @@ export const AddCollectionMemberResponseSchema = z.object({
228
234
  productId: z.string().optional(),
229
235
  position: z.number().int(),
230
236
  });
237
+
238
+ /** One daily summary row returned by `GET /v1/collections/:slug/daily-summaries`. */
239
+ export const CollectionDailySummarySchema = z.object({
240
+ /** Eastern calendar day, YYYY-MM-DD. */
241
+ date: z.string(),
242
+ title: z.string(),
243
+ summary: z.string(),
244
+ takeaways: z.array(z.string()),
245
+ releaseCount: z.number().int().nonnegative(),
246
+ });
247
+
248
+ /** Response envelope for `GET /v1/collections/:slug/daily-summaries`. */
249
+ export const CollectionDailySummariesResponseSchema = z.object({
250
+ summaries: z.array(CollectionDailySummarySchema),
251
+ });
@@ -29,6 +29,13 @@ export const ReleaseLatestSourceSchema = z.object({
29
29
  /** Owning org's display name (e.g. "Cloudflare"), distinct from the source
30
30
  * name. Null when the source has no org. Additive — older responses omit it. */
31
31
  orgName: z.string().nullable().optional(),
32
+ /** Owning org's explicit avatar URL (`organizations.avatar_url`), when set.
33
+ * Null/absent when the org has no stored avatar — clients fall back to the
34
+ * GitHub handle below, then an initial. Additive — older responses omit it. */
35
+ orgAvatarUrl: z.string().nullable().optional(),
36
+ /** Owning org's GitHub handle, used to derive `github.com/<handle>.png` as the
37
+ * avatar fallback when `orgAvatarUrl` is unset. Additive — older responses omit it. */
38
+ orgGithubHandle: z.string().nullable().optional(),
32
39
  });
33
40
 
34
41
  export const ReleaseLatestProductSchema = z.object({
@@ -178,6 +185,17 @@ export const ReleaseDetailOrgSchema = z.object({
178
185
  * related-rail feed.
179
186
  */
180
187
  avatarUrl: z.string().nullable().optional(),
188
+ /**
189
+ * How the org was created. Additive — older servers omit it; treat
190
+ * `undefined` as `"curated"`. Used by the web to emit `noindex` on
191
+ * on-demand-materialized org pages.
192
+ */
193
+ discovery: z.enum(["curated", "agent", "on_demand"]).optional(),
194
+ /**
195
+ * Whether the org is hidden from public listings. Additive — older servers
196
+ * omit it; treat `undefined` as `false`. Used by the web to emit `noindex`.
197
+ */
198
+ isHidden: z.boolean().optional(),
181
199
  });
182
200
 
183
201
  /**
@@ -219,6 +237,12 @@ export const ReleaseDetailResponseSchema = z.object({
219
237
  composition: ReleaseCompositionSchema.nullable(),
220
238
  appStore: AppStoreSourceInfoSchema.nullable().optional(),
221
239
  video: VideoSourceInfoSchema.nullable().optional(),
240
+ /**
241
+ * Whether the release's source is hidden from public listings. Additive —
242
+ * older servers omit it; treat `undefined` as `false`. Used by the web to
243
+ * emit `noindex` on pages whose source is an on-demand-materialized stub.
244
+ */
245
+ sourceIsHidden: z.boolean().optional(),
222
246
  });
223
247
 
224
248
  /**
@@ -287,6 +311,32 @@ export const ReleaseDeleteResponseSchema = z.object({
287
311
  deleted: z.literal(true),
288
312
  });
289
313
 
314
+ /** Body accepted by `DELETE /v1/releases/batch`. */
315
+ export const ReleaseBatchDeleteBodySchema = z.object({
316
+ releaseIds: z.array(z.string()).min(1),
317
+ });
318
+
319
+ /** Response returned by `DELETE /v1/releases/batch`. */
320
+ export const ReleaseBatchDeleteResponseSchema = z.object({
321
+ deleted: z.number().int().min(0),
322
+ });
323
+
324
+ /**
325
+ * Body accepted by `POST /v1/releases/batch-suppress`. When `suppressed` is
326
+ * true the optional `reason` is stored in `suppressed_reason`; when false the
327
+ * reason is cleared.
328
+ */
329
+ export const ReleaseBatchSuppressBodySchema = z.object({
330
+ releaseIds: z.array(z.string()).min(1),
331
+ suppressed: z.boolean(),
332
+ reason: z.string().optional(),
333
+ });
334
+
335
+ /** Response returned by `POST /v1/releases/batch-suppress`. */
336
+ export const ReleaseBatchSuppressResponseSchema = z.object({
337
+ updated: z.number().int().min(0),
338
+ });
339
+
290
340
  /** Response returned by `POST /v1/releases/:id/suppress`. */
291
341
  export const ReleaseSuppressResponseSchema = z.object({
292
342
  suppressed: z.literal(true),
@@ -8,6 +8,7 @@ import {
8
8
  } from "./shared.js";
9
9
  import { SourceTypeSchema } from "./sources.js";
10
10
  import { LookupStatusSchema } from "./lookups.js";
11
+ import { CollectionMemberSchema } from "./collections.js";
11
12
 
12
13
  /**
13
14
  * Org hit on the unified `/v1/search` response. The list is built either
@@ -130,6 +131,14 @@ export const SearchCollectionHitSchema = z.object({
130
131
  score: z.number().optional(),
131
132
  /** Result-set org slugs that triggered this rollup. Always present on `via=member`. */
132
133
  matchedOrgSlugs: z.array(z.string()).optional(),
134
+ /**
135
+ * A small org preview (capped at 3) for an inline avatar facepile on the
136
+ * search card, mirroring the collections list page. Org-kind only — search's
137
+ * `memberCount` counts orgs, so the facepile and its "+N more" stay
138
+ * consistent. Attached after hit merge; optional on the wire so older workers
139
+ * mid-rollout don't trip the schema.
140
+ */
141
+ previewMembers: z.array(CollectionMemberSchema).optional(),
133
142
  });
134
143
 
135
144
  /**
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { SiteNoticeSchema, SiteNoticeResponseSchema } from "./site-notice";
3
+
4
+ const valid = {
5
+ active: true,
6
+ message: "We shipped a new feed",
7
+ linkText: "See it",
8
+ href: "https://releases.sh/updates",
9
+ placement: "banner" as const,
10
+ color: "#0081e7",
11
+ dismissible: false,
12
+ };
13
+
14
+ describe("SiteNoticeSchema", () => {
15
+ it("accepts a fully-specified notice", () => {
16
+ expect(SiteNoticeSchema.safeParse(valid).success).toBe(true);
17
+ });
18
+ it("accepts a site-relative href and omitted link", () => {
19
+ const { href, linkText, ...rest } = valid;
20
+ expect(SiteNoticeSchema.safeParse({ ...rest, href: "/updates" }).success).toBe(true);
21
+ expect(SiteNoticeSchema.safeParse(rest).success).toBe(true);
22
+ });
23
+ it("rejects an over-length message", () => {
24
+ expect(SiteNoticeSchema.safeParse({ ...valid, message: "x".repeat(281) }).success).toBe(false);
25
+ });
26
+ it("rejects an empty message", () => {
27
+ expect(SiteNoticeSchema.safeParse({ ...valid, message: "" }).success).toBe(false);
28
+ });
29
+ it("rejects a bad placement", () => {
30
+ expect(SiteNoticeSchema.safeParse({ ...valid, placement: "footer" }).success).toBe(false);
31
+ });
32
+ it("rejects a non-hex color", () => {
33
+ expect(SiteNoticeSchema.safeParse({ ...valid, color: "blue" }).success).toBe(false);
34
+ expect(SiteNoticeSchema.safeParse({ ...valid, color: "#fff" }).success).toBe(false);
35
+ });
36
+ it("rejects an href that is neither absolute http(s) nor root-relative", () => {
37
+ expect(SiteNoticeSchema.safeParse({ ...valid, href: "ftp://x.y" }).success).toBe(false);
38
+ expect(SiteNoticeSchema.safeParse({ ...valid, href: "updates" }).success).toBe(false);
39
+ });
40
+ it("rejects linkText without an href (dead CTA)", () => {
41
+ const { href, ...noHref } = valid; // keeps linkText
42
+ expect(SiteNoticeSchema.safeParse(noHref).success).toBe(false);
43
+ });
44
+ it("accepts href without linkText", () => {
45
+ const { linkText, ...noLinkText } = valid;
46
+ expect(SiteNoticeSchema.safeParse(noLinkText).success).toBe(true);
47
+ });
48
+ it("rejects javascript:/data: schemes and protocol-relative hrefs", () => {
49
+ expect(SiteNoticeSchema.safeParse({ ...valid, href: "javascript:alert(1)" }).success).toBe(
50
+ false,
51
+ );
52
+ expect(SiteNoticeSchema.safeParse({ ...valid, href: "data:text/html,x" }).success).toBe(false);
53
+ expect(SiteNoticeSchema.safeParse({ ...valid, href: "//evil.com" }).success).toBe(false);
54
+ });
55
+ });
56
+
57
+ describe("SiteNoticeResponseSchema", () => {
58
+ it("accepts null", () => {
59
+ expect(SiteNoticeResponseSchema.safeParse({ notice: null }).success).toBe(true);
60
+ });
61
+ it("accepts a stored notice with updatedAt", () => {
62
+ const r = SiteNoticeResponseSchema.safeParse({
63
+ notice: { ...valid, updatedAt: "2026-06-11T00:00:00.000Z" },
64
+ });
65
+ expect(r.success).toBe(true);
66
+ });
67
+ });
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ import { SITE_NOTICE_PLACEMENTS } from "@buildinternet/releases-core/site-notice";
3
+
4
+ /**
5
+ * Absolute http(s) URL or a site-relative path beginning with a single "/".
6
+ * Rejects other schemes (javascript:, data:, …) and protocol-relative "//host"
7
+ * so a stored href can never become an unexpected-scheme or off-site link.
8
+ */
9
+ const HrefSchema = z
10
+ .string()
11
+ .max(500)
12
+ .refine((h) => /^https?:\/\//.test(h) || (h.startsWith("/") && !h.startsWith("//")), {
13
+ message: "href must be an absolute http(s) URL or a site-relative path",
14
+ });
15
+
16
+ /**
17
+ * Editable site-notice fields. Kept structurally in sync with the
18
+ * `SiteNotice` interface in `@buildinternet/releases-core/site-notice`. Split
19
+ * from the refined `SiteNoticeSchema` below so the response schema can `.extend()`
20
+ * it (a `.refine()` returns a non-extendable effect).
21
+ */
22
+ const SiteNoticeFieldsSchema = z.object({
23
+ active: z.boolean(),
24
+ message: z.string().min(1).max(280),
25
+ linkText: z.string().min(1).max(60).optional(),
26
+ href: HrefSchema.optional(),
27
+ placement: z.enum(SITE_NOTICE_PLACEMENTS),
28
+ color: z.string().regex(/^#[0-9a-fA-F]{6}$/, "color must be a 6-digit hex like #0081e7"),
29
+ dismissible: z.boolean(),
30
+ });
31
+
32
+ /**
33
+ * Editable site-notice payload. A bare `linkText` with no `href` renders nothing
34
+ * (the view only draws a link when there's a target), so require `href` whenever
35
+ * `linkText` is set.
36
+ */
37
+ export const SiteNoticeSchema = SiteNoticeFieldsSchema.refine((n) => !(n.linkText && !n.href), {
38
+ message: "href is required when linkText is set",
39
+ path: ["href"],
40
+ });
41
+
42
+ /** GET /v1/site-notice response: the stored notice (+updatedAt) or null. */
43
+ export const SiteNoticeResponseSchema = z.object({
44
+ notice: SiteNoticeFieldsSchema.extend({ updatedAt: z.iso.datetime() }).nullable(),
45
+ });
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { CategorySchema } from "./shared.js";
3
- import { CollectionReleaseItemSchema } from "./collections.js";
3
+ import { CollectionMemberSchema, CollectionReleaseItemSchema } from "./collections.js";
4
4
 
5
5
  /**
6
6
  * Org rollup row shared by category and tag detail responses. Slim by
@@ -38,6 +38,16 @@ export const CategoryListItemSchema = z.object({
38
38
  aliases: z.array(z.string()),
39
39
  orgCount: z.number().int().min(0),
40
40
  productCount: z.number().int().min(0),
41
+ /**
42
+ * A small mixed-kind preview (capped at 3) of the orgs and products in this
43
+ * category, so the list page can render an inline avatar facepile without a
44
+ * second round trip — same shape and intent as a collection's
45
+ * `previewMembers`. Org entries are surfaced first (they carry the avatar);
46
+ * product entries fill any remaining slots and dedupe against orgs already
47
+ * shown. The list page derives "+N more" from `orgCount + productCount`.
48
+ * Optional on the wire so older workers mid-rollout don't trip the schema.
49
+ */
50
+ previewMembers: z.array(CollectionMemberSchema).optional(),
41
51
  });
42
52
 
43
53
  export const CategoryListResponseSchema = z.array(CategoryListItemSchema);