@read-frog/api-contract 0.24.0 → 0.25.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/dist/index.d.ts +575 -77
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +281 -105
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/contracts/ebook.ts +7 -15
- package/src/contracts/mcp-connection.ts +11 -0
- package/src/contracts/media.ts +60 -0
- package/src/contracts/telemetry.ts +17 -0
- package/src/index.ts +6 -0
- package/src/public-errors.ts +20 -4
- package/src/schemas/billing.ts +16 -0
- package/src/schemas/ebook.ts +7 -0
- package/src/schemas/mcp-connection.ts +17 -0
- package/src/schemas/media.ts +77 -0
- package/src/schemas/srs.ts +3 -1
- package/src/schemas/web-sync.ts +121 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@read-frog/api-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@orpc/contract": "^1.
|
|
28
|
-
"@read-frog/definitions": "0.
|
|
29
|
-
"zod": "^4.6.
|
|
27
|
+
"@orpc/contract": "^1.16.1",
|
|
28
|
+
"@read-frog/definitions": "0.13.0",
|
|
29
|
+
"zod": "^4.6.7"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@repo/tsdown-config": "0.0.0",
|
|
33
33
|
"@repo/typescript-config": "0.0.0",
|
|
34
|
-
"@types/node": "^26.6.
|
|
35
|
-
"tsdown": "^0.23.
|
|
34
|
+
"@types/node": "^26.6.6",
|
|
35
|
+
"tsdown": "^0.23.4"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsdown",
|
package/src/contracts/ebook.ts
CHANGED
|
@@ -23,7 +23,6 @@ const ebookProcedure = oc;
|
|
|
23
23
|
|
|
24
24
|
export const ebookContract = {
|
|
25
25
|
list: ebookProcedure
|
|
26
|
-
.errors(pickPublicErrorMap("EBOOK_BETA_RESTRICTED"))
|
|
27
26
|
.route({
|
|
28
27
|
method: "GET",
|
|
29
28
|
path: "/ebooks",
|
|
@@ -34,7 +33,7 @@ export const ebookContract = {
|
|
|
34
33
|
.output(ebookListOutputSchema),
|
|
35
34
|
|
|
36
35
|
get: ebookProcedure
|
|
37
|
-
.errors(pickPublicErrorMap("
|
|
36
|
+
.errors(pickPublicErrorMap("EBOOK_NOT_FOUND"))
|
|
38
37
|
.route({
|
|
39
38
|
method: "GET",
|
|
40
39
|
path: "/ebooks/{id}",
|
|
@@ -45,9 +44,7 @@ export const ebookContract = {
|
|
|
45
44
|
.output(ebookGetOutputSchema),
|
|
46
45
|
|
|
47
46
|
requestUpload: ebookProcedure
|
|
48
|
-
.errors(
|
|
49
|
-
pickPublicErrorMap("EBOOK_BETA_RESTRICTED", "EBOOK_LIMIT_EXCEEDED", "EBOOK_FILE_TOO_LARGE"),
|
|
50
|
-
)
|
|
47
|
+
.errors(pickPublicErrorMap("EBOOK_LIMIT_EXCEEDED", "EBOOK_FILE_TOO_LARGE"))
|
|
51
48
|
.route({
|
|
52
49
|
method: "POST",
|
|
53
50
|
path: "/ebooks/uploads",
|
|
@@ -59,12 +56,7 @@ export const ebookContract = {
|
|
|
59
56
|
|
|
60
57
|
finalizeUpload: ebookProcedure
|
|
61
58
|
.errors(
|
|
62
|
-
pickPublicErrorMap(
|
|
63
|
-
"EBOOK_BETA_RESTRICTED",
|
|
64
|
-
"EBOOK_NOT_FOUND",
|
|
65
|
-
"EBOOK_UPLOAD_NOT_COMPLETED",
|
|
66
|
-
"EBOOK_FILE_TOO_LARGE",
|
|
67
|
-
),
|
|
59
|
+
pickPublicErrorMap("EBOOK_NOT_FOUND", "EBOOK_UPLOAD_NOT_COMPLETED", "EBOOK_FILE_TOO_LARGE"),
|
|
68
60
|
)
|
|
69
61
|
.route({
|
|
70
62
|
method: "POST",
|
|
@@ -76,7 +68,7 @@ export const ebookContract = {
|
|
|
76
68
|
.output(ebookFinalizeUploadOutputSchema),
|
|
77
69
|
|
|
78
70
|
getDownloadUrl: ebookProcedure
|
|
79
|
-
.errors(pickPublicErrorMap("
|
|
71
|
+
.errors(pickPublicErrorMap("EBOOK_NOT_FOUND"))
|
|
80
72
|
.route({
|
|
81
73
|
method: "GET",
|
|
82
74
|
path: "/ebooks/{id}/download-url",
|
|
@@ -87,7 +79,7 @@ export const ebookContract = {
|
|
|
87
79
|
.output(ebookGetDownloadUrlOutputSchema),
|
|
88
80
|
|
|
89
81
|
updateProgress: ebookProcedure
|
|
90
|
-
.errors(pickPublicErrorMap("
|
|
82
|
+
.errors(pickPublicErrorMap("EBOOK_NOT_FOUND"))
|
|
91
83
|
.route({
|
|
92
84
|
method: "PATCH",
|
|
93
85
|
path: "/ebooks/{id}/progress",
|
|
@@ -98,7 +90,7 @@ export const ebookContract = {
|
|
|
98
90
|
.output(ebookUpdateProgressOutputSchema),
|
|
99
91
|
|
|
100
92
|
rename: ebookProcedure
|
|
101
|
-
.errors(pickPublicErrorMap("
|
|
93
|
+
.errors(pickPublicErrorMap("EBOOK_NOT_FOUND"))
|
|
102
94
|
.route({
|
|
103
95
|
method: "PATCH",
|
|
104
96
|
path: "/ebooks/{id}",
|
|
@@ -109,7 +101,7 @@ export const ebookContract = {
|
|
|
109
101
|
.output(ebookRenameOutputSchema),
|
|
110
102
|
|
|
111
103
|
delete: ebookProcedure
|
|
112
|
-
.errors(pickPublicErrorMap("
|
|
104
|
+
.errors(pickPublicErrorMap("EBOOK_NOT_FOUND"))
|
|
113
105
|
.route({
|
|
114
106
|
method: "DELETE",
|
|
115
107
|
path: "/ebooks/{id}",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { oc } from "@orpc/contract";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import {
|
|
4
|
+
mcpCallUsageSchema,
|
|
3
5
|
mcpConnectionListOutputSchema,
|
|
4
6
|
mcpConnectionRevokeInputSchema,
|
|
5
7
|
mcpConnectionRevokeOutputSchema,
|
|
@@ -23,4 +25,13 @@ export const mcpConnectionContract = {
|
|
|
23
25
|
})
|
|
24
26
|
.input(mcpConnectionRevokeInputSchema)
|
|
25
27
|
.output(mcpConnectionRevokeOutputSchema),
|
|
28
|
+
usage: oc
|
|
29
|
+
.route({
|
|
30
|
+
method: "GET",
|
|
31
|
+
path: "/mcp-connections/usage",
|
|
32
|
+
summary: "Get the MCP tool calls the caller has used of their plan's daily limit",
|
|
33
|
+
tags: ["MCP Connections"],
|
|
34
|
+
})
|
|
35
|
+
.input(z.object({}).strict())
|
|
36
|
+
.output(mcpCallUsageSchema),
|
|
26
37
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { pickPublicErrorMap } from "#/public-errors";
|
|
4
|
+
import {
|
|
5
|
+
mediaAssetIdInputSchema,
|
|
6
|
+
mediaAssetOutputSchema,
|
|
7
|
+
mediaRequestUploadInputSchema,
|
|
8
|
+
mediaRequestUploadOutputSchema,
|
|
9
|
+
mediaUsageOutputSchema,
|
|
10
|
+
} from "#/schemas/media";
|
|
11
|
+
|
|
12
|
+
export const mediaContract = {
|
|
13
|
+
requestUpload: oc
|
|
14
|
+
.errors(
|
|
15
|
+
pickPublicErrorMap("MEDIA_BETA_RESTRICTED", "MEDIA_FILE_TOO_LARGE", "MEDIA_QUOTA_EXCEEDED"),
|
|
16
|
+
)
|
|
17
|
+
.route({
|
|
18
|
+
method: "POST",
|
|
19
|
+
path: "/media/uploads",
|
|
20
|
+
summary: "Reserve space and get a signed upload URL for a media file",
|
|
21
|
+
tags: ["Media"],
|
|
22
|
+
})
|
|
23
|
+
.input(mediaRequestUploadInputSchema)
|
|
24
|
+
.output(mediaRequestUploadOutputSchema),
|
|
25
|
+
|
|
26
|
+
completeUpload: oc
|
|
27
|
+
.errors(
|
|
28
|
+
pickPublicErrorMap("MEDIA_BETA_RESTRICTED", "MEDIA_NOT_FOUND", "MEDIA_UPLOAD_NOT_COMPLETED"),
|
|
29
|
+
)
|
|
30
|
+
.route({
|
|
31
|
+
method: "POST",
|
|
32
|
+
path: "/media/{id}/complete",
|
|
33
|
+
summary: "Queue an uploaded media file for validation",
|
|
34
|
+
tags: ["Media"],
|
|
35
|
+
})
|
|
36
|
+
.input(mediaAssetIdInputSchema)
|
|
37
|
+
.output(mediaAssetOutputSchema),
|
|
38
|
+
|
|
39
|
+
get: oc
|
|
40
|
+
.errors(pickPublicErrorMap("MEDIA_BETA_RESTRICTED", "MEDIA_NOT_FOUND"))
|
|
41
|
+
.route({
|
|
42
|
+
method: "GET",
|
|
43
|
+
path: "/media/{id}",
|
|
44
|
+
summary: "Get a media asset and its upload status",
|
|
45
|
+
tags: ["Media"],
|
|
46
|
+
})
|
|
47
|
+
.input(mediaAssetIdInputSchema)
|
|
48
|
+
.output(mediaAssetOutputSchema),
|
|
49
|
+
|
|
50
|
+
usage: oc
|
|
51
|
+
.errors(pickPublicErrorMap("MEDIA_BETA_RESTRICTED"))
|
|
52
|
+
.route({
|
|
53
|
+
method: "GET",
|
|
54
|
+
path: "/media/usage",
|
|
55
|
+
summary: "Get the media space used, reserved and allowed",
|
|
56
|
+
tags: ["Media"],
|
|
57
|
+
})
|
|
58
|
+
.input(z.object({}).strict())
|
|
59
|
+
.output(mediaUsageOutputSchema),
|
|
60
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { reportWebSyncInputSchema } from "#/schemas/web-sync";
|
|
4
|
+
|
|
5
|
+
export const telemetryContract = {
|
|
6
|
+
// Public: a page whose session ended still reports the streams that failed
|
|
7
|
+
// because of it.
|
|
8
|
+
reportWebSync: oc
|
|
9
|
+
.route({
|
|
10
|
+
method: "POST",
|
|
11
|
+
path: "/telemetry/web-sync",
|
|
12
|
+
summary: "Report problems the web app had keeping its synced data live",
|
|
13
|
+
tags: ["Telemetry"],
|
|
14
|
+
})
|
|
15
|
+
.input(reportWebSyncInputSchema)
|
|
16
|
+
.output(z.void()),
|
|
17
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { featureAccessContract } from "./contracts/feature-access";
|
|
|
8
8
|
import { hostedAiContract } from "./contracts/hosted-ai";
|
|
9
9
|
import { masteryGoalContract } from "./contracts/mastery-goal";
|
|
10
10
|
import { mcpConnectionContract } from "./contracts/mcp-connection";
|
|
11
|
+
import { mediaContract } from "./contracts/media";
|
|
11
12
|
import { notebaseContract } from "./contracts/notebase";
|
|
12
13
|
import { notebaseColumnContract } from "./contracts/notebase-column";
|
|
13
14
|
import { notebaseRowContract } from "./contracts/notebase-row";
|
|
@@ -15,6 +16,7 @@ import { pdfTranslationContract } from "./contracts/pdf-translation";
|
|
|
15
16
|
import { speechContract } from "./contracts/speech";
|
|
16
17
|
import { srsContract } from "./contracts/srs";
|
|
17
18
|
import { statsContract } from "./contracts/stats";
|
|
19
|
+
import { telemetryContract } from "./contracts/telemetry";
|
|
18
20
|
import { userContract } from "./contracts/user";
|
|
19
21
|
import { videoTranscriptContract } from "./contracts/video-transcript";
|
|
20
22
|
|
|
@@ -28,6 +30,7 @@ export const contract = {
|
|
|
28
30
|
hostedAi: hostedAiContract,
|
|
29
31
|
masteryGoal: masteryGoalContract,
|
|
30
32
|
mcpConnection: mcpConnectionContract,
|
|
33
|
+
media: mediaContract,
|
|
31
34
|
notebase: notebaseContract,
|
|
32
35
|
notebaseColumn: notebaseColumnContract,
|
|
33
36
|
notebaseRow: notebaseRowContract,
|
|
@@ -35,6 +38,7 @@ export const contract = {
|
|
|
35
38
|
speech: speechContract,
|
|
36
39
|
srs: srsContract,
|
|
37
40
|
stats: statsContract,
|
|
41
|
+
telemetry: telemetryContract,
|
|
38
42
|
user: userContract,
|
|
39
43
|
videoTranscript: videoTranscriptContract,
|
|
40
44
|
};
|
|
@@ -50,6 +54,7 @@ export * from "./schemas/ebook";
|
|
|
50
54
|
export * from "./schemas/email-preferences";
|
|
51
55
|
export * from "./schemas/feature-access";
|
|
52
56
|
export * from "./schemas/hosted-ai";
|
|
57
|
+
export * from "./schemas/media";
|
|
53
58
|
export * from "./schemas/mastery-goal";
|
|
54
59
|
export * from "./schemas/mcp-connection";
|
|
55
60
|
export * from "./schemas/notebase";
|
|
@@ -65,6 +70,7 @@ export * from "./schemas/stats";
|
|
|
65
70
|
export * from "./schemas/timezone";
|
|
66
71
|
export * from "./schemas/user";
|
|
67
72
|
export * from "./schemas/video-transcript";
|
|
73
|
+
export * from "./schemas/web-sync";
|
|
68
74
|
|
|
69
75
|
export * from "./schemas/pagination";
|
|
70
76
|
export * from "./schemas/resource-mutation";
|
package/src/public-errors.ts
CHANGED
|
@@ -289,10 +289,6 @@ export const PUBLIC_APP_ERROR_DEFS = {
|
|
|
289
289
|
message: "Concurrent position conflict, please retry",
|
|
290
290
|
status: 409,
|
|
291
291
|
},
|
|
292
|
-
EBOOK_BETA_RESTRICTED: {
|
|
293
|
-
message: "Ebooks are currently in beta for selected accounts",
|
|
294
|
-
status: 403,
|
|
295
|
-
},
|
|
296
292
|
EBOOK_NOT_FOUND: {
|
|
297
293
|
message: "Ebook not found",
|
|
298
294
|
status: 404,
|
|
@@ -309,6 +305,26 @@ export const PUBLIC_APP_ERROR_DEFS = {
|
|
|
309
305
|
message: "Ebook upload has not been completed",
|
|
310
306
|
status: 409,
|
|
311
307
|
},
|
|
308
|
+
MEDIA_BETA_RESTRICTED: {
|
|
309
|
+
message: "Media uploads are currently in beta for selected accounts",
|
|
310
|
+
status: 403,
|
|
311
|
+
},
|
|
312
|
+
MEDIA_NOT_FOUND: {
|
|
313
|
+
message: "Media file not found",
|
|
314
|
+
status: 404,
|
|
315
|
+
},
|
|
316
|
+
MEDIA_FILE_TOO_LARGE: {
|
|
317
|
+
message: "Media file exceeds the maximum allowed size",
|
|
318
|
+
status: 413,
|
|
319
|
+
},
|
|
320
|
+
MEDIA_QUOTA_EXCEEDED: {
|
|
321
|
+
message: "Not enough media storage space left on your plan",
|
|
322
|
+
status: 403,
|
|
323
|
+
},
|
|
324
|
+
MEDIA_UPLOAD_NOT_COMPLETED: {
|
|
325
|
+
message: "Media upload has not been completed",
|
|
326
|
+
status: 409,
|
|
327
|
+
},
|
|
312
328
|
// An email's unsubscribe or preferences link that does not verify, or whose
|
|
313
329
|
// account no longer exists. The page asks the reader to sign in instead.
|
|
314
330
|
EMAIL_LINK_INVALID: {
|
package/src/schemas/billing.ts
CHANGED
|
@@ -37,6 +37,22 @@ export const MCP_DAILY_CALL_LIMITS = {
|
|
|
37
37
|
ultra: null,
|
|
38
38
|
} as const satisfies Record<BillingPlan, number | null>;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* How many ebooks each plan may keep at once. Free keeps one, so it reads one
|
|
42
|
+
* book at a time; deleting a book frees its place for the next.
|
|
43
|
+
*
|
|
44
|
+
* Read by the API's upload gate AND by the website's copy (the pricing page),
|
|
45
|
+
* so the pages cannot promise books the server refuses.
|
|
46
|
+
*
|
|
47
|
+
* Companion edit when this changes: `account-and-plans` under
|
|
48
|
+
* `apps/www/content/docs/`, which states the numbers.
|
|
49
|
+
*/
|
|
50
|
+
export const EBOOK_LIMIT_BY_PLAN = {
|
|
51
|
+
free: 1,
|
|
52
|
+
pro: 10,
|
|
53
|
+
ultra: 80,
|
|
54
|
+
} as const satisfies Record<BillingPlan, number>;
|
|
55
|
+
|
|
40
56
|
// Plans a user can actively check out into (the free tier has no price).
|
|
41
57
|
export const billingCheckoutPlanSchema = z.enum(["pro", "ultra"]);
|
|
42
58
|
export type BillingCheckoutPlan = z.infer<typeof billingCheckoutPlanSchema>;
|
package/src/schemas/ebook.ts
CHANGED
|
@@ -77,6 +77,13 @@ export const ebookRequestUploadInputSchema = z
|
|
|
77
77
|
})
|
|
78
78
|
.strict()
|
|
79
79
|
.optional(),
|
|
80
|
+
/**
|
|
81
|
+
* The client's key for this upload. Asking again with the same key, after
|
|
82
|
+
* an answer that never arrived or for a retry, returns the same pending
|
|
83
|
+
* book with fresh targets instead of reserving another place in the
|
|
84
|
+
* library's allowance.
|
|
85
|
+
*/
|
|
86
|
+
uploadKey: z.uuid().optional(),
|
|
80
87
|
})
|
|
81
88
|
.strict();
|
|
82
89
|
export type EbookRequestUploadInput = z.infer<typeof ebookRequestUploadInputSchema>;
|
|
@@ -26,3 +26,20 @@ export const mcpConnectionRevokeInputSchema = z.object({ clientId: z.string().mi
|
|
|
26
26
|
export type McpConnectionRevokeInput = z.infer<typeof mcpConnectionRevokeInputSchema>;
|
|
27
27
|
|
|
28
28
|
export const mcpConnectionRevokeOutputSchema = z.object({ clientId: z.string() }).strict();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How many MCP tool calls the caller has used of their plan's
|
|
32
|
+
* `MCP_DAILY_CALL_LIMITS`, read without counting one. The window is 24 hours
|
|
33
|
+
* from the first call in it, not a calendar day.
|
|
34
|
+
*/
|
|
35
|
+
export const mcpCallUsageSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
/** Calls used in the current window, at most `limit`. */
|
|
38
|
+
usedCalls: z.number().int().min(0),
|
|
39
|
+
/** null: the plan has no daily limit. */
|
|
40
|
+
limit: z.number().int().positive().nullable(),
|
|
41
|
+
/** When the current 24-hour window ends; null when no window is open or there is no limit. */
|
|
42
|
+
resetAt: z.iso.datetime().nullable(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
export type McpCallUsage = z.infer<typeof mcpCallUsageSchema>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { MEDIA_ASSET_STATUS_VALUES, MEDIA_CONTENT_TYPES } from "@read-frog/definitions";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const mediaContentTypeSchema = z.enum(MEDIA_CONTENT_TYPES);
|
|
5
|
+
export type MediaContentType = z.infer<typeof mediaContentTypeSchema>;
|
|
6
|
+
|
|
7
|
+
export const mediaAssetStatusSchema = z.enum(MEDIA_ASSET_STATUS_VALUES);
|
|
8
|
+
export type MediaAssetStatus = z.infer<typeof mediaAssetStatusSchema>;
|
|
9
|
+
|
|
10
|
+
/** Why the worker rejected an upload. */
|
|
11
|
+
export const mediaFailureCodeSchema = z.enum([
|
|
12
|
+
"unsupported_format",
|
|
13
|
+
"invalid_file",
|
|
14
|
+
"file_too_large",
|
|
15
|
+
"image_too_large",
|
|
16
|
+
"audio_too_long",
|
|
17
|
+
"quota_exceeded",
|
|
18
|
+
"processing_failed",
|
|
19
|
+
]);
|
|
20
|
+
export type MediaFailureCode = z.infer<typeof mediaFailureCodeSchema>;
|
|
21
|
+
|
|
22
|
+
export const mediaAssetMetadataSchema = z.discriminatedUnion("kind", [
|
|
23
|
+
z
|
|
24
|
+
.object({ kind: z.literal("image"), width: z.number().int(), height: z.number().int() })
|
|
25
|
+
.strict(),
|
|
26
|
+
z.object({ kind: z.literal("audio"), durationMs: z.number().int() }).strict(),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const mediaAssetSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
id: z.uuid(),
|
|
32
|
+
/** What a cell stores to reference the asset; null until it is ready. */
|
|
33
|
+
uri: z.string().nullable(),
|
|
34
|
+
status: mediaAssetStatusSchema,
|
|
35
|
+
contentType: z.string().nullable(),
|
|
36
|
+
fileByteSize: z.number().int(),
|
|
37
|
+
metadata: mediaAssetMetadataSchema.nullable(),
|
|
38
|
+
failureCode: mediaFailureCodeSchema.nullable(),
|
|
39
|
+
createdAt: z.coerce.date(),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
export type MediaAsset = z.infer<typeof mediaAssetSchema>;
|
|
43
|
+
|
|
44
|
+
export const mediaRequestUploadInputSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
contentType: mediaContentTypeSchema,
|
|
47
|
+
// The exact size: the upload is signed for it, and storage refuses any other file.
|
|
48
|
+
byteSize: z.number().int().min(1),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
export type MediaRequestUploadInput = z.infer<typeof mediaRequestUploadInputSchema>;
|
|
52
|
+
|
|
53
|
+
export const mediaRequestUploadOutputSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
assetId: z.uuid(),
|
|
56
|
+
/** A signed PUT: send the file to `url` with exactly `headers`. */
|
|
57
|
+
upload: z.object({ url: z.url(), headers: z.record(z.string(), z.string()) }).strict(),
|
|
58
|
+
expiresAt: z.coerce.date(),
|
|
59
|
+
})
|
|
60
|
+
.strict();
|
|
61
|
+
export type MediaRequestUploadOutput = z.infer<typeof mediaRequestUploadOutputSchema>;
|
|
62
|
+
|
|
63
|
+
export const mediaAssetIdInputSchema = z.object({ id: z.uuid() }).strict();
|
|
64
|
+
export type MediaAssetIdInput = z.infer<typeof mediaAssetIdInputSchema>;
|
|
65
|
+
|
|
66
|
+
export const mediaAssetOutputSchema = z.object({ asset: mediaAssetSchema }).strict();
|
|
67
|
+
export type MediaAssetOutput = z.infer<typeof mediaAssetOutputSchema>;
|
|
68
|
+
|
|
69
|
+
export const mediaUsageOutputSchema = z
|
|
70
|
+
.object({
|
|
71
|
+
usedBytes: z.number().int().min(0),
|
|
72
|
+
/** Held by uploads that are not done yet. */
|
|
73
|
+
reservedBytes: z.number().int().min(0),
|
|
74
|
+
limitBytes: z.number().int().min(0),
|
|
75
|
+
})
|
|
76
|
+
.strict();
|
|
77
|
+
export type MediaUsageOutput = z.infer<typeof mediaUsageOutputSchema>;
|
package/src/schemas/srs.ts
CHANGED
|
@@ -154,7 +154,9 @@ export type SrsListCardRevlogsOutput = z.infer<typeof srsListCardRevlogsOutputSc
|
|
|
154
154
|
export const srsReviewOutputSchema = z.object({
|
|
155
155
|
card: cardSchema,
|
|
156
156
|
revlog: srsRevlogSchema,
|
|
157
|
-
|
|
157
|
+
// Null for an idempotent replay: it writes nothing, so no shape ever carries
|
|
158
|
+
// its transaction. Wait for the returned card and revlog instead.
|
|
159
|
+
txid: z.number().nullable(),
|
|
158
160
|
// False when an idempotent replay returned an existing revlog instead of creating one.
|
|
159
161
|
created: z.boolean(),
|
|
160
162
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// What the web app reports about keeping its Electric data live. Every field is
|
|
4
|
+
// an enum, so each label the API counts them under has a fixed set of values.
|
|
5
|
+
|
|
6
|
+
/** The shapes the web app syncs, by their names in `@repo/electric-contract`. */
|
|
7
|
+
export const webSyncShapeSchema = z.enum([
|
|
8
|
+
"notebases",
|
|
9
|
+
"notebaseColumns",
|
|
10
|
+
"notebaseRows",
|
|
11
|
+
"cardTemplates",
|
|
12
|
+
"cards",
|
|
13
|
+
"revlogs",
|
|
14
|
+
"mediaAssets",
|
|
15
|
+
]);
|
|
16
|
+
export type WebSyncShape = z.infer<typeof webSyncShapeSchema>;
|
|
17
|
+
|
|
18
|
+
/** The write whose change the page waited to see come back over Electric. */
|
|
19
|
+
export const webSyncOperationSchema = z.enum([
|
|
20
|
+
// The review session's own writes.
|
|
21
|
+
"review",
|
|
22
|
+
// A review the server had already recorded: it waits for that review's card
|
|
23
|
+
// and revlog, as its own transaction wrote nothing.
|
|
24
|
+
"reviewReplay",
|
|
25
|
+
"undo",
|
|
26
|
+
"bury",
|
|
27
|
+
"suspend",
|
|
28
|
+
"deleteNote",
|
|
29
|
+
// A synced collection's own mutation handlers.
|
|
30
|
+
"insert",
|
|
31
|
+
"update",
|
|
32
|
+
"delete",
|
|
33
|
+
// Other writes that wait for their change.
|
|
34
|
+
"setSleep",
|
|
35
|
+
"srsSettings",
|
|
36
|
+
"cardStatus",
|
|
37
|
+
"createTemplate",
|
|
38
|
+
"updateTemplate",
|
|
39
|
+
]);
|
|
40
|
+
export type WebSyncOperation = z.infer<typeof webSyncOperationSchema>;
|
|
41
|
+
|
|
42
|
+
/** How the shape that lagged was receiving live changes. */
|
|
43
|
+
export const webSyncTransportSchema = z.enum(["sse", "longPoll"]);
|
|
44
|
+
export type WebSyncTransport = z.infer<typeof webSyncTransportSchema>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A write wait is only reported once its first wait ran out: `recovered` when
|
|
48
|
+
* reconnecting the lagging shapes brought the change, `failed` when the page
|
|
49
|
+
* gave up on it.
|
|
50
|
+
*/
|
|
51
|
+
export const webSyncWriteOutcomeSchema = z.enum(["recovered", "failed"]);
|
|
52
|
+
export type WebSyncWriteOutcome = z.infer<typeof webSyncWriteOutcomeSchema>;
|
|
53
|
+
|
|
54
|
+
export const webSyncStreamErrorTypeSchema = z.enum([
|
|
55
|
+
// 401: the session ended.
|
|
56
|
+
"unauthorized",
|
|
57
|
+
// 403: the notebase was deleted, or is someone else's.
|
|
58
|
+
"forbidden",
|
|
59
|
+
// 400 and any other 4xx.
|
|
60
|
+
"badRequest",
|
|
61
|
+
"clientError",
|
|
62
|
+
// A response without Electric's headers: a proxy or CORS in the way.
|
|
63
|
+
"missingHeaders",
|
|
64
|
+
// Anything that is not an HTTP status: an unreadable body, a parse error.
|
|
65
|
+
"unexpected",
|
|
66
|
+
]);
|
|
67
|
+
export type WebSyncStreamErrorType = z.infer<typeof webSyncStreamErrorTypeSchema>;
|
|
68
|
+
|
|
69
|
+
/** `initial` before the shape first loaded, when the page shows the error at once. */
|
|
70
|
+
export const webSyncStreamPhaseSchema = z.enum(["initial", "live"]);
|
|
71
|
+
export type WebSyncStreamPhase = z.infer<typeof webSyncStreamPhaseSchema>;
|
|
72
|
+
|
|
73
|
+
export const webSyncPageSchema = z.enum(["reviewCombined", "reviewNotebase"]);
|
|
74
|
+
export type WebSyncPage = z.infer<typeof webSyncPageSchema>;
|
|
75
|
+
|
|
76
|
+
export const webSyncUnavailableReasonSchema = z.enum([
|
|
77
|
+
// A write's change never came back, even after reconnecting.
|
|
78
|
+
"syncTimeout",
|
|
79
|
+
// A shape stream stopped.
|
|
80
|
+
"streamFailed",
|
|
81
|
+
// The notebase is gone: its shapes answer 403.
|
|
82
|
+
"notebaseGone",
|
|
83
|
+
]);
|
|
84
|
+
export type WebSyncUnavailableReason = z.infer<typeof webSyncUnavailableReasonSchema>;
|
|
85
|
+
|
|
86
|
+
export const webSyncEventSchema = z.discriminatedUnion("type", [
|
|
87
|
+
z
|
|
88
|
+
.object({
|
|
89
|
+
type: z.literal("writeWait"),
|
|
90
|
+
operation: webSyncOperationSchema,
|
|
91
|
+
outcome: webSyncWriteOutcomeSchema,
|
|
92
|
+
transport: webSyncTransportSchema,
|
|
93
|
+
})
|
|
94
|
+
.strict(),
|
|
95
|
+
z
|
|
96
|
+
.object({
|
|
97
|
+
type: z.literal("streamError"),
|
|
98
|
+
shape: webSyncShapeSchema,
|
|
99
|
+
errorType: webSyncStreamErrorTypeSchema,
|
|
100
|
+
phase: webSyncStreamPhaseSchema,
|
|
101
|
+
})
|
|
102
|
+
.strict(),
|
|
103
|
+
z
|
|
104
|
+
.object({
|
|
105
|
+
type: z.literal("unavailable"),
|
|
106
|
+
page: webSyncPageSchema,
|
|
107
|
+
reason: webSyncUnavailableReasonSchema,
|
|
108
|
+
})
|
|
109
|
+
.strict(),
|
|
110
|
+
]);
|
|
111
|
+
export type WebSyncEvent = z.infer<typeof webSyncEventSchema>;
|
|
112
|
+
|
|
113
|
+
/** A page sends what happened within a second or so as one batch. */
|
|
114
|
+
export const WEB_SYNC_REPORT_MAX_EVENTS = 20;
|
|
115
|
+
|
|
116
|
+
export const reportWebSyncInputSchema = z
|
|
117
|
+
.object({
|
|
118
|
+
events: z.array(webSyncEventSchema).min(1).max(WEB_SYNC_REPORT_MAX_EVENTS),
|
|
119
|
+
})
|
|
120
|
+
.strict();
|
|
121
|
+
export type ReportWebSyncInput = z.infer<typeof reportWebSyncInputSchema>;
|