import type { Settings } from "@opengeni/config"; import type { ManagedEmailTransport } from "@opengeni/core"; const encoder = new TextEncoder(); export type OrganizationUserSetupEmailTokenTransport = "fragment" | "query"; /** * Prove that the stable invited-user setup bearer can be constructed before an * invitation commits. Provider availability is intentionally outside this * precondition: the durable delivery journal records a failed or ambiguous * transport outcome after the invitation exists. */ export function assertOrganizationUserSetupDeliveryConfigured( settings: Settings, transport: ManagedEmailTransport, ): void { assertOrganizationUserSetupQueryTransportConfigured(settings); requiredSetupSecret(settings); requiredPublicBaseUrl(settings); assertManagedEmailTransportMetadata(transport); } /** Enforce the query-bearing edge proof for env and embedded settings alike. */ export function assertOrganizationUserSetupQueryTransportConfigured( settings: Pick< Settings, | "organizationUserSetupEmailTokenTransport" | "organizationUserSetupQueryEdgeSanitizationConfirmed" >, ): void { if ( settings.organizationUserSetupEmailTokenTransport === "query" && !settings.organizationUserSetupQueryEdgeSanitizationConfirmed ) { throw new Error( "OPENGENI_ORGANIZATION_USER_SETUP_QUERY_EDGE_SANITIZATION_CONFIRMED=true is required when OPENGENI_ORGANIZATION_USER_SETUP_EMAIL_TOKEN_TRANSPORT=query", ); } } /** Reject an invalid embedded-provider contract before any durable boundary. */ export function assertManagedEmailTransportMetadata(transport: ManagedEmailTransport): void { if ( transport.sender.trim() !== transport.sender || encoder.encode(transport.sender).byteLength < 3 || encoder.encode(transport.sender).byteLength > 320 ) { throw new Error("Managed email sender is invalid"); } const { scope, retentionSeconds } = transport.idempotency; if ( scope.trim() !== scope || !/^[a-z0-9][a-z0-9:._-]*$/.test(scope) || encoder.encode(scope).byteLength > 200 || !Number.isInteger(retentionSeconds) || retentionSeconds < 0 || retentionSeconds > 31_536_000 ) { throw new Error("Managed email idempotency contract is invalid"); } } export async function deriveOrganizationUserSetupToken( settings: Settings, input: { invitationId: string; deliveryId: string; transport?: OrganizationUserSetupEmailTokenTransport; }, ): Promise<{ token: string; digest: string; url: string }> { const secret = requiredSetupSecret(settings); const key = await crypto.subtle.importKey( "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const signature = await crypto.subtle.sign( "HMAC", key, encoder.encode( `opengeni:organization-user-setup-delivery:v1:${input.deliveryId}:${input.invitationId}`, ), ); const token = base64Url(new Uint8Array(signature)); const digest = await sha256Hex(token); const url = new URL("/setup-account", requiredPublicBaseUrl(settings)); if ((input.transport ?? settings.organizationUserSetupEmailTokenTransport) === "query") { // Email security gateways routinely wrap links and may discard URL // fragments. Enable this only after the compatibility web rollout; the // browser's first executable head bootstrap scrubs query authority without // redirecting or allowing a query-bearing subresource Referer. url.searchParams.set("token", token); } else { url.hash = new URLSearchParams({ token }).toString(); } return { token, digest, url: url.toString() }; } export type OrganizationUserSetupEmailSnapshot = { senderEmail: string; recipientEmail: string; recipientName: string | null; organizationName: string; organizationRole: "owner" | "admin" | "member"; sharedWorkspaceAccess: Array<{ workspaceId: string; workspaceName: string; role: "viewer" | "member" | "admin"; }>; setupUrl: string; }; export function renderOrganizationUserSetupEmail(input: OrganizationUserSetupEmailSnapshot): { from: string; to: string; subject: string; text: string; html: string; } { const greeting = input.recipientName ? `Hi ${input.recipientName},` : "Hello,"; const role = titleCase(input.organizationRole); const workspaceSummary = input.sharedWorkspaceAccess.length === 0 ? "No shared workspaces are assigned yet." : `Shared workspace access:\n${input.sharedWorkspaceAccess .map((workspace) => `- ${workspace.workspaceName}: ${titleCase(workspace.role)}`) .join("\n")}`; const workspaceHtml = input.sharedWorkspaceAccess.length === 0 ? "
No shared workspaces are assigned yet.
" : `Shared workspace access:
${escapeHtml(greeting)}
You've been invited to join ${escapeHtml(input.organizationName)} on OpenGeni as ${escapeHtml(role)}.
${workspaceHtml}This invitation grants only the organization role and shared workspace access listed above. It never shares anyone's Personal workspace.
Accept invitation to ${escapeHtml(input.organizationName)}
You'll sign in or create an account before joining. Already use OpenGeni? Open this invitation and choose Sign in as ${escapeHtml(input.recipientEmail)}. OpenGeni will show the invitation immediately after you sign in.
`, }; } export async function organizationUserSetupPayloadDigest(input: { from: string; to: string; subject: string; text: string; html: string; providerIdempotencyScope: string; }): Promise