feat: 기능 추가 과정중

This commit is contained in:
donghyeon-ka
2026-07-30 15:58:20 +09:00
parent d3ef801fe6
commit 6c52cdb916
648 changed files with 126325 additions and 6680 deletions
@@ -0,0 +1,134 @@
import type {
AuthSessionPort,
CredentialPatch,
CredentialRequestBinding,
SessionState,
} from "../../application/ports/auth-session-port.ts";
export type ExternalSessionOwner = Readonly<{
readState(): SessionState;
subscribe(listener: () => void): () => void;
beginSignIn(returnTo?: string): Promise<void>;
signOut(): Promise<void>;
attachCredential(binding: CredentialRequestBinding): Promise<CredentialPatch>;
recoverSession(): Promise<"restored" | "no-session">;
notifyUnauthenticated(): void;
}>;
const ALLOWED_CREDENTIAL_HEADERS = new Set([
"authorization",
"x-csrf-token",
]);
const MAX_HEADER_VALUE_BYTES = 8_192;
export function validateCredentialPatch(value: unknown): CredentialPatch {
if (!value || typeof value !== "object") {
throw new TypeError("Auth owner returned an invalid credential patch");
}
const headers = (value as Record<string, unknown>).headers;
if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
throw new TypeError("Auth owner returned an invalid credential patch");
}
const projected: Record<string, string> = {};
for (const [name, headerValue] of Object.entries(headers)) {
const normalizedName = name.toLowerCase();
if (
!ALLOWED_CREDENTIAL_HEADERS.has(normalizedName) ||
typeof headerValue !== "string" ||
headerValue.length === 0 ||
new TextEncoder().encode(headerValue).byteLength > MAX_HEADER_VALUE_BYTES ||
/[\r\n]/.test(headerValue)
) {
throw new TypeError("Auth owner returned a forbidden credential patch");
}
projected[normalizedName] = headerValue;
}
return Object.freeze({ headers: Object.freeze(projected) });
}
export function createExternalAuthSessionAdapter(
owner: ExternalSessionOwner,
): AuthSessionPort {
return Object.freeze({
getState: () => owner.readState(),
subscribe: (listener) => owner.subscribe(listener),
beginSignIn: (returnTo) => owner.beginSignIn(returnTo),
signOut: () => owner.signOut(),
async credentialPatch(binding) {
return validateCredentialPatch(await owner.attachCredential(binding));
},
async recover() {
const result = await owner.recoverSession();
if (result !== "restored" && result !== "no-session") {
throw new TypeError("Auth owner returned an invalid recovery state");
}
return result;
},
onUnauthenticated: () => owner.notifyUnauthenticated(),
});
}
const EMPTY_PATCH = Object.freeze({ headers: Object.freeze({}) });
export function createAnonymousSessionAdapter(): AuthSessionPort {
return createExternalAuthSessionAdapter({
readState: () => "unauthenticated",
subscribe: () => () => {},
beginSignIn: async () => {},
signOut: async () => {},
attachCredential: async () => EMPTY_PATCH,
recoverSession: async () => "no-session",
notifyUnauthenticated: () => {},
});
}
export type DemoSessionAdapter = AuthSessionPort &
Readonly<{ setState(next: SessionState): void }>;
export function createDemoSessionAdapter(
initialState: SessionState = "unauthenticated",
): DemoSessionAdapter {
let state = initialState;
const listeners = new Set<() => void>();
const setState = (next: SessionState) => {
state = next;
for (const listener of listeners) listener();
};
return Object.freeze({
getState: () => state,
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
async beginSignIn() {
setState("authenticated");
},
async signOut() {
setState("unauthenticated");
},
credentialPatch: async () => EMPTY_PATCH,
async recover() {
if (state === "recovery-pending") {
setState("authenticated");
return "restored";
}
return "no-session";
},
onUnauthenticated: () => setState("unauthenticated"),
setState,
});
}
export function createUnavailableSessionAdapter(): AuthSessionPort {
return Object.freeze({
getState: () => "integration-failed",
subscribe: () => () => {},
beginSignIn: async () => {},
signOut: async () => {},
credentialPatch: async () => {
throw new TypeError("External session integration is unavailable");
},
recover: async () => "no-session" as const,
onUnauthenticated: () => {},
});
}