@iyulab/modern-app 0.44.2 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.45.0] - 2026-10-08
4
+
5
+ ### Changed (breaking)
6
+
7
+ - **The boot auth gate reads `me()`'s answer by `status`.** `me()` returns an `AuthSession` —
8
+ `{ status: 'authenticated', user }`, `{ status: 'anonymous' }` or `{ status: 'unknown', error }` — the same shape
9
+ `@iyulab/enterprise`'s `createAuthClient().fetchMe()` returns, so the two connect as `me: () => auth.fetchMe()`.
10
+ Before, any value meant "signed in": a session client that answers "signed out" with an object (`fetchMe()` gives
11
+ `{ status: 'anonymous' }` on a 401) opened the shell to a user who had not signed in, with that object as
12
+ `app.user` — and `me`'s `unknown` return type let it type-check.
13
+ `anonymous` renders `renderLogin`; `unknown` (or `me()` throwing) renders `renderUnavailable` with the answer's
14
+ `error`. An answer whose `status` is not one of the three makes `app.load()` reject with a `TypeError` instead of
15
+ being guessed to mean signed in.
16
+ **Migrating:** return `{ status: 'authenticated', user }` where you returned the user and `{ status: 'anonymous' }`
17
+ where you returned `null`; with `@iyulab/enterprise`, drop the adapter and pass `auth.fetchMe` through.
18
+
19
+ ## [0.44.3] - 2026-10-08
20
+
21
+ ### Fixed
22
+
23
+ - `u-wizard` keeps its live announcement inside the element — the absolutely positioned announcement escaped an
24
+ unpositioned scroll container and could stretch the document.
25
+
3
26
  ## [0.44.2] - 2026-10-07
4
27
 
5
28
  ### Fixed
package/README.md CHANGED
@@ -184,11 +184,7 @@ const auth = createAuthClient<User, Cred>({ meUrl: '/api/auth/me', loginUrl: '/a
184
184
  await app.load({
185
185
  layout: { type: 'sidebar', /* ... */ },
186
186
  auth: {
187
- me: async () => {
188
- const s = await auth.fetchMe();
189
- if (s.status === 'unknown') throw s.error; // 모름(서버 다운·오프라인) → renderUnavailable
190
- return s.status === 'authenticated' ? s.user : null; // null → 미인증 → renderLogin
191
- },
187
+ me: () => auth.fetchMe(), // SessionState: authenticated · anonymous → renderLogin · unknown → renderUnavailable
192
188
  renderLogin: ({ root, onSuccess }) => renderLoginPage(root, auth, onSuccess),
193
189
  renderUnavailable: ({ root, retry }) => renderOfflinePage(root, retry),
194
190
  onAuthenticated: (user) => setPermissions((user as User).Permissions),
@@ -199,8 +195,9 @@ await app.load({
199
195
  app.user; // 인증된 현재 사용자(미인증/미사용 시 undefined)
200
196
  ```
201
197
 
202
- - `me()` 가 값을 반환하면 셸 로드, `null`/`undefined` 면 `renderLogin({ root, onSuccess })`.
203
- - `me()` 가 **던지면** 세션을 «모름» 으로 보고 로그인 UI 를 그리지 않는다 — `renderUnavailable({ root, error, retry })` 를 그리고, 없으면 `app.load()` 가 그 오류로 실패한다. 서버가 잠깐 503 을 낸 것을 «미인증» 으로 돌려주면 로그인된 사용자가 로그인 화면으로 간다.
198
+ - `me()` 는 `AuthSession` — `{ status: 'authenticated', user }` · `{ status: 'anonymous' }` · `{ status: 'unknown', error }` — 을 돌려준다. `authenticated` 면 셸을 로드하고 `app.user` 는 `user` 다. `anonymous` 면 `renderLogin({ root, onSuccess })`.
199
+ - `unknown`(또는 `me()` 가 **던짐**)이면 세션을 «모름» 으로 보고 로그인 UI 를 그리지 않는다 — `renderUnavailable({ root, error, retry })` 를 그리고, 없으면 `app.load()` 가 그 오류로 실패한다. 서버가 잠깐 503 을 낸 것을 «미인증» 으로 다루면 로그인된 사용자가 로그인 화면으로 간다.
200
+ - `status` 가 셋 중 하나가 아닌 답(사용자 객체 · `null`)은 `app.load()` 가 `TypeError` 로 거절한다 — 인증으로 추측하지 않는다.
204
201
  - 로그인 성공 시 `onSuccess()` 를 호출하면 앱이 재로드되어 셸이 나타나고 로그인 UI 는 정리된다.
205
202
  - `auth` 미지정 시 완전히 하위호환(게이트 없이 기존대로 로드).
206
203
 
package/dist/App.js CHANGED
@@ -52,17 +52,23 @@ var u = class s {
52
52
  }, n;
53
53
  try {
54
54
  n = await a.auth.me();
55
- } catch (n) {
56
- if (!a.auth.renderUnavailable) throw n;
55
+ } catch (e) {
56
+ n = {
57
+ status: "unknown",
58
+ error: e
59
+ };
60
+ }
61
+ if (n?.status === "unknown") {
62
+ if (!a.auth.renderUnavailable) throw n.error;
57
63
  let r = a.auth.renderUnavailable({
58
64
  root: e,
59
- error: n,
65
+ error: n.error,
60
66
  retry: t
61
67
  });
62
68
  r && (this._gateTeardown = r);
63
69
  return;
64
70
  }
65
- if (n == null) {
71
+ if (n?.status === "anonymous") {
66
72
  let n = a.auth.renderLogin({
67
73
  root: e,
68
74
  onSuccess: t
@@ -70,7 +76,8 @@ var u = class s {
70
76
  n && (this._gateTeardown = n);
71
77
  return;
72
78
  }
73
- this._user = n, await a.auth.onAuthenticated?.(n);
79
+ if (n?.status !== "authenticated") throw TypeError("auth.me() must return { status: 'authenticated', user } | { status: 'anonymous' } | { status: 'unknown', error }");
80
+ this._user = n.user, await a.auth.onAuthenticated?.(n.user);
74
81
  }
75
82
  let o = a.root || document.body;
76
83
  this._layout = await this.createLayout(o, a.layout), this._screen = new e({
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorate.js
2
2
  function e(e, t, n, r) {
3
3
  var i = arguments.length, a = i < 3 ? t : r === null ? r = Object.getOwnPropertyDescriptor(t, n) : r, o;
4
4
  if (typeof Reflect == "object" && typeof Reflect.decorate == "function") a = Reflect.decorate(e, t, n, r);
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorateMetadata.js
1
+ //#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorateMetadata.js
2
2
  function e(e, t) {
3
3
  if (typeof Reflect == "object" && typeof Reflect.metadata == "function") return Reflect.metadata(e, t);
4
4
  }
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { slotHasContent as r } from "../internals/slotted.js";
5
5
  import { styles as i } from "./ActionBar.styles.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { getLocaleStrings as r } from "../internals/locale.js";
5
5
  import { slotHasContent as i } from "../internals/slotted.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { slotHasContent as r } from "../internals/slotted.js";
5
5
  import { styles as i } from "./GroupBox.styles.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { styles as r } from "./InfoField.styles.js";
5
5
  import { html as i } from "lit";
@@ -26,11 +26,7 @@ var f = class extends n {
26
26
  let e = t.getAttribute("role") ?? (t.localName === "progress" ? "progressbar" : t.localName === "meter" ? "meter" : null);
27
27
  if (e !== "progressbar" && e !== "meter") continue;
28
28
  let n = this.namedWidgets.get(t), r = t.getAttribute("aria-label");
29
- if (t.hasAttribute("aria-labelledby") || r !== null && r !== n) {
30
- this.namedWidgets.delete(t);
31
- continue;
32
- }
33
- this.label ? (t.setAttribute("aria-label", this.label), this.namedWidgets.set(t, this.label)) : n !== void 0 && (t.removeAttribute("aria-label"), this.namedWidgets.delete(t));
29
+ t.hasAttribute("aria-labelledby") || r !== null && r !== n ? this.namedWidgets.delete(t) : this.label ? (t.setAttribute("aria-label", this.label), this.namedWidgets.set(t, this.label)) : n !== void 0 && (t.removeAttribute("aria-label"), this.namedWidgets.delete(t));
34
30
  }
35
31
  }
36
32
  updated(e) {
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { styles as r } from "./InfoSection.styles.js";
5
5
  import { html as i } from "lit";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { getLocaleStrings as r } from "../internals/locale.js";
5
5
  import { slotHasContent as i } from "../internals/slotted.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { getLocaleStrings as r } from "../internals/locale.js";
5
5
  import { slotHasContent as i } from "../internals/slotted.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
5
5
  import { styles as i } from "./SidebarButton.styles.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { DEFAULT_NAV_ICON as r } from "../internals/nav-icon.js";
5
5
  import { SidebarLink as i } from "./SidebarLink.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { getLocaleStrings as r } from "../internals/locale.js";
5
5
  import { DEFAULT_NAV_ICON as i } from "../internals/nav-icon.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { styles as r } from "./SidebarSection.styles.js";
5
5
  import { html as i } from "lit";
@@ -1,4 +1,4 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
2
2
  import { StyledElement as t } from "../internals/StyledElement.js";
3
3
  import "./TimelineItem.js";
4
4
  import { styles as n } from "./Timeline.styles.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { slotHasContent as r } from "../internals/slotted.js";
5
5
  import { styles as i } from "./TimelineItem.styles.js";
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { StyledElement as n } from "../internals/StyledElement.js";
4
4
  import { getLocaleStrings as r } from "../internals/locale.js";
5
5
  import { styles as i } from "./Wizard.styles.js";
@@ -3,6 +3,9 @@ import { css as e } from "lit";
3
3
  var t = e`
4
4
  :host {
5
5
  display: flex;
6
+ /* 위치 지정 — 안의 숨김 문구(절대 위치 · 1px)의 포함 블록이 이 요소가 된다. 없으면 포함 블록이 문서 전체라 호스트의
7
+ overflow 감싸개를 건너뛰고 그 정적 위치에서 문서를 늘렸다(위치 지정되지 않은 스크롤 상자 안에서). */
8
+ position: relative;
6
9
  flex-direction: row;
7
10
  gap: var(--u-space-xl, 20px);
8
11
  min-width: 0;
@@ -1,5 +1,5 @@
1
- import e from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import t from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import e from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
2
+ import t from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
3
3
  import { property as n } from "lit/decorators.js";
4
4
  import { UElement as r } from "@iyulab/components/dist/components/UElement.js";
5
5
  //#region src/internals/StyledElement.ts
@@ -25,11 +25,8 @@ var i = class extends r {
25
25
  for (let [n, r] of Object.entries(t)) {
26
26
  if (r == null) continue;
27
27
  let t = String(r);
28
- if (n.startsWith("--") || n.includes("-")) {
29
- e.style.setProperty(n, t);
30
- continue;
31
- }
32
- try {
28
+ if (n.startsWith("--") || n.includes("-")) e.style.setProperty(n, t);
29
+ else try {
33
30
  e.style[n] = t;
34
31
  } catch {
35
32
  let r = n.replace(/[A-Z]/g, (e) => "-" + e.toLowerCase());
@@ -1,7 +1,7 @@
1
1
  import { app as e } from "../App.js";
2
2
  import { filterSidebarItems as t } from "./filterSidebarItems.js";
3
- import n from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
4
- import r from "../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
3
+ import n from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
4
+ import r from "../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
5
5
  import { StyledElement as i } from "../internals/StyledElement.js";
6
6
  import { getLocaleStrings as a } from "../internals/locale.js";
7
7
  import { slotHasContent as o } from "../internals/slotted.js";
@@ -101,8 +101,8 @@ export interface AppConfig {
101
101
  */
102
102
  i18n?: I18nInitOptions;
103
103
  /**
104
- * 부팅 인증 게이트(선택). 지정하면 앱 셸을 만들기 전에 `me()` 로 세션을 판정하여
105
- * 인증 시 셸 로드, 미인증 시 `renderLogin` 으로 로그인 UI 를 띄운다. `me()` 가 던지면(세션 모름)
104
+ * 부팅 인증 게이트(선택). 지정하면 앱 셸을 만들기 전에 `me()` 의 답(`AuthSession`)으로 세션을 판정하여
105
+ * `authenticated` 면 셸 로드, `anonymous` 면 `renderLogin` 으로 로그인 UI, `unknown`(또는 `me()` 가 던짐)이면
106
106
  * `renderUnavailable` 을 그린다.
107
107
  * @see AuthGateConfig
108
108
  * @example
@@ -110,11 +110,7 @@ export interface AppConfig {
110
110
  * app.load({
111
111
  * layout: { type: 'sidebar', ... },
112
112
  * auth: {
113
- * me: async () => { // @iyulab/enterprise createAuthClient
114
- * const s = await authClient.fetchMe();
115
- * if (s.status === 'unknown') throw s.error; // 모름 — 로그인이 아니라 renderUnavailable
116
- * return s.status === 'authenticated' ? s.user : null; // null → 미인증
117
- * },
113
+ * me: () => authClient.fetchMe(), // @iyulab/enterprise createAuthClient — SessionState 그대로
118
114
  * renderLogin: ({ root, onSuccess }) => renderLoginPage(root, onSuccess),
119
115
  * renderUnavailable: ({ root, retry }) => renderOfflinePage(root, retry),
120
116
  * onAuthenticated: (user) => setPermissions((user as User).Permissions),
@@ -1,12 +1,17 @@
1
1
  /**
2
2
  * 부팅 인증 게이트 설정.
3
3
  *
4
- * `app.load({ auth })` 에 넘기면, 앱 셸(레이아웃·라우터)을 만들기 전에 세션을 판정한다:
5
- * `me()` 가 값을 반환하면 인증된 것으로 보고 셸을 로드하고, `null`/`undefined` 면
6
- * 미인증으로 보고 `renderLogin` 으로 로그인 UI 를 띄운다. 로그인 성공 시 `onSuccess()` 를
7
- * 호출하면 앱이 (재)로드되어 셸이 나타난다. `me()` 가 **던지면** 세션을 «모름»(서버 다운·오프라인)
8
- * 으로 보고 로그인 UI 를 그리지 않는다 — `renderUnavailable` 이 있으면 그것을, 없으면 `load` 가 그
9
- * 오류로 실패한다.
4
+ * `app.load({ auth })` 에 넘기면, 앱 셸(레이아웃·라우터)을 만들기 전에 세션을 판정한다. `me()` 는
5
+ * 세 답 중 하나(`AuthSession`)를 돌려준다:
6
+ * - `authenticated` — 셸을 로드한다(`user` 가 `app.user` 가 된다).
7
+ * - `anonymous` — 셸 대신 `renderLogin` 으로 로그인 UI 를 띄운다. 로그인 성공 시 `onSuccess()` 를
8
+ * 호출하면 앱이 (재)로드되어 셸이 나타난다.
9
+ * - `unknown` — 세션을 확인하지 못했다(서버 다운·오프라인). 미인증이 아니므로 로그인 UI 를 그리지
10
+ * 않는다 — `renderUnavailable` 이 있으면 그것을, 없으면 `load` 가 그 오류로 실패한다. `me()` 가
11
+ * **던지는** 것도 같은 답이다.
12
+ *
13
+ * `@iyulab/enterprise` 의 `createAuthClient().fetchMe()` 가 돌려주는 `SessionState` 가 이 형태 그대로라
14
+ * `me: () => auth.fetchMe()` 로 잇는다.
10
15
  *
11
16
  * 프레임워크는 인증의 **오케스트레이션**(판정 → 분기 → 재로드)만 소유한다. 세션 조회/로그인
12
17
  * 자체(HTTP)와 사용자·권한 형태, 세션-중 401 처리는 앱/`@iyulab/enterprise` 가 소유한다.
@@ -14,33 +19,46 @@
14
19
  export interface AuthGateContext {
15
20
  /** 로그인 UI 를 그릴 루트 요소(= `app.load` 의 `root`, 기본 `document.body`). */
16
21
  root: Element;
17
- /** 로그인 성공 시 호출한다. 앱이 (재)로드되어 셸을 띄운다(이때 `me()` 는 사용자를 반환해야 한다). */
22
+ /** 로그인 성공 시 호출한다. 앱이 (재)로드되어 셸을 띄운다(이때 `me()` 는 `authenticated` 를 돌려줘야 한다). */
18
23
  onSuccess: () => void;
19
24
  }
20
25
  /** `renderUnavailable` 이 받는 맥락 — 세션을 확인하지 못했을 때. */
21
26
  export interface AuthGateUnavailableContext {
22
27
  /** 안내 UI 를 그릴 루트 요소(= `app.load` 의 `root`, 기본 `document.body`). */
23
28
  root: Element;
24
- /** `me()` 가 던진 값. */
29
+ /** `unknown` 답의 `error`, 또는 `me()` 가 던진 값. */
25
30
  error: unknown;
26
31
  /** 다시 확인한다 — 앱이 (재)로드되어 `me()` 를 다시 부른다(재시도 버튼·`online` 이벤트에서). */
27
32
  retry: () => void;
28
33
  }
34
+ /**
35
+ * 세션 조회의 답 — `status` 로 가른다. «미인증»과 «모름»은 다른 답이다: 서버가 잠깐 503 을 낸 것을
36
+ * 미인증으로 다루면 로그인된 사용자가 로그인 화면으로 간다.
37
+ */
38
+ export type AuthSession<TUser = unknown> = {
39
+ status: 'authenticated';
40
+ user: TUser;
41
+ } | {
42
+ status: 'anonymous';
43
+ } | {
44
+ status: 'unknown';
45
+ error: unknown;
46
+ };
29
47
  export interface AuthGateConfig {
30
48
  /**
31
- * 현재 세션 조회. 값을 반환하면 인증, `null`/`undefined` 면 미인증으로 본다. **세션을 확인하지
32
- * 못했으면 던진다**(서버 다운·오프라인) — 그것을 `null` 로 돌려주면 로그인된 사용자가 로그인 화면으로 간다.
33
- * 동기/비동기 모두 허용한다.
49
+ * 현재 세션 조회 — `AuthSession` 을 돌려준다(동기/비동기 모두 허용). 던지면 `unknown` 으로 본다.
50
+ * `status` 가 셋 중 하나가 아닌 값을 돌려주면 `load` 가 `TypeError` 로 실패한다 — 사용자 객체를 그대로
51
+ * 돌려주는 옛 형태를 «인증됨»으로 추측하지 않는다.
34
52
  */
35
- me: () => Promise<unknown | null | undefined> | unknown | null | undefined;
53
+ me: () => AuthSession | Promise<AuthSession>;
36
54
  /**
37
55
  * 미인증 시 `context.root` 에 로그인 UI 를 그린다. 성공하면 `context.onSuccess()` 를 호출해야 한다.
38
56
  * 정리 함수를 반환하면 앱 로드/`unload` 시 호출되어 로그인 UI 를 제거한다.
39
57
  */
40
58
  renderLogin: (context: AuthGateContext) => (() => void) | void;
41
59
  /**
42
- * `me()` 가 던졌을 때(세션 모름) `context.root` 에 안내 UI 를 그린다(선택). `context.retry()` 로 다시
43
- * 확인한다. 정리 함수를 반환하면 앱 로드/`unload` 시 호출된다. 없으면 `load` 가 `me()` 의 오류로 실패한다.
60
+ * 세션을 모를 때(`unknown` · `me()` 가 던짐) `context.root` 에 안내 UI 를 그린다(선택). `context.retry()` 로
61
+ * 다시 확인한다. 정리 함수를 반환하면 앱 로드/`unload` 시 호출된다. 없으면 `load` 가 그 오류로 실패한다.
44
62
  */
45
63
  renderUnavailable?: (context: AuthGateUnavailableContext) => (() => void) | void;
46
64
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/modern-app",
3
3
  "description": "web-framework by iyulab based on lit-element",
4
- "version": "0.44.2",
4
+ "version": "0.45.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -81,8 +81,8 @@
81
81
  "@iyulab/components": ">=2.8.0",
82
82
  "@lit/react": "^1.0.8",
83
83
  "@types/node": "^26.1.1",
84
- "@types/react": "^19.2.14",
85
- "@types/react-dom": "^19.2.3",
84
+ "@types/react": "^19.3.0",
85
+ "@types/react-dom": "^19.3.0",
86
86
  "@vitest/browser-playwright": "^5.0.0",
87
87
  "glob": "^13.0.6",
88
88
  "happy-dom": "^20.10.6",
@@ -227,13 +227,23 @@ The framework owns only the orchestration (check → branch → reload) — sess
227
227
  `@iyulab/enterprise`'s `createAuthClient`/`createODataService`).
228
228
 
229
229
  ```typescript
230
+ /**
231
+ * A session lookup's answer. "Signed out" and "could not tell" are different answers — treating a
232
+ * brief 503 as signed out sends a signed-in user to the login screen. `@iyulab/enterprise`'s
233
+ * `createAuthClient().fetchMe()` returns exactly this shape: `me: () => auth.fetchMe()`.
234
+ */
235
+ type AuthSession<TUser = unknown> =
236
+ | { status: 'authenticated'; user: TUser } // shell loads; app.user = user
237
+ | { status: 'anonymous' } // renderLogin
238
+ | { status: 'unknown'; error: unknown }; // renderUnavailable
239
+
230
240
  interface AuthGateConfig {
231
241
  /**
232
- * Resolve the current session. Return a value for authenticated, `null`/`undefined` for not.
233
- * **Throw when the session could not be checked** (server down, offline) — returning `null`
234
- * there would send a signed-in user to the login screen.
242
+ * Resolve the current session — sync or async. Throwing counts as `unknown`. An answer whose
243
+ * `status` is not one of the three makes `app.load()` reject with a `TypeError`: a user object
244
+ * or `null` is never guessed to mean "signed in".
235
245
  */
236
- me: () => Promise<unknown | null | undefined> | unknown | null | undefined;
246
+ me: () => AuthSession | Promise<AuthSession>;
237
247
 
238
248
  /**
239
249
  * Renders login UI into `context.root` when unauthenticated. Call `context.onSuccess()` on
@@ -242,10 +252,11 @@ interface AuthGateConfig {
242
252
  renderLogin: (context: AuthGateContext) => (() => void) | void;
243
253
 
244
254
  /**
245
- * Renders a "can't reach the server" view into `context.root` when `me()` throws. Call
255
+ * Renders a "can't reach the server" view into `context.root` when the session is `unknown`
256
+ * (or `me()` throws). Call
246
257
  * `context.retry()` to check again (a retry button, the `online` event). Return a cleanup
247
258
  * function to have it called on app load/`unload`. Without it, `app.load()` rejects with
248
- * `me()`'s error.
259
+ * that error.
249
260
  */
250
261
  renderUnavailable?: (context: AuthGateUnavailableContext) => (() => void) | void;
251
262
 
@@ -263,7 +274,7 @@ interface AuthGateContext {
263
274
  interface AuthGateUnavailableContext {
264
275
  /** Root element to render into (same as `AppConfig.root`, default `document.body`). */
265
276
  root: Element;
266
- /** What `me()` threw. */
277
+ /** The `unknown` answer's `error`, or what `me()` threw. */
267
278
  error: unknown;
268
279
  /** Check again — the app (re)loads and calls `me()` again. */
269
280
  retry: () => void;