feat: connect application input and output boundaries

This commit is contained in:
donghyeon-ka
2026-07-26 13:52:35 +09:00
parent 38ad69236b
commit 2dda17cf19
43 changed files with 697 additions and 215 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { systemClock } from "../../application/ports/clock-port.js";
import { systemClock } from "../platform/system-clock.js";
import { getApiOperation } from "../../contracts/api-operations.js";
import {
createFailure as failure,
+19
View File
@@ -0,0 +1,19 @@
/** @type {import("../../application/ports/clock-port.js").ClockPort} */
export const systemClock = Object.freeze({
now: () => Date.now(),
sleep(milliseconds, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(signal.reason);
return;
}
const timer = setTimeout(resolve, milliseconds);
const onAbort = () => {
clearTimeout(timer);
reject(signal?.reason);
};
signal?.addEventListener("abort", onAbort, { once: true });
});
},
});
-46
View File
@@ -1,46 +0,0 @@
/**
* Application facade factory. Concrete dependencies are supplied by bootstrap.
*
* @param {{
* resources?: {
* query: import("./ports/resource-ports.js").ResourceQueryPort<unknown, unknown>,
* command: import("./ports/resource-ports.js").ResourceCommandPort<unknown, unknown>
* },
* cache: import("./ports/query-cache-port.js").QueryCachePort,
* storage: import("./ports/storage-port.js").StoragePort,
* telemetry: import("./ports/telemetry-port.js").TelemetryPort
* }} ports
*/
export function createApplication(ports) {
/**
* @param {unknown} query
* @param {import("./ports/resource-ports.js").RequestContext} [context]
*/
function queryResources(query, context) {
return /** @type {NonNullable<typeof ports.resources>} */ (
ports.resources
).query.execute(query, context);
}
/**
* @param {unknown} command
* @param {import("./ports/resource-ports.js").RequestContext} [context]
*/
function commandResources(command, context) {
return /** @type {NonNullable<typeof ports.resources>} */ (
ports.resources
).command.execute(command, context);
}
return Object.freeze({
resources: ports.resources
? Object.freeze({
query: queryResources,
command: commandResources,
})
: null,
cache: ports.cache,
storage: ports.storage,
telemetry: ports.telemetry,
});
}
+73
View File
@@ -0,0 +1,73 @@
import { normalizeColorSchemePreference } from "./policies/color-scheme.js";
import type {
ApplicationApi,
ColorSchemePreference,
RenderFailureReport,
} from "./ports/in/application-api.js";
import type { ApplicationOutputPorts } from "./ports/out/application-output-ports.js";
export type { ApplicationApi, ApplicationOutputPorts };
/**
* Builds the driving API consumed by inbound adapters. Concrete output ports
* remain inside these closures and are never returned to React.
*/
export function createApplication(
outputPorts: ApplicationOutputPorts,
): ApplicationApi {
const session = Object.freeze({
getSnapshot: () => outputPorts.session.getState(),
subscribe: (listener: () => void) =>
outputPorts.session.subscribe(listener),
beginSignIn: (returnTo?: string) =>
outputPorts.session.beginSignIn(returnTo),
signOut: () => outputPorts.session.signOut(),
recover: () => outputPorts.session.recover(),
});
const preferences = Object.freeze({
getColorScheme(): ColorSchemePreference {
const result = outputPorts.preferences.read("COLOR_SCHEME");
return normalizeColorSchemePreference(
result.ok ? result.value : undefined,
);
},
setColorScheme(preference: ColorSchemePreference) {
const normalized = normalizeColorSchemePreference(preference);
return outputPorts.preferences.write("COLOR_SCHEME", normalized);
},
});
const diagnostics = Object.freeze({
reportRenderFailure(report: RenderFailureReport) {
try {
outputPorts.diagnostics.emit("ui.render.failed", {
route_id: report.routeId,
build_id: report.buildId,
component_boundary: report.boundaryName,
});
} catch {
// Diagnostics are best-effort and cannot become an application failure.
}
},
});
const runtime = Object.freeze({
async getReleaseSummary() {
const release = await outputPorts.releaseInfo.getCurrent();
return Object.freeze({
buildId: release.buildId,
releaseId: release.releaseId,
configSchemaVersion: release.configSchemaVersion,
apiContractVersion: release.apiContractVersion,
});
},
});
return Object.freeze({
session,
preferences,
diagnostics,
runtime,
});
}
+16 -2
View File
@@ -10,10 +10,24 @@
* subscribe(listener: () => void): () => void,
* beginSignIn(returnTo?: string): Promise<void>,
* signOut(): Promise<void>,
* recover(): Promise<"restored" | "no-session">
* }} SessionGateway
*/
/**
* Credential attachment is an HTTP-adapter collaboration, not an application
* input capability.
*
* @typedef {{
* attach(request: Request): Promise<Request>,
* recover(): Promise<"restored" | "no-session">,
* onUnauthenticated(): void
* }} AuthSessionPort
* }} CredentialAttacher
*/
/**
* External auth adapters implement both segregated capabilities.
*
* @typedef {SessionGateway & CredentialAttacher} AuthSessionPort
*/
export {};
+1 -22
View File
@@ -5,25 +5,4 @@
* }} ClockPort
*/
/** @type {ClockPort} */
export const systemClock = Object.freeze({
now: () => Date.now(),
sleep(milliseconds, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(signal.reason);
return;
}
const timer = setTimeout(resolve, milliseconds);
signal?.addEventListener(
"abort",
() => {
clearTimeout(timer);
reject(signal.reason);
},
{ once: true },
);
});
},
});
export {};
@@ -0,0 +1,41 @@
import type { SessionState } from "../auth-session-port.js";
import type { StoragePort } from "../storage-port.js";
export type { SessionState } from "../auth-session-port.js";
export type ColorSchemePreference = "system" | "light" | "dark";
export type RenderFailureReport = Readonly<{
routeId: string;
buildId: string;
boundaryName: "route" | "feature";
}>;
export type ReleaseSummary = Readonly<{
buildId: string;
releaseId: string;
configSchemaVersion: string;
apiContractVersion: string;
}>;
export type ApplicationApi = Readonly<{
session: Readonly<{
getSnapshot(): SessionState;
subscribe(listener: () => void): () => void;
beginSignIn(returnTo?: string): Promise<void>;
signOut(): Promise<void>;
recover(): Promise<"restored" | "no-session">;
}>;
preferences: Readonly<{
getColorScheme(): ColorSchemePreference;
setColorScheme(
preference: ColorSchemePreference,
): ReturnType<StoragePort["write"]>;
}>;
diagnostics: Readonly<{
reportRenderFailure(report: RenderFailureReport): void;
}>;
runtime: Readonly<{
getReleaseSummary(): Promise<ReleaseSummary>;
}>;
}>;
+7
View File
@@ -0,0 +1,7 @@
export type {
ApplicationApi,
ColorSchemePreference,
ReleaseSummary,
RenderFailureReport,
SessionState,
} from "./application-api.js";
@@ -0,0 +1,18 @@
import type { AuthSessionPort } from "../auth-session-port.js";
import type { ReleaseInfoPort } from "../release-info-port.js";
import type { StoragePort } from "../storage-port.js";
import type { TelemetryPort } from "../telemetry-port.js";
/**
* Capabilities required by application use cases. Implementations live in
* outbound adapters and are selected only by bootstrap.
*/
export type ApplicationOutputPorts = Readonly<{
session: Pick<
AuthSessionPort,
"getState" | "subscribe" | "beginSignIn" | "signOut" | "recover"
>;
preferences: StoragePort;
diagnostics: TelemetryPort;
releaseInfo: ReleaseInfoPort;
}>;
+17
View File
@@ -0,0 +1,17 @@
export type { ApplicationOutputPorts } from "./application-output-ports.js";
export type {
AuthSessionPort,
CredentialAttacher,
SessionGateway,
} from "../auth-session-port.js";
export type { ClockPort } from "../clock-port.js";
export type { QueryCachePort } from "../query-cache-port.js";
export type { ReleaseInfoPort } from "../release-info-port.js";
export type {
RequestContext,
ResourceCommandPort,
ResourceQueryPort,
Result,
} from "../resource-ports.js";
export type { StoragePort } from "../storage-port.js";
export type { TelemetryPort } from "../telemetry-port.js";
+5 -1
View File
@@ -1,11 +1,15 @@
/**
* @typedef {{
* getCurrent(): Promise<{
* schemaVersion?: number,
* appVersion?: string,
* buildId: string,
* commitSha?: string,
* configSchemaVersion: string,
* apiContractVersion: string,
* assetManifestHash: string,
* releaseId: string
* releaseId: string,
* builtAt?: string
* }>
* }} ReleaseInfoPort
*/
+15 -6
View File
@@ -6,27 +6,36 @@ import { createApplication } from "../application/create-application.js";
*
* @template Config
* @template Release
* @template {Parameters<typeof createApplication>[0]} Ports
* @template {Parameters<typeof createApplication>[0]} OutputPorts
* @template Infrastructure
* @param {{
* loadConfig(): Promise<Config>,
* loadRelease(config: Config): Promise<Release>,
* createAdapters(context: {
* config: Config,
* release: Release
* }): Promise<Ports>
* }): Promise<{
* outputPorts: OutputPorts,
* infrastructure: Infrastructure
* }>
* }} factories
* @returns {Promise<Readonly<{
* config: Config,
* release: Release,
* ports: Ports,
* infrastructure: Infrastructure,
* application: ReturnType<typeof createApplication>
* }>>}
*/
export async function createCompositionRoot(factories) {
const config = await factories.loadConfig();
const release = await factories.loadRelease(config);
const ports = await factories.createAdapters({ config, release });
const application = createApplication(ports);
const adapters = await factories.createAdapters({ config, release });
const application = createApplication(adapters.outputPorts);
return Object.freeze({ config, release, ports, application });
return Object.freeze({
config,
release,
infrastructure: adapters.infrastructure,
application,
});
}
+3 -4
View File
@@ -6,22 +6,21 @@ import {
/**
* Applies the persisted public preference before React paints.
*
* @param {import("../application/ports/storage-port.js").StoragePort} storage
* @param {Pick<import("../application/ports/in/application-api.js").ApplicationApi["preferences"], "getColorScheme">} preferences
* @param {{
* documentElement?: HTMLElement,
* matchMedia?: (query: string) => MediaQueryList
* }} [browser]
*/
export function initializeColorScheme(storage, browser = {}) {
export function initializeColorScheme(preferences, browser = {}) {
const documentElement = browser.documentElement ?? document.documentElement;
const matchMedia =
browser.matchMedia ??
(typeof window.matchMedia === "function"
? window.matchMedia.bind(window)
: () => /** @type {MediaQueryList} */ ({ matches: false }));
const stored = storage.read("COLOR_SCHEME");
const preference = normalizeColorSchemePreference(
stored.ok ? stored.value : undefined,
preferences.getColorScheme(),
);
const resolved = resolveColorScheme(
preference,
+3 -17
View File
@@ -1,13 +1,11 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClientProvider } from "@tanstack/react-query";
import { BootErrorShell } from "../presentation/boundaries/boot-error-shell.jsx";
import { AppRouter } from "../presentation/routes/app-router.jsx";
import { createRuntimeComposition } from "./create-runtime-composition.js";
import { initializeColorScheme } from "./initialize-color-scheme.js";
import { BootConfigError } from "./load-runtime-config.js";
import { ReleaseManifestError } from "./load-release-manifest.js";
import { RuntimeApplication } from "./runtime-application.jsx";
import "../presentation/styles/theme.css";
const rootElement = document.getElementById("root");
@@ -21,20 +19,8 @@ const root = createRoot(rootElement);
async function boot() {
try {
const composition = await createRuntimeComposition();
initializeColorScheme(composition.ports.storage);
root.render(
<StrictMode>
<QueryClientProvider client={composition.ports.queryClient}>
<AppRouter
authSession={composition.ports.authSession}
basename={composition.config.build.routerBasePath}
buildId={composition.release.buildId}
storage={composition.ports.storage}
telemetry={composition.ports.telemetry}
/>
</QueryClientProvider>
</StrictMode>,
);
initializeColorScheme(composition.application.preferences);
root.render(<RuntimeApplication composition={composition} />);
} catch (error) {
const safe =
error instanceof BootConfigError || error instanceof ReleaseManifestError
+9 -15
View File
@@ -3,9 +3,7 @@ import {
createExternalAuthSessionAdapter,
createUnavailableSessionAdapter,
} from "../adapters/auth/external-session-adapter.js";
import { createHttpClient } from "../adapters/http/client.js";
import {
createQueryCacheAdapter,
createQueryClient,
} from "../adapters/query-cache/tanstack-query-cache.js";
import { createBrowserStorageAdapter } from "../adapters/storage/browser-storage-adapter.js";
@@ -61,7 +59,6 @@ export async function createRuntimeAdapters(context) {
? createExternalAuthSessionAdapter(externalOwner)
: createUnavailableSessionAdapter();
const queryClient = createQueryClient();
const cache = createQueryCacheAdapter(queryClient);
const storage = createBrowserStorageAdapter({
localStorage: storageOrUndefined(host.localStorage),
sessionStorage: storageOrUndefined(host.sessionStorage),
@@ -71,11 +68,6 @@ export async function createRuntimeAdapters(context) {
endpoint: config.TELEMETRY_ENDPOINT,
fetcher: context.fetcher,
});
const http = createHttpClient({
baseUrl: config.API_BASE_URL,
authSession,
fetcher: context.fetcher,
});
const releaseInfo = Object.freeze({
async getCurrent() {
return structuredClone(context.release);
@@ -83,12 +75,14 @@ export async function createRuntimeAdapters(context) {
});
return Object.freeze({
authSession,
cache,
http,
queryClient,
releaseInfo,
storage,
telemetry,
outputPorts: Object.freeze({
session: authSession,
preferences: storage,
diagnostics: telemetry,
releaseInfo,
}),
infrastructure: Object.freeze({
queryClient,
}),
});
}
+28
View File
@@ -0,0 +1,28 @@
import { StrictMode } from "react";
import { QueryClientProvider } from "@tanstack/react-query";
import { ApplicationProvider } from "../presentation/providers/application-provider.js";
import { AppRouter } from "../presentation/routes/app-router.jsx";
/**
* Production provider tree. Tests import this component so the validated
* composition is proven against the same provider order used by main.
*
* @param {{
* composition: Awaited<ReturnType<typeof import("./create-runtime-composition.js").createRuntimeComposition>>
* }} props
*/
export function RuntimeApplication({ composition }) {
return (
<StrictMode>
<QueryClientProvider client={composition.infrastructure.queryClient}>
<ApplicationProvider application={composition.application}>
<AppRouter
basename={composition.config.build.routerBasePath}
buildId={composition.release.buildId}
/>
</ApplicationProvider>
</QueryClientProvider>
</StrictMode>
);
}
@@ -6,7 +6,7 @@ import { Component } from "react";
* boundaryName: string,
* routeId: string,
* buildId: string,
* telemetry?: import("../../application/ports/telemetry-port.js").TelemetryPort,
* onRenderFailure?: (report: import("../../application/ports/in/application-api.js").RenderFailureReport) => void,
* fallback?: React.ReactNode
* }} RenderBoundaryProps
* @typedef {{ hasError: boolean }} RenderBoundaryState
@@ -26,13 +26,14 @@ export class RenderErrorBoundary extends Component {
componentDidCatch() {
try {
this.props.telemetry?.emit("ui.render.failed", {
route_id: this.props.routeId,
build_id: this.props.buildId,
component_boundary: this.props.boundaryName,
this.props.onRenderFailure?.({
routeId: this.props.routeId,
buildId: this.props.buildId,
boundaryName:
/** @type {"route" | "feature"} */ (this.props.boundaryName),
});
} catch {
// Telemetry must never recurse into another render failure.
// Diagnostics must never recurse into another render failure.
}
}
+24
View File
@@ -1,7 +1,9 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { routePath } from "../../contracts/routes.js";
import { PageHeader } from "../components/page-header.jsx";
import { useApplication } from "../providers/application-provider.js";
const READINESS_ITEMS = Object.freeze([
{
@@ -19,6 +21,23 @@ const READINESS_ITEMS = Object.freeze([
]);
export default function HomePage() {
const { runtime } = useApplication();
const [release, setRelease] = useState(
/** @type {Awaited<ReturnType<typeof runtime.getReleaseSummary>> | null} */ (
null
),
);
useEffect(() => {
let active = true;
void runtime.getReleaseSummary().then((summary) => {
if (active) setRelease(summary);
});
return () => {
active = false;
};
}, [runtime]);
return (
<section className="ui-page">
<PageHeader
@@ -34,6 +53,11 @@ export default function HomePage() {
</article>
))}
</div>
<p className="ui-runtime-summary" aria-live="polite">
{release
? `빌드 ${release.buildId} · 릴리스 ${release.releaseId}`
: "검증된 런타임 정보를 확인하고 있습니다."}
</p>
<section className="ui-panel starter-actions" aria-labelledby="starter-title">
<div>
<h2 id="starter-title">준비된 화면 살펴보기</h2>
@@ -0,0 +1,31 @@
import {
createContext,
type ReactNode,
useContext,
} from "react";
import type { ApplicationApi } from "../../application/create-application.js";
const ApplicationContext = createContext<ApplicationApi | null>(null);
export function ApplicationProvider({
application,
children,
}: Readonly<{
application: ApplicationApi;
children: ReactNode;
}>) {
return (
<ApplicationContext.Provider value={application}>
{children}
</ApplicationContext.Provider>
);
}
export function useApplication(): ApplicationApi {
const application = useContext(ApplicationContext);
if (!application) {
throw new Error("ApplicationProvider is required");
}
return application;
}
+15 -16
View File
@@ -1,11 +1,12 @@
import { createContext, useContext, useMemo, useSyncExternalStore } from "react";
import { useApplication } from "./application-provider.js";
/**
* @typedef {{
* sessionState: import("../../application/ports/auth-session-port.js").SessionState,
* beginSignIn: import("../../application/ports/auth-session-port.js").AuthSessionPort["beginSignIn"],
* signOut: import("../../application/ports/auth-session-port.js").AuthSessionPort["signOut"],
* recover: import("../../application/ports/auth-session-port.js").AuthSessionPort["recover"]
* sessionState: import("../../application/ports/in/application-api.js").SessionState,
* beginSignIn: import("../../application/ports/in/application-api.js").ApplicationApi["session"]["beginSignIn"],
* signOut: import("../../application/ports/in/application-api.js").ApplicationApi["session"]["signOut"],
* recover: import("../../application/ports/in/application-api.js").ApplicationApi["session"]["recover"]
* }} SessionContextValue
*/
@@ -14,26 +15,24 @@ const SessionContext = createContext(
);
/**
* @param {{
* authSession: import("../../application/ports/auth-session-port.js").AuthSessionPort,
* children: React.ReactNode
* }} props
* @param {{ children: React.ReactNode }} props
*/
export function SessionProvider({ authSession, children }) {
export function SessionProvider({ children }) {
const { session } = useApplication();
const sessionState = useSyncExternalStore(
authSession.subscribe,
authSession.getState,
authSession.getState,
session.subscribe,
session.getSnapshot,
session.getSnapshot,
);
const value = useMemo(
() =>
Object.freeze({
sessionState,
beginSignIn: authSession.beginSignIn,
signOut: authSession.signOut,
recover: authSession.recover,
beginSignIn: session.beginSignIn,
signOut: session.signOut,
recover: session.recover,
}),
[authSession, sessionState],
[session, sessionState],
);
return (
+9 -13
View File
@@ -11,6 +11,7 @@ import {
normalizeColorSchemePreference,
resolveColorScheme,
} from "../../application/policies/color-scheme.js";
import { useApplication } from "./application-provider.js";
/**
* @typedef {{
@@ -30,18 +31,13 @@ function systemPrefersDark() {
}
/**
* @param {{
* storage?: import("../../application/ports/storage-port.js").StoragePort,
* children: React.ReactNode
* }} props
* @param {{ children: React.ReactNode }} props
*/
export function ThemeProvider({ storage, children }) {
const [preference, updatePreference] = useState(() => {
const result = storage?.read("COLOR_SCHEME");
return normalizeColorSchemePreference(
result?.ok ? result.value : undefined,
);
});
export function ThemeProvider({ children }) {
const { preferences } = useApplication();
const [preference, updatePreference] = useState(
preferences.getColorScheme,
);
const [darkSystemTheme, setDarkSystemTheme] = useState(systemPrefersDark);
const resolvedTheme = resolveColorScheme(preference, darkSystemTheme);
@@ -70,10 +66,10 @@ export function ThemeProvider({ storage, children }) {
setPreference(next) {
const normalized = normalizeColorSchemePreference(next);
updatePreference(normalized);
storage?.write("COLOR_SCHEME", normalized);
preferences.setColorScheme(normalized);
},
}),
[preference, resolvedTheme, storage],
[preference, preferences, resolvedTheme],
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
+9 -21
View File
@@ -10,6 +10,7 @@ import { getRoute, routePath } from "../../contracts/routes.js";
import { RouteBoundary } from "../boundaries/render-error-boundary.jsx";
import { AppShell } from "../layouts/app-shell.jsx";
import { PageHeader } from "../components/page-header.jsx";
import { useApplication } from "../providers/application-provider.js";
import { SessionProvider, useSession } from "../providers/session-provider.jsx";
import { ThemeProvider } from "../providers/theme-provider.jsx";
import { decideRouteAccess } from "./navigation-policy.js";
@@ -54,16 +55,16 @@ function RouteFailureSurface() {
* @param {{
* routeId: string,
* buildId: string,
* telemetry?: import("../../application/ports/telemetry-port.js").TelemetryPort,
* children: React.ReactNode
* }} props
*/
function RouteSurface({ routeId, buildId, telemetry, children }) {
function RouteSurface({ routeId, buildId, children }) {
const { diagnostics } = useApplication();
return (
<RouteBoundary
routeId={routeId}
buildId={buildId}
telemetry={telemetry}
onRenderFailure={diagnostics.reportRenderFailure}
fallback={<RouteFailureSurface />}
>
<Suspense fallback={<RouteLoadingSurface routeId={routeId} />}>
@@ -155,13 +156,12 @@ function ProtectedRoute({ routeId, children }) {
* @param {{
* routeId: string,
* buildId: string,
* telemetry?: import("../../application/ports/telemetry-port.js").TelemetryPort,
* children: React.ReactNode
* }} props
*/
function PublicRoute({ routeId, buildId, telemetry, children }) {
function PublicRoute({ routeId, buildId, children }) {
return (
<RouteSurface routeId={routeId} buildId={buildId} telemetry={telemetry}>
<RouteSurface routeId={routeId} buildId={buildId}>
{children}
</RouteSurface>
);
@@ -169,24 +169,18 @@ function PublicRoute({ routeId, buildId, telemetry, children }) {
/**
* @param {{
* authSession: import("../../application/ports/auth-session-port.js").AuthSessionPort,
* basename?: string,
* buildId?: string,
* storage?: import("../../application/ports/storage-port.js").StoragePort,
* telemetry?: import("../../application/ports/telemetry-port.js").TelemetryPort
* buildId?: string
* }} props
*/
export function AppRouter({
authSession,
basename = "/",
buildId = "local-build",
storage,
telemetry,
}) {
return (
<BrowserRouter basename={basename}>
<ThemeProvider storage={storage}>
<SessionProvider authSession={authSession}>
<ThemeProvider>
<SessionProvider>
<Routes>
<Route element={<AppShell />}>
<Route
@@ -195,7 +189,6 @@ export function AppRouter({
<PublicRoute
routeId="APP_HOME"
buildId={buildId}
telemetry={telemetry}
>
<HomePage />
</PublicRoute>
@@ -207,7 +200,6 @@ export function AppRouter({
<PublicRoute
routeId="EXAMPLES_UI"
buildId={buildId}
telemetry={telemetry}
>
<UiGalleryPage />
</PublicRoute>
@@ -219,7 +211,6 @@ export function AppRouter({
<PublicRoute
routeId="EXAMPLES_STATES"
buildId={buildId}
telemetry={telemetry}
>
<StateGalleryPage />
</PublicRoute>
@@ -231,7 +222,6 @@ export function AppRouter({
<PublicRoute
routeId="EXAMPLES_AUTH"
buildId={buildId}
telemetry={telemetry}
>
<AuthExamplePage />
</PublicRoute>
@@ -243,7 +233,6 @@ export function AppRouter({
<RouteSurface
routeId="SAMPLE_RESOURCE_LIST"
buildId={buildId}
telemetry={telemetry}
>
<ProtectedRoute routeId="SAMPLE_RESOURCE_LIST">
<SampleContractPage />
@@ -257,7 +246,6 @@ export function AppRouter({
<PublicRoute
routeId="NOT_FOUND"
buildId={buildId}
telemetry={telemetry}
>
<NotFoundPage />
</PublicRoute>
+1 -1
View File
@@ -2,7 +2,7 @@ import { getRoute } from "../../contracts/routes.js";
/**
* @param {string} routeId
* @param {import("../../application/ports/auth-session-port.js").SessionState} sessionState
* @param {import("../../application/ports/in/application-api.js").SessionState} sessionState
*/
export function decideRouteAccess(routeId, sessionState) {
const route = getRoute(routeId);