toga-ai 1.0.859 → 1.0.861

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.
@@ -3,4 +3,5 @@
3
3
  | Doc | Summary |
4
4
  |-----|---------|
5
5
  | [Store (1.0 Storefront / SAML ACS) Architecture](architecture.md) | 1.0 (App_) storefront that also hosts the SAML ACS for several `*.togahub.com` clients (inbound IdP assertion decrypted here, not in the 2.0 `saml` gateway); op |
6
+ | [Prudential SAML/hash SSO login — user+contact provisioning (and the unsigned ?hash impersonation debt)](features/prudential-sso-login.md) | The Prudential storefront SSO block that provisions the user + contact on login, why it now links `Users.contactId`, and the pre-existing unsigned `?hash=` impe |
6
7
  | [Store SAML SP Certificate & Private Keys (togahub.com)](features/saml-sso-certificate-and-keys.md) | How the store SAML SP credential (`togahub.com` X.509 cert + RSA private key) is stored, loaded and rotated — incl. |
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: "Prudential SAML/hash SSO login — user+contact provisioning (and the unsigned ?hash impersonation debt)"
3
+ framework: "1.0"
4
+ repo: store
5
+ project: Store
6
+ client: prudential
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-21
10
+ owners: [jcardinal]
11
+ files:
12
+ - store/_/app/framework.php
13
+ related:
14
+ - ../architecture.md
15
+ - ../../../../2.0/apps/_underscore/features/client-user-contactid-required.md
16
+ - ../../../../clients/prudential/features/device-information-import-and-contact-linking.md
17
+ ---
18
+ The Prudential storefront SSO block that provisions the user + contact on login, why it now links `Users.contactId`, and the pre-existing unsigned `?hash=` impersonation debt. Open before touching store SSO login or Prudential user provisioning.
19
+
20
+ ## Summary
21
+
22
+ On Prudential SAML SSO (`store/_/app/framework.php`), after the assertion is decrypted the block **finds/creates the user and the contact separately**. It historically **never linked them** — the root cause of ~14k null-`contactId` Prudential users. It now links them.
23
+
24
+ ## How it works
25
+
26
+ - After find/create of the user and contact, the block issues a **non-fatal** `PUT /users/{uuid} {contact:{uuid}}` to link `Users.contactId`.
27
+ - `Users.contactId` is a normal forward FK, so `{uuid}` links safely (not a singular-FK back-reference case).
28
+ - The link checks `isSuccess` and **logs on failure without breaking login** — a failed link must never block a user's SSO.
29
+
30
+ This is the Prudential storefront's slice of the platform-wide contactId initiative — see [client-user-contactid-required](../../../../2.0/apps/_underscore/features/client-user-contactid-required.md). Existing null-contactId Prudential users are backfilled by the `Client_Prudential/2026-09-21a` migration (XID-keeper link) — see [device-information-import-and-contact-linking](../../../../clients/prudential/features/device-information-import-and-contact-linking.md).
31
+
32
+ ## Gotchas
33
+
34
+ - **⚠ SECURITY DEBT — the `?hash=` login trusts an UNSIGNED identity from the URL (impersonation).** After assertion, the Prudential branch base64-encodes a plain `xid:first:last:email` string and redirects to `<returnTo>?hash=<base64>` (`framework.php` ~L120-130). The hash is **not signed / no HMAC**, so any crafted `?hash=` can **impersonate any employee** — it creates the user+contact and reads their devices/ServiceNow assets — and PII rides in the URL query string (web-server logs). Pre-existing; a ticket is being filed separately. Do not extend this handoff without signing it. The store architecture also flags this handoff as unsigned/unencrypted.
35
+
36
+ ## Change history
37
+ - 2026-09-21 — Initial. Added the non-fatal `PUT /users {contact:{uuid}}` link so SSO no longer leaves `contactId` null (root cause of ~14k null-contactId Prudential users). Recorded the pre-existing unsigned `?hash=` impersonation debt as a known security gotcha (ticket filed separately). (jcardinal)
@@ -15,6 +15,7 @@
15
15
  | [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent |
16
16
  | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind Fulfill & Ship: buying/persisting a carrier label (UPS/FedEx) and creating the NetSuite Item Fulfillment; open for label, reprint, retu |
17
17
  | [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | Minimum recipe + traps for running real 2.0 framework code (`_Model`/`_Query`/`_Database`) from a bare `php script.php`; open before writing a CLI harness/backf |
18
+ | [Client Users require a contactId — the postPost backfill interceptor & every creation path](features/client-user-contactid-required.md) | Every `Client_<Tenant>.Users` row must have a non-null `contactId`; the shared `_Model_Client_User::postPost` interceptor guarantees it for api2-created users, |
18
19
  | [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` S3 helpers (copy/get/delete/list) with per-bucket region override and a 5 GB single-PUT limit; open before doing S3 access from 2.0 code. |
19
20
  | [_Cloud Secrets Manager helper (_Cloud::getSecret)](features/cloud-secrets-manager.md) | `_Cloud::getSecret` reads AWS Secrets Manager (own default-provider auth, never throws, per-process cache); open before pulling secret material at request time. |
20
21
  | [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Project-local `_Component_*`/`_Model_*` must declare the app namespace AND live in a `<Name>/<Name>.php` directory, or the autoloader fatals at runtime; open wh |
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: "Client Users require a contactId — the postPost backfill interceptor & every creation path"
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-21
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Model/Client/User.php
13
+ - worker2/Worker/Sync/ActiveDirectory.php
14
+ - worker2/Worker/Netsuite/Opportunity.php
15
+ - dbchanges2/Core/2026-09-21d - Register users postPost contact interceptor.sql
16
+ related:
17
+ - ../../api2/features/api-payload-interceptors.md
18
+ - ../../api2/features/nested-relationship-writes.md
19
+ - ./note-record-authorship.md
20
+ - ./user-email-as-identity-and-password-reset.md
21
+ - ../../../1.0/apps/store/features/prudential-sso-login.md
22
+ - ../../../clients/prudential/features/device-information-import-and-contact-linking.md
23
+ - ../../dbchanges2/features/rerunnable-additive-inserts.md
24
+ ---
25
+ Every `Client_<Tenant>.Users` row must have a non-null `contactId`; the shared `_Model_Client_User::postPost` interceptor guarantees it for api2-created users, and each other creation path was fixed to link one too. Open before touching user creation or `Users.contactId`.
26
+
27
+ ## Summary
28
+
29
+ Decision (2026-09-21, CTO Option B): `Client_<Tenant>.Users.contactId` is made **NOT NULL per tenant** after backfilling a Contact for every user missing one. Null `contactId` was the root of the missing/duplicate-contact bugs, so the goal is to kill it and keep it dead — a **forward fix on every creation path** plus a per-client backfill+ALTER.
30
+
31
+ **Rollout order that matters:** deploy the forward fixes first, then run the per-client backfill+`NOT NULL` migrations. A backfill run before the code fix regenerates null rows.
32
+
33
+ ## How it works
34
+
35
+ ### The shared forward fix — `_Model_Client_User::postPost` (CTO Option B)
36
+
37
+ `postPost(&$api, &$payload)` on the **base client user model** creates a Contact + a primary **Work** email and links `Users.contactId` when an api2-created user has none. Registered as a **postPost interceptor** via `Core.ApiPayloadInterceptors` (`recordId 2` = users, `clientId` + `apiId` NULL = all clients, `prePostProcessing 'POST'`, `httpMethod 'POST'`, guarded insert) — see [api-payload-interceptors](../../api2/features/api-payload-interceptors.md) for how the row makes the hook live.
38
+
39
+ - **No-op** when `contactId` is already set, or the user has no email. Runs **inside the create transaction**.
40
+ - Chosen over a base-model `save()` override for blast radius / "app-level over core" — the interceptor is the row-gated switch, not a change every save inherits.
41
+ - Placed on `_Model_Client_User` **on purpose** (the "row is the switch" pattern): the `apiId NULL / clientId NULL` row makes it fire for every tenant's api2 CRUD.
42
+
43
+ **⚠ Set `Contacts.primaryContactEmailAddressId` SERVER-SIDE — a second model load+save — NOT via a nested payload.** The nested singular-FK path is unreliable: the CREATE path re-parents a value-matched child, and the UPDATE path echoes null. See [nested-relationship-writes](../../api2/features/nested-relationship-writes.md) (singular-FK back-reference defects).
44
+
45
+ ### Every 2.0 user-creation origin path (does it set contactId?)
46
+
47
+ The interceptor covers the **generic api2 CRUD** for everyone. The dedicated creation paths each had to be fixed or already set it:
48
+
49
+ | Path | Repo / code | Client | contactId |
50
+ |---|---|---|---|
51
+ | api2 CRUD (all clients) | `_Model_Client_User::postPost` | all | now linked (interceptor) |
52
+ | AD sync | `worker2/Worker/Sync/ActiveDirectory.php` | True | **fixed** — now creates+links |
53
+ | ClickUp user cache | `worker2/Worker/Netsuite/Opportunity.php` `cacheClickupUser()` (raw SQL) | True | **fixed** — `ensureTrueContactId` |
54
+ | Store SAML/hash SSO | `store/_/app/framework.php` | Prudential | **fixed** — see [prudential-sso-login](../../../1.0/apps/store/features/prudential-sso-login.md) |
55
+ | SAML gateway | `_Model_Rate_ClientAuthentication` (saml) | Rate | already sets it |
56
+ | Compass PeopleFile | worker2 (raw SQL) | Compass | already sets it |
57
+
58
+ Northwell ServiceNow users arrive via api2 eBonding (`_Model_Northwell_Ticket` interceptors), not a dedicated worker, so the interceptor covers them.
59
+
60
+ ## Gotchas
61
+
62
+ - **⚠ A reconstructed `new _Model_Client_User($id)` does NOT carry `contactId`.** In `ActiveDirectory.php` the AD sync reads the existing `contactId` from the **bulk SELECT** and only creates a contact when it is truly missing. A naive `empty($user->contactId)` guard against a freshly reconstructed model reads null even when the row has a contact — and would **mint a duplicate contact every sync**. Trust the SELECT'd value, not the rehydrated model property.
63
+ - **⚠ Set `primaryContactEmailAddressId` with a second server-side load+save, never a nested payload.** The nested singular-FK write re-parents (CREATE) or echoes null (UPDATE).
64
+ - **Interceptor no-ops on a user with no email** — a no-email straggler gets no Contact from this path (the backfill migrations handle those name-only, per client). It is why the fan-out `NOT NULL` skips a client that still has a no-email residual (see [rerunnable-additive-inserts](../../dbchanges2/features/rerunnable-additive-inserts.md)).
65
+
66
+ ## Change history
67
+ - 2026-09-21 — Initial. Decided `Client_*.Users.contactId` goes NOT NULL platform-wide (CTO Option B); built the shared `_Model_Client_User::postPost` interceptor (Core row: recordId 2, clientId+apiId NULL) to create+link a Contact for api2-created users; fixed the worker2 AD-sync and `cacheClickupUser` True paths; recorded the origin map and the reconstructed-model `contactId` gotcha. (jcardinal)
@@ -268,6 +268,50 @@ Worked example: `Client/2026-08-28b - TransferOrderItemsTimestamps.sql` (adds
268
268
  `Client/2026-08-18` `qtyCommitted` file uses a bare `ALTER` and is exactly the shape this avoids** —
269
269
  it is not the template to copy.
270
270
 
271
+ ### ⚠ Guarding a `NOT NULL` ALTER — `sql_mode = ''` SILENTLY coerces leftover NULLs
272
+
273
+ `ALTER … MODIFY col <type> NOT NULL` after a backfill looks safe, but this cluster's `sql_mode` is
274
+ `''` (empty), so MySQL does **not** reject rows that are still NULL — it **silently coerces them to
275
+ `0`** (an invalid FK, e.g. `Users.contactId = 0`). Same `sql_mode` family as the zero-date `dtCreated`
276
+ trap above. Guard the ALTER on a residual count and compile it to the ALTER only when clean:
277
+
278
+ ```sql
279
+ SET @n = (SELECT COUNT(*) FROM Users WHERE contactId IS NULL);
280
+ SET @sql = IF(@n = 0,
281
+ 'ALTER TABLE Users MODIFY contactId INT NOT NULL',
282
+ '<abort-or-skip>'
283
+ );
284
+ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
285
+ ```
286
+
287
+ - **Single-tenant file → abort LOUD.** Make `<abort-or-skip>` a statement that errors under `sql_mode ''`
288
+ — `SELECT \`unknown col\` FROM DUAL` throws *Unknown column*, so the file stops instead of shipping a
289
+ broken column.
290
+ - **Fan-out (`Client/`) file → skip with `DO 0`.** Make `<abort-or-skip>` = `'DO 0'` so one tenant's
291
+ no-email straggler does not abort the ALTER for every other tenant. Wrap the backfill in
292
+ `START TRANSACTION … COMMIT` and put the ALTER **after** the COMMIT.
293
+
294
+ ### ⚠ latin1 schema drift breaks a forced COLLATE in a fan-out file
295
+
296
+ Some tenants' `uuid`/text columns are `latin1`, not utf8mb4 (verified Nychh, Spglobal). So a forced
297
+ `Contacts.uuid COLLATE utf8mb4_0900_ai_ci` in a `Client/` fan-out dies on those tenants:
298
+ `ERROR 1253 COLLATION 'utf8mb4_0900_ai_ci' is not valid for CHARACTER SET 'latin1'`. Join
299
+ charset-agnostically instead:
300
+
301
+ ```sql
302
+ CONVERT(Contacts.uuid USING utf8mb4) COLLATE utf8mb4_0900_ai_ci = <temp.uuid>
303
+ ```
304
+
305
+ A single-tenant file targeting a **known-utf8mb4** client may keep the plain `COLLATE`.
306
+
307
+ ### Backfilling a user→contact link deduped by a client identifier key
308
+
309
+ When users are keyed to a contact by a client identifier (e.g. `c_employeeXid`, `c_sysId`), pick
310
+ **ONE keeper per key** with `ROW_NUMBER()` (prefer primary-email, then name, then oldest id), link
311
+ **all** users in that key group to the keeper, and dedupe residual **creates by key too** — one Contact
312
+ per key group, never one per user when they share a key. Create a Work email only when the user has one;
313
+ a no-email user gets a name-only Contact from its own `firstName`/`lastName`.
314
+
271
315
  ### `LAST_INSERT_ID()` into a session variable ties the whole file to one session
272
316
 
273
317
  The common shape — `INSERT` a parent, `SET @parentId = LAST_INSERT_ID();`, then insert children
@@ -366,6 +410,12 @@ concluding a migration is production-safe.
366
410
  [FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
367
411
 
368
412
  ## Change history
413
+ - 2026-09-21 — Added three lessons from the platform-wide `Users.contactId` NOT NULL backfill: a guarded
414
+ `NOT NULL` ALTER (`sql_mode ''` silently coerces leftover NULLs to `0`, so gate on a residual count —
415
+ single-tenant aborts loud, fan-out `DO 0` skips); latin1 schema drift breaks a forced
416
+ `COLLATE utf8mb4_0900_ai_ci` in a `Client/` fan-out (Nychh/Spglobal are latin1) so join via
417
+ `CONVERT(col USING utf8mb4) COLLATE …`; and the deduped user→contact backfill (one keeper per client
418
+ key via ROW_NUMBER, dedupe residual creates by key, name-only Contact for a no-email user). (jcardinal)
369
419
  - 2026-09-21 — Narrowed the "reuse one uuid literal across tenants" rule: it holds only when the rows share a
370
420
  natural key. An env-specific twin file (guarded on `recordFieldId`, which differs per environment) reusing the
371
421
  literal dies on `Duplicate entry … aclfieldpermissions.uuid`. Also added the DELETE-first transaction rehearsal
@@ -17,6 +17,7 @@
17
17
  | [ClickUp Rich-Text Custom Fields via Quill Delta (API)](features/clickup-richtext-api.md) | How to write formatted text into a ClickUp text/long-text custom field via the undocumented `value_richtext` Quill Delta key; open when a richtext ClickUp field |
18
18
  | [ClickUp Subtask Activity → Parent Opportunity/Epic Comments](features/clickup-subtask-activity.md) | Mirrors subtask status/comment activity up to its top-level Opportunity/Epic as one comment; open for the subtask fan-out, its dedup ledger, or structural ances |
19
19
  | [ClickUp Task Description Fluffer (Talos KB Investigation)](features/clickup-task-fluffer-talos.md) | Async worker that "fluffs" a new ClickUp task by running its name+description through the Talos `dev-core` KB agent and writing the report back; open for the fl |
20
+ | [ClickUp Webhook Health Monitor → OneUptime (Monitor/Clickup/CheckWebhook)](features/clickup-webhook-health-monitor.md) | worker2 cron that checks the team's ClickUp webhook subscriptions and pages via a OneUptime Incoming-Request monitor; open when tuning the ClickUp webhook alarm |
20
21
  | [ClickUp Work Type Automation (Committed / Conditional / Stretch)](features/clickup-work-type-automation.md) | Auto-maintains each ClickUp task's Work Type (Committed/Conditional/Stretch) from its dependencies, status, and due date; open when work-type promotion/demotion |
21
22
  | [worker2 — Committed Secrets & Config Exposure (debt register)](features/committed-secrets-and-config-debt.md) | Standing list of credentials committed to the worker2 repo (locations only); open before adding a credential to worker2 or assuming a config value is private. |
22
23
  | [Compass VIP Support Importer (worker2)](features/compass-vip-support-importer.md) | worker2 action that ingests Compass's quarterly VIP spreadsheet and sets each VIP's support tech (`Users.c_supportedByUserId` in `Client_Compass`); open when wo |
@@ -6,13 +6,14 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [ajean]
9
+ updated: 2026-09-21
10
+ owners: [ajean, jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Clickup/Health.php
13
13
  - worker2/Database/ClickupHealthWatchdog.sql
14
14
  related:
15
15
  - ./clickup-project-routing.md
16
+ - ./clickup-webhook-health-monitor.md
16
17
  - ../architecture.md
17
18
  ---
18
19
 
@@ -41,6 +42,10 @@ Data model:
41
42
  - **Inbound query must match `Clickup/Webhook` exactly.** `LIKE 'Clickup/%'` also matches the watchdog's own `Clickup/Health/Check` rows and the timer never expires.
42
43
  - **Email identifier is `'True'`** (internal/team `clientIdentifier`, via `_Worker_Notification_Email::Send`).
43
44
  - **Business window enforced in code too** (Mon–Fri 09:00–17:00 Central), not just the cron — a manual off-hours run is a safe no-op.
45
+ - **⚠ Latent blind spot: the webhook-health probe only sees webhooks the query token created.** `GET /team/{id}/webhook` returns **only** webhooks created by the calling identity. `Check()` queries as the worker2 service account (`_Config::clickup('token')`, `pk_87469444`), so a webhook owned by a **different** ClickUp identity is invisible — the probe sees zero webhooks and always reports healthy, and can never catch a real failure. Proven 2026-09-21 on team `8577082` (the live webhook was owned by an OAuth app, user `12633140`). Fix is to keep the monitored webhook **created under the same service token** — see [ClickUp Webhook Health Monitor](./clickup-webhook-health-monitor.md) for the full gotcha and the recreation done on 2026-09-21. (Not yet fixed in code here.)
46
+
47
+ ## Change history
48
+ - 2026-09-21 — Recorded the token-ownership blind spot in the webhook-health probe (queries as the service account, so a webhook created under another identity reads as "no webhooks / healthy"). Sibling OneUptime monitor added — see [ClickUp Webhook Health Monitor](./clickup-webhook-health-monitor.md). (jcardinal)
44
49
 
45
50
  ## Related
46
51
 
@@ -0,0 +1,52 @@
1
+ ---
2
+ title: ClickUp Webhook Health Monitor → OneUptime (Monitor/Clickup/CheckWebhook)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-21
10
+ owners: [jcardinal]
11
+ files:
12
+ - worker2/Worker/Monitor/Clickup.php
13
+ related:
14
+ - ./oneuptime-worker2-monitoring.md
15
+ - ./clickup-connectivity-watchdog.md
16
+ - ./clickup-project-routing.md
17
+ - ../workflows/clickup-credential-rotation.md
18
+ ---
19
+
20
+ worker2 cron that checks the team's ClickUp webhook subscriptions and pages via a OneUptime Incoming-Request monitor; open when tuning the ClickUp webhook alarm or the token it queries under.
21
+
22
+ ## Summary
23
+
24
+ `_Worker_Monitor_Clickup::CheckWebhook()` (action `Monitor/Clickup/CheckWebhook`, cron `*/10 * * * *` Central) is a **"dumb reporter, smart monitor"** heartbeat on the [OneUptime push-metric pattern](./oneuptime-worker2-monitoring.md). No DB. Modeled on `_Worker_Monitor_Operations`.
25
+
26
+ Catches the **silent webhook outage** EB health checks cannot see: if ClickUp suspends or fails our inbound webhook (`https://webhook.togahub.com/clickup`), inbound tasks stop arriving and nothing else notices.
27
+
28
+ Distinct from and **stricter than** the sibling email watchdog [`_Worker_Clickup_Health`](./clickup-connectivity-watchdog.md): this alarms on any `togahub.com` webhook not `health.status = "active"`; the watchdog only emails on `status = "failing"` in business hours.
29
+
30
+ ## How it works
31
+
32
+ - Reads subscriptions: `_Component_Api_Clickup::send('GET','/team')` → for each team `_Component_Api_Clickup::send('GET','/team/{id}/webhook')`.
33
+ - POSTs a metric body via `_ApiRequest` to a OneUptime incoming-request heartbeat URL: `alarm` (`OK`/`DEGRADED`), `probe` (`OK`/`DEGRADED`), plus `webhookCount`, `activeCount`, `failures`.
34
+ - **Alarm rule:** `DEGRADED` when any webhook whose endpoint contains `togahub.com` is not `health.status = "active"`.
35
+ - **Push URL is an in-file `$ONEUPTIME_URL` local, not `[oneuptime]` config** — the established team convention for every `Worker/Monitor/*` file (Operations, Staples, Compass, Aig, Prudential, Nycdoe, Fleet). A cso review suggested reading it from config; the developer rejected that on purpose — the heartbeat URL is treated as low-sensitivity and stays inline, matching the siblings. See the credential note in [OneUptime push-metric monitors](./oneuptime-worker2-monitoring.md). Ships blank with a soft-fail guard until the real URL is pasted in.
36
+ - Cron row: `dbchanges2/Core/2026-09-21c - Insert ClickUp Webhook Health Monitor CronJob.sql`. OneUptime import artifact `clickup-webhook-health.json` lives in a local staging dir, not the repo (import artifacts are not version-controlled — see the pattern doc's provisioning runbook).
37
+
38
+ ## Gotchas
39
+
40
+ - **`GET /team/{id}/webhook` only returns webhooks CREATED BY the calling token.** A webhook owned by a different ClickUp identity is **invisible** even on the same team. Proven 2026-09-21: the worker2 service token (`_Config::clickup('token')`, `pk_87469444` "Development Team") saw **zero** webhooks on team `8577082`, while the webhook actually feeding worker2 was owned by a different identity (an OAuth app, user `12633140`) and visible only to that token. So the monitor (and `_Worker_Clickup_Health`) can only see a webhook if it was created under the **same token they query with** — recreate any monitored webhook under the service account.
41
+ - **`pk_` personal tokens go RAW in the `Authorization` header (no `Bearer`); OAuth access tokens use `Bearer`.** `_Component_Api_Clickup::send()` sends the token raw, so it works with a `pk_` token and would need the `Bearer` prefix for an OAuth token.
42
+ - Keep the metric POST non-fatal — a monitoring outage must never break the worker job.
43
+
44
+ ## Change history
45
+ - 2026-09-21 — Created `_Worker_Monitor_Clickup::CheckWebhook` (OneUptime, `*/10`, stricter than the email watchdog). Recreated the inbound webhook under the service account (`pk_87469444`, new id `bd6168ab-…`, deleting the old OAuth-owned one) so both this monitor and `_Worker_Clickup_Health` can see it — the two were previously blind to it (token-ownership gotcha above). (jcardinal)
46
+
47
+ ## Related
48
+
49
+ - [OneUptime push-metric monitors for 2.0 workers](./oneuptime-worker2-monitoring.md) — the reporter pattern, payload contract, and monitor-import JSON shape.
50
+ - [ClickUp Connectivity Watchdog](./clickup-connectivity-watchdog.md) — the sibling email-alert webhook-health cron (`_Worker_Clickup_Health`).
51
+ - [ClickUp Project & Opportunity Multi-List Routing](./clickup-project-routing.md) — the integration this protects.
52
+ - [Rotating the ClickUp API credential](../workflows/clickup-credential-rotation.md) — the service-account token this queries under.
@@ -14,13 +14,13 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
14
14
  - **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
15
15
  - **test** (Test) — 16 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
16
16
  - **toga** (TOGa) — 3 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
17
- - **store** (Store) — 2 doc(s) → [1.0/apps/store/INDEX.md](1.0/apps/store/INDEX.md)
17
+ - **store** (Store) — 3 doc(s) → [1.0/apps/store/INDEX.md](1.0/apps/store/INDEX.md)
18
18
  - **tools** (Tools) — 18 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
19
19
 
20
20
  ## 2.0 framework
21
21
 
22
- - **_underscore** (_Underscore) _(framework core)_ — 88 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
23
- - **worker2** (Worker) — 73 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
+ - **_underscore** (_Underscore) _(framework core)_ — 89 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
23
+ - **worker2** (Worker) — 74 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
24
24
  - **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
25
25
  - **dbchanges2** (Database Changes) _(framework core)_ — 19 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
26
26
  - **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -55,6 +55,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
55
55
  - **Endeavor Health** (`endeavor-health`) → [clients/endeavor-health/INDEX.md](clients/endeavor-health/INDEX.md)
56
56
  - **Fordham University** (`fordham`) → [clients/fordham/INDEX.md](clients/fordham/INDEX.md)
57
57
  - **GroWrk** (`growrk`) → [clients/growrk/INDEX.md](clients/growrk/INDEX.md)
58
+ - **Managelife** (`managelife`) → [clients/managelife/INDEX.md](clients/managelife/INDEX.md)
58
59
  - **Northwell Health** (`northwell`) → [clients/northwell/INDEX.md](clients/northwell/INDEX.md)
59
60
  - **New York City Department of Education** (`nycdoe`) → [clients/nycdoe/INDEX.md](clients/nycdoe/INDEX.md)
60
61
  - **NYC Health & Hospitals** (`nychh`) → [clients/nychh/INDEX.md](clients/nychh/INDEX.md)
@@ -0,0 +1,5 @@
1
+ # Client: Managelife `managelife`
2
+
3
+ | Doc | Framework | Summary |
4
+ |-----|-----------|---------|
5
+ | [Managelife](profile.md) | 2.0 | Managelife 2.0 platform tenant (`Client_Managelife`). |
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: "Managelife"
3
+ framework: "2.0"
4
+ apps:
5
+ - dbchanges2
6
+ project: Database Changes
7
+ client: managelife
8
+ type: profile
9
+ status: active
10
+ updated: 2026-09-21
11
+ owners: [jcardinal]
12
+ files: []
13
+ related:
14
+ - ../../2.0/apps/_underscore/features/client-user-contactid-required.md
15
+ ---
16
+
17
+ Managelife 2.0 platform tenant (`Client_Managelife`). Stub profile — expand as work touches it. Formal client name not yet confirmed.
18
+
19
+ ## Summary
20
+
21
+ - 2.0 tenant with a `Client_Managelife` database.
22
+ - First recorded work: the platform-wide `Users.contactId` NOT NULL backfill — `Client_Managelife/2026-09-21a` gave its 1 user (id 5, no email) a name-only Contact from its own `firstName`/`lastName`, linked it, then set `contactId` NOT NULL. See [client-user-contactid-required](../../2.0/apps/_underscore/features/client-user-contactid-required.md).
@@ -57,6 +57,11 @@ Resumable, unit-driven. For each `Unit` with `contactId` NULL it queries Service
57
57
  - **No `continue`** in the helpers added to this cron (team standard); the new backfill file is tab-indented (the legacy cron body is space-indented — only the added helpers were converted).
58
58
  - API auth uses the `App_Api_Toga2::CLIENT_UUID_PRUDENTIAL` / `API_UUID_PRUDENTIAL` / `API_SECRET_PRUDENTIAL` constants (names only — values live in config, not here).
59
59
 
60
+ - **⚠ `Client_Prudential.Users.contactId` is now NOT NULL — and do NOT blind email-create contacts for Prudential.** The `Client_Prudential/2026-09-21a` backfill linked ~14,298 null-contactId users to their `c_employeeXid` **keeper** contact (ROW_NUMBER: primary-email, then name, then oldest id), created a Contact+email for only ~20 residual, then set `contactId` NOT NULL. Prudential's Contacts were just deduped 893k→19.7k, so a blanket email-create backfill would **re-pollute** them — link by XID keeper, never create-per-user. Part of the platform-wide contactId initiative: [client-user-contactid-required](../../../2.0/apps/_underscore/features/client-user-contactid-required.md).
61
+
62
+ ## Change history
63
+ - 2026-09-21 — `Client_Prudential.Users.contactId` made NOT NULL after the `2026-09-21a` backfill linked null-contactId users to their XID keeper contact. Noted the "never blind email-create for Prudential" rule (would re-pollute the just-deduped Contacts) and cross-linked the platform contactId initiative. (jcardinal)
64
+
60
65
  ## Related
61
66
  - [Prudential profile](../profile.md)
62
67
  - [2.0 api2 — Nested-relationship writes & child matching](../../../2.0/apps/api2/features/nested-relationship-writes.md) — the general api2 behavior this incident exposed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.859",
3
+ "version": "1.0.861",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",