diff --git a/documentation/BOOTSTRAP__AI_Agent_Quickstart.md b/documentation/BOOTSTRAP__AI_Agent_Quickstart.md index 8a82bd3d..2ce7c955 100644 --- a/documentation/BOOTSTRAP__AI_Agent_Quickstart.md +++ b/documentation/BOOTSTRAP__AI_Agent_Quickstart.md @@ -206,6 +206,10 @@ x-aether-api-key: x-account-id: ``` +**Do not treat `params.key` as an auth bypass.** +Only explicit `x-no-account-id: bypass` means "drop account context". +If `key` is present for business logic, keep `x-account-id` intact. + ### Dexie queries — always use the object ID index, not `.get()` All `db_core` (and other module) Dexie tables define their schema with `id` as the first field (primary key), followed by the object's string ID (e.g. `person_id`). V3 **never** @@ -288,6 +292,10 @@ These are real incidents — know them before you start. clean of data loads in private modules. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` → "SvelteKit Layout Hierarchy: Security and Execution Order" for the full explanation. +10. **Using query `key` as a proxy for bypass stripped `x-account-id`** — this caused + valid account-scoped requests to lose account context and 403. `key` can be a valid + endpoint/business param, but it is not equivalent to `x-no-account-id: bypass`. + --- ## 8. Source Layout (Quick Reference) diff --git a/documentation/GUIDE__AE_API_V3_for_Frontend.md b/documentation/GUIDE__AE_API_V3_for_Frontend.md index d63c09ea..19d01e7b 100644 --- a/documentation/GUIDE__AE_API_V3_for_Frontend.md +++ b/documentation/GUIDE__AE_API_V3_for_Frontend.md @@ -21,6 +21,9 @@ Required for any non-public data (Journals, Badges, Users, etc.). * **Header:** `x-no-account-id: bypass` 3. **Token Access**: Provide a **JWT** in the query string. * **Query Param:** `?jwt=` +4. **Important Distinction:** A query parameter named `key` is **not** an account-context bypass signal. + * `key` may be used by specific endpoints/business logic, but it must **not** cause the frontend to remove `x-account-id`. + * Only explicit `x-no-account-id: bypass` should strip account context. > [!CAUTION] > **UNSUPPORTED HEADERS:** The header `x-aether-api-token` is **NOT recognized** by the V3 API. If you send it, the backend will treat you as a guest and block access to private data. @@ -587,3 +590,5 @@ If you receive a 403 on a valid ID: 2. Ensure you are sending `x-account-id` and NOT `x-aether-api-token`. 3. Verify the record actually belongs to the account ID you are sending. 4. Check if the object is marked `public_read: True` in the registry. (Posts and Archive Content allow guest access; Journals and Badges do not). +5. Confirm the frontend is not treating `params.key` as an implicit bypass and stripping `x-account-id`. +6. If list/search endpoints work but `GET /v3/crud/{obj_type}/{id}` still returns 403, this is likely endpoint-level policy (e.g., requires stronger auth like JWT) rather than a transport/header bug. diff --git a/src/lib/ae_api/api_get_object.ts b/src/lib/ae_api/api_get_object.ts index 0dc78e71..15dbe3b0 100644 --- a/src/lib/ae_api/api_get_object.ts +++ b/src/lib/ae_api/api_get_object.ts @@ -108,7 +108,6 @@ export const get_object = async function get_object({ const is_valid_bypass = bypass_val === 'bypass' || bypass_val === 'Nothing to See Here' || - params['key'] || bypass_val === 'direct-download'; if (is_valid_bypass) { diff --git a/src/lib/ae_api/api_patch_object.ts b/src/lib/ae_api/api_patch_object.ts index 3c8da66e..e2a62a42 100644 --- a/src/lib/ae_api/api_patch_object.ts +++ b/src/lib/ae_api/api_patch_object.ts @@ -82,7 +82,6 @@ export const patch_object = async function patch_object({ const is_valid_bypass = bypass_val === 'bypass' || bypass_val === 'Nothing to See Here' || - params['key'] || bypass_val === 'direct-download'; if (is_valid_bypass) { diff --git a/src/lib/ae_api/api_post_object.ts b/src/lib/ae_api/api_post_object.ts index 9206824f..057eed67 100644 --- a/src/lib/ae_api/api_post_object.ts +++ b/src/lib/ae_api/api_post_object.ts @@ -104,7 +104,6 @@ export const post_object = async function post_object({ const is_valid_bypass = bypass_val === 'bypass' || bypass_val === 'Nothing to See Here' || - params['key'] || bypass_val === 'direct-download'; if (is_valid_bypass) { diff --git a/src/routes/core/accounts/[account_id]/+page.svelte b/src/routes/core/accounts/[account_id]/+page.svelte index cd222f41..7ee2a062 100644 --- a/src/routes/core/accounts/[account_id]/+page.svelte +++ b/src/routes/core/accounts/[account_id]/+page.svelte @@ -3,9 +3,11 @@ import { onMount } from 'svelte'; import { page } from '$app/stores'; import { load_ae_obj_id__account, + load_ae_obj_li__account, update_ae_obj__account, delete_ae_obj_id__account } from '$lib/ae_core/ae_core__account'; +import { db_core } from '$lib/ae_core/db_core'; import { editable_fields__account } from '$lib/ae_core/ae_core__account.editable_fields'; import { ae_api, ae_loc } from '$lib/stores/ae_stores'; import { goto } from '$app/navigation'; @@ -24,15 +26,60 @@ let loading = $state(true); let saving = $state(false); let save_success = $state(false); let confirm_action = $state(null); +let load_error = $state(null); +let loaded_from_cache = $state(false); async function load_account() { loading = true; - account = await load_ae_obj_id__account({ - api_cfg: $ae_api, - account_id, - log_lvl: 1 - }); - loading = false; + load_error = null; + loaded_from_cache = false; + try { + account = await load_ae_obj_id__account({ + api_cfg: $ae_api, + account_id, + log_lvl: 1 + }); + + if (!account) { + const account_li = await load_ae_obj_li__account({ + api_cfg: $ae_api, + enabled: 'all', + hidden: 'all', + try_cache: false, + log_lvl: 0 + }); + + const account_from_list = + account_li?.find((a: any) => a.account_id === account_id) ?? + null; + + if (account_from_list) { + account = account_from_list; + loaded_from_cache = true; + } + + const cached_account = await db_core.account + .where('account_id') + .equals(account_id) + .first(); + + if (!account && cached_account) { + account = cached_account; + loaded_from_cache = true; + } + + if (!account) { + load_error = + 'Unable to load this account. Authentication or permissions may be missing for this record.'; + } + } + } catch (error) { + console.error('Failed to load account:', error); + load_error = + 'Unable to load this account due to a network or API error. Please retry.'; + } finally { + loading = false; + } } onMount(() => { @@ -40,6 +87,7 @@ onMount(() => { goto('/core'); return; } + load_account(); }); @@ -151,7 +199,36 @@ async function handle_delete() {
+ {:else if load_error} +
+

Account Load Failed

+

{load_error}

+ {#if !$ae_loc.jwt} +

+ This session is using manager passcode access (no JWT). Some account-by-id API routes may require stronger auth than list views. +

+ {/if} +
+ + + Back to Accounts + +
+
{:else if account} + {#if loaded_from_cache} +
+

+ This record loaded from local cache because direct account API access is currently restricted for this session. +

+
+ {/if}
diff --git a/src/routes/core/sites/[site_id]/+page.svelte b/src/routes/core/sites/[site_id]/+page.svelte index 5c467e4f..44d668f9 100644 --- a/src/routes/core/sites/[site_id]/+page.svelte +++ b/src/routes/core/sites/[site_id]/+page.svelte @@ -70,8 +70,8 @@ async function handle_save_site() { } }); - // Ensure cfg_json is included explicitly - data_kv.cfg_json = site.cfg_json; + // Pretty-print cfg_json so it's human-readable in the DB (TEXT column) + data_kv.cfg_json = JSON.stringify(site.cfg_json, null, 2); await update_ae_obj__site({ api_cfg: $ae_api,