Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/core/src/org/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,46 @@ export async function resolveOrgIdForEmail(
}
}

/**
* Create a new organization and add the caller as a member with the given
* role. Generates a per-org A2A secret for cross-app delegation and writes
* the caller's `active-org-id` user-setting so the new org is immediately
* active.
*
*/
export async function createOrganization(
name: string,
email: string,
role: OrgRole = "owner",
): Promise<{
id: string;
name: string;
role: OrgRole;
a2aSecret: string;
createdAt: number;
}> {
const trimmedName = name.trim();
const exec = getDbExec();
const id = nanoid();
const createdAt = Date.now();
const { randomBytes } = await import("node:crypto");
const a2aSecret = randomBytes(32).toString("base64url");

await exec.execute({
sql: `INSERT INTO organizations (id, name, created_by, created_at, a2a_secret) VALUES (?, ?, ?, ?, ?)`,
args: [id, trimmedName, email, createdAt, a2aSecret],
});

await exec.execute({
sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
args: [nanoid(), id, email, role, createdAt],
});

await putUserSetting(email, "active-org-id", { orgId: id });

return { id, name: trimmedName, role, a2aSecret, createdAt };
}

function defaultOrgName(
email: string,
session: { name?: string } | null,
Expand Down
25 changes: 3 additions & 22 deletions packages/core/src/org/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { getDbExec } from "../db/client.js";
import { sendEmail, isEmailConfigured } from "../server/email.js";
import { renderInviteEmail } from "../server/email-templates.js";
import { getAppProductionUrl } from "../server/app-url.js";
import { getOrgContext } from "./context.js";
import { getOrgContext, createOrganization } from "./context.js";
import { isFreeEmailProvider } from "./free-email-providers.js";
import type { OrgRole } from "./types.js";

Expand Down Expand Up @@ -180,27 +180,8 @@ export const createOrgHandler = defineEventHandler(async (event: H3Event) => {
});
}

const orgId = nanoid();
const now = Date.now();
const e = await exec();

// Auto-generate a per-org A2A secret for cross-app delegation
const { randomBytes } = await import("node:crypto");
const a2aSecret = randomBytes(32).toString("base64url");

await e.execute({
sql: `INSERT INTO organizations (id, name, created_by, created_at, a2a_secret) VALUES (?, ?, ?, ?, ?)`,
args: [orgId, name, email, now, a2aSecret],
});

await e.execute({
sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
args: [nanoid(), orgId, email, "owner", now],
});

await putUserSetting(email, "active-org-id", { orgId });

return { id: orgId, name, role: "owner" };
const { id, name: createdName, role } = await createOrganization(name, email);
return { id, name: createdName, role };
});

/** GET /_agent-native/org/members — list org members */
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/org/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
getA2ASecretByDomain,
resolveOrgByDomain,
resolveOrgIdForEmail,
createOrganization,
} from "./context.js";

export { acceptPendingInvitationsForEmail } from "./accept-pending.js";
Expand Down
7 changes: 0 additions & 7 deletions packages/core/src/server/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ describe("server/auth", () => {
signInEmail: vi.fn(),
signUpEmail: vi.fn(),
signOut: vi.fn(),
listOrganizations: vi.fn(),
},
})),
getBetterAuthSync: vi.fn(() => undefined),
Expand Down Expand Up @@ -169,7 +168,6 @@ describe("server/auth", () => {
signInEmail: vi.fn(),
signUpEmail: vi.fn(),
signOut: vi.fn(),
listOrganizations: vi.fn(),
},
})),
getBetterAuthSync: vi.fn(() => undefined),
Expand Down Expand Up @@ -205,7 +203,6 @@ describe("server/auth", () => {
signInEmail: vi.fn(),
signUpEmail: vi.fn(),
signOut: vi.fn(),
listOrganizations: vi.fn(),
},
})),
getBetterAuthSync: vi.fn(() => undefined),
Expand Down Expand Up @@ -666,7 +663,6 @@ describe("server/auth", () => {
signInEmail,
signUpEmail: vi.fn(),
signOut: vi.fn(),
listOrganizations: vi.fn(),
},
})),
getBetterAuthSync: vi.fn(() => undefined),
Expand Down Expand Up @@ -776,7 +772,6 @@ describe("server/auth", () => {
signInEmail: vi.fn(),
signUpEmail: vi.fn(),
signOut: vi.fn(),
listOrganizations: vi.fn(),
},
})),
getBetterAuthSync: vi.fn(() => undefined),
Expand Down Expand Up @@ -826,7 +821,6 @@ describe("server/auth", () => {
signInEmail: vi.fn(),
signUpEmail: vi.fn(),
signOut: vi.fn(),
listOrganizations: vi.fn(),
},
})),
getBetterAuthSync: vi.fn(() => undefined),
Expand Down Expand Up @@ -882,7 +876,6 @@ describe("server/auth", () => {
signInEmail: vi.fn(),
signUpEmail: vi.fn(),
signOut: vi.fn(),
listOrganizations: vi.fn(),
},
})),
getBetterAuthSync: vi.fn(() => undefined),
Expand Down
5 changes: 0 additions & 5 deletions packages/core/src/server/better-auth-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { betterAuth, type BetterAuthOptions } from "better-auth";
import { organization } from "better-auth/plugins/organization";
import { jwt } from "better-auth/plugins/jwt";
import { bearer } from "better-auth/plugins/bearer";
import { sendEmail, isEmailConfigured } from "./email.js";
Expand Down Expand Up @@ -196,7 +195,6 @@ export interface BetterAuthInstance {
id: string;
token: string;
expiresAt: Date;
activeOrganizationId?: string;
};
} | null>;
signInEmail: (opts: {
Expand All @@ -211,7 +209,6 @@ export interface BetterAuthInstance {
};
}) => Promise<any>;
signOut: (opts: { headers: Headers }) => Promise<any>;
listOrganizations: (opts: { headers: Headers }) => Promise<any[] | null>;
};
}

Expand Down Expand Up @@ -817,8 +814,6 @@ async function createBetterAuthInstance(
: {}),
},
plugins: [
// Organizations: many:many user:org, roles, invitations
organization(),
// JWT: issue tokens for A2A calls, JWKS endpoint for verification
jwt({
jwt: {
Expand Down
Loading
Loading