@read-frog/api-contract 0.21.1 → 0.22.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 +632 -110
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +266 -78
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/contracts/ebook.ts +3 -1
- package/src/contracts/email-preferences.ts +55 -0
- package/src/contracts/feature-access.ts +17 -0
- package/src/contracts/notebase.ts +14 -0
- package/src/contracts/speech.ts +1 -1
- package/src/contracts/user.ts +22 -0
- package/src/index.ts +7 -3
- package/src/public-errors.ts +6 -11
- package/src/schemas/app-locale.ts +9 -0
- package/src/schemas/billing.ts +47 -35
- package/src/schemas/ebook.ts +16 -8
- package/src/schemas/email-preferences.ts +105 -0
- package/src/schemas/feature-access.ts +18 -0
- package/src/schemas/hosted-ai.ts +46 -3
- package/src/schemas/notebase.ts +16 -0
- package/src/schemas/speech.ts +11 -2
- package/src/schemas/srs.ts +4 -4
- package/src/schemas/stats.ts +2 -2
- package/src/schemas/timezone.ts +21 -0
- package/src/schemas/user.ts +33 -0
- package/src/contracts/beta-access.ts +0 -14
- package/src/schemas/beta-access.ts +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@read-frog/api-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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.11.0",
|
|
29
29
|
"zod": "^4.6.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
package/src/contracts/ebook.ts
CHANGED
|
@@ -45,7 +45,9 @@ export const ebookContract = {
|
|
|
45
45
|
.output(ebookGetOutputSchema),
|
|
46
46
|
|
|
47
47
|
requestUpload: ebookProcedure
|
|
48
|
-
.errors(
|
|
48
|
+
.errors(
|
|
49
|
+
pickPublicErrorMap("EBOOK_BETA_RESTRICTED", "EBOOK_LIMIT_EXCEEDED", "EBOOK_FILE_TOO_LARGE"),
|
|
50
|
+
)
|
|
49
51
|
.route({
|
|
50
52
|
method: "POST",
|
|
51
53
|
path: "/ebooks/uploads",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { pickPublicErrorMap } from "#/public-errors";
|
|
3
|
+
import {
|
|
4
|
+
emailPreferencesByTokenStateSchema,
|
|
5
|
+
emailPreferencesGetByTokenInputSchema,
|
|
6
|
+
emailPreferencesGetInputSchema,
|
|
7
|
+
emailPreferencesStateSchema,
|
|
8
|
+
emailPreferencesUpdateByTokenInputSchema,
|
|
9
|
+
emailPreferencesUpdateInputSchema,
|
|
10
|
+
} from "#/schemas/email-preferences";
|
|
11
|
+
|
|
12
|
+
// Every write returns the whole state, so a client can update optimistically
|
|
13
|
+
// and then take the server's answer.
|
|
14
|
+
export const emailPreferencesContract = {
|
|
15
|
+
get: oc
|
|
16
|
+
.route({
|
|
17
|
+
method: "GET",
|
|
18
|
+
path: "/users/me/email-preferences",
|
|
19
|
+
summary: "Get which emails the authenticated user receives",
|
|
20
|
+
tags: ["Email preferences"],
|
|
21
|
+
})
|
|
22
|
+
.input(emailPreferencesGetInputSchema)
|
|
23
|
+
.output(emailPreferencesStateSchema),
|
|
24
|
+
update: oc
|
|
25
|
+
.route({
|
|
26
|
+
method: "PATCH",
|
|
27
|
+
path: "/users/me/email-preferences",
|
|
28
|
+
summary: "Change which emails the authenticated user receives",
|
|
29
|
+
tags: ["Email preferences"],
|
|
30
|
+
})
|
|
31
|
+
.input(emailPreferencesUpdateInputSchema)
|
|
32
|
+
.output(emailPreferencesStateSchema),
|
|
33
|
+
// Public: the token in an email's link stands in for signing in. POST keeps
|
|
34
|
+
// the token out of URLs and logs.
|
|
35
|
+
getByToken: oc
|
|
36
|
+
.route({
|
|
37
|
+
method: "POST",
|
|
38
|
+
path: "/email-preferences/by-token",
|
|
39
|
+
summary: "Get the email preferences an email link is for",
|
|
40
|
+
tags: ["Email preferences"],
|
|
41
|
+
})
|
|
42
|
+
.errors(pickPublicErrorMap("EMAIL_LINK_INVALID"))
|
|
43
|
+
.input(emailPreferencesGetByTokenInputSchema)
|
|
44
|
+
.output(emailPreferencesByTokenStateSchema),
|
|
45
|
+
updateByToken: oc
|
|
46
|
+
.route({
|
|
47
|
+
method: "POST",
|
|
48
|
+
path: "/email-preferences/by-token/update",
|
|
49
|
+
summary: "Change the email preferences an email link is for",
|
|
50
|
+
tags: ["Email preferences"],
|
|
51
|
+
})
|
|
52
|
+
.errors(pickPublicErrorMap("EMAIL_LINK_INVALID"))
|
|
53
|
+
.input(emailPreferencesUpdateByTokenInputSchema)
|
|
54
|
+
.output(emailPreferencesByTokenStateSchema),
|
|
55
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import {
|
|
3
|
+
FeatureAccessStatusInputSchema,
|
|
4
|
+
FeatureAccessStatusOutputSchema,
|
|
5
|
+
} from "#/schemas/feature-access";
|
|
6
|
+
|
|
7
|
+
export const featureAccessContract = {
|
|
8
|
+
status: oc
|
|
9
|
+
.route({
|
|
10
|
+
method: "GET",
|
|
11
|
+
path: "/feature-access/status",
|
|
12
|
+
summary: "Get whether the caller can use a gated feature",
|
|
13
|
+
tags: ["Feature Access"],
|
|
14
|
+
})
|
|
15
|
+
.input(FeatureAccessStatusInputSchema)
|
|
16
|
+
.output(FeatureAccessStatusOutputSchema),
|
|
17
|
+
};
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
NotebaseGetSummaryOutputSchema,
|
|
14
14
|
NotebaseListInputSchema,
|
|
15
15
|
NotebaseListOutputSchema,
|
|
16
|
+
NotebaseSetSleepInputSchema,
|
|
17
|
+
NotebaseSetSleepOutputSchema,
|
|
16
18
|
NotebaseUpdateInputSchema,
|
|
17
19
|
NotebaseUpdateOutputSchema,
|
|
18
20
|
} from "#/schemas/notebase";
|
|
@@ -89,6 +91,18 @@ export const notebaseContract = {
|
|
|
89
91
|
.input(NotebaseUpdateInputSchema)
|
|
90
92
|
.output(NotebaseUpdateOutputSchema),
|
|
91
93
|
|
|
94
|
+
setSleep: defineAgentProcedure({
|
|
95
|
+
method: "POST",
|
|
96
|
+
path: "/notebases/{id}/sleep",
|
|
97
|
+
summary: "Set notebase sleep",
|
|
98
|
+
description:
|
|
99
|
+
"Put an owned notebase to sleep (enabled=true) or wake it (enabled=false). A sleeping notebase is left out of the combined review; its cards, memory states and review history are unchanged and it can still be reviewed directly. A request matching the current state succeeds without a change.",
|
|
100
|
+
tags: ["Notebases"],
|
|
101
|
+
})
|
|
102
|
+
.errors(pickPublicErrorMap("NOTEBASE_NOT_FOUND"))
|
|
103
|
+
.input(NotebaseSetSleepInputSchema)
|
|
104
|
+
.output(NotebaseSetSleepOutputSchema),
|
|
105
|
+
|
|
92
106
|
delete: defineAgentProcedure({
|
|
93
107
|
method: "DELETE",
|
|
94
108
|
path: "/notebases/{id}",
|
package/src/contracts/speech.ts
CHANGED
|
@@ -37,7 +37,7 @@ export const speechContract = {
|
|
|
37
37
|
.route({
|
|
38
38
|
method: "POST",
|
|
39
39
|
path: "/speech/synthesize",
|
|
40
|
-
summary: "Read a text aloud,
|
|
40
|
+
summary: "Read a text aloud, and link to its MP3 audio",
|
|
41
41
|
tags: ["Speech"],
|
|
42
42
|
})
|
|
43
43
|
.input(speechSynthesizeInputSchema)
|
package/src/contracts/user.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { oc } from "@orpc/contract";
|
|
2
2
|
import {
|
|
3
|
+
userEnsureLocaleInputSchema,
|
|
4
|
+
userEnsureLocaleOutputSchema,
|
|
3
5
|
userEnsureTimezoneInputSchema,
|
|
4
6
|
userEnsureTimezoneOutputSchema,
|
|
7
|
+
userUpdateLocaleInputSchema,
|
|
8
|
+
userUpdateLocaleOutputSchema,
|
|
5
9
|
userUpdateTimezoneInputSchema,
|
|
6
10
|
userUpdateTimezoneOutputSchema,
|
|
7
11
|
} from "#/schemas/user";
|
|
@@ -25,4 +29,22 @@ export const userContract = {
|
|
|
25
29
|
})
|
|
26
30
|
.input(userUpdateTimezoneInputSchema)
|
|
27
31
|
.output(userUpdateTimezoneOutputSchema),
|
|
32
|
+
ensureLocale: oc
|
|
33
|
+
.route({
|
|
34
|
+
method: "POST",
|
|
35
|
+
path: "/users/me/locale/ensure",
|
|
36
|
+
summary: "Ensure the authenticated user has a locale",
|
|
37
|
+
tags: ["Users"],
|
|
38
|
+
})
|
|
39
|
+
.input(userEnsureLocaleInputSchema)
|
|
40
|
+
.output(userEnsureLocaleOutputSchema),
|
|
41
|
+
updateLocale: oc
|
|
42
|
+
.route({
|
|
43
|
+
method: "PATCH",
|
|
44
|
+
path: "/users/me/locale",
|
|
45
|
+
summary: "Update the authenticated user's locale",
|
|
46
|
+
tags: ["Users"],
|
|
47
|
+
})
|
|
48
|
+
.input(userUpdateLocaleInputSchema)
|
|
49
|
+
.output(userUpdateLocaleOutputSchema),
|
|
28
50
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ContractRouterClient } from "@orpc/contract";
|
|
2
|
-
import { betaAccessContract } from "./contracts/beta-access";
|
|
3
2
|
import { billingContract } from "./contracts/billing";
|
|
4
3
|
import { cardContract } from "./contracts/card";
|
|
5
4
|
import { cardTemplateContract } from "./contracts/card-template";
|
|
6
5
|
import { ebookContract } from "./contracts/ebook";
|
|
6
|
+
import { emailPreferencesContract } from "./contracts/email-preferences";
|
|
7
|
+
import { featureAccessContract } from "./contracts/feature-access";
|
|
7
8
|
import { hostedAiContract } from "./contracts/hosted-ai";
|
|
8
9
|
import { masteryGoalContract } from "./contracts/mastery-goal";
|
|
9
10
|
import { mcpConnectionContract } from "./contracts/mcp-connection";
|
|
@@ -17,11 +18,12 @@ import { userContract } from "./contracts/user";
|
|
|
17
18
|
import { videoTranscriptContract } from "./contracts/video-transcript";
|
|
18
19
|
|
|
19
20
|
export const contract = {
|
|
20
|
-
betaAccess: betaAccessContract,
|
|
21
21
|
billing: billingContract,
|
|
22
22
|
card: cardContract,
|
|
23
23
|
cardTemplate: cardTemplateContract,
|
|
24
24
|
ebook: ebookContract,
|
|
25
|
+
emailPreferences: emailPreferencesContract,
|
|
26
|
+
featureAccess: featureAccessContract,
|
|
25
27
|
hostedAi: hostedAiContract,
|
|
26
28
|
masteryGoal: masteryGoalContract,
|
|
27
29
|
mcpConnection: mcpConnectionContract,
|
|
@@ -39,10 +41,12 @@ export type ORPCRouterClient = ContractRouterClient<typeof contract>;
|
|
|
39
41
|
|
|
40
42
|
export { type AgentRoute, defineAgentProcedure } from "./contracts/shared";
|
|
41
43
|
export * from "./public-errors";
|
|
42
|
-
export * from "./schemas/
|
|
44
|
+
export * from "./schemas/app-locale";
|
|
43
45
|
export * from "./schemas/billing";
|
|
44
46
|
export * from "./schemas/card";
|
|
45
47
|
export * from "./schemas/ebook";
|
|
48
|
+
export * from "./schemas/email-preferences";
|
|
49
|
+
export * from "./schemas/feature-access";
|
|
46
50
|
export * from "./schemas/hosted-ai";
|
|
47
51
|
export * from "./schemas/mastery-goal";
|
|
48
52
|
export * from "./schemas/mcp-connection";
|
package/src/public-errors.ts
CHANGED
|
@@ -112,21 +112,10 @@ export const PUBLIC_APP_ERROR_DEFS = {
|
|
|
112
112
|
message: "Built-in AI credit exhausted",
|
|
113
113
|
status: 429,
|
|
114
114
|
},
|
|
115
|
-
NOTEBASE_BETA_RESTRICTED: {
|
|
116
|
-
message: "Notebase is currently in beta for selected accounts",
|
|
117
|
-
status: 403,
|
|
118
|
-
},
|
|
119
115
|
NOTEBASE_NOT_FOUND: {
|
|
120
116
|
message: "Notebase not found",
|
|
121
117
|
status: 404,
|
|
122
118
|
},
|
|
123
|
-
// Retired at launch (2026-08-14) but kept defined for one release so old
|
|
124
|
-
// clients' error maps keep parsing; the server no longer emits it. Remove
|
|
125
|
-
// together with the beta feature key in the follow-up cleanup.
|
|
126
|
-
VIDEO_TRANSCRIPTION_BETA_RESTRICTED: {
|
|
127
|
-
message: "Video transcription is currently in beta for selected accounts",
|
|
128
|
-
status: 403,
|
|
129
|
-
},
|
|
130
119
|
VIDEO_TRANSCRIPTION_SUBSCRIPTION_REQUIRED: {
|
|
131
120
|
message: "AI subtitles require a Pro or Ultra subscription",
|
|
132
121
|
status: 403,
|
|
@@ -290,6 +279,12 @@ export const PUBLIC_APP_ERROR_DEFS = {
|
|
|
290
279
|
message: "Ebook upload has not been completed",
|
|
291
280
|
status: 409,
|
|
292
281
|
},
|
|
282
|
+
// An email's unsubscribe or preferences link that does not verify, or whose
|
|
283
|
+
// account no longer exists. The page asks the reader to sign in instead.
|
|
284
|
+
EMAIL_LINK_INVALID: {
|
|
285
|
+
message: "This email link is not valid",
|
|
286
|
+
status: 400,
|
|
287
|
+
},
|
|
293
288
|
} as const satisfies Record<string, PublicAppErrorDefinition>;
|
|
294
289
|
|
|
295
290
|
export type PublicAppErrorCode = keyof typeof PUBLIC_APP_ERROR_DEFS;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The languages the website is in (`project.inlang/settings.json`; a test in
|
|
5
|
+
* apps/www keeps the two in step). Used wherever the server writes for a
|
|
6
|
+
* person: Stripe pages, emails, and the account's `locale`.
|
|
7
|
+
*/
|
|
8
|
+
export const appLocaleSchema = z.enum(["en", "es", "ja", "ko", "ru", "tr", "vi", "zh", "zh-TW"]);
|
|
9
|
+
export type AppLocale = z.infer<typeof appLocaleSchema>;
|
package/src/schemas/billing.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { AppLocale } from "./app-locale";
|
|
1
2
|
import { z } from "zod";
|
|
3
|
+
import { appLocaleSchema } from "./app-locale";
|
|
2
4
|
|
|
3
5
|
export const billingIntervalSchema = z.enum(["month", "year"]);
|
|
4
6
|
export type BillingInterval = z.infer<typeof billingIntervalSchema>;
|
|
@@ -19,30 +21,20 @@ export const billingPlanSchema = z.enum(["free", "pro", "ultra"]);
|
|
|
19
21
|
export type BillingPlan = z.infer<typeof billingPlanSchema>;
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
|
-
*
|
|
24
|
+
* How many MCP tool calls each plan may make in a day (24 hours from the first
|
|
25
|
+
* one); `null` is no limit. Each `tools/call` counts once, whatever the tool.
|
|
23
26
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* down. The cost is that nothing derives from it automatically -- a feature's
|
|
27
|
-
* pricing-card placement is a companion edit, noted at each `*_ALLOWED_PLANS`.
|
|
28
|
-
*/
|
|
29
|
-
export function isPlanAllowed(plan: BillingPlan, allowed: readonly BillingPlan[]): boolean {
|
|
30
|
-
return allowed.includes(plan);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Plans that may use the remote MCP server.
|
|
27
|
+
* Read by the MCP server's gate AND by the website's copy, so the pages cannot
|
|
28
|
+
* promise calls the server refuses.
|
|
35
29
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* the UI can render the plan names from this constant instead of hardcoding
|
|
39
|
-
* them.
|
|
40
|
-
*
|
|
41
|
-
* Companion edit when this changes: the feature bullet in
|
|
42
|
-
* `apps/www/src/routes/pricing.tsx` lives on the lowest allowed plan's card
|
|
43
|
-
* (higher cards inherit it through their "everything in <lower plan>" line).
|
|
30
|
+
* Companion edit when this changes: the MCP docs under
|
|
31
|
+
* `apps/www/content/docs/mcp/` and `account-and-plans`, which state the numbers.
|
|
44
32
|
*/
|
|
45
|
-
export const
|
|
33
|
+
export const MCP_DAILY_CALL_LIMITS = {
|
|
34
|
+
free: 10,
|
|
35
|
+
pro: 50,
|
|
36
|
+
ultra: null,
|
|
37
|
+
} as const satisfies Record<BillingPlan, number | null>;
|
|
46
38
|
|
|
47
39
|
// Plans a user can actively check out into (the free tier has no price).
|
|
48
40
|
export const billingCheckoutPlanSchema = z.enum(["pro", "ultra"]);
|
|
@@ -51,18 +43,10 @@ export type BillingCheckoutPlan = z.infer<typeof billingCheckoutPlanSchema>;
|
|
|
51
43
|
export const billingCurrencySchema = z.enum(["usd", "cad", "cny"]);
|
|
52
44
|
export type BillingCurrency = z.infer<typeof billingCurrencySchema>;
|
|
53
45
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"ko",
|
|
59
|
-
"ru",
|
|
60
|
-
"tr",
|
|
61
|
-
"vi",
|
|
62
|
-
"zh",
|
|
63
|
-
"zh-TW",
|
|
64
|
-
]);
|
|
65
|
-
export type BillingLocale = z.infer<typeof billingLocaleSchema>;
|
|
46
|
+
// The billing name of `appLocaleSchema`, which it was before the account had a
|
|
47
|
+
// locale of its own; kept so existing imports keep working.
|
|
48
|
+
export const billingLocaleSchema = appLocaleSchema;
|
|
49
|
+
export type BillingLocale = AppLocale;
|
|
66
50
|
|
|
67
51
|
export const billingAccessSourceSchema = z.enum(["subscription", "pass"]);
|
|
68
52
|
export type BillingAccessSource = z.infer<typeof billingAccessSourceSchema>;
|
|
@@ -76,6 +60,33 @@ export const billingScheduledChangeSchema = z
|
|
|
76
60
|
.strict();
|
|
77
61
|
export type BillingScheduledChange = z.infer<typeof billingScheduledChangeSchema>;
|
|
78
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The zone a payment deadline is shown in, as a date with no time. UTC−12 is
|
|
65
|
+
* the last zone to finish a day, and `dueBy` is the end of a day there, so the
|
|
66
|
+
* date it shows stays open in every customer's zone until their own midnight.
|
|
67
|
+
*/
|
|
68
|
+
export const BILLING_PAYMENT_DUE_TIME_ZONE = "Etc/GMT+12";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A renewal whose payment failed. The plan stays until `dueBy`; paying the
|
|
72
|
+
* invoice before then (or later, until the subscription is canceled) restores
|
|
73
|
+
* the subscription as if the payment had gone through.
|
|
74
|
+
*/
|
|
75
|
+
export const billingPaymentDueSchema = z
|
|
76
|
+
.object({
|
|
77
|
+
/** The last instant of the plan; show it in BILLING_PAYMENT_DUE_TIME_ZONE. */
|
|
78
|
+
dueBy: z.coerce.date(),
|
|
79
|
+
/**
|
|
80
|
+
* The plan the payment keeps. Not `BillingStatus.plan`: past `dueBy` that
|
|
81
|
+
* already reads "free" while the invoice is still payable.
|
|
82
|
+
*/
|
|
83
|
+
plan: billingCheckoutPlanSchema,
|
|
84
|
+
/** The Stripe-hosted page that pays the invoice; null if Stripe gave none. */
|
|
85
|
+
url: z.url().nullable(),
|
|
86
|
+
})
|
|
87
|
+
.strict();
|
|
88
|
+
export type BillingPaymentDue = z.infer<typeof billingPaymentDueSchema>;
|
|
89
|
+
|
|
79
90
|
export const billingStatusSchema = z
|
|
80
91
|
.object({
|
|
81
92
|
plan: billingPlanSchema,
|
|
@@ -91,6 +102,7 @@ export const billingStatusSchema = z
|
|
|
91
102
|
cancelAt: z.coerce.date().nullable(),
|
|
92
103
|
accessSource: billingAccessSourceSchema.nullable(),
|
|
93
104
|
scheduledChange: billingScheduledChangeSchema.nullable(),
|
|
105
|
+
paymentDue: billingPaymentDueSchema.nullable(),
|
|
94
106
|
unlimitedNotebaseNotes: z.boolean(),
|
|
95
107
|
notebaseNoteLimit: z.number().int().positive(),
|
|
96
108
|
notebaseNoteCount: z.number().int().min(0),
|
|
@@ -102,7 +114,7 @@ export const billingCreateCheckoutSessionInputSchema = z
|
|
|
102
114
|
.object({
|
|
103
115
|
currency: billingCurrencySchema,
|
|
104
116
|
interval: billingIntervalSchema,
|
|
105
|
-
locale:
|
|
117
|
+
locale: appLocaleSchema,
|
|
106
118
|
plan: billingCheckoutPlanSchema.default("pro"),
|
|
107
119
|
})
|
|
108
120
|
.strict();
|
|
@@ -112,7 +124,7 @@ export type BillingCreateCheckoutSessionInput = z.infer<
|
|
|
112
124
|
|
|
113
125
|
export const billingCreateCustomerPortalSessionInputSchema = z
|
|
114
126
|
.object({
|
|
115
|
-
locale:
|
|
127
|
+
locale: appLocaleSchema,
|
|
116
128
|
})
|
|
117
129
|
.strict();
|
|
118
130
|
export type BillingCreateCustomerPortalSessionInput = z.infer<
|
package/src/schemas/ebook.ts
CHANGED
|
@@ -68,28 +68,36 @@ export const ebookRequestUploadInputSchema = z
|
|
|
68
68
|
title: z.string().min(1).max(500),
|
|
69
69
|
author: z.string().max(500).optional(),
|
|
70
70
|
language: z.string().max(35).optional(),
|
|
71
|
-
|
|
71
|
+
// Exact sizes: each upload is signed for them, and storage refuses any other file.
|
|
72
|
+
fileByteSize: z.number().int().min(1),
|
|
73
|
+
cover: z
|
|
74
|
+
.object({
|
|
75
|
+
contentType: ebookCoverContentTypeSchema,
|
|
76
|
+
byteSize: z.number().int().min(1),
|
|
77
|
+
})
|
|
78
|
+
.strict()
|
|
79
|
+
.optional(),
|
|
72
80
|
})
|
|
73
81
|
.strict();
|
|
74
82
|
export type EbookRequestUploadInput = z.infer<typeof ebookRequestUploadInputSchema>;
|
|
75
83
|
|
|
76
84
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
85
|
+
* A signed PUT: send the file to `url` with exactly `headers`. The signature
|
|
86
|
+
* covers the file's type and exact size, so storage refuses any other file.
|
|
79
87
|
*/
|
|
80
|
-
export const
|
|
88
|
+
export const ebookPresignedUploadSchema = z
|
|
81
89
|
.object({
|
|
82
90
|
url: z.url(),
|
|
83
|
-
|
|
91
|
+
headers: z.record(z.string(), z.string()),
|
|
84
92
|
})
|
|
85
93
|
.strict();
|
|
86
|
-
export type
|
|
94
|
+
export type EbookPresignedUpload = z.infer<typeof ebookPresignedUploadSchema>;
|
|
87
95
|
|
|
88
96
|
export const ebookRequestUploadOutputSchema = z
|
|
89
97
|
.object({
|
|
90
98
|
ebookId: z.uuid(),
|
|
91
|
-
upload:
|
|
92
|
-
coverUpload:
|
|
99
|
+
upload: ebookPresignedUploadSchema,
|
|
100
|
+
coverUpload: ebookPresignedUploadSchema.nullable(),
|
|
93
101
|
expiresAt: z.coerce.date(),
|
|
94
102
|
})
|
|
95
103
|
.strict();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { appLocaleSchema } from "./app-locale";
|
|
3
|
+
|
|
4
|
+
// Which marketing and lifecycle emails a user gets, by category. Read and
|
|
5
|
+
// changed signed in (the settings page), or through the signed token in an
|
|
6
|
+
// email's unsubscribe link (the unsubscribe page that link opens, which needs
|
|
7
|
+
// no sign-in). Category names come from the server in the requested locale.
|
|
8
|
+
|
|
9
|
+
/** A category's key, such as `promotions`. */
|
|
10
|
+
export const emailCategoryKeySchema = z
|
|
11
|
+
.string()
|
|
12
|
+
.max(64)
|
|
13
|
+
.regex(/^[a-z][a-z0-9_]*$/);
|
|
14
|
+
export type EmailCategoryKey = z.infer<typeof emailCategoryKeySchema>;
|
|
15
|
+
|
|
16
|
+
/** The token in an email's unsubscribe link. */
|
|
17
|
+
export const emailLinkTokenSchema = z.string().min(1).max(1024);
|
|
18
|
+
|
|
19
|
+
export const emailCategoryStateSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
key: z.string(),
|
|
22
|
+
// In the requested locale, or English when the category has none for it.
|
|
23
|
+
name: z.string(),
|
|
24
|
+
description: z.string().nullable(),
|
|
25
|
+
// Promotional mail.
|
|
26
|
+
commercial: z.boolean(),
|
|
27
|
+
// Notices about the account and the service, which nobody can unsubscribe
|
|
28
|
+
// from: always subscribed, "unsubscribe from all" included, and a change
|
|
29
|
+
// to it is ignored. Show it on, without a way to turn it off.
|
|
30
|
+
required: z.boolean(),
|
|
31
|
+
// Whether the user gets this category now, "unsubscribe from all" included.
|
|
32
|
+
subscribed: z.boolean(),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
35
|
+
export type EmailCategoryState = z.infer<typeof emailCategoryStateSchema>;
|
|
36
|
+
|
|
37
|
+
export const emailPreferencesStateSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
// In display order; archived categories are left out.
|
|
40
|
+
categories: z.array(emailCategoryStateSchema),
|
|
41
|
+
unsubscribedAll: z.boolean(),
|
|
42
|
+
})
|
|
43
|
+
.strict();
|
|
44
|
+
export type EmailPreferencesState = z.infer<typeof emailPreferencesStateSchema>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* What the page a link opens shows on top of the state: whose preferences they
|
|
48
|
+
* are, without the full address, and the category the link was for, if any.
|
|
49
|
+
*/
|
|
50
|
+
export const emailPreferencesByTokenStateSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
...emailPreferencesStateSchema.shape,
|
|
53
|
+
maskedEmail: z.string(),
|
|
54
|
+
// The link's category, when it is one the user can unsubscribe from: null
|
|
55
|
+
// for a link without one, or for an archived, required or unknown one.
|
|
56
|
+
highlightCategory: z.string().nullable(),
|
|
57
|
+
})
|
|
58
|
+
.strict();
|
|
59
|
+
export type EmailPreferencesByTokenState = z.infer<typeof emailPreferencesByTokenStateSchema>;
|
|
60
|
+
|
|
61
|
+
const emailPreferencesChangesShape = {
|
|
62
|
+
// Subscribes (true) or unsubscribes (false) the categories it names, and
|
|
63
|
+
// leaves the rest alone. Subscribing to one after "unsubscribe from all"
|
|
64
|
+
// ends it, and every other category stays unsubscribed. A required category
|
|
65
|
+
// is ignored.
|
|
66
|
+
categories: z.record(emailCategoryKeySchema, z.boolean()).optional(),
|
|
67
|
+
// true unsubscribes from every category but the required ones, including
|
|
68
|
+
// categories added later; false ends that and leaves each category as it was
|
|
69
|
+
// before.
|
|
70
|
+
unsubscribedAll: z.boolean().optional(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const emailPreferencesGetInputSchema = z
|
|
74
|
+
.object({
|
|
75
|
+
locale: appLocaleSchema,
|
|
76
|
+
})
|
|
77
|
+
.strict();
|
|
78
|
+
export type EmailPreferencesGetInput = z.infer<typeof emailPreferencesGetInputSchema>;
|
|
79
|
+
|
|
80
|
+
export const emailPreferencesUpdateInputSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
locale: appLocaleSchema,
|
|
83
|
+
...emailPreferencesChangesShape,
|
|
84
|
+
})
|
|
85
|
+
.strict();
|
|
86
|
+
export type EmailPreferencesUpdateInput = z.infer<typeof emailPreferencesUpdateInputSchema>;
|
|
87
|
+
|
|
88
|
+
export const emailPreferencesGetByTokenInputSchema = z
|
|
89
|
+
.object({
|
|
90
|
+
token: emailLinkTokenSchema,
|
|
91
|
+
locale: appLocaleSchema,
|
|
92
|
+
})
|
|
93
|
+
.strict();
|
|
94
|
+
export type EmailPreferencesGetByTokenInput = z.infer<typeof emailPreferencesGetByTokenInputSchema>;
|
|
95
|
+
|
|
96
|
+
export const emailPreferencesUpdateByTokenInputSchema = z
|
|
97
|
+
.object({
|
|
98
|
+
token: emailLinkTokenSchema,
|
|
99
|
+
locale: appLocaleSchema,
|
|
100
|
+
...emailPreferencesChangesShape,
|
|
101
|
+
})
|
|
102
|
+
.strict();
|
|
103
|
+
export type EmailPreferencesUpdateByTokenInput = z.infer<
|
|
104
|
+
typeof emailPreferencesUpdateByTokenInputSchema
|
|
105
|
+
>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GATED_FEATURE_KEYS } from "@read-frog/definitions";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const GatedFeatureKeySchema = z.enum(GATED_FEATURE_KEYS);
|
|
5
|
+
export type GatedFeatureKey = z.infer<typeof GatedFeatureKeySchema>;
|
|
6
|
+
|
|
7
|
+
export const FeatureAccessStatusInputSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
featureKey: GatedFeatureKeySchema,
|
|
10
|
+
})
|
|
11
|
+
.strict();
|
|
12
|
+
export type FeatureAccessStatusInput = z.infer<typeof FeatureAccessStatusInputSchema>;
|
|
13
|
+
|
|
14
|
+
export const FeatureAccessStatusOutputSchema = z.object({
|
|
15
|
+
featureKey: GatedFeatureKeySchema,
|
|
16
|
+
allowed: z.boolean(),
|
|
17
|
+
});
|
|
18
|
+
export type FeatureAccessStatusOutput = z.infer<typeof FeatureAccessStatusOutputSchema>;
|
package/src/schemas/hosted-ai.ts
CHANGED
|
@@ -196,13 +196,16 @@ export const HostedAiRateLimitErrorDataSchema = z.object({
|
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
/**
|
|
199
|
+
* A recurring pool: the plan's credit for a day or a week, which refills at
|
|
200
|
+
* `resetAt`.
|
|
201
|
+
*
|
|
199
202
|
* Deliberately free of dollar amounts and of the versioned policy id. Clients
|
|
200
203
|
* render a consumption bar and a reset time, so a percentage and a period kind
|
|
201
204
|
* are all they need, and neither leaks what a call costs us. `usedPercent`
|
|
202
205
|
* counts settled spend only: including in-flight reservations would make the
|
|
203
206
|
* bar jump forward and then back as each stream settles.
|
|
204
207
|
*/
|
|
205
|
-
export const
|
|
208
|
+
export const HostedAiRecurringCreditStatusSchema = z.strictObject({
|
|
206
209
|
periodKind: HostedAiCreditPeriodKindSchema,
|
|
207
210
|
usedPercent: z.number().min(0).max(100),
|
|
208
211
|
resetAt: z.iso.datetime().nullable(),
|
|
@@ -214,6 +217,30 @@ export const HostedAiCreditStatusSchema = z.strictObject({
|
|
|
214
217
|
features: z.array(HostedAiFeatureSchema).min(1),
|
|
215
218
|
});
|
|
216
219
|
|
|
220
|
+
/**
|
|
221
|
+
* A one-time pool, such as credit from a campaign or from support. It never
|
|
222
|
+
* refills: once spent or past `expiresAt` it is gone, so a client shows the
|
|
223
|
+
* expiry, not a reset. It is drawn on only after the plan's own pool for a
|
|
224
|
+
* feature is spent, or for a feature the plan does not fund. Listed until it
|
|
225
|
+
* expires, even when spent. Same fields as a recurring pool otherwise.
|
|
226
|
+
*/
|
|
227
|
+
export const HostedAiOneTimeCreditStatusSchema = z.strictObject({
|
|
228
|
+
periodKind: z.literal("once"),
|
|
229
|
+
usedPercent: z.number().min(0).max(100),
|
|
230
|
+
expiresAt: z.iso.datetime(),
|
|
231
|
+
features: z.array(HostedAiFeatureSchema).min(1),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* One pool that can pay for Built-in AI, told apart by `periodKind`: a
|
|
236
|
+
* recurring `daily` or `weekly` pool refills at `resetAt`, a one-time (`once`)
|
|
237
|
+
* pool ends at `expiresAt`. Narrow on `periodKind` before reading either.
|
|
238
|
+
*/
|
|
239
|
+
export const HostedAiCreditPoolStatusSchema = z.discriminatedUnion("periodKind", [
|
|
240
|
+
HostedAiRecurringCreditStatusSchema,
|
|
241
|
+
HostedAiOneTimeCreditStatusSchema,
|
|
242
|
+
]);
|
|
243
|
+
|
|
217
244
|
export const HostedAiTierStatusSchema = z.strictObject({
|
|
218
245
|
accessAllowed: z.boolean(),
|
|
219
246
|
available: z.boolean(),
|
|
@@ -234,7 +261,21 @@ export const HostedAiFeatureStatusSchema = z.strictObject({
|
|
|
234
261
|
});
|
|
235
262
|
|
|
236
263
|
export const HostedAiStatusOutputSchema = z.strictObject({
|
|
237
|
-
|
|
264
|
+
/**
|
|
265
|
+
* @deprecated Read `pools`. This lists the recurring pools only, never a
|
|
266
|
+
* one-time one: extensions released before mengxi-ream/read-frog#2395 look
|
|
267
|
+
* up a label for every entry's `periodKind`, and one they do not know
|
|
268
|
+
* crashes their usage page. They also throw if the field is missing.
|
|
269
|
+
*
|
|
270
|
+
* TODO(2026-11-07): remove once those extensions have drained (#710).
|
|
271
|
+
*/
|
|
272
|
+
credits: z.array(HostedAiRecurringCreditStatusSchema),
|
|
273
|
+
/**
|
|
274
|
+
* Everything that can pay for a feature, in the order the server draws on it:
|
|
275
|
+
* the plan's recurring pools first, then one-time pools by soonest expiry. A
|
|
276
|
+
* feature can appear in several entries.
|
|
277
|
+
*/
|
|
278
|
+
pools: z.array(HostedAiCreditPoolStatusSchema),
|
|
238
279
|
features: z.strictObject({
|
|
239
280
|
pageTranslation: HostedAiFeatureStatusSchema,
|
|
240
281
|
customAction: HostedAiFeatureStatusSchema,
|
|
@@ -265,7 +306,9 @@ export type HostedAiCreditPolicyId = z.infer<typeof HostedAiCreditPolicyIdSchema
|
|
|
265
306
|
export type HostedAiCreditPeriodKind = z.infer<typeof HostedAiCreditPeriodKindSchema>;
|
|
266
307
|
export type HostedAiQuotaScope = z.infer<typeof HostedAiQuotaScopeSchema>;
|
|
267
308
|
export type HostedAiRateLimitErrorData = z.infer<typeof HostedAiRateLimitErrorDataSchema>;
|
|
268
|
-
export type
|
|
309
|
+
export type HostedAiRecurringCreditStatus = z.infer<typeof HostedAiRecurringCreditStatusSchema>;
|
|
310
|
+
export type HostedAiOneTimeCreditStatus = z.infer<typeof HostedAiOneTimeCreditStatusSchema>;
|
|
311
|
+
export type HostedAiCreditPoolStatus = z.infer<typeof HostedAiCreditPoolStatusSchema>;
|
|
269
312
|
export type HostedAiTierStatus = z.infer<typeof HostedAiTierStatusSchema>;
|
|
270
313
|
export type HostedAiFeatureStatus = z.infer<typeof HostedAiFeatureStatusSchema>;
|
|
271
314
|
export type HostedAiStatusOutput = z.infer<typeof HostedAiStatusOutputSchema>;
|