@read-frog/api-contract 0.22.0 → 0.24.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 +1780 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +599 -130
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/contracts/billing.ts +1 -0
- package/src/contracts/hosted-ai.ts +11 -0
- package/src/contracts/notebase-column.ts +1 -1
- package/src/contracts/pdf-translation.ts +140 -0
- package/src/contracts/srs.ts +32 -0
- package/src/contracts/stats.ts +16 -0
- package/src/index.ts +3 -0
- package/src/public-errors.ts +113 -1
- package/src/schemas/billing.ts +3 -0
- package/src/schemas/email-preferences.ts +44 -0
- package/src/schemas/hosted-ai.ts +41 -4
- package/src/schemas/notebase.ts +3 -0
- package/src/schemas/pdf-translation.ts +240 -0
- package/src/schemas/srs.ts +175 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@read-frog/api-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@orpc/contract": "^1.15.2",
|
|
28
|
-
"@read-frog/definitions": "0.
|
|
28
|
+
"@read-frog/definitions": "0.12.0",
|
|
29
29
|
"zod": "^4.6.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
package/src/contracts/billing.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { eventIterator, oc } from "@orpc/contract";
|
|
2
2
|
import { pickPublicErrorMap } from "#/public-errors";
|
|
3
3
|
import {
|
|
4
|
+
HostedAiCreditPoolsInputSchema,
|
|
5
|
+
HostedAiCreditPoolsOutputSchema,
|
|
4
6
|
HostedAiNoteSuggestionStreamInputSchema,
|
|
5
7
|
HostedAiStatusInputSchema,
|
|
6
8
|
HostedAiStatusOutputSchema,
|
|
@@ -15,6 +17,15 @@ const hostedAiStreamErrors = pickPublicErrorMap(
|
|
|
15
17
|
);
|
|
16
18
|
|
|
17
19
|
export const hostedAiContract = {
|
|
20
|
+
pools: oc
|
|
21
|
+
.route({
|
|
22
|
+
method: "GET",
|
|
23
|
+
path: "/hosted-ai/pools",
|
|
24
|
+
summary: "Get shared Built-in AI credit pools for a feature",
|
|
25
|
+
tags: ["Hosted AI"],
|
|
26
|
+
})
|
|
27
|
+
.input(HostedAiCreditPoolsInputSchema)
|
|
28
|
+
.output(HostedAiCreditPoolsOutputSchema),
|
|
18
29
|
status: oc
|
|
19
30
|
.route({
|
|
20
31
|
method: "GET",
|
|
@@ -44,7 +44,7 @@ export const notebaseColumnContract = {
|
|
|
44
44
|
path: "/notebase-columns/{notebaseColumnId}",
|
|
45
45
|
summary: "Delete notebase field",
|
|
46
46
|
description:
|
|
47
|
-
|
|
47
|
+
'Delete an owned field and remove its values from notes. Primary fields cannot be deleted, nor can a field a template references: one a columns face lists, or a layout face names as {{ ["<column id>"] }}. A layout that loops over ctx.fields, as the default dictionary template\'s back does to show every other filled field, references none of them, so those can be deleted and simply leave the card.',
|
|
48
48
|
tags: ["Notebase Columns"],
|
|
49
49
|
})
|
|
50
50
|
.errors(
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { pickPublicErrorMap } from "#/public-errors";
|
|
3
|
+
import {
|
|
4
|
+
pdfTranslationDeleteOutputSchema,
|
|
5
|
+
pdfTranslationDownloadOutputSchema,
|
|
6
|
+
pdfTranslationGetOutputSchema,
|
|
7
|
+
pdfTranslationJobIdInputSchema,
|
|
8
|
+
pdfTranslationJobResourceSchema,
|
|
9
|
+
pdfTranslationListInputSchema,
|
|
10
|
+
pdfTranslationListOutputSchema,
|
|
11
|
+
pdfTranslationOutputInputSchema,
|
|
12
|
+
pdfTranslationPreviewOutputSchema,
|
|
13
|
+
pdfTranslationRequestUploadInputSchema,
|
|
14
|
+
pdfTranslationRequestUploadOutputSchema,
|
|
15
|
+
pdfTranslationStatusInputSchema,
|
|
16
|
+
pdfTranslationStatusOutputSchema,
|
|
17
|
+
pdfTranslationSubmitInputSchema,
|
|
18
|
+
pdfTranslationSubmitOutputSchema,
|
|
19
|
+
} from "#/schemas/pdf-translation";
|
|
20
|
+
|
|
21
|
+
const pdfTranslationProcedure = oc.errors(pickPublicErrorMap("PDF_TRANSLATION_UNAVAILABLE"));
|
|
22
|
+
|
|
23
|
+
export const pdfTranslationContract = {
|
|
24
|
+
status: pdfTranslationProcedure
|
|
25
|
+
.route({
|
|
26
|
+
method: "GET",
|
|
27
|
+
path: "/pdf-translations/status",
|
|
28
|
+
summary: "How many free PDF translations the account has left",
|
|
29
|
+
tags: ["PDF Translation"],
|
|
30
|
+
})
|
|
31
|
+
.input(pdfTranslationStatusInputSchema)
|
|
32
|
+
.output(pdfTranslationStatusOutputSchema),
|
|
33
|
+
|
|
34
|
+
list: pdfTranslationProcedure
|
|
35
|
+
.route({
|
|
36
|
+
method: "GET",
|
|
37
|
+
path: "/pdf-translations",
|
|
38
|
+
summary: "List recent PDF translations",
|
|
39
|
+
tags: ["PDF Translation"],
|
|
40
|
+
})
|
|
41
|
+
.input(pdfTranslationListInputSchema)
|
|
42
|
+
.output(pdfTranslationListOutputSchema),
|
|
43
|
+
|
|
44
|
+
requestUpload: pdfTranslationProcedure
|
|
45
|
+
.errors(pickPublicErrorMap("PDF_TRANSLATION_FILE_TOO_LARGE"))
|
|
46
|
+
.route({
|
|
47
|
+
method: "POST",
|
|
48
|
+
path: "/pdf-translations/uploads",
|
|
49
|
+
summary: "Request a signed upload for a PDF to translate",
|
|
50
|
+
tags: ["PDF Translation"],
|
|
51
|
+
})
|
|
52
|
+
.input(pdfTranslationRequestUploadInputSchema)
|
|
53
|
+
.output(pdfTranslationRequestUploadOutputSchema),
|
|
54
|
+
|
|
55
|
+
submit: pdfTranslationProcedure
|
|
56
|
+
.errors(
|
|
57
|
+
pickPublicErrorMap(
|
|
58
|
+
"HOSTED_AI_TIER_RESTRICTED",
|
|
59
|
+
"PDF_TRANSLATION_UPLOAD_NOT_FOUND",
|
|
60
|
+
"PDF_TRANSLATION_UPLOAD_EXPIRED",
|
|
61
|
+
"PDF_TRANSLATION_UPLOAD_NOT_COMPLETED",
|
|
62
|
+
"PDF_TRANSLATION_FILE_TOO_LARGE",
|
|
63
|
+
"PDF_TRANSLATION_INVALID_PDF",
|
|
64
|
+
"PDF_TRANSLATION_ALREADY_TRANSLATED",
|
|
65
|
+
"PDF_TRANSLATION_PAGE_LIMIT_EXCEEDED",
|
|
66
|
+
"PDF_TRANSLATION_QUEUE_FULL",
|
|
67
|
+
"PDF_TRANSLATION_IDEMPOTENCY_CONFLICT",
|
|
68
|
+
"PDF_TRANSLATION_INSUFFICIENT_CREDIT",
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
.route({
|
|
72
|
+
method: "POST",
|
|
73
|
+
path: "/pdf-translations",
|
|
74
|
+
summary: "Translate uploaded PDFs",
|
|
75
|
+
tags: ["PDF Translation"],
|
|
76
|
+
})
|
|
77
|
+
.input(pdfTranslationSubmitInputSchema)
|
|
78
|
+
.output(pdfTranslationSubmitOutputSchema),
|
|
79
|
+
|
|
80
|
+
get: pdfTranslationProcedure
|
|
81
|
+
.errors(pickPublicErrorMap("PDF_TRANSLATION_NOT_FOUND"))
|
|
82
|
+
.route({
|
|
83
|
+
method: "GET",
|
|
84
|
+
path: "/pdf-translations/{id}",
|
|
85
|
+
summary: "Get a PDF translation's status, with the others submitted with it",
|
|
86
|
+
tags: ["PDF Translation"],
|
|
87
|
+
})
|
|
88
|
+
.input(pdfTranslationJobIdInputSchema)
|
|
89
|
+
.output(pdfTranslationGetOutputSchema),
|
|
90
|
+
|
|
91
|
+
cancel: pdfTranslationProcedure
|
|
92
|
+
.errors(pickPublicErrorMap("PDF_TRANSLATION_NOT_FOUND", "PDF_TRANSLATION_NOT_CANCELLABLE"))
|
|
93
|
+
.route({
|
|
94
|
+
method: "POST",
|
|
95
|
+
path: "/pdf-translations/{id}/cancel",
|
|
96
|
+
summary: "Cancel a PDF translation",
|
|
97
|
+
tags: ["PDF Translation"],
|
|
98
|
+
})
|
|
99
|
+
.input(pdfTranslationJobIdInputSchema)
|
|
100
|
+
.output(pdfTranslationJobResourceSchema),
|
|
101
|
+
|
|
102
|
+
getPreviewUrl: pdfTranslationProcedure
|
|
103
|
+
.errors(pickPublicErrorMap("PDF_TRANSLATION_NOT_FOUND", "PDF_TRANSLATION_NOT_READY"))
|
|
104
|
+
.route({
|
|
105
|
+
method: "GET",
|
|
106
|
+
path: "/pdf-translations/{id}/preview-url",
|
|
107
|
+
summary: "Get a short-lived URL to read a translated PDF in the browser",
|
|
108
|
+
tags: ["PDF Translation"],
|
|
109
|
+
})
|
|
110
|
+
.input(pdfTranslationOutputInputSchema)
|
|
111
|
+
.output(pdfTranslationPreviewOutputSchema),
|
|
112
|
+
|
|
113
|
+
getDownloadUrl: pdfTranslationProcedure
|
|
114
|
+
.errors(
|
|
115
|
+
pickPublicErrorMap(
|
|
116
|
+
"PDF_TRANSLATION_NOT_FOUND",
|
|
117
|
+
"PDF_TRANSLATION_NOT_READY",
|
|
118
|
+
"PDF_TRANSLATION_DOWNLOAD_RESTRICTED",
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
.route({
|
|
122
|
+
method: "GET",
|
|
123
|
+
path: "/pdf-translations/{id}/download-url",
|
|
124
|
+
summary: "Get a download URL for a translated PDF",
|
|
125
|
+
tags: ["PDF Translation"],
|
|
126
|
+
})
|
|
127
|
+
.input(pdfTranslationOutputInputSchema)
|
|
128
|
+
.output(pdfTranslationDownloadOutputSchema),
|
|
129
|
+
|
|
130
|
+
delete: pdfTranslationProcedure
|
|
131
|
+
.errors(pickPublicErrorMap("PDF_TRANSLATION_NOT_FOUND", "PDF_TRANSLATION_NOT_DELETABLE"))
|
|
132
|
+
.route({
|
|
133
|
+
method: "DELETE",
|
|
134
|
+
path: "/pdf-translations/{id}",
|
|
135
|
+
summary: "Delete a finished PDF translation and its files",
|
|
136
|
+
tags: ["PDF Translation"],
|
|
137
|
+
})
|
|
138
|
+
.input(pdfTranslationJobIdInputSchema)
|
|
139
|
+
.output(pdfTranslationDeleteOutputSchema),
|
|
140
|
+
};
|
package/src/contracts/srs.ts
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
srsSetCardBuriedOutputSchema,
|
|
21
21
|
srsSetCardSuspendedInputSchema,
|
|
22
22
|
srsSetCardSuspendedOutputSchema,
|
|
23
|
+
srsApplyPostponeInputSchema,
|
|
24
|
+
srsApplyPostponeOutputSchema,
|
|
25
|
+
srsPreviewPostponeInputSchema,
|
|
26
|
+
srsPreviewPostponeOutputSchema,
|
|
23
27
|
} from "#/schemas/srs";
|
|
24
28
|
|
|
25
29
|
export const srsContract = {
|
|
@@ -141,4 +145,32 @@ export const srsContract = {
|
|
|
141
145
|
)
|
|
142
146
|
.input(srsSetCardSuspendedInputSchema)
|
|
143
147
|
.output(srsSetCardSuspendedOutputSchema),
|
|
148
|
+
|
|
149
|
+
previewPostpone: oc
|
|
150
|
+
.errors(
|
|
151
|
+
pickPublicErrorMap(
|
|
152
|
+
"NOTEBASE_NOT_FOUND",
|
|
153
|
+
"RESCHEDULE_PREVIEW_TIMEOUT",
|
|
154
|
+
"RESCHEDULE_DAILY_LIMIT_INVALID",
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
.route({
|
|
158
|
+
method: "POST",
|
|
159
|
+
path: "/srs/postpone/preview",
|
|
160
|
+
summary: "Preview postpone",
|
|
161
|
+
tags: ["SRS"],
|
|
162
|
+
})
|
|
163
|
+
.input(srsPreviewPostponeInputSchema)
|
|
164
|
+
.output(srsPreviewPostponeOutputSchema),
|
|
165
|
+
|
|
166
|
+
applyPostpone: oc
|
|
167
|
+
.errors(pickPublicErrorMap("NOTEBASE_NOT_FOUND", "RESCHEDULE_PLAN_EXPIRED", "RESCHEDULE_STALE"))
|
|
168
|
+
.route({
|
|
169
|
+
method: "POST",
|
|
170
|
+
path: "/srs/postpone",
|
|
171
|
+
summary: "Apply postpone",
|
|
172
|
+
tags: ["SRS"],
|
|
173
|
+
})
|
|
174
|
+
.input(srsApplyPostponeInputSchema)
|
|
175
|
+
.output(srsApplyPostponeOutputSchema),
|
|
144
176
|
};
|
package/src/contracts/stats.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { defineAgentProcedure } from "./shared";
|
|
1
2
|
import { oc } from "@orpc/contract";
|
|
2
3
|
import { pickPublicErrorMap } from "#/public-errors";
|
|
3
4
|
import { notebaseRowGrowthStatsOutputSchema } from "#/schemas/notebase-stats";
|
|
5
|
+
import { srsBacklogStatsInputSchema, srsBacklogStatsOutputSchema } from "#/schemas/srs";
|
|
4
6
|
import { srsMemoryGrowthStatsOutputSchema } from "#/schemas/srs-stats";
|
|
5
7
|
import { activityStatsOutputSchema, statsInputSchema } from "#/schemas/stats";
|
|
6
8
|
|
|
@@ -37,4 +39,18 @@ export const statsContract = {
|
|
|
37
39
|
})
|
|
38
40
|
.input(statsInputSchema)
|
|
39
41
|
.output(notebaseRowGrowthStatsOutputSchema),
|
|
42
|
+
|
|
43
|
+
// A statistic like the rest, so reviews, which invalidate every `stats` query,
|
|
44
|
+
// mark it stale too.
|
|
45
|
+
backlogStats: defineAgentProcedure({
|
|
46
|
+
method: "GET",
|
|
47
|
+
path: "/srs/stats/backlog",
|
|
48
|
+
summary: "Get review backlogs",
|
|
49
|
+
description:
|
|
50
|
+
"Read overdue review backlogs for owned awake notebases in the supplied timezone. Only a notebase whose overdue reviews fill two or more days of its daily review limit is listed; the rest are left out. A notebase whose daily review limit is 0 or unlimited (-1) never returns anything, even when its ID is requested. Does not return note content or card faces.",
|
|
51
|
+
tags: ["Statistics"],
|
|
52
|
+
})
|
|
53
|
+
.errors(pickPublicErrorMap("NOTEBASE_NOT_FOUND"))
|
|
54
|
+
.input(srsBacklogStatsInputSchema)
|
|
55
|
+
.output(srsBacklogStatsOutputSchema),
|
|
40
56
|
};
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { mcpConnectionContract } from "./contracts/mcp-connection";
|
|
|
11
11
|
import { notebaseContract } from "./contracts/notebase";
|
|
12
12
|
import { notebaseColumnContract } from "./contracts/notebase-column";
|
|
13
13
|
import { notebaseRowContract } from "./contracts/notebase-row";
|
|
14
|
+
import { pdfTranslationContract } from "./contracts/pdf-translation";
|
|
14
15
|
import { speechContract } from "./contracts/speech";
|
|
15
16
|
import { srsContract } from "./contracts/srs";
|
|
16
17
|
import { statsContract } from "./contracts/stats";
|
|
@@ -30,6 +31,7 @@ export const contract = {
|
|
|
30
31
|
notebase: notebaseContract,
|
|
31
32
|
notebaseColumn: notebaseColumnContract,
|
|
32
33
|
notebaseRow: notebaseRowContract,
|
|
34
|
+
pdfTranslation: pdfTranslationContract,
|
|
33
35
|
speech: speechContract,
|
|
34
36
|
srs: srsContract,
|
|
35
37
|
stats: statsContract,
|
|
@@ -55,6 +57,7 @@ export * from "./schemas/notebase-column";
|
|
|
55
57
|
export * from "./schemas/notebase-row";
|
|
56
58
|
export * from "./schemas/notebase-stats";
|
|
57
59
|
export * from "./schemas/notebase-view";
|
|
60
|
+
export * from "./schemas/pdf-translation";
|
|
58
61
|
export * from "./schemas/speech";
|
|
59
62
|
export * from "./schemas/srs";
|
|
60
63
|
export * from "./schemas/srs-stats";
|
package/src/public-errors.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ErrorMap } from "@orpc/contract";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
HostedAiCreditPolicyIdSchema,
|
|
5
|
+
HostedAiModelTierSchema,
|
|
6
|
+
HostedAiUpgradePlanSchema,
|
|
7
|
+
} from "./schemas/hosted-ai";
|
|
8
|
+
import {
|
|
9
|
+
PdfTranslationInsufficientCreditDataSchema,
|
|
10
|
+
PdfTranslationUploadErrorDataSchema,
|
|
11
|
+
} from "./schemas/pdf-translation";
|
|
4
12
|
|
|
5
13
|
interface PublicAppErrorDefinition {
|
|
6
14
|
data?: z.ZodTypeAny;
|
|
@@ -74,6 +82,8 @@ export type CardTemplateNotebaseColumnInUseData = z.infer<
|
|
|
74
82
|
|
|
75
83
|
export const HostedAiTierRestrictedDataSchema = z.strictObject({
|
|
76
84
|
modelTier: HostedAiModelTierSchema,
|
|
85
|
+
/** The cheapest plan that unlocks it; a client offers "Pro or Ultra" for `pro`. */
|
|
86
|
+
upgradeTo: HostedAiUpgradePlanSchema,
|
|
77
87
|
});
|
|
78
88
|
|
|
79
89
|
export const HostedAiQuotaExhaustedDataSchema = z.strictObject({
|
|
@@ -210,6 +220,10 @@ export const PUBLIC_APP_ERROR_DEFS = {
|
|
|
210
220
|
message: "A subscription change is already scheduled",
|
|
211
221
|
status: 409,
|
|
212
222
|
},
|
|
223
|
+
BILLING_CHECKOUT_CONFLICT: {
|
|
224
|
+
message: "Please start a new checkout from the pricing page",
|
|
225
|
+
status: 409,
|
|
226
|
+
},
|
|
213
227
|
CARD_NOT_REVIEWABLE: {
|
|
214
228
|
message: "Card cannot be reviewed in its current state",
|
|
215
229
|
status: 409,
|
|
@@ -242,6 +256,22 @@ export const PUBLIC_APP_ERROR_DEFS = {
|
|
|
242
256
|
message: "Card status change is unavailable",
|
|
243
257
|
status: 409,
|
|
244
258
|
},
|
|
259
|
+
RESCHEDULE_PLAN_EXPIRED: {
|
|
260
|
+
message: "The reschedule plan has expired; preview it again",
|
|
261
|
+
status: 409,
|
|
262
|
+
},
|
|
263
|
+
RESCHEDULE_STALE: {
|
|
264
|
+
message: "Cards changed after the reschedule preview; preview it again",
|
|
265
|
+
status: 409,
|
|
266
|
+
},
|
|
267
|
+
RESCHEDULE_PREVIEW_TIMEOUT: {
|
|
268
|
+
message: "Planning the reschedule took too long",
|
|
269
|
+
status: 503,
|
|
270
|
+
},
|
|
271
|
+
RESCHEDULE_DAILY_LIMIT_INVALID: {
|
|
272
|
+
message: "The daily limit is outside the range a reschedule may suggest",
|
|
273
|
+
status: 422,
|
|
274
|
+
},
|
|
245
275
|
NOTEBASE_VIEW_NOT_FOUND: {
|
|
246
276
|
message: "Notebase view not found",
|
|
247
277
|
status: 404,
|
|
@@ -285,6 +315,88 @@ export const PUBLIC_APP_ERROR_DEFS = {
|
|
|
285
315
|
message: "This email link is not valid",
|
|
286
316
|
status: 400,
|
|
287
317
|
},
|
|
318
|
+
// The deployment has no worker to run translations (no Batch job
|
|
319
|
+
// definition or PDF bucket configured).
|
|
320
|
+
PDF_TRANSLATION_UNAVAILABLE: {
|
|
321
|
+
message: "PDF translation is not available right now",
|
|
322
|
+
status: 503,
|
|
323
|
+
},
|
|
324
|
+
PDF_TRANSLATION_NOT_FOUND: {
|
|
325
|
+
message: "PDF translation not found",
|
|
326
|
+
status: 404,
|
|
327
|
+
},
|
|
328
|
+
PDF_TRANSLATION_UPLOAD_NOT_FOUND: {
|
|
329
|
+
data: PdfTranslationUploadErrorDataSchema,
|
|
330
|
+
message: "PDF upload not found",
|
|
331
|
+
status: 404,
|
|
332
|
+
},
|
|
333
|
+
PDF_TRANSLATION_UPLOAD_EXPIRED: {
|
|
334
|
+
data: PdfTranslationUploadErrorDataSchema,
|
|
335
|
+
message: "The PDF upload has expired; upload the file again",
|
|
336
|
+
status: 410,
|
|
337
|
+
},
|
|
338
|
+
PDF_TRANSLATION_UPLOAD_NOT_COMPLETED: {
|
|
339
|
+
data: PdfTranslationUploadErrorDataSchema,
|
|
340
|
+
message: "The PDF upload has not been completed",
|
|
341
|
+
status: 409,
|
|
342
|
+
},
|
|
343
|
+
PDF_TRANSLATION_FILE_TOO_LARGE: {
|
|
344
|
+
message: "The PDF exceeds the maximum allowed size",
|
|
345
|
+
status: 413,
|
|
346
|
+
},
|
|
347
|
+
PDF_TRANSLATION_INVALID_PDF: {
|
|
348
|
+
data: PdfTranslationUploadErrorDataSchema,
|
|
349
|
+
message: "The file is not a readable PDF",
|
|
350
|
+
status: 422,
|
|
351
|
+
},
|
|
352
|
+
// Every PDF a translation delivers is marked as one, and none is translated
|
|
353
|
+
// again.
|
|
354
|
+
PDF_TRANSLATION_ALREADY_TRANSLATED: {
|
|
355
|
+
data: PdfTranslationUploadErrorDataSchema,
|
|
356
|
+
message: "This PDF is already a translation; upload the original instead",
|
|
357
|
+
status: 422,
|
|
358
|
+
},
|
|
359
|
+
PDF_TRANSLATION_PAGE_LIMIT_EXCEEDED: {
|
|
360
|
+
data: PdfTranslationUploadErrorDataSchema,
|
|
361
|
+
message: "The PDF has more pages than one translation allows",
|
|
362
|
+
status: 422,
|
|
363
|
+
},
|
|
364
|
+
// An account's unfinished translations run one at a time, and no more than
|
|
365
|
+
// PDF_TRANSLATION_MAX_BATCH_FILES of them wait at once.
|
|
366
|
+
PDF_TRANSLATION_QUEUE_FULL: {
|
|
367
|
+
message: "Too many PDF translations are waiting; try again when some have finished",
|
|
368
|
+
status: 409,
|
|
369
|
+
},
|
|
370
|
+
PDF_TRANSLATION_IDEMPOTENCY_CONFLICT: {
|
|
371
|
+
message: "This idempotency key was already used for a different PDF translation",
|
|
372
|
+
status: 409,
|
|
373
|
+
},
|
|
374
|
+
// The estimate is over what is left of the Built-in AI credit window.
|
|
375
|
+
// Running out midway stops the job instead (its error category is
|
|
376
|
+
// quota_exhausted).
|
|
377
|
+
PDF_TRANSLATION_INSUFFICIENT_CREDIT: {
|
|
378
|
+
data: PdfTranslationInsufficientCreditDataSchema,
|
|
379
|
+
message: "Not enough Built-in AI credit left to translate this PDF",
|
|
380
|
+
status: 429,
|
|
381
|
+
},
|
|
382
|
+
PDF_TRANSLATION_NOT_CANCELLABLE: {
|
|
383
|
+
message: "This PDF translation has already finished",
|
|
384
|
+
status: 409,
|
|
385
|
+
},
|
|
386
|
+
PDF_TRANSLATION_NOT_READY: {
|
|
387
|
+
message: "The translated PDF is not available",
|
|
388
|
+
status: 409,
|
|
389
|
+
},
|
|
390
|
+
// A Free account reads its one translation online; downloading it, as any
|
|
391
|
+
// translated PDF, needs a paid plan.
|
|
392
|
+
PDF_TRANSLATION_DOWNLOAD_RESTRICTED: {
|
|
393
|
+
message: "Downloading a translated PDF needs a Pro or Ultra plan",
|
|
394
|
+
status: 403,
|
|
395
|
+
},
|
|
396
|
+
PDF_TRANSLATION_NOT_DELETABLE: {
|
|
397
|
+
message: "Cancel this PDF translation before deleting it",
|
|
398
|
+
status: 409,
|
|
399
|
+
},
|
|
288
400
|
} as const satisfies Record<string, PublicAppErrorDefinition>;
|
|
289
401
|
|
|
290
402
|
export type PublicAppErrorCode = keyof typeof PUBLIC_APP_ERROR_DEFS;
|
package/src/schemas/billing.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppLocale } from "./app-locale";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { checkoutAttributionInputSchema } from "@read-frog/definitions";
|
|
3
4
|
import { appLocaleSchema } from "./app-locale";
|
|
4
5
|
|
|
5
6
|
export const billingIntervalSchema = z.enum(["month", "year"]);
|
|
@@ -116,6 +117,8 @@ export const billingCreateCheckoutSessionInputSchema = z
|
|
|
116
117
|
interval: billingIntervalSchema,
|
|
117
118
|
locale: appLocaleSchema,
|
|
118
119
|
plan: billingCheckoutPlanSchema.default("pro"),
|
|
120
|
+
requestId: z.uuid().optional(),
|
|
121
|
+
attribution: checkoutAttributionInputSchema.optional(),
|
|
119
122
|
})
|
|
120
123
|
.strict();
|
|
121
124
|
export type BillingCreateCheckoutSessionInput = z.infer<
|
|
@@ -34,11 +34,52 @@ export const emailCategoryStateSchema = z
|
|
|
34
34
|
.strict();
|
|
35
35
|
export type EmailCategoryState = z.infer<typeof emailCategoryStateSchema>;
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The category whose emails a person can time: Practice reminders, seeded as
|
|
39
|
+
* `practice` (packages/db's migration 0067). The settings page shows the
|
|
40
|
+
* time under its switch.
|
|
41
|
+
*/
|
|
42
|
+
export const PRACTICE_REMINDER_CATEGORY_KEY = "practice";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* When practice reminders reach someone who never chose a time: 18:00 in
|
|
46
|
+
* their own time zone. The page shows it as the choice until one is made;
|
|
47
|
+
* Ribbit's reminder automation sets the same as its own time, for people
|
|
48
|
+
* without one (docs/ribbit/phase-3-automations.md).
|
|
49
|
+
*/
|
|
50
|
+
export const PRACTICE_REMINDER_DEFAULT_TIME = "18:00";
|
|
51
|
+
|
|
52
|
+
/** A time of day on the hour, `HH:00`: Ribbit takes people into a schedule once an hour. */
|
|
53
|
+
export const practiceReminderTimeSchema = z
|
|
54
|
+
.string()
|
|
55
|
+
.regex(/^([01]\d|2[0-3]):00$/, "Choose a time on the hour.");
|
|
56
|
+
export type PracticeReminderTime = z.infer<typeof practiceReminderTimeSchema>;
|
|
57
|
+
|
|
58
|
+
/** The times the page offers: every hour of the day. */
|
|
59
|
+
export const PRACTICE_REMINDER_TIMES: readonly PracticeReminderTime[] = Array.from(
|
|
60
|
+
{ length: 24 },
|
|
61
|
+
(_, hour) => `${String(hour).padStart(2, "0")}:00`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The reminders area of `user_preferences`, as the database holds it: what
|
|
66
|
+
* is not there, or no longer reads, is the default. `practiceTime` is the
|
|
67
|
+
* practice reminder's time of day, in the person's own zone; null until they
|
|
68
|
+
* choose one.
|
|
69
|
+
*/
|
|
70
|
+
export const remindersPreferencesSchema = z.object({
|
|
71
|
+
practiceTime: practiceReminderTimeSchema.nullable().catch(null).default(null),
|
|
72
|
+
});
|
|
73
|
+
export type RemindersPreferences = z.infer<typeof remindersPreferencesSchema>;
|
|
74
|
+
|
|
37
75
|
export const emailPreferencesStateSchema = z
|
|
38
76
|
.object({
|
|
39
77
|
// In display order; archived categories are left out.
|
|
40
78
|
categories: z.array(emailCategoryStateSchema),
|
|
41
79
|
unsubscribedAll: z.boolean(),
|
|
80
|
+
// When practice reminders reach them, in their own time zone; null while
|
|
81
|
+
// they never chose one (the page then shows PRACTICE_REMINDER_DEFAULT_TIME).
|
|
82
|
+
practiceReminderTime: practiceReminderTimeSchema.nullable(),
|
|
42
83
|
})
|
|
43
84
|
.strict();
|
|
44
85
|
export type EmailPreferencesState = z.infer<typeof emailPreferencesStateSchema>;
|
|
@@ -68,6 +109,9 @@ const emailPreferencesChangesShape = {
|
|
|
68
109
|
// categories added later; false ends that and leaves each category as it was
|
|
69
110
|
// before.
|
|
70
111
|
unsubscribedAll: z.boolean().optional(),
|
|
112
|
+
// Sets when practice reminders reach them (null: back to the default); left
|
|
113
|
+
// out, it stays as it is.
|
|
114
|
+
practiceReminderTime: practiceReminderTimeSchema.nullable().optional(),
|
|
71
115
|
};
|
|
72
116
|
|
|
73
117
|
export const emailPreferencesGetInputSchema = z
|
package/src/schemas/hosted-ai.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { billingPlanSchema } from "./billing";
|
|
2
3
|
|
|
3
4
|
export const HostedAiOutputFieldTypeSchema = z.enum(["string", "number"]);
|
|
4
5
|
|
|
@@ -31,6 +32,9 @@ export const HostedAiFeatureSchema = z.enum([
|
|
|
31
32
|
"videoSubtitles",
|
|
32
33
|
"inputTranslation",
|
|
33
34
|
"languageDetection",
|
|
35
|
+
// Charged by the PDF worker's proxy, not called by clients; `status`
|
|
36
|
+
// leaves it out (see the router's HOSTED_AI_FEATURES).
|
|
37
|
+
"pdfTranslation",
|
|
34
38
|
]);
|
|
35
39
|
export const HostedAiRequestIdSchema = z.uuid();
|
|
36
40
|
|
|
@@ -159,18 +163,36 @@ export const HOSTED_AI_CLIENT_STREAM_PART_TYPES = [
|
|
|
159
163
|
*/
|
|
160
164
|
export const HostedAiStatusInputSchema = z.object({});
|
|
161
165
|
|
|
166
|
+
/**
|
|
167
|
+
* `pro_required` means the Pro plan or any plan above it; `ultra_required`
|
|
168
|
+
* means Ultra. Appended last, so a client that predates a value falls back on
|
|
169
|
+
* its own default instead of misreading it.
|
|
170
|
+
*/
|
|
162
171
|
export const HostedAiUnavailableReasonSchema = z.enum([
|
|
163
172
|
"authentication_required",
|
|
164
173
|
"ultra_required",
|
|
165
174
|
"quota_exhausted",
|
|
166
175
|
"service_unavailable",
|
|
176
|
+
"pro_required",
|
|
167
177
|
]);
|
|
168
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The cheapest plan an account would need to buy for something it cannot use
|
|
181
|
+
* now. Any plan above it works too, so `pro` reads as "Pro or Ultra".
|
|
182
|
+
*/
|
|
183
|
+
export const HostedAiUpgradePlanSchema = billingPlanSchema.exclude(["free"]);
|
|
184
|
+
|
|
169
185
|
export const HostedAiCreditPolicyIdSchema = z.enum([
|
|
170
186
|
"custom-action-guest-daily-v1",
|
|
171
187
|
"custom-action-user-daily-v1",
|
|
188
|
+
// Retired: Pro draws on a weekly pool now. Kept because the database still
|
|
189
|
+
// holds pools under it.
|
|
172
190
|
"custom-action-pro-daily-v1",
|
|
173
191
|
"ultra-weekly-v1",
|
|
192
|
+
// Retired: Pro's weekly pool in UTC calendar weeks. Kept for the same reason.
|
|
193
|
+
"pro-weekly-v1",
|
|
194
|
+
// Pro's weekly pool, in weeks that run from when the plan started, as Ultra's do.
|
|
195
|
+
"pro-weekly-v2",
|
|
174
196
|
]);
|
|
175
197
|
|
|
176
198
|
export const HostedAiCreditPeriodKindSchema = z.enum(["daily", "weekly"]);
|
|
@@ -241,15 +263,29 @@ export const HostedAiCreditPoolStatusSchema = z.discriminatedUnion("periodKind",
|
|
|
241
263
|
HostedAiOneTimeCreditStatusSchema,
|
|
242
264
|
]);
|
|
243
265
|
|
|
266
|
+
/** Feature-scoped credit display, separate from the legacy extension status shape. */
|
|
267
|
+
export const HostedAiCreditPoolsInputSchema = z.strictObject({ feature: HostedAiFeatureSchema });
|
|
268
|
+
export const HostedAiCreditPoolsOutputSchema = z.strictObject({
|
|
269
|
+
pools: z.array(HostedAiCreditPoolStatusSchema),
|
|
270
|
+
});
|
|
271
|
+
|
|
244
272
|
export const HostedAiTierStatusSchema = z.strictObject({
|
|
245
273
|
accessAllowed: z.boolean(),
|
|
246
274
|
available: z.boolean(),
|
|
247
275
|
unavailableReason: HostedAiUnavailableReasonSchema.nullable(),
|
|
248
276
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
277
|
+
* The cheapest plan this account would need to use the tier, or null when it
|
|
278
|
+
* needs none: its plan or a one-time pool already covers it, or the tier is
|
|
279
|
+
* open to every signed-in account. For a guest it is the plan needed after
|
|
280
|
+
* signing in. Clients badge an option with it and offer that plan.
|
|
281
|
+
*/
|
|
282
|
+
upgradeTo: HostedAiUpgradePlanSchema.nullable(),
|
|
283
|
+
/**
|
|
284
|
+
* @deprecated Read `upgradeTo`. Viewer-independent product fact: no
|
|
285
|
+
* non-Ultra account can ever use this feature/tier. Extensions released
|
|
286
|
+
* before `upgradeTo` badge options with it, including for Ultra accounts.
|
|
287
|
+
*
|
|
288
|
+
* TODO(2026-11-21): remove once those extensions have drained (#710).
|
|
253
289
|
*/
|
|
254
290
|
requiresUltra: z.boolean(),
|
|
255
291
|
modelRevision: z.string().trim().min(1),
|
|
@@ -302,6 +338,7 @@ export type HostedAiNoteSuggestionObject = z.infer<typeof HostedAiNoteSuggestion
|
|
|
302
338
|
export type HostedAiStreamPart = z.infer<typeof HostedAiStreamPartSchema>;
|
|
303
339
|
export type HostedAiStatusInput = z.infer<typeof HostedAiStatusInputSchema>;
|
|
304
340
|
export type HostedAiUnavailableReason = z.infer<typeof HostedAiUnavailableReasonSchema>;
|
|
341
|
+
export type HostedAiUpgradePlan = z.infer<typeof HostedAiUpgradePlanSchema>;
|
|
305
342
|
export type HostedAiCreditPolicyId = z.infer<typeof HostedAiCreditPolicyIdSchema>;
|
|
306
343
|
export type HostedAiCreditPeriodKind = z.infer<typeof HostedAiCreditPeriodKindSchema>;
|
|
307
344
|
export type HostedAiQuotaScope = z.infer<typeof HostedAiQuotaScopeSchema>;
|
package/src/schemas/notebase.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
notebaseColumnConfigSchema,
|
|
4
4
|
schedulingParamsShape,
|
|
5
5
|
srsModelVersionSchema,
|
|
6
|
+
srsSleepSourceSchema,
|
|
6
7
|
srsWeightsSchema,
|
|
7
8
|
} from "@read-frog/definitions";
|
|
8
9
|
import { z } from "zod";
|
|
@@ -43,6 +44,8 @@ export const notebaseSchema = z.object({
|
|
|
43
44
|
srsModelVersion: srsModelVersionSchema,
|
|
44
45
|
// Null = awake; set = excluded from the combined review. Written by notebase.setSleep and auto sleep.
|
|
45
46
|
srsSleepingAt: z.date().nullable(),
|
|
47
|
+
// Who put it to sleep; null exactly when awake.
|
|
48
|
+
srsSleepSource: srsSleepSourceSchema.nullable(),
|
|
46
49
|
createdAt: z.date(),
|
|
47
50
|
updatedAt: z.date(),
|
|
48
51
|
});
|