module.exports = [
"[externals]/crypto [external] (crypto, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("crypto", () => require("crypto"));

module.exports = mod;
}),
"[externals]/next/dist/compiled/@opentelemetry/api [external] (next/dist/compiled/@opentelemetry/api, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("next/dist/compiled/@opentelemetry/api", () => require("next/dist/compiled/@opentelemetry/api"));

module.exports = mod;
}),
"[externals]/next/dist/compiled/next-server/app-page-turbo.runtime.dev.js [external] (next/dist/compiled/next-server/app-page-turbo.runtime.dev.js, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("next/dist/compiled/next-server/app-page-turbo.runtime.dev.js", () => require("next/dist/compiled/next-server/app-page-turbo.runtime.dev.js"));

module.exports = mod;
}),
"[externals]/next/dist/compiled/next-server/app-route-turbo.runtime.dev.js [external] (next/dist/compiled/next-server/app-route-turbo.runtime.dev.js, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("next/dist/compiled/next-server/app-route-turbo.runtime.dev.js", () => require("next/dist/compiled/next-server/app-route-turbo.runtime.dev.js"));

module.exports = mod;
}),
"[externals]/next/dist/server/app-render/action-async-storage.external.js [external] (next/dist/server/app-render/action-async-storage.external.js, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("next/dist/server/app-render/action-async-storage.external.js", () => require("next/dist/server/app-render/action-async-storage.external.js"));

module.exports = mod;
}),
"[externals]/next/dist/server/app-render/after-task-async-storage.external.js [external] (next/dist/server/app-render/after-task-async-storage.external.js, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("next/dist/server/app-render/after-task-async-storage.external.js", () => require("next/dist/server/app-render/after-task-async-storage.external.js"));

module.exports = mod;
}),
"[externals]/next/dist/server/app-render/work-async-storage.external.js [external] (next/dist/server/app-render/work-async-storage.external.js, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("next/dist/server/app-render/work-async-storage.external.js", () => require("next/dist/server/app-render/work-async-storage.external.js"));

module.exports = mod;
}),
"[externals]/next/dist/server/app-render/work-unit-async-storage.external.js [external] (next/dist/server/app-render/work-unit-async-storage.external.js, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("next/dist/server/app-render/work-unit-async-storage.external.js", () => require("next/dist/server/app-render/work-unit-async-storage.external.js"));

module.exports = mod;
}),
"[externals]/next/dist/server/runtime-reacts.external.js [external] (next/dist/server/runtime-reacts.external.js, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("next/dist/server/runtime-reacts.external.js", () => require("next/dist/server/runtime-reacts.external.js"));

module.exports = mod;
}),
"[externals]/next/dist/shared/lib/no-fallback-error.external.js [external] (next/dist/shared/lib/no-fallback-error.external.js, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("next/dist/shared/lib/no-fallback-error.external.js", () => require("next/dist/shared/lib/no-fallback-error.external.js"));

module.exports = mod;
}),
"[externals]/node:stream [external] (node:stream, cjs)", ((__turbopack_context__, module, exports) => {

var mod = __turbopack_context__.x("node:stream", () => require("node:stream"));

module.exports = mod;
}),
"[project]/src/app/api/trahs/[trahId]/relationship-finder/route.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

return __turbopack_context__.a(async (__turbopack_handle_async_dependencies__, __turbopack_async_result__) => { try {
__turbopack_context__.s([
    "GET",
    ()=>GET
]);
// GET /api/trahs/[trahId]/relationship-finder?a=[personAId]&b=[personBId] —
// calls genealogy's findRelationship() with the caller's OWN membershipId
// (resolved server-side via getCallerMembership, never client-supplied) as
// the viewerMembershipId. See src/modules/genealogy/findRelationship.ts.
//
// findRelationship() itself re-checks that viewerMembershipId is an ACTIVE
// Membership of trahId and returns FORBIDDEN otherwise — this route maps
// that to 403. A caller with no session/no ACTIVE Membership in THIS Trah at
// all never reaches findRelationship (401), same discipline as every other
// route under src/app/api/trahs/[trahId]/** (see search/route.ts, tree/route.ts).
//
// `a` and `b` (personAId/personBId) are both required. Nonexistent-person
// ids aren't rejected here — findRelationship's own header notes that a
// personId not present in the Trah's graph simply resolves to UNRELATED
// (no distinct PERSON_NOT_FOUND branch in its contract), so this route
// doesn't invent one either.
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$server$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/node_modules/next/server.js [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/lib/prisma.ts [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$genealogy$2f$findRelationship$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/modules/genealogy/findRelationship.ts [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$get$2d$caller$2d$membership$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/lib/get-caller-membership.ts [app-route] (ecmascript)");
var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__([
    __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__,
    __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$get$2d$caller$2d$membership$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__
]);
[__TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$get$2d$caller$2d$membership$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__] = __turbopack_async_dependencies__.then ? (await __turbopack_async_dependencies__)() : __turbopack_async_dependencies__;
;
;
;
;
async function GET(request, { params }) {
    const { trahId } = await params;
    const caller = await (0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$get$2d$caller$2d$membership$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["getCallerMembership"])(trahId);
    if (!caller) {
        return __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$server$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__["NextResponse"].json({
            error: "UNAUTHORIZED"
        }, {
            status: 401
        });
    }
    const url = new URL(request.url);
    const personAId = url.searchParams.get("a");
    const personBId = url.searchParams.get("b");
    if (!personAId || !personBId) {
        return __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$server$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__["NextResponse"].json({
            error: "INVALID_INPUT"
        }, {
            status: 400
        });
    }
    const result = await (0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$genealogy$2f$findRelationship$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["findRelationship"])(__TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["prisma"], trahId, personAId, personBId, caller.membershipId);
    if (!result.ok) {
        return __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$server$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__["NextResponse"].json({
            error: result.error
        }, {
            status: 403
        });
    }
    return __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$server$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__["NextResponse"].json({
        result: result.result
    }, {
        status: 200
    });
}
__turbopack_async_result__();
} catch(e) { __turbopack_async_result__(e); } }, false);}),
"[project]/src/auth.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

return __turbopack_context__.a(async (__turbopack_handle_async_dependencies__, __turbopack_async_result__) => { try {
__turbopack_context__.s([
    "auth",
    ()=>auth,
    "handlers",
    ()=>handlers,
    "signIn",
    ()=>signIn,
    "signOut",
    ()=>signOut
]);
// Auth.js (NextAuth v5) wiring for T-501.
//
// ADR-0004 calls for a Credentials provider with "database session
// strategy" backed by IAM's own users/sessions tables. Auth.js's own
// session-strategy option is more restrictive than that phrasing suggests:
// Auth.js explicitly REJECTS `session.strategy: "database"` when the only
// configured provider is Credentials (see
// node_modules/@auth/core/lib/utils/assert.js: "Signing in with
// credentials only supported if JWT strategy is enabled" /
// UnsupportedStrategy). Auth.js's built-in "database" strategy also
// assumes its own Adapter schema shape (Account/Session/User/
// VerificationToken with specific field names like `expires` rather than
// IAM's `expiresAt`), which doesn't match the schema IAM's contract
// functions already implement and fully own.
//
// Rather than fight that framework constraint or duplicate IAM's session
// logic behind a second, Adapter-shaped implementation, this config keeps
// Auth.js's *transport* (the signed cookie) on the JWT strategy, but
// treats IAM's own `sessionToken` (from login()/logout()/verifySession())
// as the actual source of truth for whether a session is valid:
//   - `authorize()` calls IAM's login(); on success, the sessionToken IAM
//     issued is embedded in the JWT payload (not a Auth.js-invented id).
//   - the `jwt` callback re-validates that sessionToken against the DB via
//     IAM's verifySession() on every subsequent request, so a session
//     revoked by logout() or confirmPasswordReset() (which deletes
//     Session rows) stops working immediately — genuine server-side
//     revocation, not just "valid until the JWT's own expiry."
//   - the `signOut` event calls IAM's logout() so the DB row is actually
//     removed, not just orphaned until it naturally expires.
//
// This satisfies the ADR's actual intent (revocable, DB-backed sessions
// reusing IAM's own tables/contract functions) within Auth.js's
// documented Credentials-provider constraint. Documented here since it's
// a deliberate deviation from the ADR's literal wording, not an oversight.
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2d$auth$2f$index$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__$3c$locals$3e$__ = __turbopack_context__.i("[project]/node_modules/next-auth/index.js [app-route] (ecmascript) <locals>");
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2d$auth$2f$providers$2f$credentials$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__$3c$locals$3e$__ = __turbopack_context__.i("[project]/node_modules/next-auth/providers/credentials.js [app-route] (ecmascript) <locals>");
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f40$auth$2f$core$2f$providers$2f$credentials$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/node_modules/@auth/core/providers/credentials.js [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/lib/prisma.ts [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$iam$2f$login$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/modules/iam/login.ts [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$iam$2f$logout$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/modules/iam/logout.ts [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$iam$2f$verifySession$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/modules/iam/verifySession.ts [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$rate$2d$limit$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/lib/rate-limit.ts [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$client$2d$ip$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/lib/client-ip.ts [app-route] (ecmascript)");
var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__([
    __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__
]);
[__TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__] = __turbopack_async_dependencies__.then ? (await __turbopack_async_dependencies__)() : __turbopack_async_dependencies__;
;
;
;
;
;
;
;
;
function parseIdentifier(raw) {
    if (typeof raw !== "string" || raw.trim().length === 0) return null;
    const type = raw.includes("@") ? "EMAIL" : "PHONE";
    return {
        type,
        value: raw
    };
}
// Security fix (T-501 review, HIGH): login() had zero throttling —
// this is the primary, most-targeted entry point for credential-stuffing/
// password-brute-force bots, and login.ts's own timing-parity defense
// only prevents user-enumeration, not unlimited online guessing. Same
// class of protection as reset/confirm's limiter (see rate-limit.ts's
// doc comment for the accepted single-instance/V1 scope), a more
// generous window since legitimate users mistype passwords far more
// often than they need 6-digit codes.
const LOGIN_LIMIT = 10;
const LOGIN_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
const loginLimiter = new __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$rate$2d$limit$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["FixedWindowRateLimiter"](LOGIN_LIMIT, LOGIN_WINDOW_MS);
const { handlers, auth, signIn, signOut } = (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2d$auth$2f$index$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__$3c$locals$3e$__["default"])({
    session: {
        strategy: "jwt"
    },
    pages: {
        signIn: "/login"
    },
    providers: [
        (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f40$auth$2f$core$2f$providers$2f$credentials$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__["default"])({
            credentials: {
                identifier: {
                    label: "Email or phone",
                    type: "text"
                },
                password: {
                    label: "Password",
                    type: "password"
                }
            },
            async authorize (credentials, request) {
                const identifier = parseIdentifier(credentials?.identifier);
                const password = typeof credentials?.password === "string" ? credentials.password : "";
                if (!identifier || !password) return null;
                // Rate-limit BEFORE calling login(), so a throttled attempt never
                // even reaches bcrypt.compare — keyed by identifier+IP, same
                // spirit as reset/confirm's limiter (identifier alone would let
                // one attacker lock out a victim; IP alone would let a botnet
                // spread guesses across addresses).
                const rateLimitKey = `${identifier.type}:${identifier.value.trim().toLowerCase()}|${(0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$client$2d$ip$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["clientIp"])(request)}`;
                const { allowed } = loginLimiter.check(rateLimitKey);
                if (!allowed) return null;
                const result = await (0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$iam$2f$login$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["login"])(__TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["prisma"], identifier, password);
                if (!result.ok) return null;
                // Everything after `id` is a custom field carried through to the
                // `jwt` callback's `user` argument on initial sign-in only.
                return {
                    id: result.userId,
                    sessionToken: result.sessionToken
                };
            }
        })
    ],
    callbacks: {
        async jwt ({ token, user }) {
            if (user) {
                // Initial sign-in: `user` is what authorize() returned above.
                token.sessionToken = user.sessionToken;
                token.userId = user.id;
                return token;
            }
            // Subsequent requests: re-check IAM's own session store so that
            // logout()/confirmPasswordReset() (both of which delete Session
            // rows) take effect immediately, not just once the JWT expires.
            if (typeof token.sessionToken === "string") {
                const userId = await (0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$iam$2f$verifySession$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["verifySession"])(__TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["prisma"], token.sessionToken);
                if (!userId) {
                    // Session was revoked server-side — strip credentials so the
                    // `session` callback below reports "signed out."
                    delete token.sessionToken;
                    delete token.userId;
                }
            }
            return token;
        },
        async session ({ session, token }) {
            if (typeof token.userId === "string") {
                session.user.id = token.userId;
            } else {
                // No valid IAM session behind this JWT (revoked or never set) —
                // surface as unauthenticated rather than a half-populated user.
                return {
                    ...session,
                    user: undefined,
                    expires: session.expires
                };
            }
            return session;
        }
    },
    events: {
        async signOut (message) {
            // With the "jwt" strategy, Auth.js's signOut event passes `{ token }`
            // rather than `{ session }`. Clean up the underlying IAM Session row
            // so it doesn't linger until its natural expiry.
            if ("token" in message && typeof message.token?.sessionToken === "string") {
                await (0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$iam$2f$logout$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["logout"])(__TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["prisma"], message.token.sessionToken);
            }
        }
    }
});
__turbopack_async_result__();
} catch(e) { __turbopack_async_result__(e); } }, false);}),
"[project]/src/lib/client-ip.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

// Shared client-IP extraction for rate-limiting purposes.
//
// Security fix (T-501 review, HIGH): a naive implementation that reads
// the FIRST entry of X-Forwarded-For is trivially spoofable — proxies
// (including Vercel) APPEND the real connecting client's address to the
// end of the list; the first entry is whatever the client itself sent in
// its own request and is fully attacker-controlled. Reading the LAST
// entry instead reflects the address the immediate, trusted proxy hop
// actually observed, which a client cannot forge (they can prepend fake
// entries, but not remove or rewrite the one their own connecting proxy
// appends).
//
// This is still not a perfect IP for a multi-hop/CDN setup (a compromised
// or misconfigured intermediate proxy could still lie), but it is not
// attacker-forgeable in the common single-reverse-proxy deployment this
// app targets (Vercel, per ADR-0003) the way "first entry" is.
__turbopack_context__.s([
    "clientIp",
    ()=>clientIp
]);
function clientIp(request) {
    const forwarded = request.headers.get("x-forwarded-for");
    if (!forwarded) return "unknown";
    const hops = forwarded.split(",").map((h)=>h.trim()).filter(Boolean);
    return hops.length > 0 ? hops[hops.length - 1] : "unknown";
}
}),
"[project]/src/lib/get-caller-membership.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

return __turbopack_context__.a(async (__turbopack_handle_async_dependencies__, __turbopack_async_result__) => { try {
__turbopack_context__.s([
    "getCallerMembership",
    ()=>getCallerMembership,
    "getCallerUserId",
    ()=>getCallerUserId
]);
// T-502 shared helper — resolves "the current session's userId, for a
// given trahId, to their membershipId" so every Route Handler under
// src/app/api/trahs/** (and later tickets that need the same pattern)
// does this exactly once, the same way.
//
// SECURITY: tier-a's changeRole/removeMember/invite contract functions
// take an `actingMembershipId` as an *input*, trusting the caller to have
// already verified it belongs to the authenticated user (see
// getMembership.ts's SECURITY NOTE — that lookup does no authorization of
// its own). This helper is the one place that trust boundary is enforced:
// it never accepts a client-supplied membershipId as "the actor" — it
// always derives it server-side from Auth.js's session + the trahId in
// the URL, via findMembershipByUserAndTrah's INV-TM-5-backed compound-key
// lookup (same discipline as getRole.ts). Route handlers should use this
// for the ACTOR only; a client-supplied membershipId may still be used as
// the TARGET of an action (e.g. which member to remove/change).
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$auth$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/auth.ts [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/lib/prisma.ts [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$trah$2d$membership$2f$findMembershipByUserAndTrah$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/modules/trah-membership/findMembershipByUserAndTrah.ts [app-route] (ecmascript)");
var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__([
    __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$auth$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__,
    __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__
]);
[__TURBOPACK__imported__module__$5b$project$5d2f$src$2f$auth$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__] = __turbopack_async_dependencies__.then ? (await __turbopack_async_dependencies__)() : __turbopack_async_dependencies__;
;
;
;
async function getCallerUserId() {
    const session = await (0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$auth$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["auth"])();
    const userId = session?.user?.id;
    return typeof userId === "string" ? userId : null;
}
async function getCallerMembership(trahId) {
    const userId = await getCallerUserId();
    if (!userId) return null;
    const membership = await (0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$trah$2d$membership$2f$findMembershipByUserAndTrah$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["findMembershipByUserAndTrah"])(__TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["prisma"], trahId, userId);
    if (!membership || membership.status !== "ACTIVE") return null;
    return {
        userId,
        membershipId: membership.id
    };
}
__turbopack_async_result__();
} catch(e) { __turbopack_async_result__(e); } }, false);}),
"[project]/src/lib/prisma.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

return __turbopack_context__.a(async (__turbopack_handle_async_dependencies__, __turbopack_async_result__) => { try {
__turbopack_context__.s([
    "prisma",
    ()=>prisma
]);
// Standard Next.js + Prisma singleton pattern: in dev, Next.js hot-reloads
// modules on every file change, which would otherwise create a fresh
// PrismaClient (and a fresh DB connection pool) on every reload and
// eventually exhaust Postgres's connection limit. Stashing the instance on
// `globalThis` (which survives hot-reload, unlike module-local state)
// avoids that. In production there's only ever one module load, so this
// is a no-op there beyond the initial construction.
//
// Queries run through @prisma/adapter-pg (a plain-JS `pg` Pool) rather than
// Prisma's default Rust query engine — the Rust engine's Tokio runtime
// spawns one OS worker thread per reported CPU core, which exceeds the
// process/thread ceiling on constrained shared hosting and crashes with
// "OS can't spawn worker thread: Resource temporarily unavailable" on the
// very first request. The adapter has no native multi-threaded runtime.
var __TURBOPACK__imported__module__$5b$externals$5d2f40$prisma$2f$client__$5b$external$5d$__$2840$prisma$2f$client$2c$__cjs$2c$__$5b$project$5d2f$node_modules$2f40$prisma$2f$client$29$__ = __turbopack_context__.i("[externals]/@prisma/client [external] (@prisma/client, cjs, [project]/node_modules/@prisma/client)");
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f40$prisma$2f$adapter$2d$pg$2f$dist$2f$index$2e$mjs__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/node_modules/@prisma/adapter-pg/dist/index.mjs [app-route] (ecmascript)");
var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__([
    __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f40$prisma$2f$adapter$2d$pg$2f$dist$2f$index$2e$mjs__$5b$app$2d$route$5d$__$28$ecmascript$29$__
]);
[__TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f40$prisma$2f$adapter$2d$pg$2f$dist$2f$index$2e$mjs__$5b$app$2d$route$5d$__$28$ecmascript$29$__] = __turbopack_async_dependencies__.then ? (await __turbopack_async_dependencies__)() : __turbopack_async_dependencies__;
;
;
const globalForPrisma = globalThis;
const adapter = new __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f40$prisma$2f$adapter$2d$pg$2f$dist$2f$index$2e$mjs__$5b$app$2d$route$5d$__$28$ecmascript$29$__["PrismaPg"]({
    connectionString: process.env.DATABASE_URL
});
const prisma = globalForPrisma.prisma ?? new __TURBOPACK__imported__module__$5b$externals$5d2f40$prisma$2f$client__$5b$external$5d$__$2840$prisma$2f$client$2c$__cjs$2c$__$5b$project$5d2f$node_modules$2f40$prisma$2f$client$29$__["PrismaClient"]({
    adapter
});
if ("TURBOPACK compile-time truthy", 1) {
    globalForPrisma.prisma = prisma;
}
__turbopack_async_result__();
} catch(e) { __turbopack_async_result__(e); } }, false);}),
"[project]/src/lib/rate-limit.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

// Simple in-memory fixed-window rate limiter.
//
// Scope: V1-appropriate protection for a low-traffic single-instance app
// (per WORKPLAN.md's environment notes, this deploys to a small number of
// serverless instances, not a large fleet) — NOT a distributed limiter.
// It resets if the process restarts and does not coordinate across
// multiple server instances. That's an accepted tradeoff for V1: the
// spec's must-do is "have SOME rate limiting on reset/confirm, a handful
// of attempts per 15-minute window," not "build a production-grade
// distributed limiter." A Redis-backed limiter would be a natural
// follow-up if this app ever runs on more than one instance.
//
// Extracted as a pure class (no Next.js/Request coupling) so it's testable
// in isolation without spinning up a route handler.
__turbopack_context__.s([
    "FixedWindowRateLimiter",
    ()=>FixedWindowRateLimiter
]);
class FixedWindowRateLimiter {
    limit;
    windowMs;
    hits;
    constructor(limit, windowMs){
        this.limit = limit;
        this.windowMs = windowMs;
        this.hits = new Map();
    }
    /**
   * Records an attempt for `key` and reports whether it's allowed under the
   * limit. Pass `now` explicitly in tests to control window boundaries.
   */ check(key, now = Date.now()) {
        const existing = this.hits.get(key);
        if (!existing || now - existing.windowStart >= this.windowMs) {
            // New window (first attempt ever for this key, or the previous
            // window has fully elapsed).
            this.hits.set(key, {
                count: 1,
                windowStart: now
            });
            return {
                allowed: true,
                remaining: this.limit - 1
            };
        }
        if (existing.count >= this.limit) {
            return {
                allowed: false,
                remaining: 0
            };
        }
        existing.count += 1;
        return {
            allowed: true,
            remaining: this.limit - existing.count
        };
    }
    /** Test/diagnostic helper — not used by route handlers. */ reset() {
        this.hits.clear();
    }
}
}),
"[project]/src/modules/genealogy/findRelationship.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

// T-304 (standard lane) — Genealogy findRelationship() contract function.
// See spec/genealogy/tier-a-contract.md:
//   findRelationship(trahId, personAId, personBId, viewerMembershipId)
//     -> { label: string, path: PersonSummary[] } | { label: "UNRELATED" }
// and spec/genealogy/tier-b-implementation.md's "Key algorithm: kinship
// computation" section (REQ-REL-005, REQ-SRCH-002).
//
// This function is the DB-touching wrapper only: load the Trah's
// Relationship + Person rows (scoped by trahId, INV-GEN-7), check the
// viewer's read permission, then delegate to the pure computeKinship()
// in kinship.ts (unit-tested independently there, per tier-a's
// "Isolation / testability note").
//
// Read-permission check: viewerMembershipId must resolve to an ACTIVE
// Membership in trahId (same requirement as tier-b's searchByName —
// "enforces REQ-FAM-005, search never crosses a Trah boundary"). Returns
// a typed FORBIDDEN error (tier-a-contract.md revised as part of T-304 to
// add this branch) rather than throwing or silently returning UNRELATED
// — UNRELATED is a real, meaningful answer the algorithm can legitimately
// produce, and conflating "not related" with "not allowed to ask" would
// hide an access-control bug behind a plausible-looking result. Matches
// every other read/write in this module returning a typed Result.
//
// Cross-tenant scoping (INV-GEN-7): both the Relationship rows and the
// Person rows fed into computeKinship() are loaded with `where: { trahId }`
// — a relationship or person belonging to a different Trah can never enter
// the in-memory graph, regardless of the personAId/personBId values passed
// in.
//
// Nonexistent-person inputs: tier-a's findRelationship has no
// PERSON_NOT_FOUND branch either (unlike createRelationship). A
// personAId/personBId that doesn't exist in this Trah simply won't appear
// in the loaded Person/Relationship graph, so computeKinship() naturally
// resolves it to UNRELATED (no ancestors, no partner, no sibling edge)
// rather than a distinct error — consistent with the signature as given.
__turbopack_context__.s([
    "findRelationship",
    ()=>findRelationship
]);
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$trah$2d$membership$2f$getMembership$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/modules/trah-membership/getMembership.ts [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$genealogy$2f$kinship$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/modules/genealogy/kinship.ts [app-route] (ecmascript)");
;
;
async function findRelationship(prisma, trahId, personAId, personBId, viewerMembershipId) {
    // Read-permission check: viewer must be an ACTIVE member of this trahId
    // (mirrors tier-b's stated requirement for searchByName — see header note).
    const membership = await (0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$trah$2d$membership$2f$getMembership$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["getMembership"])(prisma, viewerMembershipId);
    if (!membership || membership.trahId !== trahId || membership.status !== "ACTIVE") {
        return {
            ok: false,
            error: "FORBIDDEN"
        };
    }
    const [relationships, persons] = await Promise.all([
        prisma.relationship.findMany({
            where: {
                trahId
            }
        }),
        prisma.person.findMany({
            where: {
                trahId
            }
        })
    ]);
    const edges = relationships.map((r)=>({
            personAId: r.personAId,
            personBId: r.personBId,
            type: r.type,
            partnerStatus: r.partnerStatus
        }));
    const personSummaries = persons.map((p)=>({
            id: p.id,
            displayName: p.displayName
        }));
    const result = (0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$genealogy$2f$kinship$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["computeKinship"])(edges, personSummaries, personAId, personBId);
    return {
        ok: true,
        result
    };
}
}),
"[project]/src/modules/genealogy/kinship.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

// T-304 (standard lane) — pure kinship-computation algorithm.
// See spec/genealogy/tier-a-contract.md's findRelationship entry and
// spec/genealogy/tier-b-implementation.md's "Key algorithm: kinship
// computation (REQ-REL-005, REQ-SRCH-002)" section (the "nearest common
// ancestor, generation-distance" method), plus ADR-0002's Decision section
// (kinship computed in application code, not SQL, because the label-
// mapping logic is branchy and needs unit tests per blueprint §6's case,
// and a Trah's graph is small enough to load fully into memory).
//
// This file is DB-free by design (tier-a's "Isolation / testability
// note"): it operates purely on an in-memory array of Relationship-like
// edges plus a lookup of PersonSummary, no Prisma import here at all. The
// DB-touching wrapper (loads a Trah's relationships/persons, then
// delegates here) lives in findRelationship.ts.
//
// --- Algorithm, matching tier-b's numbered steps -------------------------
// 1. Build parentsOf/partnersOf adjacency from the edge list. tier-b also
//    lists childrenOf, but that's only needed by getTree (not in this
//    ticket's scope — T-304 covers findRelationship only), so it's
//    omitted here to avoid building unused structures.
// 2. ancestorsOf(personId): BFS upward through parentsOf, returning
//    otherPersonId -> { distance, ... }, personId itself included at
//    distance 0 (so direct-line cases share the same code path as
//    collateral ones, exactly as tier-b step 2 specifies).
// 3. findRelationship(A,B): intersect ancestorsOf(A)/ancestorsOf(B), pick
//    the common ancestor minimizing distA+distB, map (distA,distB) to a
//    label per tier-b step 3d, apply the qualifier overlay (step 3e), and
//    build the path. If no common ancestor, fall back to a direct PARTNER
//    edge (step 3b) before giving up as UNRELATED.
//
// --- Judgment calls (flagged for reviewer) --------------------------------
//
// 1. Direct SIBLING edges without a shared parent on record: tier-b's
//    numbered algorithm (step 3b) only names `partnersOf` as the fallback
//    checked before UNRELATED — it does not mention `SIBLING` edges there
//    at all. But tier-b's separate "Full vs. half-sibling" note (§5) says
//    "SIBLING and the (1,1) blood-line case both return the flat label
//    'sibling'," which only makes sense if a directly-recorded SIBLING
//    edge is ALSO a first-class path to the "sibling" label, independent
//    of any shared-parent inference. Reconciling these two passages: this
//    implementation adds a `siblingsOf` adjacency (parallel to
//    `partnersOf`) and checks it in the same fallback step, so a SIBLING
//    edge with no recorded common parent still resolves to "sibling"
//    instead of incorrectly falling through to UNRELATED. Flagged as an
//    inference from §5, not a literal reading of step 3's numbered list.
//
// 2. Qualifier overlay scope: tier-b step 3e's own text gives an example
//    ("adoptive grandfather") that appears to describe a 2-hop chain, but
//    its own closing sentence is explicit and more specific: "this
//    qualifier is applied only for the direct 1-hop case required by
//    REQ-REL-004; multi-hop chains with mixed subtypes return the
//    unqualified label." This implementation follows the closing
//    sentence literally (the more specific, restriction-stating text) —
//    so ONLY the following get a qualifier prefix: a direct parent/child
//    edge (distA+distB = 1) and a sibling pair (distA=distB=1, i.e. each
//    side's own single hop to the common ancestor). Grandparent/
//    grandchild, aunt/uncle, and cousin labels (any case where a hop
//    count > 1 contributes to the path) are always returned unqualified,
//    even if every edge along that longer chain happens to be the same
//    non-biological subtype. This resolves this ticket's own test
//    question (see kinship.test.ts) of whether "adoptive grandparent" is
//    ever produced — per this reading, it is not, in V1.
//
// 3. PARTNER label wording: tier-b only says "label: 'partner'/'spouse'"
//    without pinning which `partnerStatus` maps to which word. This
//    implementation uses "spouse" for MARRIED/WIDOWED (an active or
//    formerly-active marriage) and "partner" for DIVORCED/ENDED/no status
//    — a defensible reading, not stated outright in tier-b.
//
// 4. Tie-break among multiple nearest common ancestors (same minimal
//    distA+distB): not specified by tier-b. Resolved deterministically by
//    ancestor id (lexicographic) so results are stable/reproducible,
//    rather than depending on Map iteration order.
//
// 5. Cousin/aunt-uncle generalization beyond tier-b's explicit table
//    (distA=1 paired with distB>=3, e.g. "great-aunt/uncle"): tier-b's
//    table only enumerates (1,2)/(2,1) for aunt/uncle and (m,n) both>=2
//    for cousins — it does not give a rule for one side = 1 and the other
//    side >= 3. This implementation extends the same "grand-" ladder used
//    for direct ancestor/descendant labels (e.g. distA=1,distB=3 ->
//    "great-niece/nephew") for completeness/no-undefined-output, but this
//    is an extrapolation beyond tier-b's literal text, not a case any
//    REQ-ID or blueprint §6 scenario exercises.
__turbopack_context__.s([
    "computeKinship",
    ()=>computeKinship
]);
const PARENT_CHILD_TYPES = new Set([
    "BIOLOGICAL_PARENT_CHILD",
    "ADOPTIVE_PARENT_CHILD",
    "STEP_PARENT_CHILD"
]);
function buildParentsOf(edges) {
    const parentsOf = new Map();
    for (const edge of edges){
        if (!PARENT_CHILD_TYPES.has(edge.type)) continue;
        // Directionality convention (tier-b): personAId = parent, personBId = child.
        const list = parentsOf.get(edge.personBId) ?? [];
        list.push({
            parentId: edge.personAId,
            subtype: edge.type
        });
        parentsOf.set(edge.personBId, list);
    }
    return parentsOf;
}
function buildPartnersOf(edges) {
    const partnersOf = new Map();
    const add = (from, to, status)=>{
        const list = partnersOf.get(from) ?? [];
        list.push({
            partnerId: to,
            status
        });
        partnersOf.set(from, list);
    };
    for (const edge of edges){
        if (edge.type !== "PARTNER") continue;
        add(edge.personAId, edge.personBId, edge.partnerStatus ?? null);
        add(edge.personBId, edge.personAId, edge.partnerStatus ?? null);
    }
    return partnersOf;
}
function buildSiblingsOf(edges) {
    // Judgment call #1 above: direct SIBLING edges, independent of any
    // shared-parent inference.
    const siblingsOf = new Map();
    const add = (from, to)=>{
        const set = siblingsOf.get(from) ?? new Set();
        set.add(to);
        siblingsOf.set(from, set);
    };
    for (const edge of edges){
        if (edge.type !== "SIBLING") continue;
        add(edge.personAId, edge.personBId);
        add(edge.personBId, edge.personAId);
    }
    return siblingsOf;
}
function ancestorsOf(startId, parentsOf) {
    const result = new Map();
    result.set(startId, {
        distance: 0,
        nearHopSubtype: null,
        chain: [
            startId
        ]
    });
    let frontier = [
        startId
    ];
    while(frontier.length > 0){
        const next = [];
        for (const personId of frontier){
            const state = result.get(personId);
            const parents = parentsOf.get(personId) ?? [];
            for (const { parentId, subtype } of parents){
                if (result.has(parentId)) continue; // BFS: first visit = shortest distance
                const nearHopSubtype = state.distance === 0 ? subtype : state.nearHopSubtype;
                result.set(parentId, {
                    distance: state.distance + 1,
                    nearHopSubtype,
                    chain: [
                        ...state.chain,
                        parentId
                    ]
                });
                next.push(parentId);
            }
        }
        frontier = next;
    }
    return result;
}
function ordinal(n) {
    const mod100 = n % 100;
    if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
    switch(n % 10){
        case 1:
            return `${n}st`;
        case 2:
            return `${n}nd`;
        case 3:
            return `${n}rd`;
        default:
            return `${n}th`;
    }
}
// n generations of descent: 1 -> child, 2 -> grandchild, 3 -> great-grandchild, ...
// (symmetric for noun="parent": 1 -> parent, 2 -> grandparent, ...)
function descentLabel(n, noun) {
    if (n === 1) return noun;
    if (n === 2) return `grand${noun}`;
    return `${"great-".repeat(n - 2)}grand${noun}`;
}
function auntUncleLadder(generationGap, direction) {
    // generationGap = 1 -> aunt/uncle or niece/nephew.
    // generationGap >= 2 -> "great-" prefixed (judgment call #5 above).
    const base = direction === "up" ? "aunt/uncle" : "niece/nephew";
    if (generationGap === 1) return base;
    return `${"great-".repeat(generationGap - 1)}${base}`;
}
function timesRemovedWord(n) {
    if (n === 1) return "once removed";
    if (n === 2) return "twice removed";
    return `${n} times removed`;
}
function baseLabelFor(distA, distB) {
    if (distA === 0 && distB === 0) return "self";
    if (distA === 0) return descentLabel(distB, "child");
    if (distB === 0) return descentLabel(distA, "parent");
    if (distA === 1 && distB === 1) return "sibling";
    const min = Math.min(distA, distB);
    const max = Math.max(distA, distB);
    if (min === 1) {
        // Label describes "what B is to A" (see descentLabel's distB=0/distA=0
        // convention above, confirmed by the blueprint §6 grandparent
        // acceptance case). If A is the side closer to the common ancestor
        // (distA < distB), A is the elder generation relative to B, so B —
        // the further/younger side — is A's niece/nephew. If B is the closer
        // side (distB < distA), B is the elder relative to A, so B is A's
        // aunt/uncle. Per tier-b's (1,2)/(2,1) rule, extended per judgment
        // call #5 for gaps > 1.
        const gap = max - 1;
        return distA < distB ? auntUncleLadder(gap, "down") : auntUncleLadder(gap, "up");
    }
    // Both sides >= 2: cousin, possibly "removed".
    const cousinDegree = min - 1;
    if (distA === distB) {
        return `${ordinal(cousinDegree)} cousin`;
    }
    const removed = Math.abs(distA - distB);
    return `${ordinal(cousinDegree)} cousin ${timesRemovedWord(removed)}`;
}
function qualifierPrefix(subtype) {
    if (subtype === "ADOPTIVE_PARENT_CHILD") return "adoptive ";
    if (subtype === "STEP_PARENT_CHILD") return "step-";
    return "";
}
// Judgment call #2 above: qualifier overlay applies ONLY to the direct
// parent/child case (distA+distB === 1) and the sibling case
// (distA === distB === 1). Every other shape returns the unqualified base
// label, regardless of the subtypes along the (longer) path.
function applyQualifier(base, distA, distB, nearHopA, nearHopB) {
    const isDirectParentChild = distA + distB === 1;
    const isSibling = distA === 1 && distB === 1;
    if (!isDirectParentChild && !isSibling) return base;
    if (isDirectParentChild) {
        // Exactly one of nearHopA/nearHopB is set (whichever side has
        // distance 1 — the other side, at distance 0, is the ancestor itself
        // and has no incoming hop of its own to qualify).
        const subtype = distA === 1 ? nearHopA : nearHopB;
        const prefix = qualifierPrefix(subtype);
        return prefix ? `${prefix}${base}` : base;
    }
    // Sibling: qualify from whichever side is non-biological, adoptive
    // taking precedence over step if (unusually) both sides differ.
    const subtype = nearHopA === "ADOPTIVE_PARENT_CHILD" || nearHopB === "ADOPTIVE_PARENT_CHILD" ? "ADOPTIVE_PARENT_CHILD" : nearHopA === "STEP_PARENT_CHILD" || nearHopB === "STEP_PARENT_CHILD" ? "STEP_PARENT_CHILD" : null;
    const prefix = qualifierPrefix(subtype);
    // "step-sibling" (hyphen, no extra space) / "adoptive sibling" (space)
    // per tier-b's own example and qualifierPrefix's convention.
    return prefix ? `${prefix}${base}` : base;
}
function toPersonSummary(personId, persons) {
    return persons.get(personId) ?? {
        id: personId,
        displayName: personId
    };
}
function buildPath(chainA, chainB, persons) {
    // chainA: [A, ..., ancestor]. chainB: [B, ..., ancestor].
    const reversedB = [
        ...chainB
    ].reverse(); // [ancestor, ..., B]
    const combined = [
        ...chainA,
        ...reversedB.slice(1)
    ];
    return combined.map((id)=>toPersonSummary(id, persons));
}
function computeKinship(edges, personsInput, personAId, personBId) {
    const persons = personsInput instanceof Map ? personsInput : new Map(personsInput.map((p)=>[
            p.id,
            p
        ]));
    const parentsOf = buildParentsOf(edges);
    const partnersOf = buildPartnersOf(edges);
    const siblingsOf = buildSiblingsOf(edges);
    const ancA = ancestorsOf(personAId, parentsOf);
    const ancB = ancestorsOf(personBId, parentsOf);
    // Review fix (T-304 finding #1): with pedigree collapse (the same two
    // people connected through more than one common ancestor — expected in
    // this domain, e.g. repeated cousin marriage across generations, per
    // blueprint §6's own Ali+Fatimah cousin-marriage case), multiple
    // ancestors can tie on total distance (distA+distB) while producing
    // genealogically DIFFERENT labels (e.g. "1st cousin" (2,2) vs
    // "great-niece/nephew" (2,4) both summing to 4). Total distance alone
    // is not sufficient to pick a genealogically meaningful "nearest"
    // ancestor. Tie-break by the smaller max(distA, distB) — the more
    // balanced split is the closer, more immediate relationship (this is
    // the standard reading behind "Nth cousin, M times removed": N is
    // driven by the SHORTER side, and a smaller max distance means neither
    // side has to reach as far up the tree). Only falls back to
    // lexicographic ancestorId ordering for full determinism when even
    // that ties (e.g. truly symmetric structures), which no longer
    // reflects a genealogical difference at that point.
    let best = null;
    for (const [ancestorId, stateA] of ancA){
        const stateB = ancB.get(ancestorId);
        if (!stateB) continue;
        const total = stateA.distance + stateB.distance;
        const max = Math.max(stateA.distance, stateB.distance);
        const bestMax = best ? Math.max(best.distA, best.distB) : Infinity;
        if (!best || total < best.total || total === best.total && max < bestMax || total === best.total && max === bestMax && ancestorId < best.ancestorId) {
            best = {
                ancestorId,
                distA: stateA.distance,
                distB: stateB.distance,
                total
            };
        }
    }
    if (best) {
        const stateA = ancA.get(best.ancestorId);
        const stateB = ancB.get(best.ancestorId);
        const base = baseLabelFor(best.distA, best.distB);
        const label = applyQualifier(base, best.distA, best.distB, stateA.nearHopSubtype, stateB.nearHopSubtype);
        const path = buildPath(stateA.chain, stateB.chain, persons);
        return {
            label,
            path
        };
    }
    // No blood-line path — check for a direct PARTNER edge (tier-b step 3b).
    const partnerEdge = (partnersOf.get(personAId) ?? []).find((p)=>p.partnerId === personBId);
    if (partnerEdge) {
        const label = partnerEdge.status === "MARRIED" || partnerEdge.status === "WIDOWED" ? "spouse" : "partner";
        return {
            label,
            path: [
                toPersonSummary(personAId, persons),
                toPersonSummary(personBId, persons)
            ]
        };
    }
    // Direct SIBLING edge with no recorded common parent (judgment call #1).
    if ((siblingsOf.get(personAId) ?? new Set()).has(personBId)) {
        return {
            label: "sibling",
            path: [
                toPersonSummary(personAId, persons),
                toPersonSummary(personBId, persons)
            ]
        };
    }
    return {
        label: "UNRELATED"
    };
}
}),
"[project]/src/modules/iam/identifier.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

// Shared identifier type + normalization for IAM's email-or-phone login
// scheme. Extracted so register.ts and login.ts can't silently drift
// (review follow-up from T-102).
__turbopack_context__.s([
    "normalizeIdentifier",
    ()=>normalizeIdentifier
]);
function normalizeIdentifier(identifier) {
    const trimmed = identifier.value.trim();
    return identifier.type === "EMAIL" ? trimmed.toLowerCase() : trimmed;
}
}),
"[project]/src/modules/iam/login.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

__turbopack_context__.s([
    "login",
    ()=>login
]);
// T-102 — IAM login() contract function.
// See spec/iam/tier-a-contract.md (signature, INV-IAM-4) and
// spec/iam/tier-b-implementation.md (schema, session expiry, "same generic
// error for unknown identifier vs wrong password" edge case).
var __TURBOPACK__imported__module__$5b$externals$5d2f$crypto__$5b$external$5d$__$28$crypto$2c$__cjs$29$__ = __turbopack_context__.i("[externals]/crypto [external] (crypto, cjs)");
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$bcryptjs$2f$index$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/node_modules/bcryptjs/index.js [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$iam$2f$identifier$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/src/modules/iam/identifier.ts [app-route] (ecmascript)");
;
;
;
const SESSION_LIFETIME_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
const SESSION_TOKEN_BYTES = 32; // 32 bytes -> 64 hex chars, well over the 32-char floor
// Dummy hash to compare against when the identifier doesn't match a User,
// so a failed lookup still pays the bcrypt.compare cost — mitigates
// timing-based user enumeration (same code path either way).
const DUMMY_HASH_FOR_TIMING_PARITY = "$2a$10$CwTycUXWue0Thq9StjUM0uJ8i1Ax/OhpFrOTdmKGKQ8pC/S8v7B9m";
async function login(prisma, identifier, password) {
    const value = (0, __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$modules$2f$iam$2f$identifier$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["normalizeIdentifier"])(identifier);
    // Same field-scoped lookup pattern as register.ts: email checked
    // against email, phone against phone — never a combined OR.
    const user = identifier.type === "EMAIL" ? await prisma.user.findFirst({
        where: {
            email: value
        }
    }) : await prisma.user.findFirst({
        where: {
            phone: value
        }
    });
    const passwordMatches = await __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$bcryptjs$2f$index$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__["default"].compare(password, user?.passwordHash ?? DUMMY_HASH_FOR_TIMING_PARITY);
    if (!user || !passwordMatches) {
        return {
            ok: false,
            error: "INVALID_CREDENTIALS"
        };
    }
    const sessionToken = __TURBOPACK__imported__module__$5b$externals$5d2f$crypto__$5b$external$5d$__$28$crypto$2c$__cjs$29$__["default"].randomBytes(SESSION_TOKEN_BYTES).toString("hex");
    const expiresAt = new Date(Date.now() + SESSION_LIFETIME_MS);
    await prisma.session.create({
        data: {
            sessionToken,
            userId: user.id,
            expiresAt
        }
    });
    return {
        ok: true,
        sessionToken,
        userId: user.id
    };
}
}),
"[project]/src/modules/iam/logout.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

// T-102 — IAM logout() contract function.
// See spec/iam/tier-a-contract.md (signature, INV-IAM-4).
__turbopack_context__.s([
    "logout",
    ()=>logout
]);
async function logout(prisma, sessionToken) {
    // Judgment call (Tier B doesn't specify delete vs. expire-flag): delete
    // the row — simplest reading that satisfies INV-IAM-4 (a subsequent
    // verifySession lookup finds nothing).
    await prisma.session.deleteMany({
        where: {
            sessionToken
        }
    });
}
}),
"[project]/src/modules/iam/verifySession.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

// T-102 — IAM verifySession() contract function.
// See spec/iam/tier-a-contract.md (signature, INV-IAM-4) and
// spec/iam/tier-b-implementation.md (session expiry logic — expired
// sessions must be treated as invalid, same as nonexistent).
__turbopack_context__.s([
    "verifySession",
    ()=>verifySession
]);
async function verifySession(prisma, sessionToken) {
    const session = await prisma.session.findUnique({
        where: {
            sessionToken
        }
    });
    if (!session) return null;
    if (session.expiresAt.getTime() <= Date.now()) return null;
    return session.userId;
}
}),
"[project]/src/modules/trah-membership/findMembershipByUserAndTrah.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

// T-201 (danger-list: auth-RBAC) — internal read helper, NOT a
// tier-a-contract.md function by this name. tier-a's `getMembership` is a
// lookup BY membershipId (see getMembership.ts); this helper resolves the
// (trahId, userId) pair callers actually have at request time (e.g. a
// route handler with a session userId and a Trah id from the URL) into a
// Membership row, INV-TM-5's one-per-(user,trah) key. Used internally by
// this module (and by getRole.ts) — not a cross-module contract call.
//
// See spec/trah-membership/tier-b-implementation.md (`memberships` schema,
// `@@unique([trahId, userId])`) and REQ-FAM-005 / INV-TM-6 (every read
// scoped by trahId AND userId, never userId alone).
__turbopack_context__.s([
    "findMembershipByUserAndTrah",
    ()=>findMembershipByUserAndTrah
]);
async function findMembershipByUserAndTrah(prisma, trahId, userId) {
    // INV-TM-5's `@@unique([trahId, userId])` compound key backs this
    // lookup — findUnique against the (unnamed, Prisma-default-named)
    // `trahId_userId` compound key.
    return prisma.membership.findUnique({
        where: {
            trahId_userId: {
                trahId,
                userId
            }
        }
    });
}
}),
"[project]/src/modules/trah-membership/getMembership.ts [app-route] (ecmascript)", ((__turbopack_context__) => {
"use strict";

// T-201 (danger-list: auth-RBAC) — getMembership() contract function.
// See spec/trah-membership/tier-a-contract.md:
//   getMembership(membershipId: MembershipId) -> MembershipView | null
//   MembershipView = { id, trahId, userId, role, claimedPersonId, status }
// Lookup BY membership id (not by trahId/userId — see
// findMembershipByUserAndTrah.ts for that shape).
//
// SECURITY NOTE (T-201 review, must read before adding any caller): this
// function performs NO authorization check of its own — it returns
// whatever membership matches the given id, including another Trah's
// data, regardless of who's asking. That's intentional per tier-a (a raw
// lookup primitive), but it means every future caller (e.g. Audit's
// getHistory, or any route handler) MUST independently verify the
// CALLING user's own membership/session is authorized for the target
// Trah before trusting this result. Never expose membershipId as a raw,
// unauthenticated route parameter that calls straight into this
// function — that would be a horizontal privilege-escalation bug
// (enumerate membershipId, read another Trah's userId/role/claimedPersonId).
__turbopack_context__.s([
    "getMembership",
    ()=>getMembership
]);
async function getMembership(prisma, membershipId) {
    const membership = await prisma.membership.findUnique({
        where: {
            id: membershipId
        }
    });
    if (!membership) return null;
    return {
        id: membership.id,
        trahId: membership.trahId,
        userId: membership.userId,
        role: membership.role,
        claimedPersonId: membership.claimedPersonId,
        status: membership.status
    };
}
}),
];

//# sourceMappingURL=%5Broot-of-the-server%5D__1youg2s._.js.map