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.
- package/knowledge/1.0/apps/store/INDEX.md +1 -0
- package/knowledge/1.0/apps/store/features/prudential-sso-login.md +37 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/client-user-contactid-required.md +67 -0
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +50 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/clickup-connectivity-watchdog.md +7 -2
- package/knowledge/2.0/apps/worker2/features/clickup-webhook-health-monitor.md +52 -0
- package/knowledge/INDEX.md +4 -3
- package/knowledge/clients/managelife/INDEX.md +5 -0
- package/knowledge/clients/managelife/profile.md +22 -0
- package/knowledge/clients/prudential/features/device-information-import-and-contact-linking.md +5 -0
- package/package.json +1 -1
|
@@ -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-
|
|
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.
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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) —
|
|
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)_ —
|
|
23
|
-
- **worker2** (Worker) —
|
|
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,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).
|
package/knowledge/clients/prudential/features/device-information-import-and-contact-linking.md
CHANGED
|
@@ -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