- calculateEffectiveGrants queried CustomerProducts/AccessGrants with
(customer,eq,N), which matches the link field's DISPLAY NAME and always
returns 0 rows -> desiredPaths always [] -> W1/W2/W3 write empty scopes.
- Regression from the shared-lib refactor (3ecee30): W2 had already fixed
this in 36a6b3b but the shared extract reverted to the old syntax.
- Correct syntax (customer.Id,eq,N) verified against live NocoDB for
customer 6 -> /Class/Health, /Class/Other, /Template/Music.
- Updated all three test mocks to match the corrected where clause.
- Extract calculateEffectiveGrants into n8n/shared/effective-grants.js
(single source of truth), inlined at build time into W1/W2/W3 via
gen_w*.js + inline_shared.js — removes the duplicated algorithm.
- W1 now provisions with a status branch:
suspended/deleted -> disable AList user + clear role scopes (soft delete)
active -> create/update user+role, RECOMPUTE desired scopes (never restore
stale), re-enable
- Loop guard corrected: check AList user LIVE disabled state via
GET /api/admin/user/get (sync_status alone breaks suspend->resume).
- W2/W3 rebuilt on shared lib (identical behaviour).
- Tests: test_effective_grants.js (15), test_w3_refactored.js (13),
test_w1_lifecycle.js (23) — all green.
- Drop superseded w2-compute.js / w3-compute.js / test_w3.js.
Root cause: expires_at == today was NOT filtered (used '< today'), so a
product expiring ON today kept its resources in desired scopes -> no diff
-> no removal. Changed to '<='. Applied to W3 (CP + grant loops) and W2
(active filter). Also W2 previously had NO expiry filter on active CPs at
all.
AList expects bare token (not 'Bearer <token>'). With 'Bearer' prefix,
all role/get calls returned non-data responses -> currentPaths degraded
to [] -> W3 kept re-applying same scopes every run (false toAdd) and
never detected expired scopes (false toRemove). This is the root cause
of W3 not showing hoe's expiry removal.