@buildinternet/releases-api-types 0.29.0 → 0.31.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 +45 -0
- package/src/schemas/orgs.ts +13 -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,
|
|
@@ -32,6 +33,8 @@ import type {
|
|
|
32
33
|
OrgTagsResponseSchema,
|
|
33
34
|
CreateOrgBodySchema,
|
|
34
35
|
UpdateOrgBodySchema,
|
|
36
|
+
SetOrgAvatarBodySchema,
|
|
37
|
+
SetOrgAvatarResponseSchema,
|
|
35
38
|
OrgDetailSchema,
|
|
36
39
|
OrgCatalogItemSchema,
|
|
37
40
|
OrgCatalogResponseSchema,
|
|
@@ -260,6 +263,8 @@ export {
|
|
|
260
263
|
OrgTagsResponseSchema,
|
|
261
264
|
CreateOrgBodySchema,
|
|
262
265
|
UpdateOrgBodySchema,
|
|
266
|
+
SetOrgAvatarBodySchema,
|
|
267
|
+
SetOrgAvatarResponseSchema,
|
|
263
268
|
OrgDetailSchema,
|
|
264
269
|
OrgCatalogItemSchema,
|
|
265
270
|
OrgCatalogResponseSchema,
|
|
@@ -284,6 +289,12 @@ export {
|
|
|
284
289
|
OrgRecentReleasesResponseSchema,
|
|
285
290
|
DeleteOrgAccountResponseSchema,
|
|
286
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";
|
|
287
298
|
export {
|
|
288
299
|
SourceListItemSchema,
|
|
289
300
|
SourceWithOrgSchema,
|
|
@@ -485,6 +496,38 @@ export interface ListResponse<T> {
|
|
|
485
496
|
pagination: Pagination;
|
|
486
497
|
}
|
|
487
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
|
+
|
|
488
531
|
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
489
532
|
|
|
490
533
|
// ── Sitemap (bulk URL emission) ──
|
|
@@ -515,6 +558,8 @@ export type OrgAccountsResponse = z.infer<typeof OrgAccountsResponseSchema>;
|
|
|
515
558
|
export type OrgTagsResponse = z.infer<typeof OrgTagsResponseSchema>;
|
|
516
559
|
export type CreateOrgBody = z.infer<typeof CreateOrgBodySchema>;
|
|
517
560
|
export type UpdateOrgBody = z.infer<typeof UpdateOrgBodySchema>;
|
|
561
|
+
export type SetOrgAvatarBody = z.infer<typeof SetOrgAvatarBodySchema>;
|
|
562
|
+
export type SetOrgAvatarResponse = z.infer<typeof SetOrgAvatarResponseSchema>;
|
|
518
563
|
export type OrgDetail = z.infer<typeof OrgDetailSchema>;
|
|
519
564
|
|
|
520
565
|
export type OrgCatalogItem = z.infer<typeof OrgCatalogItemSchema>;
|
package/src/schemas/orgs.ts
CHANGED
|
@@ -118,6 +118,19 @@ export const UpdateOrgBodySchema = z.object({
|
|
|
118
118
|
notice: NoticeSchema.nullable().optional(),
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
/** Body for `POST /v1/orgs/:slug/avatar` — the remote image to mirror to R2 (#1406). */
|
|
122
|
+
export const SetOrgAvatarBodySchema = z.object({
|
|
123
|
+
sourceUrl: z.url(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/** Response for `POST /v1/orgs/:slug/avatar` — the mirrored avatar's public URL + dims. */
|
|
127
|
+
export const SetOrgAvatarResponseSchema = z.object({
|
|
128
|
+
avatarUrl: z.string(),
|
|
129
|
+
key: z.string(),
|
|
130
|
+
width: z.number(),
|
|
131
|
+
height: z.number(),
|
|
132
|
+
});
|
|
133
|
+
|
|
121
134
|
// Org detail's products query selects a strict subset of `ProductListItem` —
|
|
122
135
|
// no category/orgId because the parent already names the org; createdAt is
|
|
123
136
|
// included for age-based filtering by consumers.
|
|
@@ -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
|
+
});
|