From aedef84a7bafe64b31faad4aface54ead20722c1 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Sat, 11 Jul 2026 22:06:17 +0800 Subject: [PATCH 1/2] fix(app-shell): unwrap the {success,data} envelope in apiHandler so resultDialog fields resolve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apiHandler's absolute-URL branch returned the whole `{ success, data }` response body as `result.data`, while flowHandler and serverActionHandler both unwrap `json.data`. Action `resultDialog` field paths (`user.email`, `temporaryPassword`, `client.client_id`, `totpURI`, …) are written relative to the inner `data`, so leaking the envelope made ActionResultDialog's readPath resolve to undefined and blanked every reveal dialog. Most visibly "Create User": the one-shot temporary password rendered empty, so the freshly created user could never sign in. Unwrap the envelope (guarded so bare, non-enveloped better-auth bodies pass through unchanged), aligning apiHandler with its two sibling handlers and the documented "path into `data`" contract. Fixes create_user, set_user_password, enable_two_factor and create_oauth_application reveal dialogs at once. Companion: objectstack-ai/framework drops the now-redundant `data.` prefix from sys_sso_provider's resultDialog paths (the lone spec that compensated for this bug) — the two must ship together. Co-Authored-By: Claude Opus 4.8 --- .../useConsoleActionRuntime.test.tsx | 37 +++++++++++++++++++ .../src/hooks/useConsoleActionRuntime.tsx | 16 +++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx b/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx index 1c28b8bbe..e39b6f193 100644 --- a/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx +++ b/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx @@ -90,6 +90,43 @@ describe('useConsoleActionRuntime — authenticated handlers', () => { expect(onRefresh).toHaveBeenCalledTimes(1); }); + it('apiHandler unwraps the `{ success, data }` envelope so result.data is the inner payload (create_user password reveal)', async () => { + // The admin/create-user wrapper returns `{ success, data: { user, + // temporaryPassword } }`. The action `resultDialog` field paths + // (`user.email`, `temporaryPassword`) are written relative to the INNER + // `data`, matching flowHandler / serverActionHandler which already unwrap. + // Pre-fix apiHandler leaked the whole envelope, so ActionResultDialog's + // readPath(envelope, 'user.email') resolved to undefined and BOTH the email + // and temporary-password fields rendered blank — the reported bug. + authFetchSpy.mockResolvedValue({ + ok: true, + json: async () => ({ + success: true, + data: { user: { id: 'u9', email: 'new@acme.co' }, temporaryPassword: 'Tmp-abc123!' }, + }), + }); + const { result } = renderHook(() => + useConsoleActionRuntime({ dataSource: {}, objects: [] }), + ); + + let res: any; + await act(async () => { + res = await result.current.apiHandler({ + type: 'api', name: 'create_user', target: '/api/v1/auth/admin/create-user', + params: { email: 'new@acme.co' }, + } as any); + }); + + expect(res.success).toBe(true); + // The inner payload — not the envelope. readPath(data, 'user.email') and + // readPath(data, 'temporaryPassword') now resolve in ActionResultDialog. + expect(res.data).toEqual({ + user: { id: 'u9', email: 'new@acme.co' }, + temporaryPassword: 'Tmp-abc123!', + }); + expect((res.data as any).success).toBeUndefined(); + }); + it('apiHandler surfaces a failed response and does not refresh', async () => { authFetchSpy.mockResolvedValue({ ok: false, status: 403, json: async () => ({ error: 'Forbidden' }) }); const onRefresh = vi.fn(); diff --git a/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx b/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx index 3b6344d75..eb54f9ab6 100644 --- a/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx +++ b/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx @@ -315,8 +315,22 @@ export function useConsoleActionRuntime(opts: ConsoleActionRuntimeOptions): Cons const detail = (body as any)?.error || (body as any)?.message || `HTTP ${res.status}`; return { success: false, error: detail }; } - const data = await res.json().catch(() => ({})); + const json = await res.json().catch(() => ({})); if (action.refreshAfter !== false) refresh(); + // Unwrap the ObjectStack `{ success, data }` envelope so `result.data` + // is the inner payload — the contract every `result.data` consumer + // expects. The action `resultDialog` field paths (e.g. `user.email`, + // `temporaryPassword`) and the dynamic-toast `result.data.message` are + // all written relative to the inner `data`. flowHandler and + // serverActionHandler already unwrap `json.data`; apiHandler was the + // lone handler that leaked the whole envelope, which blanked every + // resultDialog whose paths didn't redundantly prefix `data.` (the + // "Create User temporary password shows empty" bug). Bare, + // non-enveloped responses (some stock better-auth bodies) pass through + // unchanged. + const data = json && typeof json === 'object' && !Array.isArray(json) && 'data' in json + ? (json as { data: unknown }).data + : json; return { success: true, data, reload: action.refreshAfter !== false }; } From 08824dafb76bfcae937b13f5f9c8f77ace94b5d6 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Sat, 11 Jul 2026 22:09:26 +0800 Subject: [PATCH 2/2] chore: add changeset Co-Authored-By: Claude Opus 4.8 --- .changeset/create-user-resultdialog-unwrap.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/create-user-resultdialog-unwrap.md diff --git a/.changeset/create-user-resultdialog-unwrap.md b/.changeset/create-user-resultdialog-unwrap.md new file mode 100644 index 000000000..9e06c0578 --- /dev/null +++ b/.changeset/create-user-resultdialog-unwrap.md @@ -0,0 +1,10 @@ +--- +"@object-ui/app-shell": patch +--- + +Fix "Create User" (and set_user_password / enable_two_factor / +create_oauth_application) result dialogs rendering an empty email + temporary +password: the console `apiHandler` now unwraps the `{ success, data }` response +envelope so `resultDialog` field paths resolve against the inner `data`, +matching `flowHandler` / `serverActionHandler` and the documented "path into +`data`" contract. Paired with framework#2842 (objectui#2396).