@dossier-wiki/os-sdk 1.4.1 → 1.4.2-staging.5.1

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/README.md CHANGED
@@ -10,6 +10,14 @@ npm install @dossier-wiki/os-sdk
10
10
  bun add @dossier-wiki/os-sdk
11
11
  ```
12
12
 
13
+ To build against the API currently on staging, install the `staging` dist-tag
14
+ instead. It is a prerelease built from the `staging` branch and may change
15
+ without a changelog entry:
16
+
17
+ ```sh
18
+ npm install @dossier-wiki/os-sdk@staging
19
+ ```
20
+
13
21
  ## Quick start
14
22
 
15
23
  ```ts
@@ -46,7 +54,7 @@ The service composes resource namespaces — call them off the root client:
46
54
  | `os.channelEndpoints` | Read and manage POS terminal, page, API client, and other endpoint identities resolved during ingestion |
47
55
  | `os.individuals` | Individual customers + nested `identificationDocuments`, `faceImages` |
48
56
  | `os.entities` | Entity customers + nested `stakeholders` |
49
- | `os.customers` | Cross-cutting per-customer: `notes`, `files`, `screening`, `riskRating`, `riskAssessments`, `expectedActivity`, `devices`, `assignment` (assign / unassign / assignMany) |
57
+ | `os.customers` | Cross-cutting per-customer: `notes`, `files`, `screening`, `kyc` (start, review, PDF report, evidence ZIP), `riskRating` (set an officer override / `clearOverride`), `riskAssessments` (list, submit a questionnaire, `evaluate` the risk matrix now), `expectedActivity`, `devices`, `assignment` (assign / unassign / assignMany) |
50
58
  | `os.teams` | Domain teams: list/get/create/update/archive/delete, plus `addMembers` / `removeMembers`. Teams carry catalogue-governed `team.tags`, and `list({ tags })` filters on them |
51
59
  | `os.accounts` | Per-customer accounts, including `freeze` / `unfreeze` |
52
60
  | `os.counterparties` | Counterparties + nested `riskAssessments`, `files` |
@@ -62,8 +70,8 @@ The service composes resource namespaces — call them off the root client:
62
70
  | `os.branches` | The institution's own physical or organizational units — list/get/create/update |
63
71
  | `os.exchangeRates` | Push exchange rates |
64
72
  | `os.states` | Read and atomically set, increment, or decrement resource state; read immutable change history |
65
- | `os.alerts` | Raise alerts by hand (`create` — free-form, or against an optional custom alert type; org-scoped `externalRequestId` idempotency), read any alert (`get`), edit markdown `notes` (`updateNotes`), link records (`resources.list/add/remove`), plus `comments` and `attemptEvidence`. Needs the `alerts:read` / `alerts:write` API-key permissions |
66
- | `os.config` | `rules` (incl. `rules.backtest` — dry-run simulation over historical or synthetic transactions), `constants`, `states`, `alertWorkflows` (one per alert type — `SCREENING`, `RULE_TRIGGER`, `CUSTOM`: list/get/save), `assignmentPolicies` (read-only legacy archive), `products`, `currencies`, `eventDefinitions`, `customAlertTypes` (org-defined alert types with a typed-field schema), `webhooks`, `tagCatalogue`, `sync`, plus org-wide `reset` |
73
+ | `os.alerts` | Raise alerts by hand (`create` — free-form, or against an optional custom alert type; org-scoped `externalRequestId` idempotency), read any alert (`get`, with `isBlocked`), edit markdown `notes` (`updateNotes`), link records (`resources.list/add/remove`), make an alert wait on another (`dependencies.list/add/remove` — see [Blocking alerts](#blocking-alerts)), plus `comments` and `attemptEvidence`. Needs the `alerts:read` / `alerts:write` API-key permissions |
74
+ | `os.config` | `rules` (incl. `rules.backtest` — dry-run simulation over historical or synthetic transactions), `constants`, `states`, `alertWorkflows` (one per alert type — `SCREENING`, `RULE_TRIGGER`, `CUSTOM`: list/get/save), `assignmentPolicies` (read-only legacy archive), `products`, `profileTemplates` (the template a customer is created against: profile fields, risk matrix, KYC workflows, creation-form setup), `currencies`, `eventDefinitions`, `customAlertTypes` (org-defined alert types with a typed-field schema), `webhooks`, `tagCatalogue`, `sync`, plus org-wide `reset` |
67
75
  | `os.setup` | Org setup status + initialization |
68
76
  | `os.settings` | Sync snapshots + drift |
69
77
  | `os.reports` | On-demand PDF reports: `alert`, `customerProfile`, `counterpartyProfile`, `resourceProfile` — each resolves to the PDF bytes (`Uint8Array`). Needs the `reports:read` API-key permission (included in Full Access) |
@@ -373,10 +381,289 @@ Migration:
373
381
  - Use `status` for the initial customer lifecycle state. New records default to
374
382
  `ONBOARDING`; `monitoringStatus` and `riskRating` are not create inputs.
375
383
 
384
+ ## Profile templates & risk matrix
385
+
386
+ A **Profile Template** is to a customer what a **Product** is to an account:
387
+ the template it is created against. It declares the customer's typed
388
+ **profile fields** (`fieldConfig`, the same `TypedFieldDefinition` shape as
389
+ `Product.accountFieldConfig`), a **risk matrix** that computes the customer's
390
+ risk rating from those fields, KYC workflow bindings, and the **creation
391
+ setup** of the simple Add Individual / Add Entity forms. Templates live under
392
+ `os.config.profileTemplates` and need `config:read` / `config:write`.
393
+
394
+ ```ts
395
+ // 1. Define a template (once, or via Tenant Sync).
396
+ await os.config.profileTemplates.create({
397
+ key: 'individual-standard', // immutable; connectors use it instead of a UUID
398
+ name: 'Individual — standard onboarding',
399
+ partyType: 'INDIVIDUAL',
400
+ isDefault: true, // new individuals get it when no key is sent
401
+ fieldConfig: [
402
+ { key: 'pepStatus', displayName: 'PEP status', type: 'SINGLE_SELECT', required: false,
403
+ options: ['No PEP involvement', 'Former PEP', 'Domestic PEP'] },
404
+ { key: 'onboardingChannel', displayName: 'Onboarding channel', type: 'SINGLE_SELECT', required: false,
405
+ options: ['Face to face', 'Non face to face'] },
406
+ ],
407
+ riskMatrix: {
408
+ scale: [{ level: 'LOW', score: 1 }, { level: 'MEDIUM', score: 2 }, { level: 'HIGH', score: 3 }],
409
+ bands: [{ rating: 'LOW', below: 1.67 }, { rating: 'MEDIUM', below: 2.34 }, { rating: 'HIGH' }],
410
+ factors: [
411
+ { key: 'pepStatus', label: 'PEP status', weight: 0.6, field: 'pepStatus', levels: [
412
+ { option: 'No PEP involvement', level: 'LOW' },
413
+ { option: 'Former PEP', level: 'MEDIUM' },
414
+ { option: 'Domestic PEP', level: 'HIGH' },
415
+ ] },
416
+ { key: 'channel', label: 'Onboarding channel', weight: 0.4, field: 'onboardingChannel', levels: [
417
+ { option: 'Face to face', level: 'LOW' },
418
+ { option: 'Non face to face', level: 'HIGH' },
419
+ ] },
420
+ ],
421
+ escalations: [
422
+ { label: 'PEP involvement', minimumRating: 'HIGH',
423
+ when: { field: 'factor.pepStatus.level', operator: '=', value: 'HIGH' } },
424
+ ],
425
+ outcomes: [
426
+ { rating: 'LOW', requiredDueDiligence: 'SDD', reviewEveryMonths: 36 },
427
+ { rating: 'MEDIUM', requiredDueDiligence: 'CDD', reviewEveryMonths: 24 },
428
+ { rating: 'HIGH', requiredDueDiligence: 'EDD', reviewEveryMonths: 12 },
429
+ ],
430
+ },
431
+ });
432
+
433
+ // 2. Create a customer with profile fields (Tier A: validated against the template).
434
+ const customer = await os.individuals.create({
435
+ externalId: 'USR-10044',
436
+ fullName: 'Nadia Perera',
437
+ profileTemplateKey: 'individual-standard', // optional here: it is the default
438
+ fields: [
439
+ { key: 'pepStatus', type: 'SINGLE_SELECT', value: 'No PEP involvement' },
440
+ { key: 'onboardingChannel', type: 'SINGLE_SELECT', value: 'Non face to face' },
441
+ ],
442
+ });
443
+
444
+ // 3. Read the rating. The matrix runs in the background (a few seconds after
445
+ // the last profile write, plus a daily sweep). `evaluate` runs it now.
446
+ const assessment = await os.customers.riskAssessments.evaluate(customer.id);
447
+ assessment.resultingRiskRating; // 'MEDIUM' (0.6 × 1 + 0.4 × 3 = 1.8)
448
+ assessment.compositeScore; // 1.8 — intermediate number, not the rating
449
+ assessment.breakdown?.factors; // input → criterion → level → score → weighted, per factor
450
+ assessment.requiredDueDiligence; // 'CDD'
451
+ assessment.nextReviewAt; // 'YYYY-MM-DD'
452
+
453
+ const latest = await os.individuals.get(customer.id);
454
+ latest.riskRating; // 'MEDIUM'
455
+ latest.riskRatingSource; // 'MATRIX'
456
+ latest.fields; // [{ key: 'pepStatus', type: 'SINGLE_SELECT', value: 'No PEP involvement' }, …]
457
+ ```
458
+
459
+ How factors score: each factor has a **weight** (weights sum to 1) and turns
460
+ one profile field into points, which the weight scales into the composite.
461
+ Select fields give each option a level (`levels`), optionally with its own
462
+ `score` — points per selection, overriding the level's scale score while the
463
+ level still drives display and escalations. **Yes/no** (BOOLEAN) fields use
464
+ the options `"true"` and `"false"`. **Number and money** fields use `ranges`
465
+ (`{ min, max, level, score? }`, min inclusive, max exclusive, either end open,
466
+ no overlaps; money compares in major units of the field's currency). A
467
+ **multi-select** scores the selected option with the highest score. Anything
468
+ else uses explicit `criteria` condition trees.
469
+
470
+ ```ts
471
+ { key: 'expectedMonthly', label: 'Expected monthly investment', weight: 0.1,
472
+ field: 'expectedMonthlyInvestment', ranges: [ // a MONETARY field
473
+ { max: 100_000, level: 'LOW' },
474
+ { min: 100_000, max: 1_000_000, level: 'MEDIUM' },
475
+ { min: 1_000_000, level: 'HIGH', score: 4 }, // own points instead of HIGH's scale score
476
+ ] }
477
+ ```
478
+
479
+ How it behaves:
480
+
481
+ - **Updates merge.** `individuals.update` / `entities.update` accept `fields`:
482
+ listed keys change, `{ key, type, value: null }` clears a field, unlisted
483
+ keys stay. Pass `profileTemplateKey` to switch template (same party type).
484
+ Each profile write re-scores the customer (bursts are coalesced), so a CRM
485
+ pushing its daily delta with `PATCH` gets a fresh rating within seconds.
486
+ - **Officer overrides are sticky.** `customers.riskRating.update` sets
487
+ `riskRatingSource: 'MANUAL'`. Matrix runs still record their computed rating
488
+ (`customers.riskAssessments.list`, `method: 'MATRIX'`) but never overwrite a
489
+ MANUAL rating. `customers.riskRating.clearOverride(customerId)` hands the
490
+ rating back to the matrix and resolves to the latest matrix assessment.
491
+ - **Assessments share one list.** `customers.riskAssessments.list` returns
492
+ questionnaires (`method: 'QUESTIONNAIRE'`) and matrix runs (`method:
493
+ 'MATRIX'`, with `compositeScore`, `breakdown`, `profileTemplateVersion`,
494
+ `trigger`, `requiredDueDiligence`, `nextReviewAt`). A matrix run is written
495
+ only when its inputs or outcome change.
496
+ - **Webhooks.** When the matrix changes a rating, `customer.risk_changed` is
497
+ delivered with `source: 'risk_matrix'`, `previousRating`, `newRating`,
498
+ `compositeScore`, `requiredDueDiligence` and `riskAssessmentId`.
499
+ - **Templates are versioned.** Changing `fieldConfig`, `riskMatrix`,
500
+ `kycWorkflows` or the creation setup publishes a new version
501
+ (`profileTemplates.versions(id)`, each with the `changes` it made, or
502
+ `version(id, n)` for one); `fieldConfig` / `riskMatrix` changes also
503
+ re-assess every customer on the template. Customers always follow the
504
+ latest version. `restoreVersion(id, n)` republishes an old version's
505
+ definition as the newest one (re-validated, so a definition naming a
506
+ deleted constant is refused). `archive(id)` stops new customers using it;
507
+ existing customers keep it.
508
+ - **Assigning existing customers.** `profileTemplateKey` on an individual or
509
+ entity update switches one customer, and the result must carry every
510
+ required field. `customers.assignProfileTemplate({ customerIds,
511
+ profileTemplateKey })` switches up to 500 at once and never refuses on
512
+ required fields: it keeps the values the template can hold, applies
513
+ defaults, and lists each customer's `missingFields`. Such a customer is
514
+ incomplete until a profile edit fills them.
515
+
516
+ ```ts
517
+ const { assigned, unchanged, skipped } = await os.customers.assignProfileTemplate({
518
+ customerIds: legacyCustomerIds,
519
+ profileTemplateKey: 'individual-standard',
520
+ });
521
+ const incomplete = assigned.filter((c) => c.missingFields.length > 0);
522
+ ```
523
+ - **Creation setup.** `individualCreation` (`INDIVIDUAL` templates) or
524
+ `entityCreation` (`ENTITY` templates) configures the simple Add Individual /
525
+ Add Entity form for the template: on/off and open-by-default, external-ID
526
+ strategy, presets (tags + assignees), and the document or registration
527
+ types offered. The other party type's field is always `null` on responses
528
+ and a 400 on writes. A template created without one copies the org's
529
+ current Simple Customer Creation preference; on `update`, a value fully
530
+ replaces the setup and `null` clears it (the org-wide preference then
531
+ applies). Read shape: `SimpleCustomerCreationPreferences` /
532
+ `SimpleEntityCreationPreferences`; write shape:
533
+ `UpdateSimpleCustomerCreationPreferencesInput` /
534
+ `UpdateSimpleEntityCreationPreferencesInput`.
535
+
536
+ ```ts
537
+ await os.config.profileTemplates.update(templateId, {
538
+ individualCreation: {
539
+ enabled: true,
540
+ openByDefault: true,
541
+ externalIdStrategy: 'DOCUMENT',
542
+ documentTypes: [{ label: 'NIC (Sri Lanka)', type: 'NATIONAL_ID', country: 'LKA' }],
543
+ presets: [{ label: 'General Insurance', tags: [{ label: 'General Insurance' }],
544
+ assignedToUserIds: [], assignedToTeamIds: [giTeamId] }],
545
+ allowTags: false,
546
+ allowAssignment: false,
547
+ },
548
+ });
549
+ ```
550
+ - Profile fields are **not** readable by rules. Promote a value to a
551
+ `$customer.*` constant if a rule needs it.
552
+
553
+ ### Derived inputs
554
+
555
+ A factor can read a value the platform derives itself instead of a profile
556
+ field, by setting `input` instead of `field` (`RiskMatrixDerivedInput`).
557
+ Nobody has to push these as picklist answers, and they re-score on their own
558
+ when the underlying data changes:
559
+
560
+ | `input` | Value | Score it with |
561
+ |---|---|---|
562
+ | `customer.nationality` | ISO-3 codes: an individual's nationalities, an entity's country of incorporation | `levels` per code, or `criteria` against a `$global.*` country list |
563
+ | `customer.productKeys` | Product keys (`syncKey`, else `externalId`) of the customer's accounts that are not closed | `levels` per product key |
564
+ | `customer.expectedMonthlyVolume` | The expected activity profile's monthly volume | `ranges` |
565
+
566
+ When a customer holds several values of a list input, the riskiest scores.
567
+
568
+ ```ts
569
+ { key: 'citizenship', label: 'Citizenship', weight: 0.05, input: 'customer.nationality', whenUnmatched: 'HIGH',
570
+ criteria: [
571
+ { label: 'FATF grey/black list', level: 'HIGH',
572
+ when: { field: 'customer.nationality', operator: 'IN', value: '$global.fatfGreyBlackCountries' } },
573
+ { label: 'Any other nationality', level: 'LOW',
574
+ when: { field: 'customer.nationality', operator: 'IS_NOT_EMPTY', value: '' } },
575
+ ] }
576
+ ```
577
+
578
+ Updating the `$global.fatfGreyBlackCountries` constant re-scores everyone in
579
+ the next daily sweep. Changes to the expected activity profile re-score at once;
580
+ accounts opened or closed are picked up by the daily sweep.
581
+
582
+ ### Dry runs: preview and impact
583
+
584
+ Score an unsaved matrix without saving it. The draft is validated exactly as a
585
+ save would be (400 lists every problem), and both calls compare it with the
586
+ template's saved matrix evaluated on the same inputs.
587
+
588
+ ```ts
589
+ // One customer, draft next to the saved matrix
590
+ const preview = await os.config.profileTemplates.previewRiskMatrix({
591
+ customerId, fieldConfig: draft.fieldConfig, riskMatrix: draft.riskMatrix,
592
+ });
593
+ preview.published?.finalRating; // 'HIGH'
594
+ preview.draft.finalRating; // 'MEDIUM'
595
+
596
+ // Every live customer on the template
597
+ const impact = await os.config.profileTemplates.riskMatrixImpact(templateId, {
598
+ fieldConfig: draft.fieldConfig, riskMatrix: draft.riskMatrix,
599
+ });
600
+ impact.changed; // customers whose rating would move
601
+ impact.movements; // [{ from: 'LOW', to: 'MEDIUM', count: 312 }, ...]
602
+ impact.customers; // who moves, rises before falls, biggest change first (at most 100)
603
+ ```
604
+
605
+ Customers with an officer override are counted in `overriddenChanged`; their
606
+ rating would not move. Up to 5,000 customers are scored per call (`truncated`
607
+ says when there were more).
608
+
609
+ ### KYC output mappings
610
+
611
+ A template's KYC binding can write a verified session's answers back to the
612
+ customer. Each mapping reads one session value and writes it to a profile field
613
+ (`fields.<key>`) or a customer-level constant (`$customer.<name>`). Constants
614
+ are the way to make a KYC answer readable by rules; profile fields never are.
615
+
616
+ ```ts
617
+ await os.config.profileTemplates.update(templateId, {
618
+ kycWorkflows: [{
619
+ purpose: 'ONBOARDING',
620
+ providerConfigId,
621
+ workflowId: 'wf_onboarding',
622
+ mappings: [
623
+ { source: 'form.occupation', target: 'fields.occupation',
624
+ valueMap: [{ from: 'Software engineer', to: 'IT / Software Professional' }] },
625
+ { source: 'session.verified', target: 'fields.onboardingChannel',
626
+ valueMap: [{ from: 'true', to: 'Non Face to Face Clients' }], mode: 'OVERWRITE' },
627
+ { source: 'person.nationality', target: '$customer.kycNationalities' },
628
+ ],
629
+ }],
630
+ });
631
+ ```
632
+
633
+ - Mappings run once, when a session of that workflow becomes verified (the
634
+ provider approved it, or an officer approved it in the OS).
635
+ - `FILL` (default) writes only an empty target; `OVERWRITE` replaces it.
636
+ - A value the target cannot hold (e.g. an answer that is not one of the field's
637
+ options) is skipped; the rest still apply.
638
+ - The save validates sources, targets, options and constants, and lists every
639
+ problem in one 400.
640
+ - Constant writes carry the session id as their reason in the constant history.
641
+ Any change re-scores the customer.
642
+
643
+ ### Periodic reviews
644
+
645
+ Every matrix run keeps the customer's `nextReviewAt` (on individual and
646
+ entity responses) in line with the review cycle of the rating in force
647
+ (`outcomes[].reviewEveryMonths`). The clock starts at the first rating. A rise
648
+ to a rating with a shorter cycle brings the review forward; a fall leaves it
649
+ where it is. When the date arrives, the daily sweep opens a Task labelled
650
+ `periodic-review`, assigned to the customer's assignees, due on the review
651
+ date and linked to the customer. Closing that Task (done or cancelled)
652
+ restarts the clock from that day.
653
+
654
+ ### Override disagreement
655
+
656
+ While an officer override is in force, the matrix keeps scoring. When its
657
+ rating moves away from the override, the customer's assignees get a
658
+ notification ("The risk matrix disagrees with an officer-set rating"). It is
659
+ sent once per change, not on every run.
660
+
661
+ Spec: [`docs/specs/profile-templates/README.md`](../../docs/specs/profile-templates/README.md).
662
+
376
663
  ## Tenant sync — config-as-code (BREAKING in v0.7.0)
377
664
 
378
665
  `os.config.sync` drives declarative tenant config (products, constants, event
379
- definitions, webhooks, rules, currencies, and tenant behaviour settings). You build a `SyncConfig` and the
666
+ definitions, webhooks, rules, currencies, profile templates, and tenant behaviour settings). You build a `SyncConfig` and the
380
667
  **server** validates, diffs, and applies it — clients hold no diff/apply logic.
381
668
  This backs the `dossiers-sync` CLI.
382
669
 
@@ -439,12 +726,14 @@ const { tokens } = await os.config.tagCatalogue.palette(); // valid `color` valu
439
726
  > accepted. Product DTOs no longer carry `metadata` — use `otherProperties`
440
727
  > instead.
441
728
 
442
- Accounts, products, and event ingestion accept the platform's Typed Fields
729
+ Accounts, products, customers, and event ingestion accept the platform's Typed Fields
443
730
  primitive — a typed key/value bag (`fields`) plus an untyped escape hatch
444
731
  (`otherProperties`). Collection fields are explicit: `LIST` is open-ended,
445
732
  while `SINGLE_SELECT` and `MULTI_SELECT` are constrained by the declared
446
- `options` on the parent schema (`Product.accountFieldConfig` or
447
- `EventDefinition.properties`). Legacy `SET` remains supported but is deprecated.
733
+ `options` on the parent schema (`Product.accountFieldConfig`,
734
+ `ProfileTemplate.fieldConfig` or `EventDefinition.properties`). Legacy `SET`
735
+ remains supported but is deprecated. Customer `fields` are always Tier A — see
736
+ [Profile templates & risk matrix](#profile-templates--risk-matrix).
448
737
 
449
738
  ```ts
450
739
  import type { TypedFieldValueInput } from '@dossier-wiki/os-sdk';
@@ -536,7 +825,9 @@ The application uses the same disposition cards, reason field, and optional cata
536
825
 
537
826
  New workflow saves require one shared **Resolved** stage. Set its `next` to a router or an automatic `action` node for follow-ups. Routers can combine `resolution.outcome` with alert, customer, screening or rule fields; after resolution, `$end` means no further action. Action nodes support `CREATE_TASK` (linked to the alert) and `NOTIFY` (in-app notification), with current alert owners or specific members/teams as recipients. Follow-ups run atomically with closure using the saved verdict and leave the alert resolved. Reopen remains a separate review action. Reopening and resolving again can run follow-ups again; retrying the same transition cannot duplicate them. Existing alerts retain their pinned definition.
538
827
 
539
- Each organization has one workflow per alert type: `SCREENING` and `RULE_TRIGGER`. Initial assignment and later handovers are stage settings; routers provide ordered conditions and a fallback. Router branches may have an optional `name`, and the router may have a `fallbackName` (nonblank, up to 200 characters). These names appear in the workflow graph and routing history; they do not change branch IDs, order, or destinations. Omitting them preserves the numbered condition and fallback labels. Read the current revision before saving:
828
+ Each organization has one workflow per alert type: `SCREENING`, `RULE_TRIGGER` and `CUSTOM`. Initial assignment and later handovers are stage settings; routers provide ordered conditions and a fallback. Router branches may have an optional `name`, and the router may have a `fallbackName` (nonblank, up to 200 characters). These names appear in the workflow graph and routing history; they do not change branch IDs, order, or destinations. Omitting them preserves the numbered condition and fallback labels.
829
+
830
+ Routers can also read `dependencies.openCount`, the number of **blocking alerts** that are still open (see [Blocking alerts](#blocking-alerts)); `0` means the alert is free to be resolved. Conditions (`WorkflowConditions`) are a tree. `conditions` holds leaf conditions and nested groups, each group with its own `logic` of `AND` or `OR`, so "high risk and (ID matched or DOB matched)" is `{ logic: 'AND', conditions: [leaf, { logic: 'OR', conditions: [leaf, leaf] }] }`. Groups nest at most three levels below the root and hold at most 30 leaves in total; a nested group must contain at least one entry. Flat lists saved by older clients remain valid. Screening workflows can also route on `screening.maxNameMatchPercent`, the strongest name match (0–100, whole number) across the alert's matches that are not marked false positive; it is unavailable (`null`) when no match carries a name score. Read the current revision before saving:
540
831
 
541
832
  ```ts
542
833
  const workflow = await client.config.alertWorkflows.get('SCREENING');
@@ -553,6 +844,8 @@ Requires `config:read` / `config:write`. Stale saves return HTTP 409. Existing a
553
844
 
554
845
  Set `definition.restrictEditingToOwners` to `true` to make alerts read-only for everyone except currently assigned people and members of active assigned teams, including administrators. This covers assignments, match decisions, alert comments and attachments, task links, and every stage transition (including return and reopen), overriding the progression-only policy. Omitting it or setting it to `false` preserves existing editing permissions. The policy is pinned when the alert is created; publishing does not change older alerts or Simple alerts. Linked tasks retain their own editing permissions. Organization admins and owners can explicitly add themselves through the application’s **Assign to me** action (`takeOverAlert` in GraphQL). This administrative recovery preserves all other assignees, status and pinned workflow settings, and is recorded in activity history; it also supports resolved alerts so an administrator can assume ownership before reopening.
555
846
 
847
+ Set `definition.restrictReportsToClosedAlerts` to `true` to allow PDF reports only once an alert is closed. While such an alert is open, `GET /v1/reports/alerts/{id}/pdf` returns HTTP 403, and `GET /v1/reports/customers/{id}/pdf` returns HTTP 403 while the customer has any open alert under this policy. GraphQL exposes the reason as `Alert.reviewWorkflow.reportRestriction` and `Customer.reportExportRestriction` (both null when a report can be generated). Like the other policies it is pinned at alert creation; Simple alerts are never restricted.
848
+
556
849
  Set `definition.restrictProgressionToOwners` to `true` to limit forward progression (including resolution) to currently assigned people and members of active assigned teams. Administrators must also be assigned. Set it to `false` to allow other organization members with `alert:update` permission to progress alerts. Stage requirements still apply. Return and reopen permissions are unchanged. Omitting the field preserves the original current-reviewers-or-administrators behavior for older API clients and stored definitions. The builder defaults the policy to enabled when editing; publishing affects new alerts only.
557
850
 
558
851
  For Screening workflows, `definition.matchAssessment` sets shared `{ note, attachment }` booleans for `CONFIRMED`, `INCONCLUSIVE`, and `FALSE_POSITIVE`. These apply to every Initial Review stage and to Secondary Review when `definition.allowSecondaryReviewMatchOverrides` is `true`; set each stage's legacy `screening` field to `null`. Enabling overrides requires a shared `matchAssessment` policy. Initial Review reviewers can replace individual match verdicts directly without resetting to pending. Authorized secondary reviewers can then also replace a match verdict directly, with the required evidence and an audit record of the previous verdict. Existing reviewer and editing permissions still apply; resolved alerts remain read-only for match decisions. Overrides default to off, and non-screening workflows must omit both settings. Older definitions that omit `matchAssessment` retain their stage-specific requirements. The builder's General policy tab consolidates older stage requirements into a shared draft, preserving the strictest requirement where they differ. Publishing never updates existing alerts.
@@ -566,3 +859,32 @@ Attach screening matches explicitly with `links: { matchIds: ["<match UUID>"] }`
566
859
  Use `client.config.alertWorkflows.getExperience()` to read the tenant's choice and revision. `saveExperience({ experience: 'SIMPLE' | 'DEFINED', expectedRevision })` changes only newly created alerts, across both supported alert types. Simple uses direct assignment and Open/Closed resolution; Defined uses the published workflow for the alert type. Existing alerts retain their process and policy version. Saved definitions are retained in Simple mode, and publishing a definition does not enable Defined mode. The initial default is Simple. Select Defined explicitly to enable the published workflow for new alerts. Existing saved choices are preserved when upgrading.
567
860
 
568
861
  Published workflow history is available through `config.alertWorkflows.versions(alertType)` (newest first, with publisher and date). `config.alertWorkflows.restore(alertType, { version, expectedRevision })` copies a version into the draft without changing the published workflow or existing alerts. Review the returned draft, then call `save` with `publish: true` and its updated revision to publish it as a new version. Old versions with no recoverable definition return `definition: null` and cannot be restored. Both operations are organization-scoped and require `config:read` / `config:write` respectively.
862
+
863
+ ### Blocking alerts
864
+
865
+ An alert can be made to **wait on** another alert. The alert that must close first is the **blocking alert**; the one that waits is the **blocked alert**. The rule is small: **an alert with an open blocking alert cannot be resolved or closed.** Everything else — assignment, comments, attachments, notes, match decisions, stage progression short of resolution — stays allowed. Closing or resolving a blocked alert returns HTTP 400 naming how many blockers are still open. Once the last blocker closes, the blocked alert's owners are notified and it can be resolved.
866
+
867
+ Links are directed and acyclic: an alert cannot block itself, and a link that would form a loop (the blocking alert already waits, directly or through other alerts, on the blocked one) is rejected with HTTP 400. Links are kept after either alert closes, so the history of why an alert waited stays readable; only *open* blockers block. Links come from three places: entity screening (an entity's screening alert is blocked by each of its stakeholders' screening alerts), analysts in the app, and the API below.
868
+
869
+ ```ts
870
+ // Make alert A wait on alert B. Idempotent — re-adding returns the existing link.
871
+ const link = await client.alerts.dependencies.add(alertA, { blockedByAlertId: alertB });
872
+ // { id, alertId: alertA, blockedByAlertId: alertB, createdBy: { userId, name }, createdAt }
873
+
874
+ // Both sides of A's links, oldest first. Compare each linked alert's status to see which still block.
875
+ const { blockedBy, blocks } = await client.alerts.dependencies.list(alertA);
876
+
877
+ // Remove the link. Removing a link that does not exist changes nothing (204 either way).
878
+ await client.alerts.dependencies.remove(alertA, alertB);
879
+
880
+ // `isBlocked` is on the flat alert view.
881
+ const alert = await client.alerts.get(alertA); // alert.isBlocked === true while B is open
882
+ ```
883
+
884
+ REST: `GET /v1/alerts/{id}/dependencies` → `{ blockedBy: AlertDependency[], blocks: AlertDependency[] }`; `POST /v1/alerts/{id}/dependencies` with `{ blockedByAlertId }` → the link (201); `DELETE /v1/alerts/{id}/dependencies/{blockedByAlertId}` → 204. Reads need `alerts:read`, writes `alerts:write`. A session user is held to the blocked alert's owner-only editing policy (`restrictEditingToOwners`); an API key acts as the key and the link's `createdBy` names the key.
885
+
886
+ GraphQL exposes the same links on every alert type as `blockedBy: [AlertDependency!]!`, `blocks: [AlertDependency!]!` and `isBlocked: Boolean!`, where `AlertDependency { id, alertId, blockedByAlertId, createdBy, createdAt, blockedAlert, blockingAlert }` resolves both alerts in full. The mutations are `addAlertDependency(alertId, blockedByAlertId)` and `removeAlertDependency(alertId, blockedByAlertId)`, both returning the blocked alert and requiring `alert:update`. The alert list filter accepts `blocked: Boolean` to show only alerts that are (or are not) currently blocked.
887
+
888
+ Workflows see the count as the `dependencies.openCount` field. Screening's automatic close (every match eliminated by the provider) skips a blocked alert; its owners resolve it once the blocking alerts have closed.
889
+
890
+ **Stakeholder screening.** When an entity is screened, every stakeholder (director, UBO, shareholder) is rescreened with it, except stakeholders screened within the last `stakeholderRescreenSkipHours` — a screening setting on `ScreeningSettings` (GraphQL `screeningSettings` / `updateScreeningSettings`), default `24`, `0` = always rescreen, maximum 90 days (`2160`). The entity then holds one ordinary Screening alert — the one its own matches raised, or one with zero matches of its own when the entity was clean — blocked by each stakeholder's open screening alert, with its risk level held at the highest risk among its open blockers. A clean entity's alert stays open until a person resolves it once the last stakeholder alert has closed; its owners are notified at that point.
@@ -9,10 +9,17 @@ export interface WorkflowCondition {
9
9
  valueField?: string;
10
10
  whenMissing?: string;
11
11
  }
12
+ /**
13
+ * A condition group. `conditions` holds leaf conditions and nested groups, so
14
+ * "A and (B or C)" is `{ logic: "AND", conditions: [A, { logic: "OR", conditions: [B, C] }] }`.
15
+ * Groups nest at most three levels below the root and hold at most 30 leaves in
16
+ * total. A nested group needs at least one entry; an empty root matches every alert.
17
+ */
12
18
  export interface WorkflowConditions {
13
19
  logic: "AND" | "OR";
14
- conditions: WorkflowCondition[];
20
+ conditions: WorkflowConditionNode[];
15
21
  }
22
+ export type WorkflowConditionNode = WorkflowCondition | WorkflowConditions;
16
23
  export interface WorkflowAssignment {
17
24
  mode: "KEEP" | "HANDOVER" | "ADD";
18
25
  source: "SPECIFIC" | "CUSTOMER_MEMBERS" | "CUSTOMER_TEAMS" | "CUSTOMER_OWNERS" | "ORIGINAL";
@@ -37,6 +44,11 @@ export interface WorkflowStage {
37
44
  requiredReviews: ("CUSTOMER_RISK" | "COUNTERPARTY_RISK")[];
38
45
  /** Legacy stage policy. New definitions use matchAssessment and set this to null. */
39
46
  screening: MatchAssessmentPolicy | null;
47
+ /** What happens to assignees when an alert is returned to this stage.
48
+ * RULE (omission): run this stage's assignment rule. RESTORE: reassign to whoever held the
49
+ * alert when it last left this stage. ASK: the returning reviewer chooses, defaulting to restore.
50
+ * Never set on a Resolved stage: nothing is returned to it. */
51
+ returnAssignment?: "RULE" | "RESTORE" | "ASK";
40
52
  }
41
53
  export interface WorkflowRouter {
42
54
  id: string;
@@ -80,6 +92,10 @@ export interface WorkflowDefinition {
80
92
  matchAssessment?: MatchAssessmentPolicy;
81
93
  /** Screening only. Allow reviewers to override match verdicts in Secondary Review using matchAssessment requirements. Omission/false keeps decisions in Initial Review. */
82
94
  allowSecondaryReviewMatchOverrides?: boolean;
95
+ /** PDF reports of an alert can be generated only once it is closed, and a customer profile
96
+ * cannot be exported while the customer has an open alert under this policy. Pinned at alert
97
+ * creation like the other policies. Omission/false allows reports at any time. */
98
+ restrictReportsToClosedAlerts?: boolean;
83
99
  entry: string;
84
100
  /** Exactly one Resolved stage. Its optional next step runs automatically after closure.
85
101
  * Routers and actions after resolution may use "$end" to finish without another action. */
@@ -111,5 +127,12 @@ export interface WorkflowExecution {
111
127
  enteredAt: string;
112
128
  initialUserIds: string[];
113
129
  initialTeamIds: string[];
130
+ /** Who held the alert when it last left each stage, by node id. Written on every
131
+ * transition out of a stage; absent on alerts enrolled before it existed. */
132
+ stageAssignees?: Record<string, {
133
+ userIds: string[];
134
+ teamIds: string[];
135
+ leftAt: string;
136
+ }>;
114
137
  }
115
138
  //# sourceMappingURL=alert-workflow-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"alert-workflow-types.d.ts","sourceRoot":"","sources":["../src/alert-workflow-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"alert-workflow-types.d.ts","sourceRoot":"","sources":["../src/alert-workflow-types.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,cAAc,GAAG,QAAQ,CAAC;AACxE,MAAM,MAAM,kBAAkB,GAC1B,gBAAgB,GAChB,kBAAkB,GAClB,UAAU,CAAC;AACf,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AACD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,qBAAqB,EAAE,CAAC;CACrC;AACD,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;AAC3E,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,KAAK,CAAC;IAClC,MAAM,EACF,UAAU,GACV,kBAAkB,GAClB,gBAAgB,GAChB,iBAAiB,GACjB,UAAU,CAAC;IACf,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AACD,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;CACrB;AACD,MAAM,MAAM,qBAAqB,GAAG,MAAM,CACxC,WAAW,GAAG,gBAAgB,GAAG,cAAc,EAC/C,uBAAuB,CACxB,CAAC;AACF,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,kBAAkB,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,UAAU,EAAE,kBAAkB,CAAC;IAC/B,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,CAAC,eAAe,GAAG,mBAAmB,CAAC,EAAE,CAAC;IAC3D,qFAAqF;IACrF,SAAS,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACxC;;;mEAG+D;IAC/D,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,CAAC;CAC/C;AACD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,oFAAoF;QACpF,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,kBAAkB,CAAC;QAC/B,MAAM,EAAE,MAAM,CAAC;KAChB,EAAE,CAAC;IACJ,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AACD,0FAA0F;AAC1F,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,GAAG,QAAQ,CAAC;IACjC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE;QACV,MAAM,EAAE,cAAc,GAAG,UAAU,CAAC;QACpC,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;CACH;AACD,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,GAAG,cAAc,CAAC;AAC3E,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,CAAC,CAAC;IACjB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,qOAAqO;IACrO,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,iQAAiQ;IACjQ,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,6IAA6I;IAC7I,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC,2KAA2K;IAC3K,kCAAkC,CAAC,EAAE,OAAO,CAAC;IAC7C;;sFAEkF;IAClF,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd;+FAC2F;IAC3F,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AACD,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC7D,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAChC;AACD,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,CAAC;IACvD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;CACnB;AACD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,KAAK,EAAE,kBAAkB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB;iFAC6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3F"}