@jonsoc/convex 1.1.46 → 1.1.51

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/convex/http.ts CHANGED
@@ -1,9 +1,213 @@
1
1
  import { httpRouter } from "convex/server"
2
+ import type { HttpRouter } from "convex/server"
2
3
 
3
4
  import { authComponent, createAuth } from "./auth"
4
5
 
5
- const http = httpRouter()
6
+ const http: HttpRouter = httpRouter()
6
7
 
7
8
  authComponent.registerRoutes(http, createAuth, { cors: true })
8
9
 
10
+ // Share API endpoints - POST /api/share
11
+ http.route({
12
+ path: "/api/share",
13
+ method: "POST",
14
+ handler: async (ctx, request) => {
15
+ try {
16
+ const body = await request.json()
17
+
18
+ // Import the share module dynamically
19
+ const { create } = await import("./share")
20
+
21
+ const result = await ctx.runMutation(create, {
22
+ sessionID: body.sessionID,
23
+ slug: body.sessionID, // Use sessionID as slug
24
+ secret: generateSecret(),
25
+ })
26
+
27
+ return new Response(JSON.stringify(result), {
28
+ status: 200,
29
+ headers: {
30
+ "Content-Type": "application/json",
31
+ "Access-Control-Allow-Origin": "*",
32
+ },
33
+ })
34
+ } catch (error) {
35
+ return new Response(JSON.stringify({ error: String(error) }), {
36
+ status: 500,
37
+ headers: {
38
+ "Content-Type": "application/json",
39
+ "Access-Control-Allow-Origin": "*",
40
+ },
41
+ })
42
+ }
43
+ },
44
+ })
45
+
46
+ // Share sync endpoint - POST /api/share/:slug/sync
47
+ http.route({
48
+ path: "/api/share/:slug/sync",
49
+ method: "POST",
50
+ handler: async (ctx, request) => {
51
+ try {
52
+ const slug = request.params.slug
53
+ const body = await request.json()
54
+
55
+ const { sync } = await import("./share")
56
+
57
+ await ctx.runMutation(sync, {
58
+ slug,
59
+ secret: body.secret,
60
+ data: body.data,
61
+ })
62
+
63
+ return new Response(JSON.stringify({ success: true }), {
64
+ status: 200,
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ "Access-Control-Allow-Origin": "*",
68
+ },
69
+ })
70
+ } catch (error) {
71
+ return new Response(JSON.stringify({ error: String(error) }), {
72
+ status: 400,
73
+ headers: {
74
+ "Content-Type": "application/json",
75
+ "Access-Control-Allow-Origin": "*",
76
+ },
77
+ })
78
+ }
79
+ },
80
+ })
81
+
82
+ // Share delete endpoint - DELETE /api/share/:slug
83
+ http.route({
84
+ path: "/api/share/:slug",
85
+ method: "DELETE",
86
+ handler: async (ctx, request) => {
87
+ try {
88
+ const slug = request.params.slug
89
+ const body = await request.json()
90
+
91
+ const { remove } = await import("./share")
92
+
93
+ await ctx.runMutation(remove, {
94
+ slug,
95
+ secret: body.secret,
96
+ })
97
+
98
+ return new Response(JSON.stringify({ success: true }), {
99
+ status: 200,
100
+ headers: {
101
+ "Content-Type": "application/json",
102
+ "Access-Control-Allow-Origin": "*",
103
+ },
104
+ })
105
+ } catch (error) {
106
+ return new Response(JSON.stringify({ error: String(error) }), {
107
+ status: 400,
108
+ headers: {
109
+ "Content-Type": "application/json",
110
+ "Access-Control-Allow-Origin": "*",
111
+ },
112
+ })
113
+ }
114
+ },
115
+ })
116
+
117
+ // Share get endpoint - GET /api/share/:slug
118
+ http.route({
119
+ path: "/api/share/:slug",
120
+ method: "GET",
121
+ handler: async (ctx, request) => {
122
+ try {
123
+ const slug = request.params.slug
124
+
125
+ const { getPublic } = await import("./share")
126
+
127
+ const result = await ctx.runQuery(getPublic, { slug })
128
+
129
+ if (!result) {
130
+ return new Response(JSON.stringify({ error: "Share not found" }), {
131
+ status: 404,
132
+ headers: {
133
+ "Content-Type": "application/json",
134
+ "Access-Control-Allow-Origin": "*",
135
+ },
136
+ })
137
+ }
138
+
139
+ return new Response(JSON.stringify(result), {
140
+ status: 200,
141
+ headers: {
142
+ "Content-Type": "application/json",
143
+ "Access-Control-Allow-Origin": "*",
144
+ },
145
+ })
146
+ } catch (error) {
147
+ return new Response(JSON.stringify({ error: String(error) }), {
148
+ status: 500,
149
+ headers: {
150
+ "Content-Type": "application/json",
151
+ "Access-Control-Allow-Origin": "*",
152
+ },
153
+ })
154
+ }
155
+ },
156
+ })
157
+
158
+ // CORS preflight for share endpoints
159
+ http.route({
160
+ path: "/api/share",
161
+ method: "OPTIONS",
162
+ handler: async () => {
163
+ return new Response(null, {
164
+ status: 204,
165
+ headers: {
166
+ "Access-Control-Allow-Origin": "*",
167
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
168
+ "Access-Control-Allow-Headers": "Content-Type",
169
+ },
170
+ })
171
+ },
172
+ })
173
+
174
+ http.route({
175
+ path: "/api/share/:slug",
176
+ method: "OPTIONS",
177
+ handler: async () => {
178
+ return new Response(null, {
179
+ status: 204,
180
+ headers: {
181
+ "Access-Control-Allow-Origin": "*",
182
+ "Access-Control-Allow-Methods": "GET, DELETE, OPTIONS",
183
+ "Access-Control-Allow-Headers": "Content-Type",
184
+ },
185
+ })
186
+ },
187
+ })
188
+
189
+ http.route({
190
+ path: "/api/share/:slug/sync",
191
+ method: "OPTIONS",
192
+ handler: async () => {
193
+ return new Response(null, {
194
+ status: 204,
195
+ headers: {
196
+ "Access-Control-Allow-Origin": "*",
197
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
198
+ "Access-Control-Allow-Headers": "Content-Type",
199
+ },
200
+ })
201
+ },
202
+ })
203
+
204
+ function generateSecret(): string {
205
+ // Generate a random UUID-like string
206
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
207
+ const r = (Math.random() * 16) | 0
208
+ const v = c === "x" ? r : (r & 0x3) | 0x8
209
+ return v.toString(16)
210
+ })
211
+ }
212
+
9
213
  export default http
package/convex/schema.ts CHANGED
@@ -1,4 +1,40 @@
1
1
  import { defineSchema, defineTable } from "convex/server"
2
2
  import { v } from "convex/values"
3
3
 
4
- export default defineSchema({})
4
+ export default defineSchema({
5
+ shares: defineTable({
6
+ // Unique share ID (ULID from CLI)
7
+ slug: v.string(),
8
+ // Secret for authentication (only share creator knows this)
9
+ secret: v.string(),
10
+ // Session metadata
11
+ sessionID: v.string(),
12
+ title: v.optional(v.string()),
13
+ model: v.optional(v.string()),
14
+ // Visibility
15
+ isPublic: v.boolean(),
16
+ // Timestamps
17
+ createdAt: v.number(),
18
+ updatedAt: v.number(),
19
+ // Soft delete
20
+ isDeleted: v.boolean(),
21
+ })
22
+ .index("by_slug", ["slug"])
23
+ .index("by_secret", ["secret"])
24
+ .index("by_session", ["sessionID"]),
25
+
26
+ // Share data stored as JSON blobs (messages, diffs, etc)
27
+ shareData: defineTable({
28
+ shareID: v.id("shares"),
29
+ // Type: "session" | "message" | "part" | "session_diff" | "model"
30
+ dataType: v.string(),
31
+ // Data ID (message ID, part ID, etc)
32
+ dataID: v.string(),
33
+ // JSON string of the actual data
34
+ payload: v.string(),
35
+ createdAt: v.number(),
36
+ })
37
+ .index("by_share", ["shareID"])
38
+ .index("by_share_data", ["shareID", "dataType"])
39
+ .index("by_data_id", ["dataID"]),
40
+ })
@@ -0,0 +1,220 @@
1
+ import { v } from "convex/values"
2
+ import { mutation, query } from "./_generated/server"
3
+
4
+ export const create = mutation({
5
+ args: {
6
+ sessionID: v.string(),
7
+ slug: v.string(),
8
+ secret: v.string(),
9
+ },
10
+ handler: async (ctx, args) => {
11
+ const now = Date.now()
12
+
13
+ // Check if share already exists for this session
14
+ const existing = await ctx.db
15
+ .query("shares")
16
+ .withIndex("by_session", (q) => q.eq("sessionID", args.sessionID))
17
+ .first()
18
+
19
+ if (existing) {
20
+ // Return existing share
21
+ return {
22
+ id: existing.slug,
23
+ url: `/share/${existing.slug}`,
24
+ secret: existing.secret,
25
+ }
26
+ }
27
+
28
+ // Create new share
29
+ const share = await ctx.db.insert("shares", {
30
+ slug: args.slug,
31
+ secret: args.secret,
32
+ sessionID: args.sessionID,
33
+ title: undefined,
34
+ model: undefined,
35
+ isPublic: true,
36
+ createdAt: now,
37
+ updatedAt: now,
38
+ isDeleted: false,
39
+ })
40
+
41
+ return {
42
+ id: args.slug,
43
+ url: `/share/${args.slug}`,
44
+ secret: args.secret,
45
+ }
46
+ },
47
+ })
48
+
49
+ export const sync = mutation({
50
+ args: {
51
+ slug: v.string(),
52
+ secret: v.string(),
53
+ data: v.array(
54
+ v.object({
55
+ type: v.string(),
56
+ data: v.any(),
57
+ }),
58
+ ),
59
+ },
60
+ handler: async (ctx, args) => {
61
+ // Verify share exists and secret matches
62
+ const share = await ctx.db
63
+ .query("shares")
64
+ .withIndex("by_slug", (q) => q.eq("slug", args.slug))
65
+ .first()
66
+
67
+ if (!share || share.secret !== args.secret) {
68
+ throw new Error("Share not found or invalid secret")
69
+ }
70
+
71
+ if (share.isDeleted) {
72
+ throw new Error("Share has been deleted")
73
+ }
74
+
75
+ // Process each data item
76
+ for (const item of args.data) {
77
+ const dataID = item.data?.id || crypto.randomUUID()
78
+ const payload = JSON.stringify(item.data)
79
+
80
+ // Check if data already exists
81
+ const existing = await ctx.db
82
+ .query("shareData")
83
+ .withIndex("by_data_id", (q) => q.eq("dataID", dataID))
84
+ .first()
85
+
86
+ if (existing) {
87
+ // Update existing
88
+ await ctx.db.patch(existing._id, {
89
+ payload,
90
+ createdAt: Date.now(),
91
+ })
92
+ } else {
93
+ // Create new
94
+ await ctx.db.insert("shareData", {
95
+ shareID: share._id,
96
+ dataType: item.type,
97
+ dataID,
98
+ payload,
99
+ createdAt: Date.now(),
100
+ })
101
+ }
102
+
103
+ // Update share title/model if provided
104
+ if (item.type === "session") {
105
+ const sessionData = item.data
106
+ if (sessionData?.title) {
107
+ await ctx.db.patch(share._id, {
108
+ title: sessionData.title,
109
+ updatedAt: Date.now(),
110
+ })
111
+ }
112
+ }
113
+ }
114
+
115
+ // Update share timestamp
116
+ await ctx.db.patch(share._id, {
117
+ updatedAt: Date.now(),
118
+ })
119
+
120
+ return { success: true }
121
+ },
122
+ })
123
+
124
+ export const remove = mutation({
125
+ args: {
126
+ slug: v.string(),
127
+ secret: v.string(),
128
+ },
129
+ handler: async (ctx, args) => {
130
+ const share = await ctx.db
131
+ .query("shares")
132
+ .withIndex("by_slug", (q) => q.eq("slug", args.slug))
133
+ .first()
134
+
135
+ if (!share || share.secret !== args.secret) {
136
+ throw new Error("Share not found or invalid secret")
137
+ }
138
+
139
+ // Soft delete
140
+ await ctx.db.patch(share._id, {
141
+ isDeleted: true,
142
+ updatedAt: Date.now(),
143
+ })
144
+
145
+ return { success: true }
146
+ },
147
+ })
148
+
149
+ export const get = query({
150
+ args: {
151
+ slug: v.string(),
152
+ },
153
+ handler: async (ctx, args) => {
154
+ const share = await ctx.db
155
+ .query("shares")
156
+ .withIndex("by_slug", (q) => q.eq("slug", args.slug))
157
+ .first()
158
+
159
+ if (!share || share.isDeleted || !share.isPublic) {
160
+ return null
161
+ }
162
+
163
+ // Get all share data
164
+ const data = await ctx.db
165
+ .query("shareData")
166
+ .withIndex("by_share", (q) => q.eq("shareID", share._id))
167
+ .collect()
168
+
169
+ return {
170
+ id: share.slug,
171
+ sessionID: share.sessionID,
172
+ title: share.title,
173
+ model: share.model,
174
+ createdAt: share.createdAt,
175
+ updatedAt: share.updatedAt,
176
+ data: data.map((d) => ({
177
+ type: d.dataType,
178
+ id: d.dataID,
179
+ data: JSON.parse(d.payload),
180
+ })),
181
+ }
182
+ },
183
+ })
184
+
185
+ export const getPublic = query({
186
+ args: {
187
+ slug: v.string(),
188
+ },
189
+ handler: async (ctx, args) => {
190
+ // Reuse get logic
191
+ const share = await ctx.db
192
+ .query("shares")
193
+ .withIndex("by_slug", (q) => q.eq("slug", args.slug))
194
+ .first()
195
+
196
+ if (!share || share.isDeleted || !share.isPublic) {
197
+ return null
198
+ }
199
+
200
+ // Get all share data
201
+ const data = await ctx.db
202
+ .query("shareData")
203
+ .withIndex("by_share", (q) => q.eq("shareID", share._id))
204
+ .collect()
205
+
206
+ return {
207
+ id: share.slug,
208
+ sessionID: share.sessionID,
209
+ title: share.title,
210
+ model: share.model,
211
+ createdAt: share.createdAt,
212
+ updatedAt: share.updatedAt,
213
+ data: data.map((d) => ({
214
+ type: d.dataType,
215
+ id: d.dataID,
216
+ data: JSON.parse(d.payload),
217
+ })),
218
+ }
219
+ },
220
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jonsoc/convex",
3
- "version": "1.1.46",
3
+ "version": "1.1.51",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,15 +17,15 @@
17
17
  "check-types": "tsc --noEmit"
18
18
  },
19
19
  "dependencies": {
20
- "@convex-dev/better-auth": "catalog:",
21
- "better-auth": "catalog:",
22
- "convex": "catalog:",
23
- "zod": "catalog:"
20
+ "@convex-dev/better-auth": "0.10.10",
21
+ "better-auth": "1.4.9",
22
+ "convex": "1.31.7",
23
+ "zod": "4.1.8"
24
24
  },
25
25
  "devDependencies": {
26
- "@jonsoc/config": "workspace:*",
27
- "@types/node": "catalog:",
28
- "typescript": "catalog:"
26
+ "@jonsoc/config": "1.1.51",
27
+ "@types/node": "22.13.9",
28
+ "typescript": "5.8.2"
29
29
  },
30
30
  "publishConfig": {
31
31
  "access": "public"