@buildinternet/releases-api-types 0.15.0 → 0.17.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.
@@ -0,0 +1,140 @@
1
+ import { z } from "zod";
2
+ import { ReleaseItemSchema } from "./shared.js";
3
+
4
+ /**
5
+ * One org as it appears inside a collection's preview list or detail page.
6
+ * Joined through `organizations_public`, so soft-deleted / on_demand orgs
7
+ * never leak via a collection.
8
+ */
9
+ export const CollectionMemberOrgSchema = z.object({
10
+ slug: z.string(),
11
+ name: z.string(),
12
+ domain: z.string().nullable(),
13
+ avatarUrl: z.string().nullable(),
14
+ /** GitHub handle from `org_accounts`; lets the avatar fall back to github.com/<handle>.png. */
15
+ githubHandle: z.string().nullable(),
16
+ description: z.string().nullable(),
17
+ });
18
+
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.
23
+ */
24
+ export const CollectionListItemSchema = z.object({
25
+ slug: z.string(),
26
+ name: z.string(),
27
+ description: z.string().nullable(),
28
+ memberCount: z.number().int().min(0),
29
+ previewMembers: z.array(CollectionMemberOrgSchema).optional(),
30
+ });
31
+
32
+ export const CollectionListResponseSchema = z.array(CollectionListItemSchema);
33
+
34
+ /** Returned by `GET /v1/collections/:slug`. */
35
+ export const CollectionDetailSchema = z.object({
36
+ slug: z.string(),
37
+ name: z.string(),
38
+ description: z.string().nullable(),
39
+ orgs: z.array(CollectionMemberOrgSchema),
40
+ });
41
+
42
+ /**
43
+ * Cross-org release feed row on `GET /v1/collections/:slug/releases`. Same
44
+ * shape as `OrgReleaseItem` plus the origin `org` block (and optional
45
+ * `product`) so the web's release card can render it identically. `source.type`
46
+ * stays as `z.string()` because the SQL passes the column through unvalidated —
47
+ * tightening to the SourceType enum would break pass-through code.
48
+ */
49
+ export const CollectionReleaseItemSchema = ReleaseItemSchema.extend({
50
+ source: z.object({ slug: z.string(), name: z.string(), type: z.string() }),
51
+ org: z.object({ slug: z.string(), name: z.string() }),
52
+ /**
53
+ * Product the release belongs to, if the source is bound to one. `null` for
54
+ * orgs without products or standalone sources. Optional on the wire so older
55
+ * workers mid-rollout (and hand-constructed test fixtures) don't trip the
56
+ * typecheck — clients should treat missing and `null` identically.
57
+ */
58
+ product: z.object({ slug: z.string(), name: z.string() }).nullable().optional(),
59
+ });
60
+
61
+ /** Cursor pagination shape matches `/v1/orgs/:slug/releases`. */
62
+ export const CollectionFeedPaginationSchema = z.object({
63
+ nextCursor: z.string().nullable(),
64
+ limit: z.number().int().min(1),
65
+ });
66
+
67
+ export const CollectionReleasesResponseSchema = z.object({
68
+ releases: z.array(CollectionReleaseItemSchema),
69
+ pagination: CollectionFeedPaginationSchema,
70
+ });
71
+
72
+ /**
73
+ * Bare row returned by `POST /v1/collections` and `PATCH /v1/collections/:slug`.
74
+ * Mirrors the `collections` table columns (no member rollup — that's a
75
+ * separate detail call).
76
+ */
77
+ export const CollectionRowSchema = z.object({
78
+ id: z.string(),
79
+ slug: z.string(),
80
+ name: z.string(),
81
+ description: z.string().nullable(),
82
+ createdAt: z.string(),
83
+ updatedAt: z.string(),
84
+ });
85
+
86
+ /**
87
+ * Body for `POST /v1/collections`. `slug` derives from `name` via `toSlug()`
88
+ * when omitted. Slug shape is lowercased alnum + hyphens, alnum-start, 2–64
89
+ * chars — enforced server-side.
90
+ */
91
+ export const CreateCollectionRequestSchema = z.object({
92
+ slug: z.string().optional(),
93
+ name: z.string().min(1).max(200),
94
+ description: z.string().max(2000).nullable().optional(),
95
+ });
96
+
97
+ /** Body for `PATCH /v1/collections/:slug`. All fields optional; slug rename allowed. */
98
+ export const UpdateCollectionRequestSchema = z.object({
99
+ slug: z.string().optional(),
100
+ name: z.string().min(1).max(200).optional(),
101
+ description: z.string().max(2000).nullable().optional(),
102
+ });
103
+
104
+ /**
105
+ * One member entry accepted by the member-write endpoints. Either `orgId`
106
+ * (`org_…`) or `orgSlug` is required; when both are given, `orgId` wins.
107
+ */
108
+ export const CollectionMemberInputSchema = z.object({
109
+ orgId: z.string().optional(),
110
+ orgSlug: z.string().optional(),
111
+ /** Authoring position (default 0). For PUT, omit to use the array index. */
112
+ position: z.number().int().optional(),
113
+ });
114
+
115
+ /** Body for `POST /v1/collections/:slug/members`. */
116
+ export const AddCollectionMemberRequestSchema = CollectionMemberInputSchema;
117
+
118
+ /** Body for `PUT /v1/collections/:slug/members`. Replaces the full membership atomically. */
119
+ export const ReplaceCollectionMembersRequestSchema = z.object({
120
+ orgs: z.array(CollectionMemberInputSchema),
121
+ });
122
+
123
+ /** Resolved member returned by `PUT /v1/collections/:slug/members`. */
124
+ export const ResolvedCollectionMemberSchema = z.object({
125
+ orgId: z.string(),
126
+ position: z.number().int(),
127
+ });
128
+
129
+ /** Response from `PUT /v1/collections/:slug/members`. */
130
+ export const ReplaceCollectionMembersResponseSchema = z.object({
131
+ collectionSlug: z.string(),
132
+ members: z.array(ResolvedCollectionMemberSchema),
133
+ });
134
+
135
+ /** Response from `POST /v1/collections/:slug/members`. */
136
+ export const AddCollectionMemberResponseSchema = z.object({
137
+ collectionSlug: z.string(),
138
+ orgId: z.string(),
139
+ position: z.number().int(),
140
+ });
@@ -0,0 +1,123 @@
1
+ import { z } from "zod";
2
+ import { CategorySchema } from "./shared.js";
3
+
4
+ /**
5
+ * Status flags returned by `POST /v1/lookups`:
6
+ * - `indexed` — the source was just materialized and releases were ingested.
7
+ * - `existing` — the source already lives in the registry; we returned it as-is.
8
+ * - `empty` — the repo exists but has no releases or CHANGELOG to ingest. A
9
+ * hidden stub row is left behind so the next cron pass can pick it up.
10
+ * - `not_found` — the repo doesn't exist, is archived, or isn't reachable.
11
+ * - `deferred` — GitHub probe rate-limited or 5xx'd; the next cron pass retries.
12
+ */
13
+ export const LookupStatusSchema = z.enum(["indexed", "existing", "empty", "not_found", "deferred"]);
14
+
15
+ /** Body accepted by `POST /v1/lookups`. v1 only supports GitHub coordinates. */
16
+ export const LookupBodySchema = z.object({
17
+ provider: z.literal("github"),
18
+ coordinate: z.string().regex(/^[^/\s]+\/[^/\s]+$/, {
19
+ message: "coordinate must match {org}/{repo}",
20
+ }),
21
+ });
22
+
23
+ /**
24
+ * Source row embedded in `LookupResponse`. The handler returns the full
25
+ * Drizzle row, so `z.looseObject(...)` lets stable fields stay typed while
26
+ * letting timestamps/counters that aren't load-bearing for callers ride
27
+ * along unmodelled. Same pattern as `SourceMutationResponseSchema`.
28
+ */
29
+ export const LookupSourceSchema = z.looseObject({
30
+ id: z.string(),
31
+ slug: z.string(),
32
+ name: z.string(),
33
+ type: z.string(),
34
+ url: z.string(),
35
+ orgId: z.string().nullable(),
36
+ productId: z.string().nullable(),
37
+ isHidden: z.boolean().nullable().optional(),
38
+ discovery: z.enum(["curated", "agent", "on_demand"]).optional(),
39
+ metadata: z.string().nullable(),
40
+ createdAt: z.string().optional(),
41
+ });
42
+
43
+ /** Release row embedded in `LookupResponse.releases`. Loose for the same reason as `LookupSourceSchema`. */
44
+ export const LookupReleaseSchema = z.looseObject({
45
+ id: z.string(),
46
+ sourceId: z.string(),
47
+ version: z.string().nullable(),
48
+ title: z.string(),
49
+ url: z.string().nullable(),
50
+ publishedAt: z.string().nullable(),
51
+ });
52
+
53
+ /** "Did you mean" rail returned alongside not_found / empty / deferred outcomes. */
54
+ export const LookupRelatedOrgSchema = z
55
+ .object({
56
+ org: z.object({ id: z.string(), slug: z.string(), name: z.string() }),
57
+ sources: z.array(
58
+ z.object({ id: z.string(), slug: z.string(), name: z.string(), url: z.string() }),
59
+ ),
60
+ })
61
+ .nullable();
62
+
63
+ /**
64
+ * Response returned by `POST /v1/lookups`. The `source` and `releases` fields
65
+ * are populated for `indexed` / `existing` outcomes; `empty` populates only
66
+ * `source`; `not_found` / `deferred` populate neither.
67
+ */
68
+ export const LookupResponseSchema = z.object({
69
+ status: LookupStatusSchema,
70
+ source: LookupSourceSchema.optional(),
71
+ releases: z.array(LookupReleaseSchema).optional(),
72
+ relatedOrg: LookupRelatedOrgSchema,
73
+ });
74
+
75
+ /** Response from `GET /v1/lookups/source-by-slug?slug=…`. */
76
+ export const LookupSourceBySlugResponseSchema = z.object({
77
+ sourceId: z.string(),
78
+ sourceSlug: z.string(),
79
+ orgSlug: z.string(),
80
+ });
81
+
82
+ /** Response from `GET /v1/lookups/product-by-slug?slug=…`. */
83
+ export const LookupProductBySlugResponseSchema = z.object({
84
+ productId: z.string(),
85
+ productSlug: z.string(),
86
+ orgSlug: z.string(),
87
+ });
88
+
89
+ /**
90
+ * Response from `GET /v1/lookups/by-domain?domain=…`. Pure resolution: an
91
+ * unknown domain returns 404, never probes. Both an org and product list can
92
+ * be present when a product alias points at the same domain its parent org
93
+ * owns.
94
+ */
95
+ export const DomainLookupOrgSchema = z.object({
96
+ id: z.string(),
97
+ slug: z.string(),
98
+ name: z.string(),
99
+ domain: z.string().nullable(),
100
+ description: z.string().nullable(),
101
+ category: z.string().nullable(),
102
+ avatarUrl: z.string().nullable(),
103
+ matchedVia: z.enum(["primary", "alias"]),
104
+ });
105
+
106
+ export const DomainLookupProductSchema = z.object({
107
+ id: z.string(),
108
+ slug: z.string(),
109
+ name: z.string(),
110
+ orgId: z.string(),
111
+ orgSlug: z.string(),
112
+ orgName: z.string(),
113
+ // Reuses the canonical `CategorySchema` enum because the products read
114
+ // path resolves through `resolveCategoryInput` before persisting — unlike
115
+ // the search catalog category, which passes the column through raw.
116
+ category: CategorySchema.nullable(),
117
+ });
118
+
119
+ export const DomainLookupResponseSchema = z.object({
120
+ domain: z.string(),
121
+ org: DomainLookupOrgSchema.nullable(),
122
+ products: z.array(DomainLookupProductSchema),
123
+ });
@@ -1,7 +1,13 @@
1
1
  import { z } from "zod";
2
- import { CategorySchema, ListResponseSchema, OverviewPageItemSchema } from "./shared.js";
2
+ import {
3
+ CategorySchema,
4
+ ListResponseSchema,
5
+ OverviewPageItemSchema,
6
+ ReleaseItemSchema,
7
+ } from "./shared.js";
3
8
  import { SourceListItemSchema } from "./sources.js";
4
9
  import { ProductListItemSchema } from "./products.js";
10
+ import { CollectionListItemSchema } from "./collections.js";
5
11
 
6
12
  export const OrgListItemSchema = z.object({
7
13
  id: z.string(),
@@ -26,24 +32,49 @@ export const OrgAccountItemSchema = z.object({
26
32
  handle: z.string(),
27
33
  });
28
34
 
29
- export const OrgAccountsResponseSchema = ListResponseSchema(OrgAccountItemSchema);
35
+ export const OrgAccountsListResponseSchema = ListResponseSchema(OrgAccountItemSchema);
36
+
37
+ /**
38
+ * Response shape for `GET /v1/orgs/:slug/accounts`. Returns the paginated list
39
+ * by default; with `?platform=<name>` returns a single matching account or
40
+ * `null` when no row matches. The dual shape is preserved for compatibility
41
+ * with the OSS CLI which calls `?platform=` and expects a single row.
42
+ */
43
+ export const OrgAccountsResponseSchema = z.union([
44
+ OrgAccountsListResponseSchema,
45
+ OrgAccountItemSchema,
46
+ z.null(),
47
+ ]);
30
48
  export const OrgTagsResponseSchema = ListResponseSchema(z.string());
31
49
 
50
+ /**
51
+ * Body accepted by `POST /v1/orgs`. `category` is a non-empty string at the
52
+ * wire boundary so callers can pass either a canonical slug or one of its
53
+ * configured aliases (resolved server-side via `resolveCategoryInput`).
54
+ * Responses always carry the canonical slug. Empty strings are rejected so
55
+ * the truthy-guard around `resolveCategoryInput` in the handler can't be
56
+ * bypassed into persisting an invalid `""` value.
57
+ */
32
58
  export const CreateOrgBodySchema = z.object({
33
59
  name: z.string().min(1),
34
60
  slug: z.string().optional(),
35
61
  domain: z.string().optional(),
36
62
  description: z.string().optional(),
37
- category: CategorySchema.optional(),
63
+ category: z.string().min(1).optional(),
38
64
  tags: z.array(z.string()).optional(),
39
65
  });
40
66
 
67
+ /**
68
+ * Body accepted by `PATCH /v1/orgs/:slug`. Same alias-allowed string rule
69
+ * as create, plus `null` to explicitly clear the overlay. Empty strings
70
+ * stay rejected so callers must pick between a slug/alias or `null`.
71
+ */
41
72
  export const UpdateOrgBodySchema = z.object({
42
73
  name: z.string().optional(),
43
74
  slug: z.string().optional(),
44
75
  domain: z.string().nullable().optional(),
45
76
  description: z.string().nullable().optional(),
46
- category: CategorySchema.nullable().optional(),
77
+ category: z.string().min(1).nullable().optional(),
47
78
  tags: z.array(z.string()).optional(),
48
79
  aliases: z.array(z.string()).optional(),
49
80
  });
@@ -66,6 +97,265 @@ const OrgDetailPlaybookSchema = z.object({
66
97
  updatedAt: z.string(),
67
98
  });
68
99
 
100
+ // ── Org catalog (/orgs/:slug/catalog) ──
101
+
102
+ const OrgCatalogProductItemSchema = z.object({
103
+ kind: z.literal("product"),
104
+ id: z.string(),
105
+ slug: z.string(),
106
+ name: z.string(),
107
+ url: z.string().nullable(),
108
+ description: z.string().nullable(),
109
+ category: CategorySchema.nullable(),
110
+ });
111
+
112
+ const OrgCatalogSourceItemSchema = z.object({
113
+ kind: z.literal("source"),
114
+ id: z.string(),
115
+ slug: z.string(),
116
+ name: z.string(),
117
+ type: z.string(),
118
+ url: z.string(),
119
+ productId: z.string().nullable(),
120
+ });
121
+
122
+ /**
123
+ * Combined product + source catalog for `GET /v1/orgs/:slug/catalog`.
124
+ * `kind` discriminates between the two row shapes. Products carry
125
+ * `category` and `description`; sources carry `type` and `productId`.
126
+ */
127
+ export const OrgCatalogItemSchema = z.discriminatedUnion("kind", [
128
+ OrgCatalogProductItemSchema,
129
+ OrgCatalogSourceItemSchema,
130
+ ]);
131
+
132
+ export const OrgCatalogResponseSchema = z.object({
133
+ org: z.object({ id: z.string(), slug: z.string(), name: z.string() }),
134
+ items: z.array(OrgCatalogItemSchema),
135
+ });
136
+
137
+ // ── Org collections membership (/orgs/:slug/collections) ──
138
+
139
+ /**
140
+ * List of collections the org belongs to, returned by
141
+ * `GET /v1/orgs/:slug/collections`. Each item follows the same shape as
142
+ * `CollectionListItem` on `GET /v1/collections` — `previewMembers` is
143
+ * omitted here because the context (one org's collections) makes inline
144
+ * member previews redundant.
145
+ */
146
+ export const OrgCollectionsResponseSchema = z.array(CollectionListItemSchema);
147
+
148
+ // ── Org accounts (/orgs/:slug/accounts) ──
149
+
150
+ /**
151
+ * Body accepted by `POST /v1/orgs/:slug/accounts`.
152
+ * Both `platform` and `handle` are required.
153
+ */
154
+ export const AddOrgAccountBodySchema = z.object({
155
+ platform: z.string().min(1),
156
+ handle: z.string().min(1),
157
+ });
158
+
159
+ // ── Org ignored URLs (/orgs/:slug/ignored-urls) ──
160
+
161
+ export const IgnoredUrlItemSchema = z.object({
162
+ id: z.string(),
163
+ url: z.string(),
164
+ orgId: z.string(),
165
+ reason: z.string().nullable(),
166
+ ignoredAt: z.string(),
167
+ });
168
+
169
+ export const OrgIgnoredUrlsListResponseSchema = ListResponseSchema(IgnoredUrlItemSchema);
170
+
171
+ /**
172
+ * Response shape for `GET /v1/orgs/:slug/ignored-urls`. Returns the paginated
173
+ * list by default; with `?url=<encoded>&single=1` returns the matching single
174
+ * row or `null` when not present.
175
+ */
176
+ export const OrgIgnoredUrlsResponseSchema = z.union([
177
+ OrgIgnoredUrlsListResponseSchema,
178
+ IgnoredUrlItemSchema,
179
+ z.null(),
180
+ ]);
181
+
182
+ /**
183
+ * Body accepted by `POST /v1/orgs/:slug/ignored-urls`.
184
+ * `reason` is optional.
185
+ */
186
+ export const AddIgnoredUrlBodySchema = z.object({
187
+ url: z.string().min(1),
188
+ reason: z.string().optional(),
189
+ });
190
+
191
+ /** Response shape for `POST /v1/orgs/:slug/ignored-urls`. */
192
+ export const AddIgnoredUrlResponseSchema = z.object({
193
+ ignored: z.literal(true),
194
+ });
195
+
196
+ /** Response shape for `DELETE /v1/orgs/:slug/ignored-urls/:url`. */
197
+ export const DeleteIgnoredUrlResponseSchema = z.object({
198
+ deleted: z.literal(true),
199
+ });
200
+
201
+ // ── Org tags (/orgs/:slug/tags and PUT /DELETE) ──
202
+
203
+ /** Body accepted by `PUT` and `DELETE` `/v1/orgs/:slug/tags`. */
204
+ export const OrgTagsBodySchema = z.object({
205
+ tags: z.array(z.string()),
206
+ });
207
+
208
+ /** Response shape returned by `PUT` and `DELETE` `/v1/orgs/:slug/tags`. */
209
+ export const OrgTagsMutationResponseSchema = z.object({
210
+ ok: z.literal(true),
211
+ });
212
+
213
+ // ── POST /tags (global tag creation) ──
214
+
215
+ /** Body accepted by `POST /v1/tags`. */
216
+ export const CreateTagBodySchema = z.object({
217
+ name: z.string().min(1),
218
+ });
219
+
220
+ /** Row returned by `POST /v1/tags` (201 on create, 200 if already exists). */
221
+ export const TagRowSchema = z.object({
222
+ id: z.string(),
223
+ name: z.string(),
224
+ slug: z.string(),
225
+ createdAt: z.string(),
226
+ });
227
+
228
+ // ── Org activity (/orgs/:slug/activity) ──
229
+
230
+ const OrgActivityWeeklyBucketSchema = z.object({
231
+ weekStart: z.string(),
232
+ count: z.number().int().min(0),
233
+ earliestVersion: z.string().nullable(),
234
+ latestVersion: z.string().nullable(),
235
+ });
236
+
237
+ const OrgActivitySourceSchema = z.object({
238
+ slug: z.string(),
239
+ name: z.string(),
240
+ releaseCount: z.number().int().min(0),
241
+ avgReleasesPerWeek: z.number(),
242
+ earliestVersion: z.string().nullable(),
243
+ latestVersion: z.string().nullable(),
244
+ latestDate: z.string().nullable(),
245
+ weeklyBuckets: z.array(OrgActivityWeeklyBucketSchema),
246
+ });
247
+
248
+ /**
249
+ * Response shape for `GET /v1/orgs/:slug/activity`. Returns per-source
250
+ * weekly release buckets plus an aggregate rollup — used for timeline
251
+ * visualization in the web org detail page.
252
+ */
253
+ export const OrgActivityResponseSchema = z.object({
254
+ org: z.object({ slug: z.string(), name: z.string() }),
255
+ range: z.object({ from: z.string(), to: z.string() }),
256
+ sources: z.array(OrgActivitySourceSchema),
257
+ aggregateWeekly: z.array(z.object({ weekStart: z.string(), count: z.number().int().min(0) })),
258
+ });
259
+
260
+ // ── Org heatmap (/orgs/:slug/heatmap) ──
261
+
262
+ /**
263
+ * Response shape for `GET /v1/orgs/:slug/heatmap`. Returns daily release
264
+ * counts for the trailing 365 days — used for contribution-graph
265
+ * visualization in the web org detail page.
266
+ */
267
+ export const OrgHeatmapResponseSchema = z.object({
268
+ org: z.object({ slug: z.string(), name: z.string() }),
269
+ range: z.object({ from: z.string(), to: z.string() }),
270
+ dailyCounts: z.array(z.object({ date: z.string(), count: z.number().int().min(0) })),
271
+ total: z.number().int().min(0),
272
+ });
273
+
274
+ // ── Org sparklines (/orgs/:slug/sparklines) ──
275
+
276
+ const SparklineSourceSchema = z.object({
277
+ slug: z.string(),
278
+ name: z.string(),
279
+ sparkline: z.array(z.number().int().min(0)).length(30),
280
+ });
281
+
282
+ /**
283
+ * Response shape for `GET /v1/orgs/:slug/sparklines`. Returns 30-day daily
284
+ * release counts per source and per product, plus an aggregate rollup.
285
+ */
286
+ export const OrgSparklinesResponseSchema = z.object({
287
+ org: z.object({ slug: z.string(), name: z.string() }),
288
+ range: z.object({ from: z.string(), to: z.string() }),
289
+ aggregate: z.array(z.number().int().min(0)).length(30),
290
+ sources: z.array(SparklineSourceSchema),
291
+ products: z.array(SparklineSourceSchema),
292
+ });
293
+
294
+ // ── Org releases feed (/orgs/:slug/releases) ──
295
+
296
+ /**
297
+ * Release feed item on `GET /v1/orgs/:slug/releases`. Extends `ReleaseItem`
298
+ * with a `source` block identifying the originating source within the org.
299
+ */
300
+ export const OrgReleaseItemSchema = ReleaseItemSchema.extend({
301
+ source: z.object({ slug: z.string(), name: z.string(), type: z.string() }),
302
+ });
303
+
304
+ /** Cursor pagination shape for the org releases feed. */
305
+ export const OrgFeedPaginationSchema = z.object({
306
+ nextCursor: z.string().nullable(),
307
+ limit: z.number().int().min(1),
308
+ });
309
+
310
+ /**
311
+ * Response shape for `GET /v1/orgs/:slug/releases`.
312
+ * Cursor-paginated; `nextCursor` is `null` when this is the last page.
313
+ */
314
+ export const OrgReleasesFeedResponseSchema = z.object({
315
+ releases: z.array(OrgReleaseItemSchema),
316
+ pagination: OrgFeedPaginationSchema,
317
+ });
318
+
319
+ // ── Org recent releases (/orgs/:slug/recent-releases) ──
320
+
321
+ /**
322
+ * Row returned by `GET /v1/orgs/:slug/recent-releases`. This is the full
323
+ * `releasesVisible` row with `sourceName` and `sourceSlug` joined from
324
+ * `sourcesVisible` — intended for summarization / grouping by agents.
325
+ * Many fields are nullable because they may not be populated at ingest time.
326
+ */
327
+ export const OrgRecentReleaseItemSchema = z.object({
328
+ id: z.string(),
329
+ sourceId: z.string(),
330
+ version: z.string().nullable(),
331
+ type: z.string().optional(),
332
+ title: z.string(),
333
+ content: z.string(),
334
+ summary: z.string().nullable(),
335
+ titleGenerated: z.string().nullable().optional(),
336
+ titleShort: z.string().nullable().optional(),
337
+ url: z.string().nullable(),
338
+ contentHash: z.string().nullable(),
339
+ metadata: z.string().nullable(),
340
+ media: z.string().nullable(),
341
+ publishedAt: z.string().nullable(),
342
+ suppressed: z.number().optional(),
343
+ suppressedReason: z.string().nullable().optional(),
344
+ fetchedAt: z.string().optional(),
345
+ embeddedAt: z.string().nullable().optional(),
346
+ sourceName: z.string(),
347
+ sourceSlug: z.string(),
348
+ });
349
+
350
+ export const OrgRecentReleasesResponseSchema = z.array(OrgRecentReleaseItemSchema);
351
+
352
+ // ── Org accounts mutation responses ──
353
+
354
+ /** Response returned by `DELETE /v1/orgs/:slug/accounts/:platform/:handle`. */
355
+ export const DeleteOrgAccountResponseSchema = z.object({
356
+ deleted: z.literal(true),
357
+ });
358
+
69
359
  export const OrgDetailSchema = z.object({
70
360
  id: z.string().optional(),
71
361
  slug: z.string(),