@camox/api 0.2.0-alpha.5 → 0.3.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": "@camox/api",
3
- "version": "0.2.0-alpha.5",
3
+ "version": "0.3.0",
4
4
  "files": [
5
5
  "src"
6
6
  ],
@@ -2,44 +2,46 @@ import { ORPCError } from "@orpc/server";
2
2
  import { and, eq, or } from "drizzle-orm";
3
3
 
4
4
  import type { Database } from "./db";
5
- import {
6
- member,
7
- organizationTable,
8
- blocks,
9
- files,
10
- layouts,
11
- pages,
12
- projects,
13
- repeatableItems,
14
- } from "./schema";
5
+ import { member, blocks, files, layouts, pages, projects, repeatableItems } from "./schema";
15
6
 
16
- // --- Membership Helpers ---
7
+ // --- Sync Secret ---
17
8
 
18
- export async function assertOrgMembership(db: Database, userId: string, orgSlug: string) {
19
- const result = await db
20
- .select({ id: member.id })
21
- .from(member)
22
- .innerJoin(organizationTable, eq(organizationTable.id, member.organizationId))
23
- .where(and(eq(organizationTable.slug, orgSlug), eq(member.userId, userId)))
24
- .get();
25
- if (!result) throw new ORPCError("FORBIDDEN");
9
+ export async function assertSyncSecret(db: Database, projectSlug: string, syncSecret: string) {
10
+ const project = await db.select().from(projects).where(eq(projects.slug, projectSlug)).get();
11
+ if (!project) throw new ORPCError("NOT_FOUND");
12
+
13
+ if (syncSecret !== project.syncSecret) {
14
+ throw new ORPCError("UNAUTHORIZED");
15
+ }
16
+
17
+ return project;
26
18
  }
27
19
 
20
+ // --- Membership Helpers ---
21
+
28
22
  /** Verify user is a member of the org that owns a project (by project ID). */
29
23
  async function assertProjectMembership(db: Database, projectId: number, userId: string) {
30
24
  const result = await db
31
25
  .select({ id: member.id })
32
26
  .from(projects)
33
- .innerJoin(organizationTable, eq(organizationTable.slug, projects.organizationSlug))
34
27
  .innerJoin(
35
28
  member,
36
- and(eq(member.organizationId, organizationTable.id), eq(member.userId, userId)),
29
+ and(eq(member.organizationId, projects.organizationId), eq(member.userId, userId)),
37
30
  )
38
31
  .where(eq(projects.id, projectId))
39
32
  .get();
40
33
  if (!result) throw new ORPCError("FORBIDDEN");
41
34
  }
42
35
 
36
+ export async function assertOrgMembership(db: Database, userId: string, orgId: string) {
37
+ const result = await db
38
+ .select({ id: member.id })
39
+ .from(member)
40
+ .where(and(eq(member.organizationId, orgId), eq(member.userId, userId)))
41
+ .get();
42
+ if (!result) throw new ORPCError("FORBIDDEN");
43
+ }
44
+
43
45
  // --- Authorization Helpers ---
44
46
 
45
47
  export async function getAuthorizedProject(db: Database, projectId: number, userId: string) {
package/src/index.ts CHANGED
@@ -28,13 +28,7 @@ app.use(
28
28
  "*",
29
29
  cors({
30
30
  origin: (origin) => origin,
31
- allowHeaders: [
32
- "Content-Type",
33
- "Authorization",
34
- "Better-Auth-Cookie",
35
- "x-sync-secret",
36
- "x-environment-name",
37
- ],
31
+ allowHeaders: ["Content-Type", "Authorization", "Better-Auth-Cookie", "x-environment-name"],
38
32
  allowMethods: ["POST", "GET", "OPTIONS"],
39
33
  exposeHeaders: ["Content-Length", "Set-Better-Auth-Cookie"],
40
34
  maxAge: 600,
@@ -44,7 +38,8 @@ app.use(
44
38
 
45
39
  // Session middleware — populates c.var.user/session
46
40
  app.use("*", async (c, next) => {
47
- const auth = createAuth(c.var.db, c.env);
41
+ const url = new URL(c.req.url);
42
+ const auth = createAuth(c.var.db, c.env, url.origin);
48
43
  const session = await auth.api.getSession({ headers: c.req.raw.headers });
49
44
 
50
45
  if (!session) {
@@ -72,8 +67,18 @@ app.use(
72
67
  options: {
73
68
  onBeforeConnect: async (req, _lobby, c) => {
74
69
  const db = createDb(c.env.DB);
75
- const auth = createAuth(db, c.env);
76
- const session = await auth.api.getSession({ headers: req.headers });
70
+ const url = new URL(req.url);
71
+ const auth = createAuth(db, c.env, url.origin);
72
+
73
+ // WebSocket upgrades can't carry custom headers, so the client
74
+ // sends the cross-domain auth cookie as a query parameter instead.
75
+ const headers = new Headers(req.headers);
76
+ const authCookie = url.searchParams.get("_authCookie");
77
+ if (authCookie) {
78
+ headers.set("Better-Auth-Cookie", authCookie);
79
+ }
80
+
81
+ const session = await auth.api.getSession({ headers });
77
82
  if (!session) return new Response("Unauthorized", { status: 401 });
78
83
  },
79
84
  },
package/src/orpc.ts CHANGED
@@ -35,20 +35,6 @@ export const pub = os.$context<BaseContext>().use(async ({ next, path }) => {
35
35
  }
36
36
  });
37
37
 
38
- /** Sync procedure — requires either a valid session or the x-sync-secret header */
39
- export const synced = pub.use(async ({ context, next }) => {
40
- if (context.user && context.session) {
41
- return next({ context });
42
- }
43
-
44
- const secret = context.headers.get("x-sync-secret");
45
- if (!secret || secret !== context.env.SYNC_SECRET) {
46
- throw new ORPCError("UNAUTHORIZED");
47
- }
48
-
49
- return next({ context });
50
- });
51
-
52
38
  /** Authed procedure — requires authenticated user */
53
39
  export const authed = pub.use(async ({ context, next }) => {
54
40
  if (!context.user || !context.session) {
@@ -38,13 +38,24 @@ function generateSlug(name: string): string {
38
38
  return `${base}-${suffix}`;
39
39
  }
40
40
 
41
- export function createAuth(db: Database, env: Bindings) {
41
+ export function createAuth(db: Database, env: Bindings, baseURL: string) {
42
+ // Derive the cookie domain from SITE_URL so cookies are shared between
43
+ // the web app (camox.ai) and the API (api.camox.ai).
44
+ // In dev (localhost), this is undefined — cookies work without an explicit domain.
45
+ let cookieDomain: string | undefined;
46
+ try {
47
+ const siteHost = new URL(env.SITE_URL).hostname;
48
+ if (siteHost !== "localhost") cookieDomain = `.${siteHost}`;
49
+ } catch {
50
+ // SITE_URL missing or malformed — fall back to default cookie domain
51
+ }
52
+
42
53
  const auth = betterAuth({
43
54
  database: drizzleAdapter(db, {
44
55
  provider: "sqlite",
45
56
  schema: authSchema,
46
57
  }),
47
- baseURL: env.BETTER_AUTH_URL,
58
+ baseURL,
48
59
  secret: env.BETTER_AUTH_SECRET,
49
60
  emailAndPassword: {
50
61
  enabled: true,
@@ -66,6 +77,12 @@ export function createAuth(db: Database, env: Bindings) {
66
77
  },
67
78
  // Accept requests from any origin — Camox sites run on arbitrary customer domains
68
79
  trustedOrigins: ["*"],
80
+ advanced: {
81
+ crossSubDomainCookies: {
82
+ enabled: true,
83
+ domain: cookieDomain,
84
+ },
85
+ },
69
86
  plugins: [organization(), crossDomain({ siteUrl: env.SITE_URL }), oneTimeToken(), bearer()],
70
87
  databaseHooks: {
71
88
  user: {
@@ -105,6 +122,7 @@ export const authRoutes = new Hono<AppEnv>()
105
122
  }),
106
123
  )
107
124
  .on(["POST", "GET"], "/*", async (c) => {
108
- const auth = createAuth(c.var.db, c.env);
125
+ const url = new URL(c.req.url);
126
+ const auth = createAuth(c.var.db, c.env, url.origin);
109
127
  return auth.handler(c.req.raw);
110
128
  });
@@ -1,15 +1,16 @@
1
- import { ORPCError } from "@orpc/server";
2
1
  import { and, eq } from "drizzle-orm";
3
2
  import { z } from "zod";
4
3
 
4
+ import { assertSyncSecret } from "../authorization";
5
5
  import { resolveEnvironment } from "../lib/resolve-environment";
6
- import { pub, synced } from "../orpc";
7
- import { blockDefinitions, projects } from "../schema";
6
+ import { pub } from "../orpc";
7
+ import { blockDefinitions } from "../schema";
8
8
 
9
9
  // --- Procedures ---
10
10
 
11
11
  const definitionSchema = z.object({
12
12
  projectSlug: z.string(),
13
+ syncSecret: z.string(),
13
14
  blockId: z.string(),
14
15
  title: z.string(),
15
16
  description: z.string(),
@@ -22,6 +23,7 @@ const definitionSchema = z.object({
22
23
 
23
24
  const syncSchema = z.object({
24
25
  projectSlug: z.string(),
26
+ syncSecret: z.string(),
25
27
  definitions: z.array(
26
28
  z.object({
27
29
  blockId: z.string(),
@@ -54,14 +56,9 @@ const list = pub.input(z.object({ projectId: z.number() })).handler(async ({ con
54
56
  return result;
55
57
  });
56
58
 
57
- const sync = synced.input(syncSchema).handler(async ({ context, input }) => {
59
+ const sync = pub.input(syncSchema).handler(async ({ context, input }) => {
58
60
  const { projectSlug, definitions } = input;
59
- const project = await context.db
60
- .select()
61
- .from(projects)
62
- .where(eq(projects.slug, projectSlug))
63
- .get();
64
- if (!project) throw new ORPCError("NOT_FOUND");
61
+ const project = await assertSyncSecret(context.db, projectSlug, input.syncSecret);
65
62
  const projectId = project.id;
66
63
  const environment = await resolveEnvironment(context.db, projectId, context.environmentName, {
67
64
  autoCreate: true,
@@ -125,14 +122,9 @@ const sync = synced.input(syncSchema).handler(async ({ context, input }) => {
125
122
  return { results, environmentCreated: environment.created };
126
123
  });
127
124
 
128
- const upsert = synced.input(definitionSchema).handler(async ({ context, input }) => {
129
- const { projectSlug, ...body } = input;
130
- const project = await context.db
131
- .select()
132
- .from(projects)
133
- .where(eq(projects.slug, projectSlug))
134
- .get();
135
- if (!project) throw new ORPCError("NOT_FOUND");
125
+ const upsert = pub.input(definitionSchema).handler(async ({ context, input }) => {
126
+ const { projectSlug, syncSecret: _, ...body } = input;
127
+ const project = await assertSyncSecret(context.db, projectSlug, input.syncSecret);
136
128
  const projectId = project.id;
137
129
  const environment = await resolveEnvironment(context.db, projectId, context.environmentName, {
138
130
  autoCreate: true,
@@ -188,16 +180,11 @@ const upsert = synced.input(definitionSchema).handler(async ({ context, input })
188
180
  return { ...result, action: "created" as const };
189
181
  });
190
182
 
191
- const deleteFn = synced
192
- .input(z.object({ projectSlug: z.string(), blockId: z.string() }))
183
+ const deleteFn = pub
184
+ .input(z.object({ projectSlug: z.string(), syncSecret: z.string(), blockId: z.string() }))
193
185
  .handler(async ({ context, input }) => {
194
186
  const { projectSlug, blockId } = input;
195
- const project = await context.db
196
- .select()
197
- .from(projects)
198
- .where(eq(projects.slug, projectSlug))
199
- .get();
200
- if (!project) throw new ORPCError("NOT_FOUND");
187
+ const project = await assertSyncSecret(context.db, projectSlug, input.syncSecret);
201
188
  const environment = await resolveEnvironment(context.db, project.id, context.environmentName);
202
189
  const result = await context.db
203
190
  .delete(blockDefinitions)
@@ -20,7 +20,6 @@ import {
20
20
  files,
21
21
  layouts,
22
22
  member,
23
- organizationTable,
24
23
  pages,
25
24
  projects,
26
25
  repeatableItems,
@@ -625,10 +624,9 @@ const deleteMany = authed
625
624
  .leftJoin(pages, eq(blocks.pageId, pages.id))
626
625
  .leftJoin(layouts, eq(blocks.layoutId, layouts.id))
627
626
  .innerJoin(projects, or(eq(projects.id, pages.projectId), eq(projects.id, layouts.projectId)))
628
- .innerJoin(organizationTable, eq(organizationTable.slug, projects.organizationSlug))
629
627
  .innerJoin(
630
628
  member,
631
- and(eq(member.organizationId, organizationTable.id), eq(member.userId, context.user.id)),
629
+ and(eq(member.organizationId, projects.organizationId), eq(member.userId, context.user.id)),
632
630
  )
633
631
  .where(inArray(blocks.id, blockIds));
634
632
  if (authorizedBlocks.length !== blockIds.length) {
@@ -13,7 +13,7 @@ import { queryKeys } from "../lib/query-keys";
13
13
  import { resolveEnvironment } from "../lib/resolve-environment";
14
14
  import { scheduleAiJob } from "../lib/schedule-ai-job";
15
15
  import { pub, authed } from "../orpc";
16
- import { blocks, files, member, organizationTable, projects, repeatableItems } from "../schema";
16
+ import { blocks, files, member, projects, repeatableItems } from "../schema";
17
17
  import type { AppEnv } from "../types";
18
18
 
19
19
  // --- AI Executor ---
@@ -270,10 +270,9 @@ const deleteMany = authed
270
270
  .select({ id: files.id, blobId: files.blobId, projectId: files.projectId })
271
271
  .from(files)
272
272
  .innerJoin(projects, eq(projects.id, files.projectId))
273
- .innerJoin(organizationTable, eq(organizationTable.slug, projects.organizationSlug))
274
273
  .innerJoin(
275
274
  member,
276
- and(eq(member.organizationId, organizationTable.id), eq(member.userId, context.user.id)),
275
+ and(eq(member.organizationId, projects.organizationId), eq(member.userId, context.user.id)),
277
276
  )
278
277
  .where(inArray(files.id, ids));
279
278
 
@@ -1,13 +1,13 @@
1
- import { ORPCError } from "@orpc/server";
2
1
  import { and, eq } from "drizzle-orm";
3
2
  import { generateKeyBetween } from "fractional-indexing";
4
3
  import { z } from "zod";
5
4
 
5
+ import { assertSyncSecret } from "../authorization";
6
6
  import { broadcastInvalidation } from "../lib/broadcast-invalidation";
7
7
  import { queryKeys } from "../lib/query-keys";
8
8
  import { resolveEnvironment } from "../lib/resolve-environment";
9
- import { pub, synced } from "../orpc";
10
- import { blocks, layouts, projects, repeatableItems } from "../schema";
9
+ import { pub } from "../orpc";
10
+ import { blocks, layouts, repeatableItems } from "../schema";
11
11
 
12
12
  // --- Procedures ---
13
13
 
@@ -21,6 +21,7 @@ const repeatableItemSeedSchema = z.object({
21
21
 
22
22
  const syncLayoutsSchema = z.object({
23
23
  projectSlug: z.string(),
24
+ syncSecret: z.string(),
24
25
  layouts: z.array(
25
26
  z.object({
26
27
  layoutId: z.string(),
@@ -47,18 +48,9 @@ const list = pub.input(z.object({ projectId: z.number() })).handler(async ({ con
47
48
  .where(and(eq(layouts.projectId, projectId), eq(layouts.environmentId, environment.id)));
48
49
  });
49
50
 
50
- const sync = synced.input(syncLayoutsSchema).handler(async ({ context, input }) => {
51
+ const sync = pub.input(syncLayoutsSchema).handler(async ({ context, input }) => {
51
52
  const { projectSlug, layouts: layoutDefs } = input;
52
- const project = await context.db
53
- .select()
54
- .from(projects)
55
- .where(eq(projects.slug, projectSlug))
56
- .get();
57
-
58
- if (!project) {
59
- throw new ORPCError("NOT_FOUND");
60
- }
61
-
53
+ const project = await assertSyncSecret(context.db, projectSlug, input.syncSecret);
62
54
  const projectId = project.id;
63
55
  const environment = await resolveEnvironment(context.db, projectId, context.environmentName, {
64
56
  autoCreate: true,
@@ -519,6 +519,23 @@ const list = pub.input(z.object({ projectId: z.number() })).handler(async ({ con
519
519
  .where(and(eq(pages.projectId, input.projectId), eq(pages.environmentId, environment.id)));
520
520
  });
521
521
 
522
+ const listBySlug = pub
523
+ .input(z.object({ projectSlug: z.string() }))
524
+ .handler(async ({ context, input }) => {
525
+ const project = await context.db
526
+ .select()
527
+ .from(projects)
528
+ .where(eq(projects.slug, input.projectSlug))
529
+ .get();
530
+ if (!project) throw new ORPCError("NOT_FOUND");
531
+
532
+ const environment = await resolveEnvironment(context.db, project.id, context.environmentName);
533
+ return await context.db
534
+ .select()
535
+ .from(pages)
536
+ .where(and(eq(pages.projectId, project.id), eq(pages.environmentId, environment.id)));
537
+ });
538
+
522
539
  const get = pub.input(z.object({ id: z.number() })).handler(async ({ context, input }) => {
523
540
  const { id } = input;
524
541
  const result = await context.db.select().from(pages).where(eq(pages.id, id)).get();
@@ -806,6 +823,7 @@ export const pageProcedures = {
806
823
  getByPath,
807
824
  getStructure,
808
825
  list,
826
+ listBySlug,
809
827
  get,
810
828
  create,
811
829
  update,
@@ -3,17 +3,25 @@ import { and, eq } from "drizzle-orm";
3
3
  import { generateKeyBetween } from "fractional-indexing";
4
4
  import { z } from "zod";
5
5
 
6
- import { assertOrgMembership, getAuthorizedProject } from "../authorization";
6
+ import { assertOrgMembership, assertSyncSecret, getAuthorizedProject } from "../authorization";
7
7
  import { resolveEnvironment } from "../lib/resolve-environment";
8
8
  import { generateUniqueSlug } from "../lib/slug";
9
- import { authed, synced } from "../orpc";
10
- import { blocks, environments, layouts, pages, projects, repeatableItems } from "../schema";
9
+ import { authed, pub } from "../orpc";
10
+ import {
11
+ blocks,
12
+ environments,
13
+ layouts,
14
+ organizationTable,
15
+ pages,
16
+ projects,
17
+ repeatableItems,
18
+ } from "../schema";
11
19
 
12
20
  // --- Procedures ---
13
21
 
14
22
  const createProjectSchema = z.object({
15
23
  name: z.string(),
16
- organizationSlug: z.string(),
24
+ organizationId: z.string(),
17
25
  });
18
26
 
19
27
  const updateProjectSchema = z.object({
@@ -21,23 +29,23 @@ const updateProjectSchema = z.object({
21
29
  });
22
30
 
23
31
  const list = authed
24
- .input(z.object({ organizationSlug: z.string() }))
32
+ .input(z.object({ organizationId: z.string() }))
25
33
  .handler(async ({ context, input }) => {
26
- await assertOrgMembership(context.db, context.user.id, input.organizationSlug);
34
+ await assertOrgMembership(context.db, context.user.id, input.organizationId);
27
35
  return context.db
28
36
  .select()
29
37
  .from(projects)
30
- .where(eq(projects.organizationSlug, input.organizationSlug));
38
+ .where(eq(projects.organizationId, input.organizationId));
31
39
  });
32
40
 
33
41
  const getFirst = authed
34
- .input(z.object({ organizationSlug: z.string() }))
42
+ .input(z.object({ organizationId: z.string() }))
35
43
  .handler(async ({ context, input }) => {
36
- await assertOrgMembership(context.db, context.user.id, input.organizationSlug);
44
+ await assertOrgMembership(context.db, context.user.id, input.organizationId);
37
45
  const result = await context.db
38
46
  .select()
39
47
  .from(projects)
40
- .where(eq(projects.organizationSlug, input.organizationSlug))
48
+ .where(eq(projects.organizationId, input.organizationId))
41
49
  .limit(1)
42
50
  .get();
43
51
  if (!result) throw new ORPCError("NOT_FOUND");
@@ -48,24 +56,36 @@ const getBySlug = authed
48
56
  .input(z.object({ slug: z.string() }))
49
57
  .handler(async ({ context, input }) => {
50
58
  const result = await context.db
51
- .select()
59
+ .select({
60
+ project: projects,
61
+ organizationSlug: organizationTable.slug,
62
+ })
52
63
  .from(projects)
64
+ .innerJoin(organizationTable, eq(organizationTable.id, projects.organizationId))
53
65
  .where(eq(projects.slug, input.slug))
54
66
  .get();
55
67
  if (!result) throw new ORPCError("NOT_FOUND");
56
- await assertOrgMembership(context.db, context.user.id, result.organizationSlug);
57
- return result;
68
+ await assertOrgMembership(context.db, context.user.id, result.project.organizationId);
69
+ return { ...result.project, organizationSlug: result.organizationSlug };
58
70
  });
59
71
 
60
72
  const get = authed.input(z.object({ id: z.number() })).handler(async ({ context, input }) => {
61
- const result = await context.db.select().from(projects).where(eq(projects.id, input.id)).get();
73
+ const result = await context.db
74
+ .select({
75
+ project: projects,
76
+ organizationSlug: organizationTable.slug,
77
+ })
78
+ .from(projects)
79
+ .innerJoin(organizationTable, eq(organizationTable.id, projects.organizationId))
80
+ .where(eq(projects.id, input.id))
81
+ .get();
62
82
  if (!result) throw new ORPCError("NOT_FOUND");
63
- await assertOrgMembership(context.db, context.user.id, result.organizationSlug);
64
- return result;
83
+ await assertOrgMembership(context.db, context.user.id, result.project.organizationId);
84
+ return { ...result.project, organizationSlug: result.organizationSlug };
65
85
  });
66
86
 
67
87
  const create = authed.input(createProjectSchema).handler(async ({ context, input }) => {
68
- await assertOrgMembership(context.db, context.user.id, input.organizationSlug);
88
+ await assertOrgMembership(context.db, context.user.id, input.organizationId);
69
89
 
70
90
  const slug = await generateUniqueSlug(context.db);
71
91
  const syncSecret = crypto.randomUUID();
@@ -77,7 +97,7 @@ const create = authed.input(createProjectSchema).handler(async ({ context, input
77
97
  name: input.name,
78
98
  slug,
79
99
  syncSecret,
80
- organizationSlug: input.organizationSlug,
100
+ organizationId: input.organizationId,
81
101
  createdAt: now,
82
102
  updatedAt: now,
83
103
  })
@@ -131,6 +151,7 @@ const repeatableItemSeedSchema = z.object({
131
151
 
132
152
  const initializeContentSchema = z.object({
133
153
  projectSlug: z.string(),
154
+ syncSecret: z.string(),
134
155
  layoutId: z.string(),
135
156
  blocks: z.array(
136
157
  z.object({
@@ -142,118 +163,111 @@ const initializeContentSchema = z.object({
142
163
  ),
143
164
  });
144
165
 
145
- const initializeContent = synced
146
- .input(initializeContentSchema)
147
- .handler(async ({ context, input }) => {
148
- const project = await context.db
149
- .select()
150
- .from(projects)
151
- .where(eq(projects.slug, input.projectSlug))
152
- .get();
153
- if (!project) throw new ORPCError("NOT_FOUND");
166
+ const initializeContent = pub.input(initializeContentSchema).handler(async ({ context, input }) => {
167
+ const project = await assertSyncSecret(context.db, input.projectSlug, input.syncSecret);
154
168
 
155
- const environment = await resolveEnvironment(context.db, project.id, context.environmentName);
169
+ const environment = await resolveEnvironment(context.db, project.id, context.environmentName);
156
170
 
157
- // Check if environment already has pages — if so, skip (idempotent)
158
- const existingPage = await context.db
159
- .select()
160
- .from(pages)
161
- .where(eq(pages.environmentId, environment.id))
162
- .limit(1)
163
- .get();
164
- if (existingPage) {
165
- return { created: false };
166
- }
171
+ // Check if environment already has pages — if so, skip (idempotent)
172
+ const existingPage = await context.db
173
+ .select()
174
+ .from(pages)
175
+ .where(eq(pages.environmentId, environment.id))
176
+ .limit(1)
177
+ .get();
178
+ if (existingPage) {
179
+ return { created: false };
180
+ }
167
181
 
168
- const now = Date.now();
182
+ const now = Date.now();
169
183
 
170
- // Find the specified layout
171
- const layout = await context.db
172
- .select()
173
- .from(layouts)
174
- .where(
175
- and(
176
- eq(layouts.projectId, project.id),
177
- eq(layouts.environmentId, environment.id),
178
- eq(layouts.layoutId, input.layoutId),
179
- ),
180
- )
181
- .get();
182
- if (!layout) {
183
- return { created: false };
184
- }
184
+ // Find the specified layout
185
+ const layout = await context.db
186
+ .select()
187
+ .from(layouts)
188
+ .where(
189
+ and(
190
+ eq(layouts.projectId, project.id),
191
+ eq(layouts.environmentId, environment.id),
192
+ eq(layouts.layoutId, input.layoutId),
193
+ ),
194
+ )
195
+ .get();
196
+ if (!layout) {
197
+ return { created: false };
198
+ }
199
+
200
+ // Create homepage
201
+ const homepage = await context.db
202
+ .insert(pages)
203
+ .values({
204
+ projectId: project.id,
205
+ environmentId: environment.id,
206
+ pathSegment: "",
207
+ fullPath: "/",
208
+ layoutId: layout.id,
209
+ metaTitle: "Untitled page",
210
+ metaDescription:
211
+ "Title and description will be generated by AI as you edit the page's content.",
212
+ createdAt: now,
213
+ updatedAt: now,
214
+ })
215
+ .returning()
216
+ .get();
185
217
 
186
- // Create homepage
187
- const homepage = await context.db
188
- .insert(pages)
218
+ // Create blocks on the homepage
219
+ let prevPosition: string | null = null;
220
+ let blockCount = 0;
221
+
222
+ for (const blockDef of input.blocks) {
223
+ const position = generateKeyBetween(prevPosition, null);
224
+ prevPosition = position;
225
+
226
+ const block = await context.db
227
+ .insert(blocks)
189
228
  .values({
190
- projectId: project.id,
191
- environmentId: environment.id,
192
- pathSegment: "",
193
- fullPath: "/",
194
- layoutId: layout.id,
195
- metaTitle: "Untitled page",
196
- metaDescription:
197
- "Title and description will be generated by AI as you edit the page's content.",
229
+ pageId: homepage.id,
230
+ type: blockDef.type,
231
+ content: blockDef.content,
232
+ settings: blockDef.settings ?? null,
233
+ position,
234
+ summary: "",
198
235
  createdAt: now,
199
236
  updatedAt: now,
200
237
  })
201
238
  .returning()
202
239
  .get();
203
240
 
204
- // Create blocks on the homepage
205
- let prevPosition: string | null = null;
206
- let blockCount = 0;
207
-
208
- for (const blockDef of input.blocks) {
209
- const position = generateKeyBetween(prevPosition, null);
210
- prevPosition = position;
211
-
212
- const block = await context.db
213
- .insert(blocks)
214
- .values({
215
- pageId: homepage.id,
216
- type: blockDef.type,
217
- content: blockDef.content,
218
- settings: blockDef.settings ?? null,
219
- position,
220
- summary: "",
221
- createdAt: now,
222
- updatedAt: now,
223
- })
224
- .returning()
225
- .get();
226
-
227
- const itemSeeds = blockDef.repeatableItems;
228
- if (itemSeeds && itemSeeds.length > 0) {
229
- const tempIdToRealId = new Map<string, number>();
230
- for (const seed of itemSeeds) {
231
- const parentItemId = seed.parentTempId
232
- ? (tempIdToRealId.get(seed.parentTempId) ?? null)
233
- : null;
234
- const inserted = await context.db
235
- .insert(repeatableItems)
236
- .values({
237
- blockId: block.id,
238
- parentItemId,
239
- fieldName: seed.fieldName,
240
- content: seed.content,
241
- summary: "",
242
- position: seed.position,
243
- createdAt: now,
244
- updatedAt: now,
245
- })
246
- .returning()
247
- .get();
248
- tempIdToRealId.set(seed.tempId, inserted.id);
249
- }
241
+ const itemSeeds = blockDef.repeatableItems;
242
+ if (itemSeeds && itemSeeds.length > 0) {
243
+ const tempIdToRealId = new Map<string, number>();
244
+ for (const seed of itemSeeds) {
245
+ const parentItemId = seed.parentTempId
246
+ ? (tempIdToRealId.get(seed.parentTempId) ?? null)
247
+ : null;
248
+ const inserted = await context.db
249
+ .insert(repeatableItems)
250
+ .values({
251
+ blockId: block.id,
252
+ parentItemId,
253
+ fieldName: seed.fieldName,
254
+ content: seed.content,
255
+ summary: "",
256
+ position: seed.position,
257
+ createdAt: now,
258
+ updatedAt: now,
259
+ })
260
+ .returning()
261
+ .get();
262
+ tempIdToRealId.set(seed.tempId, inserted.id);
250
263
  }
251
-
252
- blockCount++;
253
264
  }
254
265
 
255
- return { created: true, pageId: homepage.id, blockCount };
256
- });
266
+ blockCount++;
267
+ }
268
+
269
+ return { created: true, pageId: homepage.id, blockCount };
270
+ });
257
271
 
258
272
  export const projectProcedures = {
259
273
  list,
package/src/schema.ts CHANGED
@@ -97,13 +97,15 @@ export const projects = sqliteTable(
97
97
  slug: text().notNull(),
98
98
  name: text().notNull(),
99
99
  syncSecret: text("sync_secret").notNull().default(""),
100
- organizationSlug: text("organization_slug").notNull(),
100
+ organizationId: text("organization_id")
101
+ .notNull()
102
+ .references(() => organizationTable.id),
101
103
  createdAt: int("created_at").notNull(),
102
104
  updatedAt: int("updated_at").notNull(),
103
105
  },
104
106
  (table) => [
105
107
  uniqueIndex("projects_slug_idx").on(table.slug),
106
- index("projects_organization_idx").on(table.organizationSlug),
108
+ index("projects_organization_idx").on(table.organizationId),
107
109
  ],
108
110
  );
109
111
 
package/src/types.ts CHANGED
@@ -5,7 +5,6 @@ import type { Auth } from "./routes/auth";
5
5
  export type Bindings = {
6
6
  DB: D1Database;
7
7
  BETTER_AUTH_SECRET: string;
8
- BETTER_AUTH_URL: string;
9
8
  GITHUB_CLIENT_ID: string;
10
9
  GITHUB_CLIENT_SECRET: string;
11
10
  GOOGLE_CLIENT_ID: string;
@@ -15,7 +14,6 @@ export type Bindings = {
15
14
  AI_JOB_SCHEDULER: DurableObjectNamespace;
16
15
  ProjectRoom: DurableObjectNamespace;
17
16
  FILES_BUCKET: R2Bucket;
18
- SYNC_SECRET: string;
19
17
  };
20
18
 
21
19
  export type AppEnv = {