toga-ai 1.0.574 → 1.0.577
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/knowledge/2.0/apps/api2/INDEX.md +1 -0
- package/knowledge/2.0/apps/api2/features/login-cross-client-user-resolution.md +76 -0
- package/knowledge/2.0/apps/api2/features/surface-meta-option.md +9 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/elite/profile.md +1 -1
- package/package.json +1 -1
- package/scripts/hooks/kickoff-gate.js +12 -9
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [ENVIRONMENT (not the EB environment name) decides the _underscore branch and Config file](features/environment-variable-drives-underscore-branch.md) | An api2 Elastic Beanstalk instance decides **which `_underscore` branch it clones** and **which `Config/<env>.ini` it loads** from the EB environment property * | api2/.ebextensions/git.php, api2/.ebextensions/php_include_underscore.config, api2/.ebextensions/git.sandbox-dev.json, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, api2/Component/Api/V2/V2.php |
|
|
10
10
|
| [Health-check endpoint (/health liveness short-circuit)](features/health-check-endpoint.md) | `_Controller_Index::api()` short-circuits **liveness/health-probe** requests to an HTTP 200 **before** any routing, DB bootstrap, or V2 engine work runs. | api2/Controller/Index.php |
|
|
11
11
|
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text — `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
|
|
12
|
+
| [/auth/login resolves the client from the email domain, not the Bearer token (cross-client user path)](features/login-cross-client-user-resolution.md) | `POST /v2/auth/login` (email/password user login) can silently swap the target client mid-request. | api2/Component/Api/V2/V2.php, _underscore/String.php |
|
|
12
13
|
| [Nested FK object embedding is gated by the CHILD record's own ACL](features/nested-fk-acl-embedding.md) | When the V2 JSON engine serializes a foreign-key field into a **nested object** (in `getFullModelData()`, ~V2.php L6016-6060), it re-checks the **child** record | api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-23b - PurchaseOrdersRecordReadAcl.sql |
|
|
13
14
|
| [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/ContactEmailAddress.php |
|
|
14
15
|
| [Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)](features/record-scripts.md) | In api2 you almost never write a controller. | api2/Component/Api/V2/V2.php, _underscore/Model/Team/Sprint.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Query.php |
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "/auth/login resolves the client from the email domain, not the Bearer token (cross-client user path)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: api2
|
|
5
|
+
project: API
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-13
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- api2/Component/Api/V2/V2.php
|
|
13
|
+
- _underscore/String.php
|
|
14
|
+
related:
|
|
15
|
+
- ./cross-client-data-retrieval.md
|
|
16
|
+
- ./surface-meta-option.md
|
|
17
|
+
- ../../_underscore/features/per-client-database-connections.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What it is
|
|
21
|
+
|
|
22
|
+
`POST /v2/auth/login` (email/password user login) can silently swap the target client
|
|
23
|
+
mid-request. When the credentialed user is a **cross-client (home) user**, the engine
|
|
24
|
+
abandons the client it started on and re-registers the user's **home** client's databases
|
|
25
|
+
before minting the token. Understanding this path is the difference between diagnosing a
|
|
26
|
+
login `EO-1` in minutes vs. hours — especially in local dev, where the home client's DB
|
|
27
|
+
usually does not exist.
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
`_Component_Api_V2::execute()` (~L701-744):
|
|
32
|
+
|
|
33
|
+
1. The user is loaded **by email** from the current client's DB (~L705) — the client is
|
|
34
|
+
resolved from `ClientEmailDomains`, **not** from any Bearer token.
|
|
35
|
+
2. The submitted password is checked with `_String::passwordVerify()` → `password_verify()`
|
|
36
|
+
(bcrypt) against the local user row (`_underscore/String.php` L563-576).
|
|
37
|
+
3. **If the password does NOT verify AND the loaded user row has a non-null `clientId` that
|
|
38
|
+
differs from the current client**, the engine treats it as a **cross-client user**: it
|
|
39
|
+
calls `registerClientDatabases($user->clientId)` and re-loads the *home* user via
|
|
40
|
+
`homeClientUserId` (~L716-731). Authentication then completes against the home client.
|
|
41
|
+
|
|
42
|
+
This is the same `homeClientUserId` bridge that
|
|
43
|
+
[cross-client data retrieval](./cross-client-data-retrieval.md) uses to fan a home user out
|
|
44
|
+
across tenant DBs — here it governs which client a login actually authenticates against.
|
|
45
|
+
|
|
46
|
+
## Gotchas
|
|
47
|
+
|
|
48
|
+
- **A login `EO-1 "Unknown database 'client_<x>'"` is the cross-client branch failing to
|
|
49
|
+
open the home client's DB.** Observed: logging into **Elite** locally with
|
|
50
|
+
`devteam@togatech.com` throws `EO-1 Unknown database 'client_true'`. That account is an
|
|
51
|
+
internal TOGA (client `true` / `Client_True`) user shared into Elite via
|
|
52
|
+
`homeClientUserId`; because its Elite-local password does not verify, the cross-client
|
|
53
|
+
path tries to open `Client_True`, which does not exist in local dev. It works on the dev
|
|
54
|
+
sandbox only because `Client_True` exists there.
|
|
55
|
+
- **Local-dev workaround (never commit):** set the **local** `Client_<Client>` user's own
|
|
56
|
+
bcrypt password hash to a known value so login succeeds on the **direct** path and never
|
|
57
|
+
enters the cross-client branch. Generate with `_String::passwordHash()` /
|
|
58
|
+
`password_hash($pw, PASSWORD_BCRYPT)`. This is a per-machine local fixup, not a code or
|
|
59
|
+
data change to commit.
|
|
60
|
+
- **The api2 dev server talks to `localhost` as `root` — sandbox DB edits are invisible to
|
|
61
|
+
it.** Local stack traces show `mysqli_connect(["localhost","root",...])`. Any ACL/grant/
|
|
62
|
+
password change must be applied to the **localhost** client DB, not a sandbox connection
|
|
63
|
+
in a DB GUI, or the local API will never see it. (In DbGate specifically: "Run"
|
|
64
|
+
autocommits; a query-tab "unsaved" label refers to the unsaved `.sql` *script file*, not
|
|
65
|
+
pending uncommitted data.) This exact confusion — a grant present in sandbox while the API
|
|
66
|
+
read localhost — cost real debugging time.
|
|
67
|
+
|
|
68
|
+
## Change history
|
|
69
|
+
- 2026-08-13 — Initial capture from an Elite local-dev login debug session (no code
|
|
70
|
+
changes). Documented that `/auth/login` resolves the client from the email domain and, on
|
|
71
|
+
a local-password miss for a user whose `clientId` differs, follows the `homeClientUserId`
|
|
72
|
+
cross-client branch (`V2.php` ~L701-744), which throws `EO-1 Unknown database` when the
|
|
73
|
+
home client's DB is absent locally; plus the local-dev workarounds (set the local bcrypt
|
|
74
|
+
hash; apply changes to the localhost DB, not sandbox). (apeterson)
|
|
75
|
+
</content>
|
|
76
|
+
</invoke>
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-13
|
|
10
10
|
owners: [jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -94,6 +94,14 @@ FE, the type `RecordSurfaceState` (`src/surface/types.ts`) is
|
|
|
94
94
|
silently lack it.
|
|
95
95
|
|
|
96
96
|
## Change history
|
|
97
|
+
- 2026-08-13 — Confirmed the per-client seed gap in the wild: **Elite** was missing the
|
|
98
|
+
`surfaces/meta` dispatch grant in its client DB, so `GET /v2/surfaces/meta` 403'd EZ-1
|
|
99
|
+
with `identifiers.script = "meta"` while other clients worked. Concrete reference data:
|
|
100
|
+
Core `Records` route `surfaces` id **333** (`aclDatabase = CORE`), Core `RecordScripts`
|
|
101
|
+
`meta` id **28** (GET); the grant lives in `Client_<slug>.AclRecordScripts`
|
|
102
|
+
(`recordScriptId = 28`, `roleId = 1` for CORE Public). This is a per-client seed-gap class
|
|
103
|
+
— the grant must be captured in a `dbchanges2` `Client_<Name>` migration, not just
|
|
104
|
+
inserted locally. (apeterson)
|
|
97
105
|
- 2026-07-20 — Documented the `meta.surface`/`recordSurfaceState` payload flow: computed only when the
|
|
98
106
|
`surface` option is present AND action is READ (V2.php ~5688-5717); single-GET shape
|
|
99
107
|
`{slug, state:{<elementUuid>:{isVisible,isEnabled}}}`, LIST shape
|
package/knowledge/INDEX.md
CHANGED
|
@@ -20,7 +20,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
20
20
|
|
|
21
21
|
- **_underscore** (_Underscore) _(framework core)_ — 58 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 49 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
|
-
- **api2** (API) —
|
|
23
|
+
- **api2** (API) — 24 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 6 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
package/package.json
CHANGED
|
@@ -68,12 +68,15 @@ function readPayload() {
|
|
|
68
68
|
try { data.tool_input = JSON.parse(process.env.CLAUDE_TOOL_INPUT); } catch (e) {}
|
|
69
69
|
}
|
|
70
70
|
if (!data.prompt && process.env.CLAUDE_USER_PROMPT) data.prompt = process.env.CLAUDE_USER_PROMPT;
|
|
71
|
-
if (!data.session_id
|
|
71
|
+
if (!data.session_id) data.session_id = process.env.CLAUDE_SESSION_ID || process.env.CLAUDE_CODE_SESSION_ID;
|
|
72
72
|
return data;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function lockPath() {
|
|
76
|
-
|
|
76
|
+
// Anchor to the project dir (reliably provided by the harness) rather than the shell's
|
|
77
|
+
// cwd, so the hook and a manually-run `kickoff-primed` always agree on the lock location.
|
|
78
|
+
const base = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
79
|
+
return path.join(base, '.claude', '.kickoff-gate.json');
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
function readLock() {
|
|
@@ -250,10 +253,10 @@ function main() {
|
|
|
250
253
|
// preflight ADVANCES to stage 2 (does NOT release) — the team KB is still unprimed.
|
|
251
254
|
if (isPreflightBash(command)) { writeLock(lock.session, 'primer'); process.exit(0); }
|
|
252
255
|
if (isPrimingBash(command)) process.exit(0); // other read-only priming step
|
|
253
|
-
console.
|
|
256
|
+
console.error(armedMessage());
|
|
254
257
|
process.exit(2);
|
|
255
258
|
}
|
|
256
|
-
if (BLOCKED_TOOLS.has(tool)) { console.
|
|
259
|
+
if (BLOCKED_TOOLS.has(tool)) { console.error(armedMessage()); process.exit(2); }
|
|
257
260
|
process.exit(0); // AskUserQuestion, Skill, TodoWrite, … — part of the interview
|
|
258
261
|
}
|
|
259
262
|
|
|
@@ -263,7 +266,7 @@ function main() {
|
|
|
263
266
|
// Priming must run in the MAIN session; delegating it (or fanning out other agents
|
|
264
267
|
// alongside it) is exactly what this stage forbids.
|
|
265
268
|
if (tool === 'Agent' || tool === 'Task') {
|
|
266
|
-
console.
|
|
269
|
+
console.error(primerMessage());
|
|
267
270
|
process.exit(2);
|
|
268
271
|
}
|
|
269
272
|
if (tool === 'Bash') {
|
|
@@ -271,22 +274,22 @@ function main() {
|
|
|
271
274
|
// has read every resolved doc itself, in the main session.
|
|
272
275
|
if (isPrimedSentinelBash(command)) { clearLock(); process.exit(0); }
|
|
273
276
|
if (isPrimingBash(command)) process.exit(0); // knowledge.js search/get, version check, …
|
|
274
|
-
console.
|
|
277
|
+
console.error(primerMessage());
|
|
275
278
|
process.exit(2);
|
|
276
279
|
}
|
|
277
280
|
if (tool === 'Read') {
|
|
278
281
|
const p = toolInput.file_path || toolInput.path;
|
|
279
282
|
if (isMemoryReadPath(p)) process.exit(0); // repo-path lookup
|
|
280
283
|
if (isTeamKbPath(p)) process.exit(0); // reading a team-KB doc IS the priming
|
|
281
|
-
console.
|
|
284
|
+
console.error(primerMessage()); // repo code — blocked until primed
|
|
282
285
|
process.exit(2);
|
|
283
286
|
}
|
|
284
287
|
if (tool === 'Grep' || tool === 'Glob') {
|
|
285
288
|
if (isTeamKbSearch(toolInput)) process.exit(0); // locating a doc inside knowledge/
|
|
286
|
-
console.
|
|
289
|
+
console.error(primerMessage()); // searching repo code — blocked
|
|
287
290
|
process.exit(2);
|
|
288
291
|
}
|
|
289
|
-
if (BLOCKED_TOOLS.has(tool)) { console.
|
|
292
|
+
if (BLOCKED_TOOLS.has(tool)) { console.error(primerMessage()); process.exit(2); } // Edit/Write/…
|
|
290
293
|
process.exit(0); // AskUserQuestion (ask for a repo path), Skill, … — allowed
|
|
291
294
|
}
|
|
292
295
|
|