140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
import { randomBytes } from "node:crypto";
|
|
import { signupPolicyFromEnv } from "@rakazo/core";
|
|
import type { PrismaClient } from "./client.js";
|
|
|
|
export interface SignupPolicyEnv {
|
|
signupsEnabled: string | undefined;
|
|
signupAllowlist: string | undefined;
|
|
}
|
|
|
|
function newId(): string {
|
|
return randomBytes(16).toString("hex");
|
|
}
|
|
|
|
/** Prisma unique-constraint violation; anything else must still throw. */
|
|
function isUniqueViolation(error: unknown): boolean {
|
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === "P2002");
|
|
}
|
|
|
|
/**
|
|
* Everything a brand-new user needs around their account row: a personal
|
|
* organization, its default space, owner memberships for both boundaries,
|
|
* deployment-owner claim, user memory, and notification preferences. Shared by
|
|
* the Better Auth `user.create.after` hook and phone-identity provisioning so
|
|
* both paths stay in lockstep.
|
|
*
|
|
* `claimDeploymentOwner: false` is for identities that did not sign up
|
|
* through the app (phone provisioning): a first texter must never become
|
|
* the deployment owner.
|
|
*/
|
|
export async function bootstrapUserSpace(
|
|
prisma: PrismaClient,
|
|
user: { id: string },
|
|
env: SignupPolicyEnv,
|
|
options: { claimDeploymentOwner?: boolean } = {},
|
|
): Promise<{ spaceId: string }> {
|
|
const claimDeploymentOwner = options.claimDeploymentOwner ?? true;
|
|
// Concurrent bootstraps for the same user (e.g. overlapping first phone
|
|
// inbounds) race on every unique key below; each step either wins or
|
|
// joins the winner's state instead of failing.
|
|
const slug = `user-${user.id.slice(0, 12)}`;
|
|
let orgId = newId();
|
|
try {
|
|
await prisma.organization.create({
|
|
data: { id: orgId, name: "Personal", slug, createdAt: new Date() },
|
|
});
|
|
} catch (error) {
|
|
if (!isUniqueViolation(error)) throw error;
|
|
orgId = (await prisma.organization.findUniqueOrThrow({ where: { slug } })).id;
|
|
}
|
|
await prisma.member
|
|
.create({
|
|
data: {
|
|
id: newId(),
|
|
organizationId: orgId,
|
|
userId: user.id,
|
|
role: "owner",
|
|
createdAt: new Date(),
|
|
},
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (!isUniqueViolation(error)) throw error;
|
|
});
|
|
await prisma.space
|
|
.create({
|
|
data: {
|
|
id: orgId,
|
|
organizationId: orgId,
|
|
name: "Personal",
|
|
isDefault: true,
|
|
createdByUserId: user.id,
|
|
createdAt: new Date(),
|
|
},
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (!isUniqueViolation(error)) throw error;
|
|
});
|
|
await prisma.spaceMember
|
|
.create({
|
|
data: {
|
|
id: newId(),
|
|
spaceId: orgId,
|
|
organizationId: orgId,
|
|
userId: user.id,
|
|
role: "owner",
|
|
createdAt: new Date(),
|
|
},
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (!isUniqueViolation(error)) throw error;
|
|
});
|
|
const policy = signupPolicyFromEnv(env);
|
|
await prisma.deploymentSettings.upsert({
|
|
where: { id: "default" },
|
|
create: {
|
|
id: "default",
|
|
ownerUserId: claimDeploymentOwner ? user.id : null,
|
|
signupsEnabled: policy.enabled,
|
|
signupAllowlist: policy.allowlist.join(","),
|
|
signupPolicyInitialized: true,
|
|
},
|
|
update: {},
|
|
});
|
|
if (claimDeploymentOwner) {
|
|
// Conditional claim: only the first concurrent claimant wins the seat.
|
|
await prisma.deploymentSettings.updateMany({
|
|
where: { id: "default", ownerUserId: null },
|
|
data: { ownerUserId: user.id },
|
|
});
|
|
}
|
|
const hasMemory = await prisma.memoryDocument.findFirst({
|
|
where: { spaceId: orgId, userId: user.id, scope: "user", path: "MEMORY.md" },
|
|
});
|
|
if (!hasMemory) {
|
|
await prisma.memoryDocument
|
|
.create({
|
|
data: {
|
|
spaceId: orgId,
|
|
userId: user.id,
|
|
scope: "user",
|
|
path: "MEMORY.md",
|
|
content: "# Space memory\n\nPreferences and context kept within this space live here.\n",
|
|
},
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (!isUniqueViolation(error)) throw error;
|
|
});
|
|
}
|
|
await prisma.notificationPreference
|
|
.create({
|
|
data: {
|
|
spaceId: orgId,
|
|
userId: user.id,
|
|
},
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (!isUniqueViolation(error)) throw error;
|
|
});
|
|
return { spaceId: orgId };
|
|
}
|