@voyant-travel/auth 0.129.0 → 0.130.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/README.md +14 -0
- package/dist/cloud-broker/members.d.ts +9 -0
- package/dist/cloud-broker/members.js +10 -0
- package/dist/cloud-broker/members.js.map +1 -1
- package/dist/cloud-broker.d.ts +1 -1
- package/dist/cloud-broker.js +1 -1
- package/dist/cloud-broker.js.map +1 -1
- package/dist/identity-access-graph-runtime.js +2 -1
- package/dist/identity-access-graph-runtime.js.map +1 -1
- package/dist/invitations-routes.js +4 -0
- package/dist/invitations-routes.js.map +1 -1
- package/dist/local-member-access.d.ts +15 -0
- package/dist/local-member-access.js +119 -0
- package/dist/local-member-access.js.map +1 -0
- package/dist/runtime-contributor.js +18 -1
- package/dist/runtime-contributor.js.map +1 -1
- package/dist/server.d.ts +10 -6
- package/dist/server.js +6 -3
- package/dist/server.js.map +1 -1
- package/dist/team-management-cloud-adapter.d.ts +7 -0
- package/dist/team-management-cloud-adapter.js +146 -0
- package/dist/team-management-cloud-adapter.js.map +1 -0
- package/dist/team-management-local-adapter.d.ts +8 -0
- package/dist/team-management-local-adapter.js +285 -0
- package/dist/team-management-local-adapter.js.map +1 -0
- package/dist/team-management-policy.d.ts +27 -0
- package/dist/team-management-policy.js +109 -0
- package/dist/team-management-policy.js.map +1 -0
- package/dist/team-management-runtime-port.d.ts +60 -0
- package/dist/team-management-runtime-port.js +25 -0
- package/dist/team-management-runtime-port.js.map +1 -0
- package/dist/team-routes.d.ts +3 -3
- package/dist/team-routes.js +89 -138
- package/dist/team-routes.js.map +1 -1
- package/dist/voyant.js +35 -2
- package/dist/voyant.js.map +1 -1
- package/openapi/admin/team.json +42 -74
- package/package.json +9 -4
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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","sourcesContent":["import { apiKey } from \"@better-auth/api-key\"\nimport { getDb } from \"@voyant-travel/db\"\nimport {\n apikeyTable,\n authAccount,\n authSession,\n authUser,\n authVerification,\n} from \"@voyant-travel/db/schema/iam\"\nimport { type BetterAuthOptions, betterAuth } from \"better-auth\"\nimport { drizzleAdapter } from \"better-auth/adapters/drizzle\"\nimport { emailOTP } from \"better-auth/plugins\"\nimport type { BetterAuthPlugin } from \"better-auth/types\"\nimport { sql } from \"drizzle-orm\"\nimport type { AnyPgTable } from \"drizzle-orm/pg-core\"\n\nimport { expandTrustedOrigins, getTrustedOrigins } from \"./trusted-origins.js\"\nimport { isFirstAuthUser, provisionCurrentUserProfile } from \"./workspace.js\"\n\nexport function getAuthSecret(): string {\n const secret = process.env.BETTER_AUTH_SECRET || \"\"\n\n if (!secret || secret.length < 32) {\n throw new Error(\"Missing BETTER_AUTH_SECRET with at least 32 characters\")\n }\n\n return secret\n}\n\nexport type {\n AccountProfileUpdateHandler,\n ApiTokenRotationStore,\n BetterAuthApiTokenManagement,\n HandleAccountProfileRequestOptions,\n HandleApiTokenManagementRequestOptions,\n HandleOrganizationMembersRequestOptions,\n OrganizationMemberRecord,\n OrganizationMembersListHandler,\n OrganizationMembersListInput,\n} from \"./auth-facades.js\"\nexport {\n handleAccountProfileRequest,\n handleApiTokenManagementRequest,\n handleOrganizationMembersRequest,\n} from \"./auth-facades.js\"\n\ntype DefinedBetterAuthUserOptions<UserOptions extends BetterAuthOptions[\"user\"]> =\n UserOptions extends undefined ? Record<PropertyKey, never> : NonNullable<UserOptions>\n\ntype ResolvedBetterAuthChangeEmail<UserOptions extends BetterAuthOptions[\"user\"]> =\n DefinedBetterAuthUserOptions<UserOptions> extends { changeEmail?: infer ChangeEmail }\n ? ChangeEmail extends { enabled: infer Enabled }\n ? Omit<ChangeEmail, \"enabled\"> & { enabled: Enabled }\n : NonNullable<ChangeEmail> & { enabled: true }\n : { enabled: true }\n\ntype ResolvedBetterAuthUserOptions<UserOptions extends BetterAuthOptions[\"user\"]> = Omit<\n DefinedBetterAuthUserOptions<UserOptions>,\n \"changeEmail\"\n> & {\n changeEmail: ResolvedBetterAuthChangeEmail<UserOptions>\n}\n\ntype VoyantBetterAuthPlugins = [ReturnType<typeof apiKey>, ReturnType<typeof emailOTP>]\n\ntype ResolvedBetterAuthPlugins<Plugins extends BetterAuthPlugin[] | undefined> =\n Plugins extends BetterAuthPlugin[]\n ? [...VoyantBetterAuthPlugins, ...Plugins]\n : VoyantBetterAuthPlugins\n\nconst DEFAULT_SIGNUP_BLOCK_SURFACES = [\"admin\"] as const\nconst CUSTOMER_SIGNUP_ENDPOINT_SUFFIXES = [\n \"/sign-up/email\",\n \"/phone-number/verify\",\n \"/sign-in/email-otp\",\n] as const\n\nexport interface DisableSignupWhenUsersExistOptions {\n /**\n * Set to false when the consuming app owns all signup admission checks.\n *\n * Defaults to true.\n */\n enabled?: boolean\n /**\n * User surfaces that should keep the single-tenant signup guard. New users\n * with only other explicit surfaces can still be created by customer-facing\n * auth plugins such as phone OTP.\n *\n * Defaults to [\"admin\"]. Users without an explicit surfaces array are\n * treated as admin users for backward compatibility.\n */\n surfaces?: readonly string[]\n}\n\ntype BetterAuthCreateBeforeResult =\n | boolean\n | undefined\n | {\n data: Record<string, unknown>\n }\n\ntype SignupBlockUserPayload = {\n surfaces?: unknown\n}\n\ntype BetterAuthHookContext = {\n path?: string\n}\n\nfunction normalizeSignupBlockSurfaces(\n options: DisableSignupWhenUsersExistOptions | undefined,\n): readonly string[] {\n return options?.surfaces ?? DEFAULT_SIGNUP_BLOCK_SURFACES\n}\n\nfunction isSignupBlockEnabled(options: DisableSignupWhenUsersExistOptions | undefined): boolean {\n return options?.enabled !== false\n}\n\nfunction normalizeSurfaceList(surfaces: readonly string[] | undefined): readonly string[] {\n if (!surfaces) return []\n\n const normalized = new Set<string>()\n for (const surface of surfaces) {\n const trimmed = surface.trim()\n if (trimmed.length > 0) {\n normalized.add(trimmed)\n }\n }\n return Array.from(normalized)\n}\n\nfunction signupBlockAppliesToUser(\n user: SignupBlockUserPayload | undefined,\n blockedSurfaces: readonly string[],\n): boolean {\n if (blockedSurfaces.length === 0) return false\n\n const surfaces = user?.surfaces\n if (Array.isArray(surfaces)) {\n const explicitSurfaces = surfaces.filter(\n (surface): surface is string => typeof surface === \"string\" && surface.trim().length > 0,\n )\n if (explicitSurfaces.length > 0) {\n return explicitSurfaces.some((surface) => blockedSurfaces.includes(surface.trim()))\n }\n }\n\n if (typeof surfaces === \"string\" && surfaces.trim().length > 0) {\n return blockedSurfaces.includes(surfaces.trim())\n }\n\n return blockedSurfaces.includes(\"admin\")\n}\n\nfunction isCustomerSignupCreate(context: BetterAuthHookContext | null | undefined): boolean {\n const path = context?.path\n if (!path) return false\n\n return CUSTOMER_SIGNUP_ENDPOINT_SUFFIXES.some((suffix) => path.endsWith(suffix))\n}\n\nfunction stampCustomerSignupSurfaces(\n user: SignupBlockUserPayload | undefined,\n context: BetterAuthHookContext | null | undefined,\n surfaces: readonly string[],\n): SignupBlockUserPayload | undefined {\n if (surfaces.length === 0 || !isCustomerSignupCreate(context)) {\n return user\n }\n\n return {\n ...(user ?? {}),\n surfaces,\n }\n}\n\ntype ResolvedCreateBetterAuthOptions<\n UserOptions extends BetterAuthOptions[\"user\"],\n Plugins extends BetterAuthPlugin[] | undefined,\n> = Omit<BetterAuthOptions, \"plugins\" | \"user\"> & {\n plugins: ResolvedBetterAuthPlugins<Plugins>\n user: ResolvedBetterAuthUserOptions<UserOptions>\n}\n\nexport type BetterAuthDrizzleSchema = Record<string, AnyPgTable>\n\nexport interface CreateBetterAuthOptions<\n UserOptions extends BetterAuthOptions[\"user\"] = BetterAuthOptions[\"user\"],\n Plugins extends BetterAuthPlugin[] | undefined = BetterAuthPlugin[] | undefined,\n> {\n db?: ReturnType<typeof getDb>\n secret?: string\n baseURL?: string\n basePath?: string\n trustedOrigins?: string[]\n /**\n * Additional Drizzle tables for Better Auth plugins. The consuming app owns\n * matching migrations for every table passed here.\n */\n extraSchema?: BetterAuthDrizzleSchema\n plugins?: Plugins\n user?: UserOptions\n /**\n * Surfaces stamped on Better Auth customer self-signups before the bundled\n * single-tenant signup guard evaluates the new user. Applies to Better\n * Auth OTP signup endpoints that create a user as part of verification.\n *\n * Leave undefined to preserve Better Auth's raw user payload.\n */\n customerSignupSurfaces?: readonly string[]\n disableSignupWhenUsersExist?: DisableSignupWhenUsersExistOptions\n /** Called when a user requests a password reset. If not provided, logs to console. */\n sendResetPassword?: (data: {\n user: { email: string; name: string }\n url: string\n token: string\n }) => Promise<void>\n /** Called to send a verification OTP. If not provided, logs to console. */\n sendVerificationOTP?: (data: { email: string; otp: string; type: string }) => Promise<void>\n /**\n * Better Auth session cookie cache: session data rides in a short-lived\n * signed cookie so `getSession` skips the Postgres lookup on most\n * requests. Enabled by default with a 5-minute TTL — the trade-off is\n * that a revoked/expired session can stay usable for up to `maxAge`\n * seconds. Pass `false` for revocation-sensitive deployments, or tune\n * `maxAge` (seconds).\n */\n sessionCookieCache?: false | { maxAge?: number }\n /** Better Auth secondary storage, typically Redis/KV, used for distributed rate limits. */\n secondaryStorage?: BetterAuthOptions[\"secondaryStorage\"]\n /** Better Auth rate-limit config. Defaults to secondary storage when present. */\n rateLimit?: BetterAuthOptions[\"rateLimit\"]\n /**\n * Additional Better Auth advanced options. Voyant supplies secure cookies\n * by default except in explicit local development. Set\n * `advanced.useSecureCookies` to override that default.\n */\n advanced?: BetterAuthOptions[\"advanced\"]\n}\n\n/**\n * Framework-agnostic Better Auth factory.\n *\n * Accepts optional overrides for db, secret, baseURL, trustedOrigins.\n * Does NOT depend on Next.js — safe to use in Hono workers, TanStack Start, etc.\n */\nexport function createBetterAuth<\n const UserOptions extends BetterAuthOptions[\"user\"] = undefined,\n const Plugins extends BetterAuthPlugin[] | undefined = undefined,\n>(options: CreateBetterAuthOptions<UserOptions, Plugins> = {}) {\n const db = options.db ?? getDb(\"edge\")\n const secret = options.secret ?? getAuthSecret()\n const baseURL =\n options.baseURL ??\n process.env.BETTER_AUTH_URL ??\n process.env.APP_URL ??\n process.env.NEXT_PUBLIC_APP_URL ??\n \"http://localhost:3000\"\n const trustedOrigins = expandTrustedOrigins(\n options.trustedOrigins ?? getTrustedOrigins(),\n baseURL,\n )\n const extraPlugins = options.plugins ?? []\n const signupBlockSurfaces = normalizeSignupBlockSurfaces(options.disableSignupWhenUsersExist)\n const signupBlockEnabled = isSignupBlockEnabled(options.disableSignupWhenUsersExist)\n const customerSignupSurfaces = normalizeSurfaceList(options.customerSignupSurfaces)\n const schema = {\n user: authUser,\n session: authSession,\n account: authAccount,\n verification: authVerification,\n apikey: apikeyTable,\n ...(options.extraSchema ?? {}),\n } satisfies BetterAuthDrizzleSchema\n const rateLimit =\n options.rateLimit ??\n (options.secondaryStorage\n ? ({\n enabled: true,\n window: 60,\n max: 100,\n storage: \"secondary-storage\",\n } as BetterAuthOptions[\"rateLimit\"])\n : undefined)\n\n const authOptions = {\n appName: \"Voyant\",\n baseURL,\n ...(options.basePath ? { basePath: options.basePath } : {}),\n secret,\n database: drizzleAdapter(db, {\n provider: \"pg\",\n schema,\n }),\n ...(options.secondaryStorage ? { secondaryStorage: options.secondaryStorage } : {}),\n ...(rateLimit ? { rateLimit } : {}),\n ...(options.sessionCookieCache === false\n ? {}\n : {\n session: {\n cookieCache: {\n enabled: true,\n // Caps how long a revoked session stays usable from the\n // cookie alone; within the window getSession answers from\n // the signed cookie with zero DB roundtrips.\n maxAge: options.sessionCookieCache?.maxAge ?? 300,\n },\n },\n }),\n emailAndPassword: {\n enabled: true,\n minPasswordLength: 8,\n maxPasswordLength: 128,\n requireEmailVerification: true,\n revokeSessionsOnPasswordReset: true,\n // eslint-disable-next-line @typescript-eslint/require-await -- owner: auth; existing suppression is intentional pending typed cleanup.\n sendResetPassword:\n options.sendResetPassword ??\n (async ({ user, url }) => {\n console.warn(\n `[Auth] No email provider configured — password reset for ${user.email}: ${url}`,\n )\n }),\n },\n emailVerification: {\n sendOnSignUp: false, // OTP plugin handles this\n autoSignInAfterVerification: true,\n },\n user: {\n ...options.user,\n changeEmail: {\n enabled: true,\n ...options.user?.changeEmail,\n },\n } as ResolvedBetterAuthUserOptions<UserOptions>,\n socialProviders: {\n google: {\n clientId: process.env.GOOGLE_CLIENT_ID || \"\",\n clientSecret: process.env.GOOGLE_CLIENT_SECRET || \"\",\n enabled: Boolean(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET),\n },\n },\n trustedOrigins,\n plugins: [\n apiKey({\n defaultPrefix: \"voy_\",\n apiKeyHeaders: [\"authorization\"],\n requireName: true,\n keyExpiration: {\n defaultExpiresIn: null,\n },\n }),\n emailOTP({\n // eslint-disable-next-line @typescript-eslint/require-await -- owner: auth; existing suppression is intentional pending typed cleanup.\n sendVerificationOTP:\n options.sendVerificationOTP ??\n (async ({ email, otp, type }) => {\n console.warn(`[Auth] OTP for ${email}: ${otp} (${type})`)\n }),\n otpLength: 6,\n expiresIn: 600,\n sendVerificationOnSignUp: true,\n changeEmail: {\n enabled: true,\n },\n }),\n ...extraPlugins,\n ] as ResolvedBetterAuthPlugins<Plugins>,\n databaseHooks: {\n user: {\n create: {\n // Single-tenant: once a user exists, reject any new-user creation.\n // Covers email sign-up AND social-provider sign-up (Google would\n // otherwise auto-create a user on first OAuth callback). Existing\n // social sign-ins still work because this hook only fires on CREATE.\n // Seed scripts do raw drizzle inserts, so they bypass this hook —\n // which is intentional.\n before: async (user, context): Promise<BetterAuthCreateBeforeResult> => {\n const normalizedUser = stampCustomerSignupSurfaces(\n user as SignupBlockUserPayload,\n context as BetterAuthHookContext | null,\n customerSignupSurfaces,\n )\n\n if (\n !signupBlockEnabled ||\n !signupBlockAppliesToUser(normalizedUser, signupBlockSurfaces)\n ) {\n if (normalizedUser === user) {\n return\n }\n return { data: normalizedUser as Record<string, unknown> }\n }\n\n const [row] = await db.select({ count: sql<number>`count(*)::int` }).from(authUser)\n if ((row?.count ?? 0) > 0) {\n throw new Error(\"Sign-up is disabled. Ask an admin to invite you.\")\n }\n\n if (normalizedUser !== user) {\n return { data: normalizedUser as Record<string, unknown> }\n }\n },\n after: async (user, context) => {\n if (\n customerSignupSurfaces.length > 0 &&\n isCustomerSignupCreate(context as BetterAuthHookContext | null)\n ) {\n return\n }\n\n // Single-tenant bootstrap: the very first user to register becomes\n // the super-admin. Runs atomically after the `user` row is\n // inserted, so a simple COUNT(*) = 1 check identifies them.\n await provisionCurrentUserProfile(db, {\n userId: user.id,\n name: user.name,\n image: user.image,\n isSuperAdmin: await isFirstAuthUser(db),\n })\n },\n },\n },\n },\n advanced: {\n ...options.advanced,\n useSecureCookies:\n options.advanced?.useSecureCookies ?? process.env.NODE_ENV !== \"development\",\n },\n } as ResolvedCreateBetterAuthOptions<UserOptions, Plugins>\n\n return betterAuth<ResolvedCreateBetterAuthOptions<UserOptions, Plugins>>(authOptions)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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","sourcesContent":["import { apiKey } from \"@better-auth/api-key\"\nimport { getDb } from \"@voyant-travel/db\"\nimport {\n apikeyTable,\n authAccount,\n authSession,\n authUser,\n authVerification,\n} from \"@voyant-travel/db/schema/iam\"\nimport { type BetterAuthOptions, betterAuth } from \"better-auth\"\nimport { drizzleAdapter } from \"better-auth/adapters/drizzle\"\nimport { emailOTP } from \"better-auth/plugins\"\nimport type { BetterAuthPlugin } from \"better-auth/types\"\nimport { sql } from \"drizzle-orm\"\nimport type { AnyPgTable } from \"drizzle-orm/pg-core\"\n\nimport {\n createLocalMemberAccessPlugin,\n isLocalMemberDeactivated,\n isLocalMemberEmailDeactivated,\n isLocalMemberSessionActive,\n} from \"./local-member-access.js\"\nimport { expandTrustedOrigins, getTrustedOrigins } from \"./trusted-origins.js\"\nimport { isFirstAuthUser, provisionCurrentUserProfile } from \"./workspace.js\"\n\nexport function getAuthSecret(): string {\n const secret = process.env.BETTER_AUTH_SECRET || \"\"\n\n if (!secret || secret.length < 32) {\n throw new Error(\"Missing BETTER_AUTH_SECRET with at least 32 characters\")\n }\n\n return secret\n}\n\nexport type {\n AccountProfileUpdateHandler,\n ApiTokenRotationStore,\n BetterAuthApiTokenManagement,\n HandleAccountProfileRequestOptions,\n HandleApiTokenManagementRequestOptions,\n HandleOrganizationMembersRequestOptions,\n OrganizationMemberRecord,\n OrganizationMembersListHandler,\n OrganizationMembersListInput,\n} from \"./auth-facades.js\"\nexport {\n handleAccountProfileRequest,\n handleApiTokenManagementRequest,\n handleOrganizationMembersRequest,\n} from \"./auth-facades.js\"\n\ntype DefinedBetterAuthUserOptions<UserOptions extends BetterAuthOptions[\"user\"]> =\n UserOptions extends undefined ? Record<PropertyKey, never> : NonNullable<UserOptions>\n\ntype ResolvedBetterAuthChangeEmail<UserOptions extends BetterAuthOptions[\"user\"]> =\n DefinedBetterAuthUserOptions<UserOptions> extends { changeEmail?: infer ChangeEmail }\n ? ChangeEmail extends { enabled: infer Enabled }\n ? Omit<ChangeEmail, \"enabled\"> & { enabled: Enabled }\n : NonNullable<ChangeEmail> & { enabled: true }\n : { enabled: true }\n\ntype ResolvedBetterAuthUserOptions<UserOptions extends BetterAuthOptions[\"user\"]> = Omit<\n DefinedBetterAuthUserOptions<UserOptions>,\n \"changeEmail\"\n> & {\n changeEmail: ResolvedBetterAuthChangeEmail<UserOptions>\n}\n\ntype VoyantBetterAuthPlugins = [\n ReturnType<typeof apiKey>,\n ReturnType<typeof emailOTP>,\n ReturnType<typeof createLocalMemberAccessPlugin>,\n]\n\ntype ResolvedBetterAuthPlugins<Plugins extends BetterAuthPlugin[] | undefined> =\n Plugins extends BetterAuthPlugin[]\n ? [...VoyantBetterAuthPlugins, ...Plugins]\n : VoyantBetterAuthPlugins\n\nconst DEFAULT_SIGNUP_BLOCK_SURFACES = [\"admin\"] as const\nconst CUSTOMER_SIGNUP_ENDPOINT_SUFFIXES = [\n \"/sign-up/email\",\n \"/phone-number/verify\",\n \"/sign-in/email-otp\",\n] as const\n\nexport interface DisableSignupWhenUsersExistOptions {\n /**\n * Set to false when the consuming app owns all signup admission checks.\n *\n * Defaults to true.\n */\n enabled?: boolean\n /**\n * User surfaces that should keep the single-tenant signup guard. New users\n * with only other explicit surfaces can still be created by customer-facing\n * auth plugins such as phone OTP.\n *\n * Defaults to [\"admin\"]. Users without an explicit surfaces array are\n * treated as admin users for backward compatibility.\n */\n surfaces?: readonly string[]\n}\n\ntype BetterAuthCreateBeforeResult =\n | boolean\n | undefined\n | {\n data: Record<string, unknown>\n }\n\ntype SignupBlockUserPayload = {\n surfaces?: unknown\n}\n\ntype BetterAuthHookContext = {\n path?: string\n}\n\nfunction normalizeSignupBlockSurfaces(\n options: DisableSignupWhenUsersExistOptions | undefined,\n): readonly string[] {\n return options?.surfaces ?? DEFAULT_SIGNUP_BLOCK_SURFACES\n}\n\nfunction isSignupBlockEnabled(options: DisableSignupWhenUsersExistOptions | undefined): boolean {\n return options?.enabled !== false\n}\n\nfunction normalizeSurfaceList(surfaces: readonly string[] | undefined): readonly string[] {\n if (!surfaces) return []\n\n const normalized = new Set<string>()\n for (const surface of surfaces) {\n const trimmed = surface.trim()\n if (trimmed.length > 0) {\n normalized.add(trimmed)\n }\n }\n return Array.from(normalized)\n}\n\nfunction signupBlockAppliesToUser(\n user: SignupBlockUserPayload | undefined,\n blockedSurfaces: readonly string[],\n): boolean {\n if (blockedSurfaces.length === 0) return false\n\n const surfaces = user?.surfaces\n if (Array.isArray(surfaces)) {\n const explicitSurfaces = surfaces.filter(\n (surface): surface is string => typeof surface === \"string\" && surface.trim().length > 0,\n )\n if (explicitSurfaces.length > 0) {\n return explicitSurfaces.some((surface) => blockedSurfaces.includes(surface.trim()))\n }\n }\n\n if (typeof surfaces === \"string\" && surfaces.trim().length > 0) {\n return blockedSurfaces.includes(surfaces.trim())\n }\n\n return blockedSurfaces.includes(\"admin\")\n}\n\nfunction isCustomerSignupCreate(context: BetterAuthHookContext | null | undefined): boolean {\n const path = context?.path\n if (!path) return false\n\n return CUSTOMER_SIGNUP_ENDPOINT_SUFFIXES.some((suffix) => path.endsWith(suffix))\n}\n\nfunction stampCustomerSignupSurfaces(\n user: SignupBlockUserPayload | undefined,\n context: BetterAuthHookContext | null | undefined,\n surfaces: readonly string[],\n): SignupBlockUserPayload | undefined {\n if (surfaces.length === 0 || !isCustomerSignupCreate(context)) {\n return user\n }\n\n return {\n ...(user ?? {}),\n surfaces,\n }\n}\n\ntype ResolvedCreateBetterAuthOptions<\n UserOptions extends BetterAuthOptions[\"user\"],\n Plugins extends BetterAuthPlugin[] | undefined,\n> = Omit<BetterAuthOptions, \"plugins\" | \"user\"> & {\n plugins: ResolvedBetterAuthPlugins<Plugins>\n user: ResolvedBetterAuthUserOptions<UserOptions>\n}\n\nexport type BetterAuthDrizzleSchema = Record<string, AnyPgTable>\n\nexport interface CreateBetterAuthOptions<\n UserOptions extends BetterAuthOptions[\"user\"] = BetterAuthOptions[\"user\"],\n Plugins extends BetterAuthPlugin[] | undefined = BetterAuthPlugin[] | undefined,\n> {\n db?: ReturnType<typeof getDb>\n secret?: string\n baseURL?: string\n basePath?: string\n trustedOrigins?: string[]\n /**\n * Additional Drizzle tables for Better Auth plugins. The consuming app owns\n * matching migrations for every table passed here.\n */\n extraSchema?: BetterAuthDrizzleSchema\n plugins?: Plugins\n user?: UserOptions\n /**\n * Surfaces stamped on Better Auth customer self-signups before the bundled\n * single-tenant signup guard evaluates the new user. Applies to Better\n * Auth OTP signup endpoints that create a user as part of verification.\n *\n * Leave undefined to preserve Better Auth's raw user payload.\n */\n customerSignupSurfaces?: readonly string[]\n disableSignupWhenUsersExist?: DisableSignupWhenUsersExistOptions\n /** Called when a user requests a password reset. If not provided, logs to console. */\n sendResetPassword?: (data: {\n user: { email: string; name: string }\n url: string\n token: string\n }) => Promise<void>\n /** Called to send a verification OTP. If not provided, logs to console. */\n sendVerificationOTP?: (data: { email: string; otp: string; type: string }) => Promise<void>\n /**\n * Better Auth session cookie cache: session data rides in a short-lived\n * signed cookie. Enabled by default with a 5-minute TTL. Voyant still\n * validates local membership and session existence against Postgres so a\n * deleted session cannot be revived from the cookie cache. Pass `false` to\n * disable payload caching, or tune `maxAge` (seconds).\n */\n sessionCookieCache?: false | { maxAge?: number }\n /** Better Auth secondary storage, typically Redis/KV, used for distributed rate limits. */\n secondaryStorage?: BetterAuthOptions[\"secondaryStorage\"]\n /** Better Auth rate-limit config. Defaults to secondary storage when present. */\n rateLimit?: BetterAuthOptions[\"rateLimit\"]\n /**\n * Additional Better Auth advanced options. Voyant supplies secure cookies\n * by default except in explicit local development. Set\n * `advanced.useSecureCookies` to override that default.\n */\n advanced?: BetterAuthOptions[\"advanced\"]\n}\n\n/**\n * Framework-agnostic Better Auth factory.\n *\n * Accepts optional overrides for db, secret, baseURL, trustedOrigins.\n * Does NOT depend on Next.js — safe to use in Hono workers, TanStack Start, etc.\n */\nexport function createBetterAuth<\n const UserOptions extends BetterAuthOptions[\"user\"] = undefined,\n const Plugins extends BetterAuthPlugin[] | undefined = undefined,\n>(options: CreateBetterAuthOptions<UserOptions, Plugins> = {}) {\n const db = options.db ?? getDb(\"edge\")\n const secret = options.secret ?? getAuthSecret()\n const baseURL =\n options.baseURL ??\n process.env.BETTER_AUTH_URL ??\n process.env.APP_URL ??\n process.env.NEXT_PUBLIC_APP_URL ??\n \"http://localhost:3000\"\n const trustedOrigins = expandTrustedOrigins(\n options.trustedOrigins ?? getTrustedOrigins(),\n baseURL,\n )\n const extraPlugins = options.plugins ?? []\n const signupBlockSurfaces = normalizeSignupBlockSurfaces(options.disableSignupWhenUsersExist)\n const signupBlockEnabled = isSignupBlockEnabled(options.disableSignupWhenUsersExist)\n const customerSignupSurfaces = normalizeSurfaceList(options.customerSignupSurfaces)\n const schema = {\n user: authUser,\n session: authSession,\n account: authAccount,\n verification: authVerification,\n apikey: apikeyTable,\n ...(options.extraSchema ?? {}),\n } satisfies BetterAuthDrizzleSchema\n const rateLimit =\n options.rateLimit ??\n (options.secondaryStorage\n ? ({\n enabled: true,\n window: 60,\n max: 100,\n storage: \"secondary-storage\",\n } as BetterAuthOptions[\"rateLimit\"])\n : undefined)\n\n const authOptions = {\n appName: \"Voyant\",\n baseURL,\n ...(options.basePath ? { basePath: options.basePath } : {}),\n secret,\n database: drizzleAdapter(db, {\n provider: \"pg\",\n schema,\n }),\n ...(options.secondaryStorage ? { secondaryStorage: options.secondaryStorage } : {}),\n ...(rateLimit ? { rateLimit } : {}),\n ...(options.sessionCookieCache === false\n ? {}\n : {\n session: {\n cookieCache: {\n enabled: true,\n maxAge: options.sessionCookieCache?.maxAge ?? 300,\n },\n },\n }),\n emailAndPassword: {\n enabled: true,\n minPasswordLength: 8,\n maxPasswordLength: 128,\n requireEmailVerification: true,\n revokeSessionsOnPasswordReset: true,\n // eslint-disable-next-line @typescript-eslint/require-await -- owner: auth; existing suppression is intentional pending typed cleanup.\n sendResetPassword:\n options.sendResetPassword ??\n (async ({ user, url }) => {\n console.warn(\n `[Auth] No email provider configured — password reset for ${user.email}: ${url}`,\n )\n }),\n },\n emailVerification: {\n sendOnSignUp: false, // OTP plugin handles this\n autoSignInAfterVerification: true,\n },\n user: {\n ...options.user,\n changeEmail: {\n enabled: true,\n ...options.user?.changeEmail,\n },\n } as ResolvedBetterAuthUserOptions<UserOptions>,\n socialProviders: {\n google: {\n clientId: process.env.GOOGLE_CLIENT_ID || \"\",\n clientSecret: process.env.GOOGLE_CLIENT_SECRET || \"\",\n enabled: Boolean(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET),\n },\n },\n trustedOrigins,\n plugins: [\n apiKey({\n defaultPrefix: \"voy_\",\n apiKeyHeaders: [\"authorization\"],\n requireName: true,\n keyExpiration: {\n defaultExpiresIn: null,\n },\n }),\n emailOTP({\n // eslint-disable-next-line @typescript-eslint/require-await -- owner: auth; existing suppression is intentional pending typed cleanup.\n sendVerificationOTP:\n options.sendVerificationOTP ??\n (async ({ email, otp, type }) => {\n console.warn(`[Auth] OTP for ${email}: ${otp} (${type})`)\n }),\n otpLength: 6,\n expiresIn: 600,\n sendVerificationOnSignUp: true,\n changeEmail: {\n enabled: true,\n },\n }),\n createLocalMemberAccessPlugin({\n isEmailDeactivated: (email) => isLocalMemberEmailDeactivated(db, email),\n isSessionActive: (sessionId, userId) => isLocalMemberSessionActive(db, sessionId, userId),\n isUserDeactivated: (userId) => isLocalMemberDeactivated(db, userId),\n }),\n ...extraPlugins,\n ] as ResolvedBetterAuthPlugins<Plugins>,\n databaseHooks: {\n user: {\n create: {\n // Single-tenant: once a user exists, reject any new-user creation.\n // Covers email sign-up AND social-provider sign-up (Google would\n // otherwise auto-create a user on first OAuth callback). Existing\n // social sign-ins still work because this hook only fires on CREATE.\n // Seed scripts do raw drizzle inserts, so they bypass this hook —\n // which is intentional.\n before: async (user, context): Promise<BetterAuthCreateBeforeResult> => {\n const normalizedUser = stampCustomerSignupSurfaces(\n user as SignupBlockUserPayload,\n context as BetterAuthHookContext | null,\n customerSignupSurfaces,\n )\n\n if (\n !signupBlockEnabled ||\n !signupBlockAppliesToUser(normalizedUser, signupBlockSurfaces)\n ) {\n if (normalizedUser === user) {\n return\n }\n return { data: normalizedUser as Record<string, unknown> }\n }\n\n const [row] = await db.select({ count: sql<number>`count(*)::int` }).from(authUser)\n if ((row?.count ?? 0) > 0) {\n throw new Error(\"Sign-up is disabled. Ask an admin to invite you.\")\n }\n\n if (normalizedUser !== user) {\n return { data: normalizedUser as Record<string, unknown> }\n }\n },\n after: async (user, context) => {\n if (\n customerSignupSurfaces.length > 0 &&\n isCustomerSignupCreate(context as BetterAuthHookContext | null)\n ) {\n return\n }\n\n // Single-tenant bootstrap: the very first user to register becomes\n // the super-admin. Runs atomically after the `user` row is\n // inserted, so a simple COUNT(*) = 1 check identifies them.\n await provisionCurrentUserProfile(db, {\n userId: user.id,\n name: user.name,\n image: user.image,\n isSuperAdmin: await isFirstAuthUser(db),\n })\n },\n },\n },\n },\n advanced: {\n ...options.advanced,\n useSecureCookies:\n options.advanced?.useSecureCookies ?? process.env.NODE_ENV !== \"development\",\n },\n } as ResolvedCreateBetterAuthOptions<UserOptions, Plugins>\n\n return betterAuth<ResolvedCreateBetterAuthOptions<UserOptions, Plugins>>(authOptions)\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CloudAdminInvitation, type CloudAdminMember } from "./cloud-broker.js";
|
|
2
|
+
import type { IdentityAccessRuntimeProvider } from "./identity-access-runtime-port.js";
|
|
3
|
+
import { type TeamManagementAdapter } from "./team-management-policy.js";
|
|
4
|
+
import type { TeamInvitationDto, TeamMemberDto } from "./team-management-runtime-port.js";
|
|
5
|
+
export declare function cloudTeamMemberDto(member: CloudAdminMember): TeamMemberDto;
|
|
6
|
+
export declare function cloudTeamInvitationDto(invitation: CloudAdminInvitation): TeamInvitationDto;
|
|
7
|
+
export declare function createCloudTeamManagementAdapter(identityAccess: IdentityAccessRuntimeProvider): TeamManagementAdapter;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { cloudAuthUserLinks } from "@voyant-travel/db/schema/iam";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { inviteCloudAdminMember, listCloudAdminInvitations, listCloudAdminMemberRoles, listCloudAdminMembers, revokeCloudAdminInvitation, setCloudAdminMemberAccess, setCloudAdminMemberRole, } from "./cloud-broker.js";
|
|
4
|
+
import { TeamManagementError } from "./team-management-policy.js";
|
|
5
|
+
function roleLevel(roleId) {
|
|
6
|
+
switch (roleId.trim().toLowerCase()) {
|
|
7
|
+
case "owner":
|
|
8
|
+
return 40;
|
|
9
|
+
case "admin":
|
|
10
|
+
return 30;
|
|
11
|
+
case "editor":
|
|
12
|
+
case "member":
|
|
13
|
+
return 20;
|
|
14
|
+
case "viewer":
|
|
15
|
+
case "guest":
|
|
16
|
+
return 10;
|
|
17
|
+
default:
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function cloudTeamMemberDto(member) {
|
|
22
|
+
const roleId = member.roleSlug ?? "member";
|
|
23
|
+
return {
|
|
24
|
+
id: member.membershipId,
|
|
25
|
+
email: member.email,
|
|
26
|
+
name: member.name ?? null,
|
|
27
|
+
roleId,
|
|
28
|
+
roleName: member.roleName ?? roleId,
|
|
29
|
+
status: member.hasDeploymentAccess && member.status.toLowerCase() === "active"
|
|
30
|
+
? "active"
|
|
31
|
+
: "deactivated",
|
|
32
|
+
joinedAt: member.createdAt ?? null,
|
|
33
|
+
lastActivityAt: member.lastActivityAt ?? null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function cloudTeamInvitationDto(invitation) {
|
|
37
|
+
const roleId = invitation.roleSlug ?? "member";
|
|
38
|
+
return {
|
|
39
|
+
id: invitation.id,
|
|
40
|
+
email: invitation.email,
|
|
41
|
+
roleId,
|
|
42
|
+
roleName: invitation.roleName ?? roleId,
|
|
43
|
+
status: invitation.state,
|
|
44
|
+
createdAt: invitation.createdAt,
|
|
45
|
+
expiresAt: invitation.expiresAt,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function createdInvitationDto(invitation) {
|
|
49
|
+
return {
|
|
50
|
+
...cloudTeamInvitationDto(invitation),
|
|
51
|
+
acceptUrl: invitation.acceptInvitationUrl || null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function resolveCloudRequest(identityAccess, context) {
|
|
55
|
+
const deployment = identityAccess.resolveDeployment(context.bindings);
|
|
56
|
+
if (!deployment.cloudAdminMembers) {
|
|
57
|
+
throw new TeamManagementError("not_configured", "Voyant Cloud team management is not configured.", 501);
|
|
58
|
+
}
|
|
59
|
+
const [link] = await context.db
|
|
60
|
+
.select({ providerAccountId: cloudAuthUserLinks.providerAccountId })
|
|
61
|
+
.from(cloudAuthUserLinks)
|
|
62
|
+
.where(eq(cloudAuthUserLinks.userId, context.userId))
|
|
63
|
+
.limit(1);
|
|
64
|
+
if (!link?.providerAccountId) {
|
|
65
|
+
throw new TeamManagementError("forbidden", "No Cloud identity is linked to this session.");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
config: deployment.cloudAdminMembers,
|
|
69
|
+
actingWorkosUserId: link.providerAccountId,
|
|
70
|
+
actingExternalUserId: link.providerAccountId,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function createCloudTeamManagementAdapter(identityAccess) {
|
|
74
|
+
return {
|
|
75
|
+
async getActor(context) {
|
|
76
|
+
const request = await resolveCloudRequest(identityAccess, context);
|
|
77
|
+
const actor = (await listCloudAdminMembers(request)).find((member) => member.externalUserId === request.actingExternalUserId);
|
|
78
|
+
if (!actor?.hasDeploymentAccess) {
|
|
79
|
+
throw new TeamManagementError("forbidden", "The current user cannot manage this team.");
|
|
80
|
+
}
|
|
81
|
+
return { memberId: actor.membershipId, roleId: actor.roleSlug ?? "member" };
|
|
82
|
+
},
|
|
83
|
+
async getCapabilities(_context, actor) {
|
|
84
|
+
const canManage = actor.roleId === "owner" || actor.roleId === "admin";
|
|
85
|
+
return {
|
|
86
|
+
viewRoster: true,
|
|
87
|
+
inviteMembers: canManage,
|
|
88
|
+
manageRoles: canManage,
|
|
89
|
+
activateMembers: canManage,
|
|
90
|
+
deactivateMembers: canManage,
|
|
91
|
+
revokeInvitations: canManage,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
async listMembers(context) {
|
|
95
|
+
return (await listCloudAdminMembers(await resolveCloudRequest(identityAccess, context))).map(cloudTeamMemberDto);
|
|
96
|
+
},
|
|
97
|
+
async listRoles(context) {
|
|
98
|
+
return (await listCloudAdminMemberRoles(await resolveCloudRequest(identityAccess, context))).map((role) => ({ id: role.slug, name: role.name, description: role.description }));
|
|
99
|
+
},
|
|
100
|
+
async listInvitations(context) {
|
|
101
|
+
return (await listCloudAdminInvitations(await resolveCloudRequest(identityAccess, context))).map(cloudTeamInvitationDto);
|
|
102
|
+
},
|
|
103
|
+
async inviteMember(context, input) {
|
|
104
|
+
return createdInvitationDto(await inviteCloudAdminMember({
|
|
105
|
+
...(await resolveCloudRequest(identityAccess, context)),
|
|
106
|
+
input: {
|
|
107
|
+
email: input.email,
|
|
108
|
+
roleSlug: input.roleId,
|
|
109
|
+
expiresInDays: input.expiresInDays,
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
112
|
+
},
|
|
113
|
+
async revokeInvitation(context, invitationId) {
|
|
114
|
+
await revokeCloudAdminInvitation({
|
|
115
|
+
...(await resolveCloudRequest(identityAccess, context)),
|
|
116
|
+
invitationId,
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
async updateMemberRole(context, memberId, roleId) {
|
|
120
|
+
return cloudTeamMemberDto(await setCloudAdminMemberRole({
|
|
121
|
+
...(await resolveCloudRequest(identityAccess, context)),
|
|
122
|
+
membershipId: memberId,
|
|
123
|
+
roleSlug: roleId,
|
|
124
|
+
}));
|
|
125
|
+
},
|
|
126
|
+
async deactivateMember(context, memberId) {
|
|
127
|
+
return cloudTeamMemberDto(await setCloudAdminMemberAccess({
|
|
128
|
+
...(await resolveCloudRequest(identityAccess, context)),
|
|
129
|
+
membershipId: memberId,
|
|
130
|
+
hasAccess: false,
|
|
131
|
+
}));
|
|
132
|
+
},
|
|
133
|
+
async activateMember(context, memberId) {
|
|
134
|
+
return cloudTeamMemberDto(await setCloudAdminMemberAccess({
|
|
135
|
+
...(await resolveCloudRequest(identityAccess, context)),
|
|
136
|
+
membershipId: memberId,
|
|
137
|
+
hasAccess: true,
|
|
138
|
+
}));
|
|
139
|
+
},
|
|
140
|
+
roleLevel,
|
|
141
|
+
isOwnerRole(roleId) {
|
|
142
|
+
return roleId === "owner";
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=team-management-cloud-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team-management-cloud-adapter.js","sourceRoot":"","sources":["../src/team-management-cloud-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import { cloudAuthUserLinks } from \"@voyant-travel/db/schema/iam\"\nimport { eq } from \"drizzle-orm\"\n\nimport {\n type CloudAdminInvitation,\n type CloudAdminMember,\n type CloudAdminMembersRequest,\n inviteCloudAdminMember,\n listCloudAdminInvitations,\n listCloudAdminMemberRoles,\n listCloudAdminMembers,\n revokeCloudAdminInvitation,\n setCloudAdminMemberAccess,\n setCloudAdminMemberRole,\n} from \"./cloud-broker.js\"\nimport type { IdentityAccessRuntimeProvider } from \"./identity-access-runtime-port.js\"\nimport { type TeamManagementAdapter, TeamManagementError } from \"./team-management-policy.js\"\nimport type {\n CreatedTeamInvitationDto,\n TeamInvitationDto,\n TeamManagementCapabilitiesDto,\n TeamManagementRequestContext,\n TeamMemberDto,\n} from \"./team-management-runtime-port.js\"\n\nfunction roleLevel(roleId: string): number {\n switch (roleId.trim().toLowerCase()) {\n case \"owner\":\n return 40\n case \"admin\":\n return 30\n case \"editor\":\n case \"member\":\n return 20\n case \"viewer\":\n case \"guest\":\n return 10\n default:\n return 0\n }\n}\n\nexport function cloudTeamMemberDto(member: CloudAdminMember): TeamMemberDto {\n const roleId = member.roleSlug ?? \"member\"\n return {\n id: member.membershipId,\n email: member.email,\n name: member.name ?? null,\n roleId,\n roleName: member.roleName ?? roleId,\n status:\n member.hasDeploymentAccess && member.status.toLowerCase() === \"active\"\n ? \"active\"\n : \"deactivated\",\n joinedAt: member.createdAt ?? null,\n lastActivityAt: member.lastActivityAt ?? null,\n }\n}\n\nexport function cloudTeamInvitationDto(invitation: CloudAdminInvitation): TeamInvitationDto {\n const roleId = invitation.roleSlug ?? \"member\"\n return {\n id: invitation.id,\n email: invitation.email,\n roleId,\n roleName: invitation.roleName ?? roleId,\n status: invitation.state,\n createdAt: invitation.createdAt,\n expiresAt: invitation.expiresAt,\n }\n}\n\nfunction createdInvitationDto(invitation: CloudAdminInvitation): CreatedTeamInvitationDto {\n return {\n ...cloudTeamInvitationDto(invitation),\n acceptUrl: invitation.acceptInvitationUrl || null,\n }\n}\n\nasync function resolveCloudRequest(\n identityAccess: IdentityAccessRuntimeProvider,\n context: TeamManagementRequestContext,\n): Promise<CloudAdminMembersRequest & { actingExternalUserId: string }> {\n const deployment = identityAccess.resolveDeployment(context.bindings)\n if (!deployment.cloudAdminMembers) {\n throw new TeamManagementError(\n \"not_configured\",\n \"Voyant Cloud team management is not configured.\",\n 501,\n )\n }\n const [link] = await context.db\n .select({ providerAccountId: cloudAuthUserLinks.providerAccountId })\n .from(cloudAuthUserLinks)\n .where(eq(cloudAuthUserLinks.userId, context.userId))\n .limit(1)\n if (!link?.providerAccountId) {\n throw new TeamManagementError(\"forbidden\", \"No Cloud identity is linked to this session.\")\n }\n return {\n config: deployment.cloudAdminMembers,\n actingWorkosUserId: link.providerAccountId,\n actingExternalUserId: link.providerAccountId,\n }\n}\n\nexport function createCloudTeamManagementAdapter(\n identityAccess: IdentityAccessRuntimeProvider,\n): TeamManagementAdapter {\n return {\n async getActor(context) {\n const request = await resolveCloudRequest(identityAccess, context)\n const actor = (await listCloudAdminMembers(request)).find(\n (member) => member.externalUserId === request.actingExternalUserId,\n )\n if (!actor?.hasDeploymentAccess) {\n throw new TeamManagementError(\"forbidden\", \"The current user cannot manage this team.\")\n }\n return { memberId: actor.membershipId, roleId: actor.roleSlug ?? \"member\" }\n },\n async getCapabilities(_context, actor): Promise<TeamManagementCapabilitiesDto> {\n const canManage = actor.roleId === \"owner\" || actor.roleId === \"admin\"\n return {\n viewRoster: true,\n inviteMembers: canManage,\n manageRoles: canManage,\n activateMembers: canManage,\n deactivateMembers: canManage,\n revokeInvitations: canManage,\n }\n },\n async listMembers(context) {\n return (await listCloudAdminMembers(await resolveCloudRequest(identityAccess, context))).map(\n cloudTeamMemberDto,\n )\n },\n async listRoles(context) {\n return (\n await listCloudAdminMemberRoles(await resolveCloudRequest(identityAccess, context))\n ).map((role) => ({ id: role.slug, name: role.name, description: role.description }))\n },\n async listInvitations(context) {\n return (\n await listCloudAdminInvitations(await resolveCloudRequest(identityAccess, context))\n ).map(cloudTeamInvitationDto)\n },\n async inviteMember(context, input) {\n return createdInvitationDto(\n await inviteCloudAdminMember({\n ...(await resolveCloudRequest(identityAccess, context)),\n input: {\n email: input.email,\n roleSlug: input.roleId,\n expiresInDays: input.expiresInDays,\n },\n }),\n )\n },\n async revokeInvitation(context, invitationId) {\n await revokeCloudAdminInvitation({\n ...(await resolveCloudRequest(identityAccess, context)),\n invitationId,\n })\n },\n async updateMemberRole(context, memberId, roleId) {\n return cloudTeamMemberDto(\n await setCloudAdminMemberRole({\n ...(await resolveCloudRequest(identityAccess, context)),\n membershipId: memberId,\n roleSlug: roleId,\n }),\n )\n },\n async deactivateMember(context, memberId) {\n return cloudTeamMemberDto(\n await setCloudAdminMemberAccess({\n ...(await resolveCloudRequest(identityAccess, context)),\n membershipId: memberId,\n hasAccess: false,\n }),\n )\n },\n async activateMember(context, memberId) {\n return cloudTeamMemberDto(\n await setCloudAdminMemberAccess({\n ...(await resolveCloudRequest(identityAccess, context)),\n membershipId: memberId,\n hasAccess: true,\n }),\n )\n },\n roleLevel,\n isOwnerRole(roleId) {\n return roleId === \"owner\"\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IdentityAccessRuntimeProvider } from "./identity-access-runtime-port.js";
|
|
2
|
+
import { type TeamManagementAdapter } from "./team-management-policy.js";
|
|
3
|
+
export { localProviderIdForStatus } from "./local-member-access.js";
|
|
4
|
+
export declare function createLocalTeamManagementAdapter(identityAccess: IdentityAccessRuntimeProvider): TeamManagementAdapter;
|
|
5
|
+
export declare function localRoleForRedeemedInvitation(metadata: Record<string, unknown> | null): {
|
|
6
|
+
isSuperAdmin: boolean;
|
|
7
|
+
permissions: string[];
|
|
8
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { newId } from "@voyant-travel/db/lib/typeid";
|
|
2
|
+
import { apikeyTable, authAccount, authSession, authUser, userInvitationsTable, userProfilesTable, } from "@voyant-travel/db/schema/iam";
|
|
3
|
+
import { scopesForRole } from "@voyant-travel/types/member-roles";
|
|
4
|
+
import { desc, eq, sql } from "drizzle-orm";
|
|
5
|
+
import { isDeactivatedProviderId, localProviderIdForStatus } from "./local-member-access.js";
|
|
6
|
+
import { TeamManagementError } from "./team-management-policy.js";
|
|
7
|
+
const LOCAL_ROLES = [
|
|
8
|
+
{ id: "owner", name: "Owner", description: "Full access, including ownership changes." },
|
|
9
|
+
{ id: "admin", name: "Admin", description: "Full access to team and operator settings." },
|
|
10
|
+
{ id: "editor", name: "Editor", description: "Can manage day-to-day operator data." },
|
|
11
|
+
{ id: "viewer", name: "Viewer", description: "Read-only operator access." },
|
|
12
|
+
];
|
|
13
|
+
const ROLE_LEVELS = { owner: 40, admin: 30, editor: 20, viewer: 10 };
|
|
14
|
+
export { localProviderIdForStatus } from "./local-member-access.js";
|
|
15
|
+
function roleName(roleId) {
|
|
16
|
+
return LOCAL_ROLES.find((role) => role.id === roleId)?.name ?? roleId;
|
|
17
|
+
}
|
|
18
|
+
function sameScopes(left, right) {
|
|
19
|
+
return [...left].sort().join("\0") === [...right].sort().join("\0");
|
|
20
|
+
}
|
|
21
|
+
function resolveLocalRole(isSuperAdmin, permissions) {
|
|
22
|
+
if (isSuperAdmin)
|
|
23
|
+
return "owner";
|
|
24
|
+
if (permissions === null || permissions.includes("*"))
|
|
25
|
+
return "admin";
|
|
26
|
+
if (sameScopes(permissions, scopesForRole("editor") ?? []))
|
|
27
|
+
return "editor";
|
|
28
|
+
if (sameScopes(permissions, scopesForRole("viewer") ?? []))
|
|
29
|
+
return "viewer";
|
|
30
|
+
return "custom";
|
|
31
|
+
}
|
|
32
|
+
function permissionsForRole(roleId) {
|
|
33
|
+
const scopes = scopesForRole(roleId);
|
|
34
|
+
if (!scopes)
|
|
35
|
+
throw new TeamManagementError("role_not_found", "Team role not found.", 404);
|
|
36
|
+
return [...scopes];
|
|
37
|
+
}
|
|
38
|
+
function invitationRole(metadata) {
|
|
39
|
+
const roleId = metadata?.roleId;
|
|
40
|
+
return typeof roleId === "string" && ROLE_LEVELS[roleId] !== undefined ? roleId : "editor";
|
|
41
|
+
}
|
|
42
|
+
function invitationStatus(input) {
|
|
43
|
+
if (input.redeemedAt)
|
|
44
|
+
return "accepted";
|
|
45
|
+
if (input.expiresAt.getTime() <= Date.now())
|
|
46
|
+
return "expired";
|
|
47
|
+
return "pending";
|
|
48
|
+
}
|
|
49
|
+
function randomTokenBase64Url(bytes) {
|
|
50
|
+
const buffer = new Uint8Array(bytes);
|
|
51
|
+
crypto.getRandomValues(buffer);
|
|
52
|
+
let binary = "";
|
|
53
|
+
for (const byte of buffer)
|
|
54
|
+
binary += String.fromCharCode(byte);
|
|
55
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
56
|
+
}
|
|
57
|
+
async function sha256Hex(input) {
|
|
58
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
59
|
+
return Array.from(new Uint8Array(digest))
|
|
60
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
61
|
+
.join("");
|
|
62
|
+
}
|
|
63
|
+
async function listLocalMembers(context) {
|
|
64
|
+
const [rows, accounts] = await Promise.all([
|
|
65
|
+
context.db
|
|
66
|
+
.select({
|
|
67
|
+
id: authUser.id,
|
|
68
|
+
email: authUser.email,
|
|
69
|
+
authName: authUser.name,
|
|
70
|
+
createdAt: authUser.createdAt,
|
|
71
|
+
firstName: userProfilesTable.firstName,
|
|
72
|
+
lastName: userProfilesTable.lastName,
|
|
73
|
+
isSuperAdmin: userProfilesTable.isSuperAdmin,
|
|
74
|
+
permissions: userProfilesTable.permissions,
|
|
75
|
+
lastActiveAt: userProfilesTable.lastActiveAt,
|
|
76
|
+
})
|
|
77
|
+
.from(authUser)
|
|
78
|
+
.leftJoin(userProfilesTable, eq(userProfilesTable.id, authUser.id)),
|
|
79
|
+
context.db
|
|
80
|
+
.select({ userId: authAccount.userId, providerId: authAccount.providerId })
|
|
81
|
+
.from(authAccount),
|
|
82
|
+
]);
|
|
83
|
+
const activeUserIds = new Set(accounts
|
|
84
|
+
.filter((account) => !isDeactivatedProviderId(account.providerId))
|
|
85
|
+
.map((account) => account.userId));
|
|
86
|
+
return rows.map((row) => {
|
|
87
|
+
const roleId = resolveLocalRole(row.isSuperAdmin ?? false, row.permissions ?? null);
|
|
88
|
+
const profileName = [row.firstName, row.lastName].filter(Boolean).join(" ").trim();
|
|
89
|
+
return {
|
|
90
|
+
id: row.id,
|
|
91
|
+
email: row.email,
|
|
92
|
+
name: profileName || row.authName || null,
|
|
93
|
+
roleId,
|
|
94
|
+
roleName: roleName(roleId),
|
|
95
|
+
status: activeUserIds.has(row.id) ? "active" : "deactivated",
|
|
96
|
+
joinedAt: row.createdAt?.toISOString() ?? null,
|
|
97
|
+
lastActivityAt: row.lastActiveAt?.toISOString() ?? null,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function memberOrThrow(members, memberId) {
|
|
102
|
+
const member = members.find((candidate) => candidate.id === memberId);
|
|
103
|
+
if (!member)
|
|
104
|
+
throw new TeamManagementError("member_not_found", "Team member not found.", 404);
|
|
105
|
+
return member;
|
|
106
|
+
}
|
|
107
|
+
function assertActiveOwnerRemains(members, target) {
|
|
108
|
+
if (target.status !== "active" || target.roleId !== "owner")
|
|
109
|
+
return;
|
|
110
|
+
const activeOwnerCount = members.filter((member) => member.status === "active" && member.roleId === "owner").length;
|
|
111
|
+
if (activeOwnerCount <= 1) {
|
|
112
|
+
throw new TeamManagementError("last_owner", "The last active owner cannot be demoted or deactivated.", 409);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function withLockedLocalTeam(context, operation) {
|
|
116
|
+
return context.db.transaction(async (transaction) => {
|
|
117
|
+
// agent-quality: raw-sql reviewed -- owner: auth; table identifiers are static Drizzle schema objects.
|
|
118
|
+
await transaction.execute(sql `SELECT ${authUser.id} FROM ${authUser} ORDER BY ${authUser.id} FOR UPDATE`);
|
|
119
|
+
return operation({ ...context, db: transaction });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export function createLocalTeamManagementAdapter(identityAccess) {
|
|
123
|
+
return {
|
|
124
|
+
async getActor(context) {
|
|
125
|
+
const actor = (await listLocalMembers(context)).find((member) => member.id === context.userId);
|
|
126
|
+
if (actor?.status !== "active") {
|
|
127
|
+
throw new TeamManagementError("forbidden", "The current user cannot manage this team.");
|
|
128
|
+
}
|
|
129
|
+
return { memberId: actor.id, roleId: actor.roleId };
|
|
130
|
+
},
|
|
131
|
+
async getCapabilities(_context, actor) {
|
|
132
|
+
const canManage = actor.roleId === "owner" || actor.roleId === "admin";
|
|
133
|
+
return {
|
|
134
|
+
viewRoster: true,
|
|
135
|
+
inviteMembers: canManage,
|
|
136
|
+
manageRoles: canManage,
|
|
137
|
+
activateMembers: canManage,
|
|
138
|
+
deactivateMembers: canManage,
|
|
139
|
+
revokeInvitations: canManage,
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
listMembers: listLocalMembers,
|
|
143
|
+
async listRoles() {
|
|
144
|
+
return LOCAL_ROLES;
|
|
145
|
+
},
|
|
146
|
+
async listInvitations(context) {
|
|
147
|
+
const rows = await context.db
|
|
148
|
+
.select({
|
|
149
|
+
id: userInvitationsTable.id,
|
|
150
|
+
email: userInvitationsTable.email,
|
|
151
|
+
metadata: userInvitationsTable.metadata,
|
|
152
|
+
expiresAt: userInvitationsTable.expiresAt,
|
|
153
|
+
redeemedAt: userInvitationsTable.redeemedAt,
|
|
154
|
+
createdAt: userInvitationsTable.createdAt,
|
|
155
|
+
})
|
|
156
|
+
.from(userInvitationsTable)
|
|
157
|
+
.orderBy(desc(userInvitationsTable.createdAt))
|
|
158
|
+
.limit(100);
|
|
159
|
+
return rows.map((row) => {
|
|
160
|
+
const roleId = invitationRole(row.metadata);
|
|
161
|
+
return {
|
|
162
|
+
id: row.id,
|
|
163
|
+
email: row.email,
|
|
164
|
+
roleId,
|
|
165
|
+
roleName: roleName(roleId),
|
|
166
|
+
status: invitationStatus(row),
|
|
167
|
+
createdAt: row.createdAt.toISOString(),
|
|
168
|
+
expiresAt: row.expiresAt.toISOString(),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
async inviteMember(context, input) {
|
|
173
|
+
const email = input.email.trim().toLowerCase();
|
|
174
|
+
const [existingUser] = await context.db
|
|
175
|
+
.select({ id: authUser.id })
|
|
176
|
+
.from(authUser)
|
|
177
|
+
.where(eq(authUser.email, email))
|
|
178
|
+
.limit(1);
|
|
179
|
+
if (existingUser) {
|
|
180
|
+
throw new TeamManagementError("forbidden", "A team member with this email already exists.", 409);
|
|
181
|
+
}
|
|
182
|
+
const rawToken = randomTokenBase64Url(32);
|
|
183
|
+
const expiresInDays = input.expiresInDays ?? 3;
|
|
184
|
+
const expiresAt = new Date(Date.now() + expiresInDays * 86_400_000);
|
|
185
|
+
const now = new Date();
|
|
186
|
+
const id = newId("user_invitations");
|
|
187
|
+
await context.db.insert(userInvitationsTable).values({
|
|
188
|
+
id,
|
|
189
|
+
email,
|
|
190
|
+
tokenHash: await sha256Hex(rawToken),
|
|
191
|
+
expiresAt,
|
|
192
|
+
createdBy: context.userId,
|
|
193
|
+
metadata: { roleId: input.roleId },
|
|
194
|
+
createdAt: now,
|
|
195
|
+
});
|
|
196
|
+
const { appUrl } = identityAccess.resolveDeployment(context.bindings);
|
|
197
|
+
const acceptUrl = `${appUrl}/accept-invite?token=${encodeURIComponent(rawToken)}`;
|
|
198
|
+
await identityAccess.sendInvitationEmail(context.bindings, {
|
|
199
|
+
acceptUrl,
|
|
200
|
+
expiresInHours: expiresInDays * 24,
|
|
201
|
+
to: email,
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
id,
|
|
205
|
+
email,
|
|
206
|
+
roleId: input.roleId,
|
|
207
|
+
roleName: roleName(input.roleId),
|
|
208
|
+
status: "pending",
|
|
209
|
+
createdAt: now.toISOString(),
|
|
210
|
+
expiresAt: expiresAt.toISOString(),
|
|
211
|
+
acceptUrl,
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
async revokeInvitation(context, invitationId) {
|
|
215
|
+
await context.db.delete(userInvitationsTable).where(eq(userInvitationsTable.id, invitationId));
|
|
216
|
+
},
|
|
217
|
+
async updateMemberRole(context, memberId, roleId) {
|
|
218
|
+
return withLockedLocalTeam(context, async (transactionContext) => {
|
|
219
|
+
const members = await listLocalMembers(transactionContext);
|
|
220
|
+
const target = memberOrThrow(members, memberId);
|
|
221
|
+
if (roleId !== "owner")
|
|
222
|
+
assertActiveOwnerRemains(members, target);
|
|
223
|
+
await transactionContext.db
|
|
224
|
+
.update(userProfilesTable)
|
|
225
|
+
.set({
|
|
226
|
+
isSuperAdmin: roleId === "owner",
|
|
227
|
+
permissions: permissionsForRole(roleId),
|
|
228
|
+
updatedAt: new Date(),
|
|
229
|
+
})
|
|
230
|
+
.where(eq(userProfilesTable.id, memberId));
|
|
231
|
+
return memberOrThrow(await listLocalMembers(transactionContext), memberId);
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
async deactivateMember(context, memberId) {
|
|
235
|
+
return withLockedLocalTeam(context, async (transactionContext) => {
|
|
236
|
+
const members = await listLocalMembers(transactionContext);
|
|
237
|
+
assertActiveOwnerRemains(members, memberOrThrow(members, memberId));
|
|
238
|
+
await transactionContext.db.delete(authSession).where(eq(authSession.userId, memberId));
|
|
239
|
+
await transactionContext.db.delete(apikeyTable).where(eq(apikeyTable.referenceId, memberId));
|
|
240
|
+
const accounts = await transactionContext.db
|
|
241
|
+
.select({ id: authAccount.id, providerId: authAccount.providerId })
|
|
242
|
+
.from(authAccount)
|
|
243
|
+
.where(eq(authAccount.userId, memberId));
|
|
244
|
+
for (const account of accounts) {
|
|
245
|
+
await transactionContext.db
|
|
246
|
+
.update(authAccount)
|
|
247
|
+
.set({ providerId: localProviderIdForStatus(account.providerId, "deactivated") })
|
|
248
|
+
.where(eq(authAccount.id, account.id));
|
|
249
|
+
}
|
|
250
|
+
return memberOrThrow(await listLocalMembers(transactionContext), memberId);
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
async activateMember(context, memberId) {
|
|
254
|
+
return withLockedLocalTeam(context, async (transactionContext) => {
|
|
255
|
+
memberOrThrow(await listLocalMembers(transactionContext), memberId);
|
|
256
|
+
const accounts = await transactionContext.db
|
|
257
|
+
.select({ id: authAccount.id, providerId: authAccount.providerId })
|
|
258
|
+
.from(authAccount)
|
|
259
|
+
.where(eq(authAccount.userId, memberId));
|
|
260
|
+
for (const account of accounts) {
|
|
261
|
+
await transactionContext.db
|
|
262
|
+
.update(authAccount)
|
|
263
|
+
.set({ providerId: localProviderIdForStatus(account.providerId, "active") })
|
|
264
|
+
.where(eq(authAccount.id, account.id));
|
|
265
|
+
}
|
|
266
|
+
const member = memberOrThrow(await listLocalMembers(transactionContext), memberId);
|
|
267
|
+
if (member.status !== "active") {
|
|
268
|
+
throw new TeamManagementError("activation_unavailable", "This local member has no sign-in account to reactivate.", 409);
|
|
269
|
+
}
|
|
270
|
+
return member;
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
roleLevel(roleId) {
|
|
274
|
+
return ROLE_LEVELS[roleId] ?? 0;
|
|
275
|
+
},
|
|
276
|
+
isOwnerRole(roleId) {
|
|
277
|
+
return roleId === "owner";
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export function localRoleForRedeemedInvitation(metadata) {
|
|
282
|
+
const roleId = invitationRole(metadata);
|
|
283
|
+
return { isSuperAdmin: roleId === "owner", permissions: permissionsForRole(roleId) };
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=team-management-local-adapter.js.map
|