@buildinternet/releases-api-types 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/releases-api-types",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "Wire protocol — request/response types for the Releases registry API. Shared by web, workers, and clients.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/api-types.ts CHANGED
@@ -309,10 +309,23 @@ export {
309
309
  } from "./schemas/account-profile.js";
310
310
  export {
311
311
  ReleasesJsonConfigSchema,
312
+ ReleasesJsonDomainReleaseSchema,
313
+ ReleasesJsonDomainSchema,
312
314
  ReleasesJsonProductSchema,
315
+ ReleasesJsonRegistriesSchema,
316
+ ReleasesJsonRepoProductSchema,
317
+ ReleasesJsonRepoReleaseSchema,
318
+ ReleasesJsonRepoSchema,
313
319
  SyncWellKnownResponseSchema,
314
320
  } from "./schemas/well-known.js";
315
- export type { ReleasesJsonConfig, ReleasesJsonProduct } from "./schemas/well-known.js";
321
+ export type {
322
+ ReleasesJsonConfig,
323
+ ReleasesJsonDomain,
324
+ ReleasesJsonDomainRelease,
325
+ ReleasesJsonProduct,
326
+ ReleasesJsonRepo,
327
+ ReleasesJsonRepoRelease,
328
+ } from "./schemas/well-known.js";
316
329
  export {
317
330
  SourceListItemSchema,
318
331
  SourceWithOrgSchema,
@@ -766,6 +779,12 @@ export interface WhatsChangedEntry {
766
779
  breaking: BreakingLevel;
767
780
  migrationNotes: string | null;
768
781
  url: string | null;
782
+ /**
783
+ * Absolute canonical web URL — the slugged `/release/<id>-<slug>` form
784
+ * (#1906), built from `WEB_BASE_URL`. Distinct from `url` (the upstream
785
+ * source URL); lets an upgrade-planning agent link the release detail page.
786
+ */
787
+ webUrl: string | null;
769
788
  }
770
789
 
771
790
  /**
@@ -818,6 +837,11 @@ export interface ReleaseDetail {
818
837
  /** Explicit upgrade/migration steps lifted from the body (#1696); null when the body gives none. */
819
838
  migrationNotes?: string | null;
820
839
  url: string | null;
840
+ /**
841
+ * Canonical releases.sh detail-page URL (friendly form: ID + current
842
+ * title slug). Additive — older servers omit it.
843
+ */
844
+ webUrl?: string;
821
845
  media: MediaItem[];
822
846
  publishedAt: string | null;
823
847
  fetchedAt: string;
@@ -1235,7 +1259,7 @@ export type UpdatePlaybookNotesResponse = z.infer<typeof UpdatePlaybookNotesResp
1235
1259
 
1236
1260
  export type OverviewStaleness = "missing" | "behind" | "fresh";
1237
1261
  /**
1238
- * `opted_out` — the org has `autoGenerateContent=false`, so the batch-overview
1262
+ * `opted_out` — the org has `autoGenerateContent=false`, so the overview-regen
1239
1263
  * eligibility filter skips it regardless of staleness. Surfaced so an
1240
1264
  * orchestrator planning a sweep doesn't queue a regen that will never run
1241
1265
  * (#1795).
@@ -1261,7 +1285,7 @@ export interface OverviewManifestRow {
1261
1285
  staleness: OverviewStaleness;
1262
1286
  /**
1263
1287
  * Whether the org is opted into automatic AI content. When `false`, the
1264
- * batch-overview cron/manual regen skips it — the staleness signal alone is
1288
+ * overview-regen cron skips it — the staleness signal alone is
1265
1289
  * misleading without this (#1795). Older workers may omit it mid-deploy.
1266
1290
  */
1267
1291
  autoGenerateContent?: boolean;
@@ -120,6 +120,13 @@ export const UpdateOrgBodySchema = z.object({
120
120
  isHidden: z.boolean().optional(),
121
121
  /** Admin-only: opt the org into automatic AI content — org overviews AND per-release summaries (single backend flag `auto_generate_content`). */
122
122
  autoGenerateContent: z.boolean().optional(),
123
+ /**
124
+ * Admin-only: manual override for the automated overview-regen cadence in
125
+ * days (#1895). `null` clears back to automatic (velocity-tiered: 7d
126
+ * default, 2d at high release velocity). A set value pins the cadence in
127
+ * either direction — faster for a hot org, slower for a bursty one.
128
+ */
129
+ overviewCadenceDays: z.number().int().min(1).max(90).nullable().optional(),
123
130
  /** Admin-only: promote this org on the home-page featured rail. */
124
131
  featured: z.boolean().optional(),
125
132
  /** Admin-only: promote or demote the org's discovery status (curated/agent/on_demand). */
@@ -465,6 +472,8 @@ export const OrgDetailSchema = z.object({
465
472
  isHidden: z.boolean().optional(),
466
473
  /** Admin display flag: org is opted into automatic overviews + per-release summaries. Optional on the wire for older workers mid-deploy. */
467
474
  autoGenerateContent: z.boolean().optional(),
475
+ /** Manual overview-regen cadence override in days (#1895); null = automatic velocity tier. Optional on the wire for older workers mid-deploy. */
476
+ overviewCadenceDays: z.number().int().nullable().optional(),
468
477
  /** Admin display flag: org is promoted on the home-page featured rail. Optional on the wire for older workers mid-deploy. */
469
478
  featured: z.boolean().optional(),
470
479
  /** Admin display flag: all ingest paused for this org. */
@@ -62,6 +62,11 @@ export const ReleaseLatestItemSchema = z.object({
62
62
  breaking: z.enum(BREAKING_LEVELS).optional(),
63
63
  publishedAt: z.string().nullable(),
64
64
  url: z.string().nullable(),
65
+ /**
66
+ * Canonical releases.sh detail-page URL (friendly form: ID + current
67
+ * title slug). Additive — older servers and the digest-cron path omit it.
68
+ */
69
+ webUrl: z.string().optional(),
65
70
  media: z.array(MediaItemSchema),
66
71
  source: ReleaseLatestSourceSchema,
67
72
  /**
@@ -237,6 +242,13 @@ export const ReleaseDetailResponseSchema = z.object({
237
242
  */
238
243
  migrationNotes: z.string().nullable().optional(),
239
244
  url: z.string().nullable(),
245
+ /**
246
+ * Canonical releases.sh detail-page URL, `https://releases.sh/release/
247
+ * rel_<id>-<slug>` (Zendesk-style: immutable ID + current title slug;
248
+ * the slug follows title regeneration, the ID keeps old links alive).
249
+ * Additive — older servers omit it.
250
+ */
251
+ webUrl: z.string().optional(),
240
252
  contentHash: z.string().nullable(),
241
253
  media: z.array(MediaItemSchema),
242
254
  publishedAt: z.string().nullable(),
@@ -1,66 +1,163 @@
1
- import { describe, it, expect } from "bun:test";
2
- import { ReleasesJsonConfigSchema } from "./well-known.js";
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ ReleasesJsonConfigSchema,
4
+ ReleasesJsonDomainSchema,
5
+ ReleasesJsonRepoSchema,
6
+ } from "./well-known.js";
3
7
 
4
- describe("ReleasesJsonConfigSchema", () => {
5
- it("accepts an empty object (no-op file)", () => {
6
- expect(ReleasesJsonConfigSchema.safeParse({}).success).toBe(true);
8
+ const location = (n: number) => ({ url: `https://acme.com/releases/${n}` });
9
+
10
+ describe("releases.json v2 schemas", () => {
11
+ it("accepts the minimal domain manifest", () => {
12
+ expect(
13
+ ReleasesJsonDomainSchema.parse({
14
+ version: 2,
15
+ releases: [{ url: "https://acme.com/updates", feed: "https://acme.com/updates.xml" }],
16
+ }),
17
+ ).toEqual({
18
+ version: 2,
19
+ releases: [{ url: "https://acme.com/updates", feed: "https://acme.com/updates.xml" }],
20
+ });
7
21
  });
8
22
 
9
- it("accepts a full org-scope file", () => {
10
- const r = ReleasesJsonConfigSchema.safeParse({
23
+ it("accepts the full domain shape with advisory taxonomy", () => {
24
+ const parsed = ReleasesJsonDomainSchema.parse({
11
25
  $schema: "https://releases.sh/schemas/releases.json",
12
- version: 1,
26
+ version: 2,
13
27
  name: "Acme",
14
28
  description: "CI for teams.",
15
- category: "developer-tools",
29
+ category: "future-category",
16
30
  avatar: "https://acme.com/logo.png",
17
- tags: ["ci", "observability"],
31
+ tags: ["future-tag"],
18
32
  social: { twitter: "acmehq", github: "acme" },
19
- notice: { message: "Docs moved", href: "https://acme.com/docs" },
33
+ products: [
34
+ {
35
+ name: "Acme Cloud",
36
+ slug: "acme-cloud",
37
+ kind: "future-kind",
38
+ category: "future-category",
39
+ description: "Managed CI runners.",
40
+ website: "https://acme.com/cloud",
41
+ docs: "https://docs.acme.com/cloud",
42
+ support: "https://acme.com/support",
43
+ social: { twitter: "acmecloud" },
44
+ archived: true,
45
+ releases: [{ github: "acme/cloud", canonical: true }],
46
+ },
47
+ ],
48
+ registries: {
49
+ "releases.sh": {
50
+ org: "org_abc123",
51
+ verification: "verification-token",
52
+ futureKey: true,
53
+ },
54
+ "example.com": { anything: "goes" },
55
+ },
20
56
  });
21
- expect(r.success).toBe(true);
57
+
58
+ expect(parsed.products?.[0]?.kind).toBe("future-kind");
59
+ expect(parsed.registries?.["example.com"]).toEqual({ anything: "goes" });
22
60
  });
23
61
 
24
- it("accepts a product-scope file", () => {
25
- const r = ReleasesJsonConfigSchema.safeParse({
26
- product: { name: "Acme Cloud", slug: "acme-cloud", category: "cloud", kind: "platform" },
27
- });
28
- expect(r.success).toBe(true);
62
+ it("accepts the repo variant including github self", () => {
63
+ expect(
64
+ ReleasesJsonRepoSchema.safeParse({
65
+ version: 2,
66
+ product: { name: "Acme Cloud", slug: "acme-cloud" },
67
+ releases: [{ github: "self", canonical: true }],
68
+ registries: { "releases.sh": { product: "prd_abc123" } },
69
+ }).success,
70
+ ).toBe(true);
29
71
  });
30
72
 
31
- it("rejects a non-https avatar", () => {
32
- expect(ReleasesJsonConfigSchema.safeParse({ avatar: "http://acme.com/x.png" }).success).toBe(
33
- false,
34
- );
73
+ it("requires the version 2 literal with no v1 compatibility", () => {
74
+ expect(ReleasesJsonConfigSchema.safeParse({ releases: [location(1)] }).success).toBe(false);
75
+ expect(ReleasesJsonConfigSchema.safeParse({ version: 1 }).success).toBe(false);
76
+ expect(ReleasesJsonConfigSchema.safeParse({ version: 3 }).success).toBe(false);
35
77
  });
36
78
 
37
- it("rejects a product with no name", () => {
38
- expect(ReleasesJsonConfigSchema.safeParse({ product: { slug: "x" } }).success).toBe(false);
79
+ it("requires at least one locator per release location", () => {
80
+ expect(
81
+ ReleasesJsonDomainSchema.safeParse({ version: 2, releases: [{ title: "Updates" }] }).success,
82
+ ).toBe(false);
39
83
  });
40
84
 
41
- it("rejects an unknown product kind", () => {
85
+ it("allows github self only in repo files", () => {
42
86
  expect(
43
- ReleasesJsonConfigSchema.safeParse({ product: { name: "Acme Cloud", kind: "saas" } }).success,
87
+ ReleasesJsonDomainSchema.safeParse({ version: 2, releases: [{ github: "self" }] }).success,
44
88
  ).toBe(false);
89
+ expect(
90
+ ReleasesJsonRepoSchema.safeParse({ version: 2, releases: [{ github: "self" }] }).success,
91
+ ).toBe(true);
45
92
  });
46
93
 
47
- it("rejects an over-long notice message", () => {
48
- const r = ReleasesJsonConfigSchema.safeParse({ notice: { message: "x".repeat(281) } });
49
- expect(r.success).toBe(false);
94
+ it("enforces product and release-location caps", () => {
95
+ expect(
96
+ ReleasesJsonDomainSchema.safeParse({
97
+ version: 2,
98
+ products: Array.from({ length: 25 }, (_, i) => ({ name: `Product ${i}` })),
99
+ }).success,
100
+ ).toBe(false);
101
+ expect(
102
+ ReleasesJsonDomainSchema.safeParse({
103
+ version: 2,
104
+ products: [{ name: "Cloud", releases: Array.from({ length: 9 }, (_, i) => location(i)) }],
105
+ }).success,
106
+ ).toBe(false);
107
+ expect(
108
+ ReleasesJsonDomainSchema.safeParse({
109
+ version: 2,
110
+ releases: Array.from({ length: 33 }, (_, i) => location(i)),
111
+ }).success,
112
+ ).toBe(false);
113
+ expect(
114
+ ReleasesJsonDomainSchema.safeParse({
115
+ version: 2,
116
+ releases: Array.from({ length: 25 }, (_, i) => location(i)),
117
+ products: [
118
+ { name: "Cloud", releases: Array.from({ length: 8 }, (_, i) => location(i + 25)) },
119
+ ],
120
+ }).success,
121
+ ).toBe(false);
50
122
  });
51
123
 
52
- it("accepts an integer version", () => {
53
- expect(ReleasesJsonConfigSchema.safeParse({ version: 1, name: "Acme" }).success).toBe(true);
124
+ it("allows at most one canonical location per scope", () => {
125
+ const canonical = (n: number) => ({ ...location(n), canonical: true });
126
+ expect(
127
+ ReleasesJsonDomainSchema.safeParse({
128
+ version: 2,
129
+ releases: [canonical(1), canonical(2)],
130
+ }).success,
131
+ ).toBe(false);
132
+ expect(
133
+ ReleasesJsonDomainSchema.safeParse({
134
+ version: 2,
135
+ products: [{ name: "Cloud", releases: [canonical(1), canonical(2)] }],
136
+ }).success,
137
+ ).toBe(false);
54
138
  });
55
139
 
56
- it("rejects a zero, negative, or non-integer version", () => {
57
- expect(ReleasesJsonConfigSchema.safeParse({ version: 0 }).success).toBe(false);
58
- expect(ReleasesJsonConfigSchema.safeParse({ version: -1 }).success).toBe(false);
59
- expect(ReleasesJsonConfigSchema.safeParse({ version: 1.5 }).success).toBe(false);
140
+ it("rejects removed v1 identity fields", () => {
141
+ expect(
142
+ ReleasesJsonDomainSchema.safeParse({ version: 2, website: "https://acme.com" }).success,
143
+ ).toBe(false);
144
+ expect(
145
+ ReleasesJsonDomainSchema.safeParse({ version: 2, notice: { message: "Moved" } }).success,
146
+ ).toBe(false);
60
147
  });
61
148
 
62
- it("strips unknown top-level keys", () => {
63
- const r = ReleasesJsonConfigSchema.parse({ name: "Acme", bogus: 1 });
64
- expect("bogus" in r).toBe(false);
149
+ it("requires typed stable ids for the releases.sh registry", () => {
150
+ expect(
151
+ ReleasesJsonDomainSchema.safeParse({
152
+ version: 2,
153
+ registries: { "releases.sh": { org: "prd_wrong" } },
154
+ }).success,
155
+ ).toBe(false);
156
+ expect(
157
+ ReleasesJsonRepoSchema.safeParse({
158
+ version: 2,
159
+ registries: { "releases.sh": { product: "org_wrong" } },
160
+ }).success,
161
+ ).toBe(false);
65
162
  });
66
163
  });
@@ -1,54 +1,172 @@
1
1
  import { z } from "zod";
2
- import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
3
- import { NoticeSchema } from "./shared.js";
4
2
 
5
- /** A single social handle/URL. Bare handles allowed; URLs must be https. */
3
+ const MAX_PRODUCTS = 24;
4
+ const MAX_PRODUCT_RELEASES = 8;
5
+ const MAX_FILE_RELEASES = 32;
6
+
7
+ /** A single social handle/URL. Bare handles are intentionally accepted. */
6
8
  const SocialValueSchema = z.string().min(1).max(200);
9
+ const SocialSchema = z.record(z.string().min(1).max(40), SocialValueSchema);
7
10
 
8
- /** Product-scope block: declares the hosting repo's source belongs to this product. */
9
- export const ReleasesJsonProductSchema = z
10
- .object({
11
- name: z.string().min(1).max(120),
12
- slug: z.string().min(1).max(120).optional(),
13
- description: z.string().max(2000).optional(),
14
- // Accepted leniently as a string; resolved/validated against CATEGORIES at apply time.
15
- category: z.string().min(1).max(120).optional(),
16
- kind: z.enum(KIND_VALUES).optional(),
11
+ const HttpsUrlSchema = z
12
+ .url()
13
+ .refine((url) => url.startsWith("https://"), "locator must be an https URL");
14
+
15
+ const ReleaseLocationFields = {
16
+ url: HttpsUrlSchema.optional(),
17
+ feed: HttpsUrlSchema.optional(),
18
+ appstore: HttpsUrlSchema.optional(),
19
+ file: HttpsUrlSchema.optional(),
20
+ title: z.string().min(1).max(200).optional(),
21
+ canonical: z.boolean().optional(),
22
+ };
23
+
24
+ function hasLocator(value: {
25
+ url?: string;
26
+ feed?: string;
27
+ github?: string;
28
+ appstore?: string;
29
+ file?: string;
30
+ }): boolean {
31
+ return Boolean(value.url || value.feed || value.github || value.appstore || value.file);
32
+ }
33
+
34
+ /** A domain manifest can name a concrete GitHub repository, but never `self`. */
35
+ export const ReleasesJsonDomainReleaseSchema = z
36
+ .strictObject({
37
+ ...ReleaseLocationFields,
38
+ github: z
39
+ .string()
40
+ .regex(/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/, "github must be owner/repo")
41
+ .optional(),
17
42
  })
18
- .strict();
43
+ .refine(hasLocator, "at least one release locator is required");
44
+
45
+ /** A repo manifest additionally accepts `github: "self"`. */
46
+ export const ReleasesJsonRepoReleaseSchema = z
47
+ .strictObject({
48
+ ...ReleaseLocationFields,
49
+ github: z
50
+ .union([
51
+ z.literal("self"),
52
+ z.string().regex(/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/, "github must be owner/repo"),
53
+ ])
54
+ .optional(),
55
+ })
56
+ .refine(hasLocator, "at least one release locator is required");
57
+
58
+ function releasesArray<T extends z.ZodType>(item: T, max: number) {
59
+ return z
60
+ .array(item)
61
+ .max(max)
62
+ .refine(
63
+ (items) =>
64
+ items.filter((entry) => Boolean((entry as { canonical?: boolean }).canonical)).length <= 1,
65
+ "at most one canonical release location is allowed per scope",
66
+ );
67
+ }
68
+
69
+ const DomainProductReleasesSchema = releasesArray(
70
+ ReleasesJsonDomainReleaseSchema,
71
+ MAX_PRODUCT_RELEASES,
72
+ );
73
+ const RepoReleasesSchema = releasesArray(ReleasesJsonRepoReleaseSchema, MAX_FILE_RELEASES);
74
+ const DomainReleasesSchema = releasesArray(ReleasesJsonDomainReleaseSchema, MAX_FILE_RELEASES);
19
75
 
20
76
  /**
21
- * One file name, two hosting scopes. Org-identity keys are honored only from a
22
- * domain `.well-known/releases.json`; `product` is honored only from a repo-root
23
- * file. The server enforces which keys it honors based on the host the file came
24
- * from — this schema only validates shape.
77
+ * Product declaration in a domain manifest. Taxonomy strings are deliberately
78
+ * lenient here; the reconciler resolves known values and ignores the rest.
25
79
  */
26
- export const ReleasesJsonConfigSchema = z
80
+ export const ReleasesJsonProductSchema = z.strictObject({
81
+ name: z.string().min(1).max(120),
82
+ slug: z.string().min(1).max(120).optional(),
83
+ kind: z.string().min(1).max(120).optional(),
84
+ category: z.string().min(1).max(120).optional(),
85
+ description: z.string().max(2000).optional(),
86
+ website: z.url().optional(),
87
+ docs: z.url().optional(),
88
+ support: z.url().optional(),
89
+ social: SocialSchema.optional(),
90
+ archived: z.boolean().optional(),
91
+ releases: DomainProductReleasesSchema.optional(),
92
+ });
93
+
94
+ /** Repo-scope product binding. Slug is only a creation suggestion. */
95
+ export const ReleasesJsonRepoProductSchema = z.strictObject({
96
+ name: z.string().min(1).max(120),
97
+ slug: z.string().min(1).max(120).optional(),
98
+ });
99
+
100
+ const ReleasesRegistrySchema = z
101
+ .object({
102
+ org: z
103
+ .string()
104
+ .regex(/^org_[A-Za-z0-9]+$/)
105
+ .optional(),
106
+ product: z
107
+ .string()
108
+ .regex(/^prd_[A-Za-z0-9]+$/)
109
+ .optional(),
110
+ verification: z.string().min(1).max(500).optional(),
111
+ })
112
+ .catchall(z.unknown());
113
+
114
+ /** Registry-specific extensions are forward-compatible by design. */
115
+ export const ReleasesJsonRegistriesSchema = z
27
116
  .object({
28
- $schema: z.url().optional(),
29
- // Manifest format version. Currently 1; optional — a missing value is read as
30
- // v1. Bumped only on a breaking change to this file's shape, so a consumer can
31
- // branch on it without fetching the JSON Schema. Lenient/fail-open: the
32
- // reconciler ignores it entirely, so an unexpected value never fails the sync.
33
- version: z.number().int().positive().max(1000).optional(),
34
- // Org scope
117
+ "releases.sh": ReleasesRegistrySchema.optional(),
118
+ })
119
+ .catchall(z.unknown());
120
+
121
+ /** Top-level fields shared by every manifest scope. */
122
+ const BaseFields = {
123
+ $schema: z.url().optional(),
124
+ version: z.literal(2),
125
+ registries: ReleasesJsonRegistriesSchema.optional(),
126
+ };
127
+
128
+ const CommonFields = {
129
+ ...BaseFields,
130
+ releases: DomainReleasesSchema.optional(),
131
+ };
132
+
133
+ /** Domain-hosted variant: flat org identity plus product and release declarations. */
134
+ export const ReleasesJsonDomainSchema = z
135
+ .strictObject({
136
+ ...CommonFields,
35
137
  name: z.string().min(1).max(120).optional(),
36
138
  description: z.string().max(2000).optional(),
37
139
  category: z.string().min(1).max(120).optional(),
38
- avatar: z
39
- .url()
40
- .refine((u) => u.startsWith("https://"), "avatar must be an https URL")
41
- .optional(),
140
+ avatar: HttpsUrlSchema.optional(),
42
141
  tags: z.array(z.string().min(1).max(60)).max(50).optional(),
43
- social: z.record(z.string().min(1).max(40), SocialValueSchema).optional(),
44
- notice: NoticeSchema.optional(),
45
- // Source/product scope
46
- product: ReleasesJsonProductSchema.optional(),
142
+ social: SocialSchema.optional(),
143
+ products: z.array(ReleasesJsonProductSchema).max(MAX_PRODUCTS).optional(),
47
144
  })
48
- .strip();
145
+ .refine(
146
+ (manifest) =>
147
+ (manifest.releases?.length ?? 0) +
148
+ (manifest.products?.reduce((sum, product) => sum + (product.releases?.length ?? 0), 0) ??
149
+ 0) <=
150
+ MAX_FILE_RELEASES,
151
+ `at most ${MAX_FILE_RELEASES} release locations are allowed per file`,
152
+ );
153
+
154
+ /** Repo-root variant: product binding plus repo-scoped release declarations. */
155
+ export const ReleasesJsonRepoSchema = z.strictObject({
156
+ ...BaseFields,
157
+ product: ReleasesJsonRepoProductSchema.optional(),
158
+ releases: RepoReleasesSchema.optional(),
159
+ });
160
+
161
+ /** Public schema accepts either hosting scope; reconcilers use the scoped schema. */
162
+ export const ReleasesJsonConfigSchema = z.union([ReleasesJsonDomainSchema, ReleasesJsonRepoSchema]);
49
163
 
50
164
  export type ReleasesJsonConfig = z.infer<typeof ReleasesJsonConfigSchema>;
165
+ export type ReleasesJsonDomain = z.infer<typeof ReleasesJsonDomainSchema>;
166
+ export type ReleasesJsonRepo = z.infer<typeof ReleasesJsonRepoSchema>;
51
167
  export type ReleasesJsonProduct = z.infer<typeof ReleasesJsonProductSchema>;
168
+ export type ReleasesJsonDomainRelease = z.infer<typeof ReleasesJsonDomainReleaseSchema>;
169
+ export type ReleasesJsonRepoRelease = z.infer<typeof ReleasesJsonRepoReleaseSchema>;
52
170
 
53
171
  /** Response shape of POST /v1/orgs/:slug/sync-well-known. */
54
172
  export const SyncWellKnownResponseSchema = z.object({