@read-frog/api-contract 0.21.1 → 0.23.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.
@@ -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
 
@@ -159,13 +160,25 @@ export const HOSTED_AI_CLIENT_STREAM_PART_TYPES = [
159
160
  */
160
161
  export const HostedAiStatusInputSchema = z.object({});
161
162
 
163
+ /**
164
+ * `pro_required` means the Pro plan or any plan above it; `ultra_required`
165
+ * means Ultra. Appended last, so a client that predates a value falls back on
166
+ * its own default instead of misreading it.
167
+ */
162
168
  export const HostedAiUnavailableReasonSchema = z.enum([
163
169
  "authentication_required",
164
170
  "ultra_required",
165
171
  "quota_exhausted",
166
172
  "service_unavailable",
173
+ "pro_required",
167
174
  ]);
168
175
 
176
+ /**
177
+ * The cheapest plan an account would need to buy for something it cannot use
178
+ * now. Any plan above it works too, so `pro` reads as "Pro or Ultra".
179
+ */
180
+ export const HostedAiUpgradePlanSchema = billingPlanSchema.exclude(["free"]);
181
+
169
182
  export const HostedAiCreditPolicyIdSchema = z.enum([
170
183
  "custom-action-guest-daily-v1",
171
184
  "custom-action-user-daily-v1",
@@ -196,13 +209,16 @@ export const HostedAiRateLimitErrorDataSchema = z.object({
196
209
  });
197
210
 
198
211
  /**
212
+ * A recurring pool: the plan's credit for a day or a week, which refills at
213
+ * `resetAt`.
214
+ *
199
215
  * Deliberately free of dollar amounts and of the versioned policy id. Clients
200
216
  * render a consumption bar and a reset time, so a percentage and a period kind
201
217
  * are all they need, and neither leaks what a call costs us. `usedPercent`
202
218
  * counts settled spend only: including in-flight reservations would make the
203
219
  * bar jump forward and then back as each stream settles.
204
220
  */
205
- export const HostedAiCreditStatusSchema = z.strictObject({
221
+ export const HostedAiRecurringCreditStatusSchema = z.strictObject({
206
222
  periodKind: HostedAiCreditPeriodKindSchema,
207
223
  usedPercent: z.number().min(0).max(100),
208
224
  resetAt: z.iso.datetime().nullable(),
@@ -214,15 +230,47 @@ export const HostedAiCreditStatusSchema = z.strictObject({
214
230
  features: z.array(HostedAiFeatureSchema).min(1),
215
231
  });
216
232
 
233
+ /**
234
+ * A one-time pool, such as credit from a campaign or from support. It never
235
+ * refills: once spent or past `expiresAt` it is gone, so a client shows the
236
+ * expiry, not a reset. It is drawn on only after the plan's own pool for a
237
+ * feature is spent, or for a feature the plan does not fund. Listed until it
238
+ * expires, even when spent. Same fields as a recurring pool otherwise.
239
+ */
240
+ export const HostedAiOneTimeCreditStatusSchema = z.strictObject({
241
+ periodKind: z.literal("once"),
242
+ usedPercent: z.number().min(0).max(100),
243
+ expiresAt: z.iso.datetime(),
244
+ features: z.array(HostedAiFeatureSchema).min(1),
245
+ });
246
+
247
+ /**
248
+ * One pool that can pay for Built-in AI, told apart by `periodKind`: a
249
+ * recurring `daily` or `weekly` pool refills at `resetAt`, a one-time (`once`)
250
+ * pool ends at `expiresAt`. Narrow on `periodKind` before reading either.
251
+ */
252
+ export const HostedAiCreditPoolStatusSchema = z.discriminatedUnion("periodKind", [
253
+ HostedAiRecurringCreditStatusSchema,
254
+ HostedAiOneTimeCreditStatusSchema,
255
+ ]);
256
+
217
257
  export const HostedAiTierStatusSchema = z.strictObject({
218
258
  accessAllowed: z.boolean(),
219
259
  available: z.boolean(),
220
260
  unavailableReason: HostedAiUnavailableReasonSchema.nullable(),
221
261
  /**
222
- * Viewer-independent product fact: no non-Ultra account can ever use this
223
- * feature/tier, so signing in alone will not unlock it. Unlike the fields
224
- * above, this does not vary with the caller's identity or plan — clients use
225
- * it to label Ultra-only options rather than to gate the current user.
262
+ * The cheapest plan this account would need to use the tier, or null when it
263
+ * needs none: its plan or a one-time pool already covers it, or the tier is
264
+ * open to every signed-in account. For a guest it is the plan needed after
265
+ * signing in. Clients badge an option with it and offer that plan.
266
+ */
267
+ upgradeTo: HostedAiUpgradePlanSchema.nullable(),
268
+ /**
269
+ * @deprecated Read `upgradeTo`. Viewer-independent product fact: no
270
+ * non-Ultra account can ever use this feature/tier. Extensions released
271
+ * before `upgradeTo` badge options with it, including for Ultra accounts.
272
+ *
273
+ * TODO(2026-11-21): remove once those extensions have drained (#710).
226
274
  */
227
275
  requiresUltra: z.boolean(),
228
276
  modelRevision: z.string().trim().min(1),
@@ -234,7 +282,21 @@ export const HostedAiFeatureStatusSchema = z.strictObject({
234
282
  });
235
283
 
236
284
  export const HostedAiStatusOutputSchema = z.strictObject({
237
- credits: z.array(HostedAiCreditStatusSchema),
285
+ /**
286
+ * @deprecated Read `pools`. This lists the recurring pools only, never a
287
+ * one-time one: extensions released before mengxi-ream/read-frog#2395 look
288
+ * up a label for every entry's `periodKind`, and one they do not know
289
+ * crashes their usage page. They also throw if the field is missing.
290
+ *
291
+ * TODO(2026-11-07): remove once those extensions have drained (#710).
292
+ */
293
+ credits: z.array(HostedAiRecurringCreditStatusSchema),
294
+ /**
295
+ * Everything that can pay for a feature, in the order the server draws on it:
296
+ * the plan's recurring pools first, then one-time pools by soonest expiry. A
297
+ * feature can appear in several entries.
298
+ */
299
+ pools: z.array(HostedAiCreditPoolStatusSchema),
238
300
  features: z.strictObject({
239
301
  pageTranslation: HostedAiFeatureStatusSchema,
240
302
  customAction: HostedAiFeatureStatusSchema,
@@ -261,11 +323,14 @@ export type HostedAiNoteSuggestionObject = z.infer<typeof HostedAiNoteSuggestion
261
323
  export type HostedAiStreamPart = z.infer<typeof HostedAiStreamPartSchema>;
262
324
  export type HostedAiStatusInput = z.infer<typeof HostedAiStatusInputSchema>;
263
325
  export type HostedAiUnavailableReason = z.infer<typeof HostedAiUnavailableReasonSchema>;
326
+ export type HostedAiUpgradePlan = z.infer<typeof HostedAiUpgradePlanSchema>;
264
327
  export type HostedAiCreditPolicyId = z.infer<typeof HostedAiCreditPolicyIdSchema>;
265
328
  export type HostedAiCreditPeriodKind = z.infer<typeof HostedAiCreditPeriodKindSchema>;
266
329
  export type HostedAiQuotaScope = z.infer<typeof HostedAiQuotaScopeSchema>;
267
330
  export type HostedAiRateLimitErrorData = z.infer<typeof HostedAiRateLimitErrorDataSchema>;
268
- export type HostedAiCreditStatus = z.infer<typeof HostedAiCreditStatusSchema>;
331
+ export type HostedAiRecurringCreditStatus = z.infer<typeof HostedAiRecurringCreditStatusSchema>;
332
+ export type HostedAiOneTimeCreditStatus = z.infer<typeof HostedAiOneTimeCreditStatusSchema>;
333
+ export type HostedAiCreditPoolStatus = z.infer<typeof HostedAiCreditPoolStatusSchema>;
269
334
  export type HostedAiTierStatus = z.infer<typeof HostedAiTierStatusSchema>;
270
335
  export type HostedAiFeatureStatus = z.infer<typeof HostedAiFeatureStatusSchema>;
271
336
  export type HostedAiStatusOutput = z.infer<typeof HostedAiStatusOutputSchema>;
@@ -41,6 +41,8 @@ export const notebaseSchema = z.object({
41
41
  srsWeights: srsWeightsSchema.nullable(),
42
42
  // Selects the scheduler/curve; read-only, changed only by the FSRS-7 migration.
43
43
  srsModelVersion: srsModelVersionSchema,
44
+ // Null = awake; set = excluded from the combined review. Written by notebase.setSleep and auto sleep.
45
+ srsSleepingAt: z.date().nullable(),
44
46
  createdAt: z.date(),
45
47
  updatedAt: z.date(),
46
48
  });
@@ -144,6 +146,20 @@ export const NotebaseUpdateOutputSchema = z.object({
144
146
  });
145
147
  export type NotebaseUpdateOutput = z.infer<typeof NotebaseUpdateOutputSchema>;
146
148
 
149
+ export const NotebaseSetSleepInputSchema = z
150
+ .object({
151
+ id: z.uuid(),
152
+ enabled: z.boolean(),
153
+ })
154
+ .strict();
155
+ export type NotebaseSetSleepInput = z.infer<typeof NotebaseSetSleepInputSchema>;
156
+
157
+ // txid is null when the notebase was already in the requested state: nothing to wait for.
158
+ export const NotebaseSetSleepOutputSchema = z.object({
159
+ txid: z.number().nullable(),
160
+ });
161
+ export type NotebaseSetSleepOutput = z.infer<typeof NotebaseSetSleepOutputSchema>;
162
+
147
163
  export const NotebaseDeleteInputSchema = z.object({
148
164
  id: z.uuid(),
149
165
  });
@@ -109,5 +109,14 @@ export const speechSynthesizeInputSchema = z
109
109
  .strict();
110
110
  export type SpeechSynthesizeInput = z.infer<typeof speechSynthesizeInputSchema>;
111
111
 
112
- /** The audio, as an MP3 file. */
113
- export const speechSynthesizeOutputSchema = z.file();
112
+ /**
113
+ * Where to download the audio, an MP3: a link to fetch at once, which stops
114
+ * working at `expiresAt`.
115
+ */
116
+ export const speechSynthesizeOutputSchema = z
117
+ .object({
118
+ url: z.url(),
119
+ expiresAt: z.coerce.date(),
120
+ })
121
+ .strict();
122
+ export type SpeechSynthesizeOutput = z.infer<typeof speechSynthesizeOutputSchema>;
@@ -13,7 +13,7 @@ import {
13
13
  import { z } from "zod";
14
14
  import { cardSchema } from "./card";
15
15
  import { notebaseSummarySchema } from "./notebase";
16
- import { timezoneSchema } from "./timezone";
16
+ import { canonicalTimezoneSchema } from "./timezone";
17
17
 
18
18
  export { reviewRatingSchema, schedulingParamsSchema, SRS_REVIEW_DURATION_MS_MAX };
19
19
  export { timezoneSchema } from "./timezone";
@@ -45,7 +45,7 @@ export const srsReviewInputSchema = z
45
45
  id: z.uuid().optional(),
46
46
  rating: reviewRatingSchema,
47
47
  durationMs: z.number().int().min(0).max(SRS_REVIEW_DURATION_MS_MAX),
48
- timezone: timezoneSchema,
48
+ timezone: canonicalTimezoneSchema,
49
49
  })
50
50
  .strict();
51
51
  export type SrsReviewInput = z.infer<typeof srsReviewInputSchema>;
@@ -67,7 +67,7 @@ export type SrsScheduleStatusStats = z.infer<typeof srsScheduleStatusStatsSchema
67
67
  export const srsScheduleStatusStatsInputSchema = z
68
68
  .object({
69
69
  notebaseIds: z.array(z.uuid()).optional(),
70
- timezone: timezoneSchema,
70
+ timezone: canonicalTimezoneSchema,
71
71
  })
72
72
  .strict();
73
73
  export type SrsScheduleStatusStatsInput = z.infer<typeof srsScheduleStatusStatsInputSchema>;
@@ -77,7 +77,7 @@ export type SrsScheduleStatusStatsOutput = z.infer<typeof srsScheduleStatusStats
77
77
 
78
78
  export const srsRecentNotebaseScheduleStatusStatsInputSchema = z
79
79
  .object({
80
- timezone: timezoneSchema,
80
+ timezone: canonicalTimezoneSchema,
81
81
  })
82
82
  .strict();
83
83
  export type SrsRecentNotebaseScheduleStatusStatsInput = z.infer<
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { notebaseActivityStatsOutputSchema } from "./notebase-stats";
3
3
  import { srsActivityStatsOutputSchema } from "./srs-stats";
4
- import { timezoneSchema } from "./timezone";
4
+ import { canonicalTimezoneSchema } from "./timezone";
5
5
 
6
6
  const statsNotebaseIdsSchema = z
7
7
  .array(z.uuid())
@@ -13,7 +13,7 @@ export const statsInputSchema = z
13
13
  notebaseIds: statsNotebaseIdsSchema.optional(),
14
14
  from: z.iso.date().optional(),
15
15
  to: z.iso.date(),
16
- timezone: timezoneSchema,
16
+ timezone: canonicalTimezoneSchema,
17
17
  })
18
18
  .strict()
19
19
  .refine((input) => !input.from || input.from <= input.to, {
@@ -1,3 +1,4 @@
1
+ import { canonicalTimeZone } from "@read-frog/definitions";
1
2
  import { z } from "zod";
2
3
 
3
4
  // @ref https://zenn.dev/herp_inc/articles/js-intl-date-time-format-performance
@@ -46,3 +47,23 @@ export const timezoneSchema = z
46
47
  }
47
48
  }, "Invalid timezone");
48
49
  export type Timezone = z.infer<typeof timezoneSchema>;
50
+
51
+ /**
52
+ * The zone a call reckons days in, under the name an account's zone is stored
53
+ * under (`canonicalTimeZone`): "Asia/Calcutta" arrives as "Asia/Kolkata" and
54
+ * "CST" as "America/Chicago", names that Temporal and a Debian Postgres both
55
+ * read. A fixed offset ("+08:00") is refused, as Postgres reads it with the
56
+ * opposite sign and would put the call's days on the wrong side of UTC.
57
+ */
58
+ export const canonicalTimezoneSchema = timezoneSchema.transform((timezone, ctx) => {
59
+ const name = canonicalTimeZone(timezone);
60
+ if (!name) {
61
+ ctx.addIssue({
62
+ code: "custom",
63
+ message:
64
+ "Use an IANA time zone identifier instead of a fixed UTC offset. See https://www.iana.org/time-zones",
65
+ });
66
+ return z.NEVER;
67
+ }
68
+ return name;
69
+ });
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { appLocaleSchema } from "./app-locale";
2
3
  import { timezoneSchema } from "./timezone";
3
4
 
4
5
  export const userEnsureTimezoneInputSchema = z
@@ -29,3 +30,35 @@ export const userUpdateTimezoneOutputSchema = z
29
30
  })
30
31
  .strict();
31
32
  export type UserUpdateTimezoneOutput = z.infer<typeof userUpdateTimezoneOutputSchema>;
33
+
34
+ // The account's language, which emails are written in. `ensureLocale` only
35
+ // fills it in when it is empty (the language www is showing on a signed-in
36
+ // visit); `updateLocale` is the user choosing a language.
37
+ export const userEnsureLocaleInputSchema = z
38
+ .object({
39
+ locale: appLocaleSchema,
40
+ })
41
+ .strict();
42
+ export type UserEnsureLocaleInput = z.infer<typeof userEnsureLocaleInputSchema>;
43
+
44
+ export const userEnsureLocaleOutputSchema = z
45
+ .object({
46
+ locale: appLocaleSchema,
47
+ updated: z.boolean(),
48
+ })
49
+ .strict();
50
+ export type UserEnsureLocaleOutput = z.infer<typeof userEnsureLocaleOutputSchema>;
51
+
52
+ export const userUpdateLocaleInputSchema = z
53
+ .object({
54
+ locale: appLocaleSchema,
55
+ })
56
+ .strict();
57
+ export type UserUpdateLocaleInput = z.infer<typeof userUpdateLocaleInputSchema>;
58
+
59
+ export const userUpdateLocaleOutputSchema = z
60
+ .object({
61
+ locale: appLocaleSchema,
62
+ })
63
+ .strict();
64
+ export type UserUpdateLocaleOutput = z.infer<typeof userUpdateLocaleOutputSchema>;
@@ -1,14 +0,0 @@
1
- import { oc } from "@orpc/contract";
2
- import { BetaAccessStatusInputSchema, BetaAccessStatusOutputSchema } from "#/schemas/beta-access";
3
-
4
- export const betaAccessContract = {
5
- status: oc
6
- .route({
7
- method: "GET",
8
- path: "/beta-access/status",
9
- summary: "Get beta access status for a feature",
10
- tags: ["Beta Access"],
11
- })
12
- .input(BetaAccessStatusInputSchema)
13
- .output(BetaAccessStatusOutputSchema),
14
- };
@@ -1,18 +0,0 @@
1
- import { BETA_FEATURE_KEYS } from "@read-frog/definitions";
2
- import { z } from "zod";
3
-
4
- export const BetaFeatureKeySchema = z.enum(BETA_FEATURE_KEYS);
5
- export type BetaFeatureKey = z.infer<typeof BetaFeatureKeySchema>;
6
-
7
- export const BetaAccessStatusInputSchema = z
8
- .object({
9
- featureKey: BetaFeatureKeySchema,
10
- })
11
- .strict();
12
- export type BetaAccessStatusInput = z.infer<typeof BetaAccessStatusInputSchema>;
13
-
14
- export const BetaAccessStatusOutputSchema = z.object({
15
- featureKey: BetaFeatureKeySchema,
16
- allowed: z.boolean(),
17
- });
18
- export type BetaAccessStatusOutput = z.infer<typeof BetaAccessStatusOutputSchema>;