@buildinternet/releases-api-types 0.32.0 → 0.34.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 +1 -1
- package/src/api-types.ts +282 -3
- package/src/schemas/account-profile.ts +28 -0
- package/src/schemas/collections.ts +21 -0
- package/src/schemas/releases.ts +59 -0
- package/src/schemas/search.ts +12 -0
- package/src/schemas/shared.ts +14 -0
- package/src/schemas/site-notice.test.ts +67 -0
- package/src/schemas/site-notice.ts +45 -0
- package/src/schemas/sources.ts +15 -1
- package/src/schemas/taxonomy.ts +11 -1
package/package.json
CHANGED
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: {
|
|
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>;
|
|
@@ -829,11 +925,152 @@ export interface FollowMutationResponse {
|
|
|
829
925
|
following: boolean;
|
|
830
926
|
}
|
|
831
927
|
|
|
928
|
+
/**
|
|
929
|
+
* A user's personalized feed token, including the full re-revealable feed URL.
|
|
930
|
+
* The token is reversible (stored recoverably) because the feed serves only
|
|
931
|
+
* public release data and carries no PII — so the URL can be re-displayed and
|
|
932
|
+
* copied on any visit (see #1519 design, decision 6). One per user.
|
|
933
|
+
*/
|
|
934
|
+
export interface FeedToken {
|
|
935
|
+
/** Absolute, tokenized Atom URL — e.g. https://api.releases.sh/v1/feed/relf_…_….atom */
|
|
936
|
+
feedUrl: string;
|
|
937
|
+
/** Non-secret public handle (for masked display). */
|
|
938
|
+
lookupId: string;
|
|
939
|
+
createdAt: string;
|
|
940
|
+
lastUsedAt: string | null;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** `GET /v1/me/feed/token` — the token, or null if the user has none yet. */
|
|
944
|
+
export interface FeedTokenResponse {
|
|
945
|
+
token: FeedToken | null;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Cursor pagination shape for GET /v1/me/feed (same as other release feeds). */
|
|
949
|
+
export type PersonalizedFeedPagination = OrgFeedPagination;
|
|
950
|
+
|
|
832
951
|
/**
|
|
833
952
|
* GET /v1/me/feed response — the personalized release feed. Reuses the same item
|
|
834
|
-
* shape as /v1/releases/latest (ReleaseLatestItem)
|
|
953
|
+
* shape as /v1/releases/latest (ReleaseLatestItem); cursor-paginated like other
|
|
954
|
+
* feed-shaped surfaces.
|
|
835
955
|
*/
|
|
836
|
-
export
|
|
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
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ── Digest emails ──
|
|
1061
|
+
|
|
1062
|
+
/** How often a user wants a digest email. `off` = no emails. */
|
|
1063
|
+
export type DigestCadence = "off" | "daily" | "weekly";
|
|
1064
|
+
|
|
1065
|
+
/** GET /v1/me/digest response — the caller's current cadence. */
|
|
1066
|
+
export interface DigestPrefsResponse {
|
|
1067
|
+
cadence: DigestCadence;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/** PUT /v1/me/digest request body. */
|
|
1071
|
+
export interface DigestPrefsRequest {
|
|
1072
|
+
cadence: DigestCadence;
|
|
1073
|
+
}
|
|
837
1074
|
|
|
838
1075
|
// ── Search ──
|
|
839
1076
|
|
|
@@ -845,6 +1082,8 @@ export type SearchSourceHit = z.infer<typeof SearchSourceHitSchema>;
|
|
|
845
1082
|
export interface RawSourceHit extends SearchSourceHit {
|
|
846
1083
|
productName?: string;
|
|
847
1084
|
productCategory?: string;
|
|
1085
|
+
/** Owning org's avatar URL — folded onto the catalog hit for the byline avatar. */
|
|
1086
|
+
orgAvatarUrl?: string | null;
|
|
848
1087
|
/** Entity taxonomy kind from the `sources.kind` column. */
|
|
849
1088
|
entityKind?: string | null;
|
|
850
1089
|
}
|
|
@@ -870,6 +1109,7 @@ export function foldSourcesIntoCatalog(
|
|
|
870
1109
|
name: s.productName ?? s.name,
|
|
871
1110
|
orgSlug: s.orgSlug,
|
|
872
1111
|
orgName: s.orgName,
|
|
1112
|
+
orgAvatarUrl: s.orgAvatarUrl ?? null,
|
|
873
1113
|
category: s.productCategory ?? null,
|
|
874
1114
|
entryType: "product",
|
|
875
1115
|
kind: undefined,
|
|
@@ -881,6 +1121,7 @@ export function foldSourcesIntoCatalog(
|
|
|
881
1121
|
name: s.name,
|
|
882
1122
|
orgSlug: s.orgSlug,
|
|
883
1123
|
orgName: s.orgName,
|
|
1124
|
+
orgAvatarUrl: s.orgAvatarUrl ?? null,
|
|
884
1125
|
category: null,
|
|
885
1126
|
entryType: "source",
|
|
886
1127
|
kind: (s.entityKind as SearchCatalogHit["kind"]) ?? undefined,
|
|
@@ -1169,6 +1410,10 @@ export type ReplaceCollectionMembersResponse = z.infer<
|
|
|
1169
1410
|
typeof ReplaceCollectionMembersResponseSchema
|
|
1170
1411
|
>;
|
|
1171
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
|
+
>;
|
|
1172
1417
|
|
|
1173
1418
|
// ── Releases (enriched) ──
|
|
1174
1419
|
|
|
@@ -1203,6 +1448,25 @@ export interface LatestRelease {
|
|
|
1203
1448
|
publishedAt: string | null;
|
|
1204
1449
|
sourceName: string;
|
|
1205
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;
|
|
1206
1470
|
/** AI-generated summary (#852, renamed in #860). Nullable — most rows unpopulated. */
|
|
1207
1471
|
summary: string | null;
|
|
1208
1472
|
/** AI-generated headline (#852, renamed in #860). See {@link ReleaseDetail.titleGenerated}. */
|
|
@@ -1487,6 +1751,21 @@ export interface TokenIdentity {
|
|
|
1487
1751
|
scopes: string[];
|
|
1488
1752
|
principalType: "internal" | "agent" | "user";
|
|
1489
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;
|
|
1490
1769
|
expiresAt?: string | null;
|
|
1491
1770
|
lastUsedAt?: string | null;
|
|
1492
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
|
+
});
|
package/src/schemas/releases.ts
CHANGED
|
@@ -26,6 +26,16 @@ 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(),
|
|
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(),
|
|
29
39
|
});
|
|
30
40
|
|
|
31
41
|
export const ReleaseLatestProductSchema = z.object({
|
|
@@ -175,6 +185,17 @@ export const ReleaseDetailOrgSchema = z.object({
|
|
|
175
185
|
* related-rail feed.
|
|
176
186
|
*/
|
|
177
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(),
|
|
178
199
|
});
|
|
179
200
|
|
|
180
201
|
/**
|
|
@@ -216,6 +237,12 @@ export const ReleaseDetailResponseSchema = z.object({
|
|
|
216
237
|
composition: ReleaseCompositionSchema.nullable(),
|
|
217
238
|
appStore: AppStoreSourceInfoSchema.nullable().optional(),
|
|
218
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(),
|
|
219
246
|
});
|
|
220
247
|
|
|
221
248
|
/**
|
|
@@ -240,6 +267,12 @@ export const UpdateReleaseBodySchema = z
|
|
|
240
267
|
titleShort: z.string().nullable().optional(),
|
|
241
268
|
// `null` removes `$.composition` from metadata; an object replaces it.
|
|
242
269
|
composition: ReleaseCompositionSchema.nullable().optional(),
|
|
270
|
+
// Replace the release's stored `media[]` wholesale (curator manual edit).
|
|
271
|
+
// Items whose `url` is not already an R2/`media.releases.sh` origin are run
|
|
272
|
+
// through the ingest R2 mirror (`processMediaForR2`) so posters/images land
|
|
273
|
+
// in `released-media` and get `r2Key`/`r2Url` stamped — same path as ingest.
|
|
274
|
+
// Reuses the shared `MediaItemSchema` (type/url/alt/r2Url/r2Key/linkUrl).
|
|
275
|
+
media: z.array(MediaItemSchema).optional(),
|
|
243
276
|
})
|
|
244
277
|
.strict();
|
|
245
278
|
|
|
@@ -278,6 +311,32 @@ export const ReleaseDeleteResponseSchema = z.object({
|
|
|
278
311
|
deleted: z.literal(true),
|
|
279
312
|
});
|
|
280
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
|
+
|
|
281
340
|
/** Response returned by `POST /v1/releases/:id/suppress`. */
|
|
282
341
|
export const ReleaseSuppressResponseSchema = z.object({
|
|
283
342
|
suppressed: z.literal(true),
|
package/src/schemas/search.ts
CHANGED
|
@@ -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
|
|
@@ -40,6 +41,9 @@ export const SearchCatalogHitSchema = z.object({
|
|
|
40
41
|
name: z.string(),
|
|
41
42
|
orgSlug: z.string().nullable(),
|
|
42
43
|
orgName: z.string().nullable(),
|
|
44
|
+
/** Owning org's avatar URL, for a small inline byline avatar. Products have
|
|
45
|
+
* no avatar of their own, so the web row anchors on the org's instead. */
|
|
46
|
+
orgAvatarUrl: z.string().nullable().optional(),
|
|
43
47
|
// Loose `z.string().nullable()` rather than the canonical `CategorySchema`
|
|
44
48
|
// enum because `foldSourcesIntoCatalog` and the catalog-search handlers
|
|
45
49
|
// pass `s.productCategory` / `o.category` through without validating
|
|
@@ -127,6 +131,14 @@ export const SearchCollectionHitSchema = z.object({
|
|
|
127
131
|
score: z.number().optional(),
|
|
128
132
|
/** Result-set org slugs that triggered this rollup. Always present on `via=member`. */
|
|
129
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(),
|
|
130
142
|
});
|
|
131
143
|
|
|
132
144
|
/**
|
package/src/schemas/shared.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -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
|
+
});
|
package/src/schemas/sources.ts
CHANGED
|
@@ -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`.
|
|
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) ──
|
package/src/schemas/taxonomy.ts
CHANGED
|
@@ -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);
|