@read-frog/api-contract 0.15.0 → 0.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@read-frog/api-contract",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@orpc/contract": "^1.14.6",
28
28
  "zod": "^4.5.4",
29
- "@read-frog/definitions": "0.5.0"
29
+ "@read-frog/definitions": "0.6.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^26.1.0",
@@ -0,0 +1,26 @@
1
+ import { oc } from "@orpc/contract";
2
+ import {
3
+ mcpConnectionListOutputSchema,
4
+ mcpConnectionRevokeInputSchema,
5
+ mcpConnectionRevokeOutputSchema,
6
+ } from "#/schemas/mcp-connection";
7
+
8
+ export const mcpConnectionContract = {
9
+ list: oc
10
+ .route({
11
+ method: "GET",
12
+ path: "/mcp-connections",
13
+ summary: "List the OAuth clients the caller has authorized",
14
+ tags: ["MCP Connections"],
15
+ })
16
+ .output(mcpConnectionListOutputSchema),
17
+ revoke: oc
18
+ .route({
19
+ method: "POST",
20
+ path: "/mcp-connections/revoke",
21
+ summary: "Withdraw a client's authorization and revoke its tokens",
22
+ tags: ["MCP Connections"],
23
+ })
24
+ .input(mcpConnectionRevokeInputSchema)
25
+ .output(mcpConnectionRevokeOutputSchema),
26
+ };
@@ -13,10 +13,14 @@ import {
13
13
  NotebaseRowCreateManyOutputSchema,
14
14
  NotebaseRowCreateOutputSchema,
15
15
  NotebaseRowDeleteInputSchema,
16
+ NotebaseRowDeleteManyInputSchema,
17
+ NotebaseRowDeleteManyOutputSchema,
16
18
  NotebaseRowDeleteOutputSchema,
17
19
  NotebaseRowReorderInputSchema,
18
20
  NotebaseRowReorderOutputSchema,
19
21
  NotebaseRowUpdateInputSchema,
22
+ NotebaseRowUpdateManyInputSchema,
23
+ NotebaseRowUpdateManyOutputSchema,
20
24
  NotebaseRowUpdateOutputSchema,
21
25
  } from "#/schemas/notebase-row";
22
26
 
@@ -96,6 +100,21 @@ export const notebaseRowContract = {
96
100
  .input(NotebaseRowUpdateInputSchema)
97
101
  .output(NotebaseRowUpdateOutputSchema),
98
102
 
103
+ updateMany: defineAgentProcedure({
104
+ method: "PATCH",
105
+ path: "/notebase-rows/batch",
106
+ summary: "Update notes",
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.",
109
+ tags: ["Notebase Rows"],
110
+ })
111
+ .errors(
112
+ pickPublicErrorMap("NOTEBASE_NOT_FOUND", "NOTEBASE_ROW_NOT_FOUND", "CELL_VALIDATION_FAILED"),
113
+ )
114
+ .errors(pickPublicErrorMap("RESOURCE_UPDATE_CONFLICT"))
115
+ .input(NotebaseRowUpdateManyInputSchema)
116
+ .output(NotebaseRowUpdateManyOutputSchema),
117
+
99
118
  delete: defineAgentProcedure({
100
119
  method: "DELETE",
101
120
  path: "/notebase-rows/{notebaseRowId}",
@@ -108,6 +127,18 @@ export const notebaseRowContract = {
108
127
  .input(NotebaseRowDeleteInputSchema)
109
128
  .output(NotebaseRowDeleteOutputSchema),
110
129
 
130
+ deleteMany: defineAgentProcedure({
131
+ method: "POST",
132
+ path: "/notebase-rows/batch/delete",
133
+ summary: "Delete notes",
134
+ description:
135
+ "Permanently delete 1–50 owned notes atomically, including their derived cards and review records. Notes may belong to different notebases. If any ID is missing or inaccessible, nothing is deleted. Return deleted IDs in input order.",
136
+ tags: ["Notebase Rows"],
137
+ })
138
+ .errors(pickPublicErrorMap("NOTEBASE_ROW_NOT_FOUND", "CONCURRENT_POSITION_CONFLICT"))
139
+ .input(NotebaseRowDeleteManyInputSchema)
140
+ .output(NotebaseRowDeleteManyOutputSchema),
141
+
111
142
  reorder: oc
112
143
  .errors(pickPublicErrorMap("NOTEBASE_NOT_FOUND", "CONCURRENT_POSITION_CONFLICT"))
113
144
  .route({
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { cardContract } from "./contracts/card";
5
5
  import { cardTemplateContract } from "./contracts/card-template";
6
6
  import { ebookContract } from "./contracts/ebook";
7
7
  import { hostedAiContract } from "./contracts/hosted-ai";
8
+ import { mcpConnectionContract } from "./contracts/mcp-connection";
8
9
  import { notebaseContract } from "./contracts/notebase";
9
10
  import { notebaseColumnContract } from "./contracts/notebase-column";
10
11
  import { notebaseRowContract } from "./contracts/notebase-row";
@@ -20,6 +21,7 @@ export const contract = {
20
21
  cardTemplate: cardTemplateContract,
21
22
  ebook: ebookContract,
22
23
  hostedAi: hostedAiContract,
24
+ mcpConnection: mcpConnectionContract,
23
25
  notebase: notebaseContract,
24
26
  notebaseColumn: notebaseColumnContract,
25
27
  notebaseRow: notebaseRowContract,
@@ -38,6 +40,7 @@ export * from "./schemas/billing";
38
40
  export * from "./schemas/card";
39
41
  export * from "./schemas/ebook";
40
42
  export * from "./schemas/hosted-ai";
43
+ export * from "./schemas/mcp-connection";
41
44
  export * from "./schemas/notebase";
42
45
  export * from "./schemas/notebase-column";
43
46
  export * from "./schemas/notebase-row";
@@ -32,7 +32,7 @@ export const CellValidationFailedDataSchema = z
32
32
  notebaseColumnId: z.string(),
33
33
  details: CellValidationFailureDetailsSchema,
34
34
  reason: CellValidationFailureReasonSchema,
35
- /** Index of the failing row within a batch input (createMany / initialRows). */
35
+ /** Index of the failing row within a batch input (createMany / updateMany / initialRows). */
36
36
  rowIndex: z.number().int().min(0).optional(),
37
37
  })
38
38
  .strict();
@@ -18,6 +18,32 @@ export type BillingSubscriptionStatus = z.infer<typeof billingSubscriptionStatus
18
18
  export const billingPlanSchema = z.enum(["free", "pro", "ultra"]);
19
19
  export type BillingPlan = z.infer<typeof billingPlanSchema>;
20
20
 
21
+ /**
22
+ * Whether a plan may use a feature, given the plans that feature allows.
23
+ *
24
+ * An explicit set rather than a minimum tier: there is no rank table to keep in
25
+ * step with the plan enum, and any combination is expressible by writing it
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.
35
+ *
36
+ * Read by the server-side gate AND by the website's copy, so the page cannot
37
+ * promise access the API refuses. `satisfies` keeps the literal tuple type so
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).
44
+ */
45
+ export const MCP_ALLOWED_PLANS = ["ultra"] as const satisfies readonly BillingPlan[];
46
+
21
47
  // Plans a user can actively check out into (the free tier has no price).
22
48
  export const billingCheckoutPlanSchema = z.enum(["pro", "ultra"]);
23
49
  export type BillingCheckoutPlan = z.infer<typeof billingCheckoutPlanSchema>;
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+
3
+ export const mcpConnectionSchema = z
4
+ .object({
5
+ /** OAuth client identifier. For a CIMD client this is an https URL. */
6
+ clientId: z.string(),
7
+ /** The client's registered display name, absent when it never gave one. */
8
+ clientName: z.string().nullable(),
9
+ scopes: z.array(z.string()),
10
+ requestedUserInfoClaims: z.array(z.string()),
11
+ grantedAt: z.date().nullable(),
12
+ updatedAt: z.date().nullable(),
13
+ /**
14
+ * When this client last obtained an access token — the closest honest
15
+ * proxy for "last used" the OAuth tables record. A token is issued on
16
+ * connect and on every refresh, so an idle client's value stops advancing.
17
+ */
18
+ lastAuthorizedAt: z.date().nullable(),
19
+ })
20
+ .strict();
21
+ export type McpConnection = z.infer<typeof mcpConnectionSchema>;
22
+
23
+ export const mcpConnectionListOutputSchema = z.array(mcpConnectionSchema);
24
+
25
+ export const mcpConnectionRevokeInputSchema = z.object({ clientId: z.string().min(1) }).strict();
26
+ export type McpConnectionRevokeInput = z.infer<typeof mcpConnectionRevokeInputSchema>;
27
+
28
+ export const mcpConnectionRevokeOutputSchema = z.object({ clientId: z.string() }).strict();
@@ -94,6 +94,42 @@ export const NotebaseRowUpdateOutputSchema = z.object({
94
94
  });
95
95
  export type NotebaseRowUpdateOutput = z.infer<typeof NotebaseRowUpdateOutputSchema>;
96
96
 
97
+ export const NOTEBASE_ROW_UPDATE_MANY_MAX_ROWS = 50;
98
+
99
+ export const NotebaseRowUpdateManyInputSchema = z
100
+ .object({
101
+ notebaseId: z.uuid(),
102
+ rows: z
103
+ .array(
104
+ z.object({
105
+ id: z.uuid(),
106
+ data: notebaseRowUpdateDataSchema,
107
+ }),
108
+ )
109
+ .min(1)
110
+ .max(NOTEBASE_ROW_UPDATE_MANY_MAX_ROWS),
111
+ })
112
+ .superRefine((input, ctx) => {
113
+ const seenIds = new Set<string>();
114
+ input.rows.forEach((row, index) => {
115
+ if (seenIds.has(row.id)) {
116
+ ctx.addIssue({
117
+ code: "custom",
118
+ message: `Duplicate row id "${row.id}".`,
119
+ path: ["rows", index, "id"],
120
+ });
121
+ }
122
+ seenIds.add(row.id);
123
+ });
124
+ });
125
+ export type NotebaseRowUpdateManyInput = z.infer<typeof NotebaseRowUpdateManyInputSchema>;
126
+
127
+ export const NotebaseRowUpdateManyOutputSchema = z.object({
128
+ items: z.array(notebaseRowSchema),
129
+ txid: z.number(),
130
+ });
131
+ export type NotebaseRowUpdateManyOutput = z.infer<typeof NotebaseRowUpdateManyOutputSchema>;
132
+
97
133
  export const NotebaseRowDeleteInputSchema = z.object({
98
134
  notebaseRowId: z.uuid(),
99
135
  });
@@ -102,6 +138,30 @@ export type NotebaseRowDeleteInput = z.infer<typeof NotebaseRowDeleteInputSchema
102
138
  export const NotebaseRowDeleteOutputSchema = deletionResultSchema;
103
139
  export type NotebaseRowDeleteOutput = z.infer<typeof NotebaseRowDeleteOutputSchema>;
104
140
 
141
+ export const NOTEBASE_ROW_DELETE_MANY_MAX_ROWS = 50;
142
+
143
+ export const NotebaseRowDeleteManyInputSchema = z
144
+ .object({
145
+ ids: z.array(z.uuid()).min(1).max(NOTEBASE_ROW_DELETE_MANY_MAX_ROWS),
146
+ })
147
+ .superRefine((input, ctx) => {
148
+ const seenIds = new Set<string>();
149
+ input.ids.forEach((id, index) => {
150
+ if (seenIds.has(id)) {
151
+ ctx.addIssue({
152
+ code: "custom",
153
+ message: `Duplicate row id "${id}".`,
154
+ path: ["ids", index],
155
+ });
156
+ }
157
+ seenIds.add(id);
158
+ });
159
+ });
160
+ export type NotebaseRowDeleteManyInput = z.infer<typeof NotebaseRowDeleteManyInputSchema>;
161
+
162
+ export const NotebaseRowDeleteManyOutputSchema = z.object({ ids: z.array(z.uuid()) });
163
+ export type NotebaseRowDeleteManyOutput = z.infer<typeof NotebaseRowDeleteManyOutputSchema>;
164
+
105
165
  export const NotebaseRowReorderInputSchema = z.object({
106
166
  notebaseId: z.uuid(),
107
167
  ids: z.array(z.uuid()),