@read-frog/api-contract 0.24.0 → 0.26.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 +1230 -77
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +324 -111
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/contracts/card.ts +16 -0
- 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/notebase-row.ts +2 -2
- package/src/contracts/telemetry.ts +17 -0
- package/src/index.ts +6 -0
- package/src/public-errors.ts +44 -4
- package/src/schemas/billing.ts +16 -0
- package/src/schemas/card.ts +10 -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/notebase-row.ts +30 -0
- package/src/schemas/notebase.ts +6 -1
- package/src/schemas/srs.ts +3 -1
- package/src/schemas/web-sync.ts +122 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@read-frog/api-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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.14.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/card.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { defineAgentProcedure } from "./shared";
|
|
|
2
2
|
import { oc } from "@orpc/contract";
|
|
3
3
|
import { pickPublicErrorMap } from "#/public-errors";
|
|
4
4
|
import {
|
|
5
|
+
CardDeleteInputSchema,
|
|
6
|
+
CardDeleteOutputSchema,
|
|
5
7
|
CardGenerateInputSchema,
|
|
6
8
|
CardGenerateOutputSchema,
|
|
7
9
|
CardGetInputSchema,
|
|
@@ -11,6 +13,20 @@ import {
|
|
|
11
13
|
} from "#/schemas/card";
|
|
12
14
|
|
|
13
15
|
export const cardContract = {
|
|
16
|
+
delete: oc
|
|
17
|
+
.errors(
|
|
18
|
+
pickPublicErrorMap(
|
|
19
|
+
"CARD_NOT_FOUND",
|
|
20
|
+
"NOTEBASE_NOT_FOUND",
|
|
21
|
+
"CARD_DELETE_UNSUPPORTED",
|
|
22
|
+
"RESOURCE_UPDATE_CONFLICT",
|
|
23
|
+
"CELL_VALIDATION_FAILED",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
.route({ method: "DELETE", path: "/cards/{id}", summary: "Delete card", tags: ["Cards"] })
|
|
27
|
+
.input(CardDeleteInputSchema)
|
|
28
|
+
.output(CardDeleteOutputSchema),
|
|
29
|
+
|
|
14
30
|
list: defineAgentProcedure({
|
|
15
31
|
method: "GET",
|
|
16
32
|
path: "/cards",
|
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
|
+
};
|
|
@@ -92,7 +92,7 @@ export const notebaseRowContract = {
|
|
|
92
92
|
path: "/notebase-rows/{notebaseRowId}",
|
|
93
93
|
summary: "Update note",
|
|
94
94
|
description:
|
|
95
|
-
"Merge supplied fields into an owned note, preserving omitted fields. Use null to clear a field. expectedUpdatedAt guards against editing stale data; reread on a modification conflict.",
|
|
95
|
+
"Merge supplied fields into an owned note, preserving omitted fields. Use null to clear a field. expectedUpdatedAt guards against editing stale data; reread on a modification conflict. In a cloze field, a deletion's number ({{c1::…}}) is its card's identity: keep the numbers of the deletions you keep, and leave any gaps for the save to close. Renumbering them yourself hands a card's review history to other text, and can delete it.",
|
|
96
96
|
tags: ["Notebase Rows"],
|
|
97
97
|
})
|
|
98
98
|
.errors(pickPublicErrorMap("NOTEBASE_ROW_NOT_FOUND", "CELL_VALIDATION_FAILED"))
|
|
@@ -105,7 +105,7 @@ export const notebaseRowContract = {
|
|
|
105
105
|
path: "/notebase-rows/batch",
|
|
106
106
|
summary: "Update notes",
|
|
107
107
|
description:
|
|
108
|
-
"Update 1–50 notes atomically in one owned notebase. Each row supplies its ID and fields to merge; omitted fields are preserved and null clears a field. Supply each note's expectedUpdatedAt to guard against stale edits. A missing row, invalid field or modification conflict rolls back the whole batch. Items are returned in input order.",
|
|
108
|
+
"Update 1–50 notes atomically in one owned notebase. Each row supplies its ID and fields to merge; omitted fields are preserved and null clears a field. Supply each note's expectedUpdatedAt to guard against stale edits. A missing row, invalid field or modification conflict rolls back the whole batch. Items are returned in input order. In a cloze field, a deletion's number ({{c1::…}}) is its card's identity: keep the numbers of the deletions you keep, and leave any gaps for the save to close. Renumbering them yourself hands a card's review history to other text, and can delete it.",
|
|
109
109
|
tags: ["Notebase Rows"],
|
|
110
110
|
})
|
|
111
111
|
.errors(
|
|
@@ -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
|
@@ -21,6 +21,24 @@ export const CellValidationFailureReasonSchema = z.enum([
|
|
|
21
21
|
"type_mismatch",
|
|
22
22
|
"invalid_select_option",
|
|
23
23
|
"invalid_date_format",
|
|
24
|
+
// A cloze deletion with a blank answer; receivedValue is the deletion as
|
|
25
|
+
// written (`{{c1::}}`).
|
|
26
|
+
"empty_cloze_answer",
|
|
27
|
+
// A cloze opening that never closes (unpaired braces, or no `}}`);
|
|
28
|
+
// receivedValue is the opening as written (`{{c1::`).
|
|
29
|
+
"unclosed_cloze",
|
|
30
|
+
// A cloze deletion whose ordinal list names no group (`{{c0::a}}`);
|
|
31
|
+
// receivedValue is the deletion as written.
|
|
32
|
+
"invalid_cloze_ordinal",
|
|
33
|
+
// A cloze deletion that cuts across a formula's delimiters (`\(a{{c1::b\) c}}`);
|
|
34
|
+
// receivedValue is the deletion as written.
|
|
35
|
+
"cloze_crosses_math",
|
|
36
|
+
// A cloze deletion in a formula that holds half of a `\left`/`\right` or
|
|
37
|
+
// `\begin`/`\end` pair; receivedValue is the deletion as written.
|
|
38
|
+
"cloze_splits_math_pair",
|
|
39
|
+
// More cloze groups in one note than CLOZE_MAX_GROUPS; receivedValue is
|
|
40
|
+
// the note's group count.
|
|
41
|
+
"too_many_cloze_groups",
|
|
24
42
|
]);
|
|
25
43
|
export type CellValidationFailureReason = z.infer<typeof CellValidationFailureReasonSchema>;
|
|
26
44
|
|
|
@@ -42,6 +60,8 @@ export const CellValidationFailedDataSchema = z
|
|
|
42
60
|
reason: CellValidationFailureReasonSchema,
|
|
43
61
|
/** Index of the failing row within a batch input (createMany / updateMany / initialRows). */
|
|
44
62
|
rowIndex: z.number().int().min(0).optional(),
|
|
63
|
+
/** The failing note, when the check ran over stored notes (a column type change). */
|
|
64
|
+
notebaseRowId: z.string().optional(),
|
|
45
65
|
})
|
|
46
66
|
.strict();
|
|
47
67
|
export type CellValidationFailedData = z.infer<typeof CellValidationFailedDataSchema>;
|
|
@@ -103,6 +123,10 @@ export const SpeechDailyLimitReachedDataSchema = z.strictObject({
|
|
|
103
123
|
});
|
|
104
124
|
|
|
105
125
|
export const PUBLIC_APP_ERROR_DEFS = {
|
|
126
|
+
CARD_DELETE_UNSUPPORTED: {
|
|
127
|
+
message: "This card cannot be deleted independently of its note.",
|
|
128
|
+
status: 400,
|
|
129
|
+
},
|
|
106
130
|
RESOURCE_UPDATE_CONFLICT: {
|
|
107
131
|
message: "The resource changed. Read it again before updating.",
|
|
108
132
|
status: 409,
|
|
@@ -289,10 +313,6 @@ export const PUBLIC_APP_ERROR_DEFS = {
|
|
|
289
313
|
message: "Concurrent position conflict, please retry",
|
|
290
314
|
status: 409,
|
|
291
315
|
},
|
|
292
|
-
EBOOK_BETA_RESTRICTED: {
|
|
293
|
-
message: "Ebooks are currently in beta for selected accounts",
|
|
294
|
-
status: 403,
|
|
295
|
-
},
|
|
296
316
|
EBOOK_NOT_FOUND: {
|
|
297
317
|
message: "Ebook not found",
|
|
298
318
|
status: 404,
|
|
@@ -309,6 +329,26 @@ export const PUBLIC_APP_ERROR_DEFS = {
|
|
|
309
329
|
message: "Ebook upload has not been completed",
|
|
310
330
|
status: 409,
|
|
311
331
|
},
|
|
332
|
+
MEDIA_BETA_RESTRICTED: {
|
|
333
|
+
message: "Media uploads are currently in beta for selected accounts",
|
|
334
|
+
status: 403,
|
|
335
|
+
},
|
|
336
|
+
MEDIA_NOT_FOUND: {
|
|
337
|
+
message: "Media file not found",
|
|
338
|
+
status: 404,
|
|
339
|
+
},
|
|
340
|
+
MEDIA_FILE_TOO_LARGE: {
|
|
341
|
+
message: "Media file exceeds the maximum allowed size",
|
|
342
|
+
status: 413,
|
|
343
|
+
},
|
|
344
|
+
MEDIA_QUOTA_EXCEEDED: {
|
|
345
|
+
message: "Not enough media storage space left on your plan",
|
|
346
|
+
status: 403,
|
|
347
|
+
},
|
|
348
|
+
MEDIA_UPLOAD_NOT_COMPLETED: {
|
|
349
|
+
message: "Media upload has not been completed",
|
|
350
|
+
status: 409,
|
|
351
|
+
},
|
|
312
352
|
// An email's unsubscribe or preferences link that does not verify, or whose
|
|
313
353
|
// account no longer exists. The page asks the reader to sign in instead.
|
|
314
354
|
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/card.ts
CHANGED
|
@@ -114,6 +114,16 @@ export type CardGetInput = z.infer<typeof CardGetInputSchema>;
|
|
|
114
114
|
export const CardGetOutputSchema = renderedCardSchema;
|
|
115
115
|
export type CardGetOutput = z.infer<typeof CardGetOutputSchema>;
|
|
116
116
|
|
|
117
|
+
export const CardDeleteInputSchema = z.strictObject({
|
|
118
|
+
id: z.uuid(),
|
|
119
|
+
expectedVariantKey: z.string().min(1),
|
|
120
|
+
expectedRowUpdatedAt: expectedUpdatedAtSchema,
|
|
121
|
+
});
|
|
122
|
+
export type CardDeleteInput = z.infer<typeof CardDeleteInputSchema>;
|
|
123
|
+
|
|
124
|
+
export const CardDeleteOutputSchema = z.object({ txid: z.number() });
|
|
125
|
+
export type CardDeleteOutput = z.infer<typeof CardDeleteOutputSchema>;
|
|
126
|
+
|
|
117
127
|
export const CardGenerateInputSchema = z.object({
|
|
118
128
|
notebaseId: z.uuid(),
|
|
119
129
|
templateId: z.uuid().optional(),
|
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>;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { paginationShape, pageSchema } from "./pagination";
|
|
2
2
|
import { deletionResultSchema, expectedUpdatedAtSchema } from "./resource-mutation";
|
|
3
|
+
import {
|
|
4
|
+
noteCreatedViaSchema,
|
|
5
|
+
noteOriginInputSchema,
|
|
6
|
+
noteOriginSchema,
|
|
7
|
+
} from "@read-frog/definitions";
|
|
3
8
|
import { z } from "zod";
|
|
4
9
|
|
|
5
10
|
export const notebaseRowCellsSchema = z.record(z.string(), z.unknown());
|
|
@@ -9,15 +14,38 @@ export const notebaseRowSchema = z.object({
|
|
|
9
14
|
notebaseId: z.string(),
|
|
10
15
|
cells: notebaseRowCellsSchema,
|
|
11
16
|
position: z.number(),
|
|
17
|
+
origin: noteOriginSchema
|
|
18
|
+
.nullish()
|
|
19
|
+
.describe(
|
|
20
|
+
"Where the note's content came from, set when it was created: a web page (url, title), a book on the user's shelf (ebookId, its title then, and cfi, the EPUB location of the selection), or a video (url, title, seconds into it). Null when it has none, or when it was created before this was recorded.",
|
|
21
|
+
),
|
|
22
|
+
createdVia: noteCreatedViaSchema
|
|
23
|
+
.nullish()
|
|
24
|
+
.describe(
|
|
25
|
+
'Which client created the note: "web", "extension", "mobile", "mcp" (with the MCP client\'s id and the name it registered under) or "unknown". Null for notes created before this was recorded.',
|
|
26
|
+
),
|
|
12
27
|
createdAt: z.coerce.date(),
|
|
13
28
|
updatedAt: z.coerce.date(),
|
|
14
29
|
});
|
|
15
30
|
export type NotebaseRow = z.infer<typeof notebaseRowSchema>;
|
|
16
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Where a new note's content came from; see `noteOriginInputSchema`. Optional
|
|
34
|
+
* metadata never costs the note: an origin that does not parse is dropped and
|
|
35
|
+
* the note is saved without one.
|
|
36
|
+
*/
|
|
37
|
+
export const notebaseRowOriginInputSchema = noteOriginInputSchema
|
|
38
|
+
.optional()
|
|
39
|
+
.catch(undefined)
|
|
40
|
+
.describe(
|
|
41
|
+
"Where the note's content came from: a web page or video by its http(s) URL, or a book on the user's shelf by its id. Stored once and never edited. Optional: an origin that cannot be used is left out and the note is still saved.",
|
|
42
|
+
);
|
|
43
|
+
|
|
17
44
|
export const notebaseRowCreateDataSchema = z
|
|
18
45
|
.object({
|
|
19
46
|
id: z.uuid().optional(),
|
|
20
47
|
cells: notebaseRowCellsSchema.optional(),
|
|
48
|
+
origin: notebaseRowOriginInputSchema,
|
|
21
49
|
})
|
|
22
50
|
.strict();
|
|
23
51
|
export type NotebaseRowCreateData = z.infer<typeof notebaseRowCreateDataSchema>;
|
|
@@ -88,6 +116,8 @@ export const NotebaseRowUpdateOutputSchema = z.object({
|
|
|
88
116
|
notebaseId: z.uuid(),
|
|
89
117
|
cells: notebaseRowCellsSchema,
|
|
90
118
|
position: z.number().int(),
|
|
119
|
+
origin: notebaseRowSchema.shape.origin,
|
|
120
|
+
createdVia: notebaseRowSchema.shape.createdVia,
|
|
91
121
|
createdAt: z.date(),
|
|
92
122
|
updatedAt: z.date(),
|
|
93
123
|
txid: z.number(),
|
package/src/schemas/notebase.ts
CHANGED
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
} from "@read-frog/definitions";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { notebaseColumnSchema } from "./notebase-column";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
NOTEBASE_ROW_CREATE_MANY_MAX_ROWS,
|
|
13
|
+
notebaseRowOriginInputSchema,
|
|
14
|
+
notebaseRowSchema,
|
|
15
|
+
} from "./notebase-row";
|
|
12
16
|
import { notebaseViewSchema } from "./notebase-view";
|
|
13
17
|
|
|
14
18
|
export const NotebaseListInputSchema = z.object({});
|
|
@@ -55,6 +59,7 @@ const notebaseInitialRowSchema = z
|
|
|
55
59
|
.object({
|
|
56
60
|
id: z.uuid().optional(),
|
|
57
61
|
cells: z.record(z.string(), z.unknown()),
|
|
62
|
+
origin: notebaseRowOriginInputSchema,
|
|
58
63
|
})
|
|
59
64
|
.strict();
|
|
60
65
|
|
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
|
});
|