@buildinternet/releases-api-types 0.30.0 → 0.32.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 +89 -1
- package/src/schemas/releases.ts +14 -0
- package/src/schemas/well-known.test.ts +55 -0
- package/src/schemas/well-known.ts +54 -0
package/package.json
CHANGED
package/src/api-types.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type {
|
|
|
12
12
|
} from "@buildinternet/releases-core/source-enums";
|
|
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
|
+
import type { ApiScope } from "@buildinternet/releases-core/api-token";
|
|
15
16
|
import type {
|
|
16
17
|
MediaItemSchema,
|
|
17
18
|
PaginationSchema,
|
|
@@ -288,6 +289,12 @@ export {
|
|
|
288
289
|
OrgRecentReleasesResponseSchema,
|
|
289
290
|
DeleteOrgAccountResponseSchema,
|
|
290
291
|
} from "./schemas/orgs.js";
|
|
292
|
+
export {
|
|
293
|
+
ReleasesJsonConfigSchema,
|
|
294
|
+
ReleasesJsonProductSchema,
|
|
295
|
+
SyncWellKnownResponseSchema,
|
|
296
|
+
} from "./schemas/well-known.js";
|
|
297
|
+
export type { ReleasesJsonConfig, ReleasesJsonProduct } from "./schemas/well-known.js";
|
|
291
298
|
export {
|
|
292
299
|
SourceListItemSchema,
|
|
293
300
|
SourceWithOrgSchema,
|
|
@@ -489,6 +496,38 @@ export interface ListResponse<T> {
|
|
|
489
496
|
pagination: Pagination;
|
|
490
497
|
}
|
|
491
498
|
|
|
499
|
+
// === User-owned API keys (relu_) — self-serve surface served by /v1/api-keys ===
|
|
500
|
+
|
|
501
|
+
/** A user-owned API key (relu_) as returned by GET /v1/api-keys. */
|
|
502
|
+
export interface UserApiKey {
|
|
503
|
+
id: string;
|
|
504
|
+
name: string | null;
|
|
505
|
+
start: string | null;
|
|
506
|
+
scope: ApiScope | null;
|
|
507
|
+
enabled: boolean | null;
|
|
508
|
+
remaining: number | null;
|
|
509
|
+
lastRequest: string | null; // ISO 8601
|
|
510
|
+
createdAt: string; // ISO 8601
|
|
511
|
+
expiresAt: string | null; // ISO 8601
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** POST /v1/api-keys create response — includes the full key string exactly once. */
|
|
515
|
+
export interface CreatedUserApiKey extends Omit<UserApiKey, "enabled" | "lastRequest"> {
|
|
516
|
+
key: string;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** GET /v1/api-keys response envelope. */
|
|
520
|
+
export interface ListUserApiKeysResponse {
|
|
521
|
+
apiKeys: UserApiKey[];
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** POST /v1/api-keys request body. Self-serve mints are capped at read server-side. */
|
|
525
|
+
export interface CreateUserApiKeyBody {
|
|
526
|
+
name: string;
|
|
527
|
+
scope?: ApiScope;
|
|
528
|
+
expiresInDays?: number;
|
|
529
|
+
}
|
|
530
|
+
|
|
492
531
|
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
493
532
|
|
|
494
533
|
// ── Sitemap (bulk URL emission) ──
|
|
@@ -709,7 +748,16 @@ export interface ReleaseDetail {
|
|
|
709
748
|
sourceName: string;
|
|
710
749
|
sourceSlug: string;
|
|
711
750
|
sourceType: string;
|
|
712
|
-
|
|
751
|
+
/**
|
|
752
|
+
* Owning org. `avatarUrl` is the resolved `organizations.avatar_url`
|
|
753
|
+
* (nullable; `.optional()` for mid-deploy / pinned-worker tolerance).
|
|
754
|
+
*/
|
|
755
|
+
org: { slug: string; name: string; avatarUrl?: string | null } | null;
|
|
756
|
+
/**
|
|
757
|
+
* Owning product, when the source is grouped under one (`sources.product_id`).
|
|
758
|
+
* `null` when ungrouped; `.optional()` so older servers that omit it still type.
|
|
759
|
+
*/
|
|
760
|
+
product?: { slug: string; name: string } | null;
|
|
713
761
|
/** Release type. See {@link ReleaseType}. */
|
|
714
762
|
type?: ReleaseType;
|
|
715
763
|
/** Per-category item counts from the AI release-content pass. See {@link ReleaseComposition}. */
|
|
@@ -747,6 +795,46 @@ export type ReleaseStreamMessage = z.infer<typeof ReleaseStreamMessageSchema>;
|
|
|
747
795
|
|
|
748
796
|
export type ReleaseSummaryItem = z.infer<typeof ReleaseSummaryItemSchema>;
|
|
749
797
|
|
|
798
|
+
// ── Follows ──
|
|
799
|
+
|
|
800
|
+
/** What a user can follow. */
|
|
801
|
+
export type FollowTarget = "org" | "product";
|
|
802
|
+
|
|
803
|
+
/** A user's follow, enriched for rendering (returned by GET /v1/me/follows). */
|
|
804
|
+
export interface Follow {
|
|
805
|
+
targetType: FollowTarget;
|
|
806
|
+
targetId: string;
|
|
807
|
+
name: string;
|
|
808
|
+
slug: string;
|
|
809
|
+
avatarUrl: string | null;
|
|
810
|
+
/** Owning org slug for product follows (null for org follows). */
|
|
811
|
+
orgSlug: string | null;
|
|
812
|
+
createdAt: string;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** GET /v1/me/follows response. */
|
|
816
|
+
export interface FollowsListResponse {
|
|
817
|
+
follows: Follow[];
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** POST /v1/me/follows request body. */
|
|
821
|
+
export interface FollowRequest {
|
|
822
|
+
targetType: FollowTarget;
|
|
823
|
+
targetId: string;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** POST/DELETE /v1/me/follows response. */
|
|
827
|
+
export interface FollowMutationResponse {
|
|
828
|
+
success: true;
|
|
829
|
+
following: boolean;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* GET /v1/me/feed response — the personalized release feed. Reuses the same item
|
|
834
|
+
* shape as /v1/releases/latest (ReleaseLatestItem) inside the standard list envelope.
|
|
835
|
+
*/
|
|
836
|
+
export type PersonalizedFeedResponse = ListResponse<ReleaseLatestItem>;
|
|
837
|
+
|
|
750
838
|
// ── Search ──
|
|
751
839
|
|
|
752
840
|
export type SearchOrgHit = z.infer<typeof SearchOrgHitSchema>;
|
package/src/schemas/releases.ts
CHANGED
|
@@ -168,6 +168,13 @@ export const ReleasesWithMediaResponseSchema = z.array(ReleaseWithMediaRowSchema
|
|
|
168
168
|
export const ReleaseDetailOrgSchema = z.object({
|
|
169
169
|
slug: z.string(),
|
|
170
170
|
name: z.string(),
|
|
171
|
+
/**
|
|
172
|
+
* Resolved org avatar URL (the `organizations.avatar_url` column). `null`
|
|
173
|
+
* when the org has no stored avatar. Additive — older servers omit it, so
|
|
174
|
+
* treat `undefined` as `null`. Mirrors the `orgAvatarUrl` field on the
|
|
175
|
+
* related-rail feed.
|
|
176
|
+
*/
|
|
177
|
+
avatarUrl: z.string().nullable().optional(),
|
|
171
178
|
});
|
|
172
179
|
|
|
173
180
|
/**
|
|
@@ -199,6 +206,13 @@ export const ReleaseDetailResponseSchema = z.object({
|
|
|
199
206
|
sourceSlug: z.string().nullable(),
|
|
200
207
|
sourceType: z.enum(SOURCE_TYPES).nullable(),
|
|
201
208
|
org: ReleaseDetailOrgSchema.nullable(),
|
|
209
|
+
/**
|
|
210
|
+
* Owning product, when the release's source is grouped under a product
|
|
211
|
+
* (`sources.product_id`). `null` when the source has no product. Additive —
|
|
212
|
+
* older servers omit this field; treat `undefined` as `null`. Reuses the
|
|
213
|
+
* `{ slug, name }` shape from the latest feed.
|
|
214
|
+
*/
|
|
215
|
+
product: ReleaseLatestProductSchema.nullable().optional(),
|
|
202
216
|
composition: ReleaseCompositionSchema.nullable(),
|
|
203
217
|
appStore: AppStoreSourceInfoSchema.nullable().optional(),
|
|
204
218
|
video: VideoSourceInfoSchema.nullable().optional(),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { ReleasesJsonConfigSchema } from "./well-known.js";
|
|
3
|
+
|
|
4
|
+
describe("ReleasesJsonConfigSchema", () => {
|
|
5
|
+
it("accepts an empty object (no-op file)", () => {
|
|
6
|
+
expect(ReleasesJsonConfigSchema.safeParse({}).success).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("accepts a full org-scope file", () => {
|
|
10
|
+
const r = ReleasesJsonConfigSchema.safeParse({
|
|
11
|
+
$schema: "https://releases.sh/schemas/releases.json",
|
|
12
|
+
name: "Acme",
|
|
13
|
+
description: "CI for teams.",
|
|
14
|
+
category: "developer-tools",
|
|
15
|
+
avatar: "https://acme.com/logo.png",
|
|
16
|
+
tags: ["ci", "observability"],
|
|
17
|
+
social: { twitter: "acmehq", github: "acme" },
|
|
18
|
+
notice: { message: "Docs moved", href: "https://acme.com/docs" },
|
|
19
|
+
});
|
|
20
|
+
expect(r.success).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("accepts a product-scope file", () => {
|
|
24
|
+
const r = ReleasesJsonConfigSchema.safeParse({
|
|
25
|
+
product: { name: "Acme Cloud", slug: "acme-cloud", category: "cloud", kind: "platform" },
|
|
26
|
+
});
|
|
27
|
+
expect(r.success).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("rejects a non-https avatar", () => {
|
|
31
|
+
expect(ReleasesJsonConfigSchema.safeParse({ avatar: "http://acme.com/x.png" }).success).toBe(
|
|
32
|
+
false,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("rejects a product with no name", () => {
|
|
37
|
+
expect(ReleasesJsonConfigSchema.safeParse({ product: { slug: "x" } }).success).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rejects an unknown product kind", () => {
|
|
41
|
+
expect(
|
|
42
|
+
ReleasesJsonConfigSchema.safeParse({ product: { name: "Acme Cloud", kind: "saas" } }).success,
|
|
43
|
+
).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects an over-long notice message", () => {
|
|
47
|
+
const r = ReleasesJsonConfigSchema.safeParse({ notice: { message: "x".repeat(281) } });
|
|
48
|
+
expect(r.success).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("strips unknown top-level keys", () => {
|
|
52
|
+
const r = ReleasesJsonConfigSchema.parse({ name: "Acme", bogus: 1 });
|
|
53
|
+
expect("bogus" in r).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { KIND_VALUES } from "@buildinternet/releases-core/kinds";
|
|
3
|
+
import { NoticeSchema } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
/** A single social handle/URL. Bare handles allowed; URLs must be https. */
|
|
6
|
+
const SocialValueSchema = z.string().min(1).max(200);
|
|
7
|
+
|
|
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(),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
|
|
20
|
+
/**
|
|
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.
|
|
25
|
+
*/
|
|
26
|
+
export const ReleasesJsonConfigSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
$schema: z.url().optional(),
|
|
29
|
+
// Org scope
|
|
30
|
+
name: z.string().min(1).max(120).optional(),
|
|
31
|
+
description: z.string().max(2000).optional(),
|
|
32
|
+
category: z.string().min(1).max(120).optional(),
|
|
33
|
+
avatar: z
|
|
34
|
+
.url()
|
|
35
|
+
.refine((u) => u.startsWith("https://"), "avatar must be an https URL")
|
|
36
|
+
.optional(),
|
|
37
|
+
tags: z.array(z.string().min(1).max(60)).max(50).optional(),
|
|
38
|
+
social: z.record(z.string().min(1).max(40), SocialValueSchema).optional(),
|
|
39
|
+
notice: NoticeSchema.optional(),
|
|
40
|
+
// Source/product scope
|
|
41
|
+
product: ReleasesJsonProductSchema.optional(),
|
|
42
|
+
})
|
|
43
|
+
.strip();
|
|
44
|
+
|
|
45
|
+
export type ReleasesJsonConfig = z.infer<typeof ReleasesJsonConfigSchema>;
|
|
46
|
+
export type ReleasesJsonProduct = z.infer<typeof ReleasesJsonProductSchema>;
|
|
47
|
+
|
|
48
|
+
/** Response shape of POST /v1/orgs/:slug/sync-well-known. */
|
|
49
|
+
export const SyncWellKnownResponseSchema = z.object({
|
|
50
|
+
fetched: z.boolean(),
|
|
51
|
+
applied: z.boolean(),
|
|
52
|
+
skippedReason: z.string().optional(),
|
|
53
|
+
plan: z.unknown().optional(),
|
|
54
|
+
});
|