feat: 기능 추가 과정중
This commit is contained in:
@@ -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: () => {},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user