@arkyc/core 1.2.11 → 2.1.0

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.
@@ -1,19 +1,18 @@
1
1
  import { Id, OrganizationScoped } from "@arkyc/types";
2
-
3
2
  //#region src/context.d.ts
4
3
  /** Identifies the organization + project a request or entity belongs to. */
5
- interface OrganizationProjectContext {
4
+ export interface OrganizationProjectContext {
6
5
  organizationId: Id;
7
6
  projectId: Id;
8
7
  }
9
8
  /**
10
9
  * Thrown when an entity is accessed outside its organization/project scope.
11
10
  */
12
- declare class OrganizationScopeError extends Error {
11
+ export declare class OrganizationScopeError extends Error {
13
12
  constructor(message?: string);
14
13
  }
15
14
  /** Organization/project scoping and storage-path helpers. */
16
- declare class OrganizationContext {
15
+ export declare class OrganizationContext {
17
16
  /**
18
17
  * Whether `entity` belongs to the given organization.
19
18
  *
@@ -44,5 +43,4 @@ declare class OrganizationContext {
44
43
  static storagePath(ctx: OrganizationProjectContext, sessionId: Id, ...parts: string[]): string;
45
44
  }
46
45
  //#endregion
47
- export { OrganizationContext, OrganizationProjectContext, OrganizationScopeError };
48
46
  //# sourceMappingURL=context.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.mts","names":[],"sources":["../src/context.ts"],"mappings":";;;;UAGiB,0BAAA;EACf,cAAA,EAAgB,EAAA;EAChB,SAAA,EAAW,EAAE;AAAA;;;;cAMF,sBAAA,SAA+B,KAAK;cACnC,OAAA;AAAA;AAPC;AAAA,cAcF,mBAAA;EARuB;;;;;;;EAAA,OAgB3B,SAAA,CAAU,MAAA,EAAQ,kBAAA,EAAoB,cAAA,EAAgB,EAAA;EARlD;;;;;;;;EAAA,OAoBJ,WAAA,WAAsB,kBAAA,EAAoB,MAAA,EAAQ,CAAA,EAAG,cAAA,EAAgB,EAAA,GAAK,CAAA;EAiBzD;;;;;;;;;;EAAA,OAAjB,WAAA,CAAY,GAAA,EAAK,0BAAA,EAA4B,SAAA,EAAW,EAAA,KAAO,KAAA;AAAA"}
1
+ {"version":3,"file":"context.d.mts","names":[],"sources":["../src/context.ts"],"mappings":";;;iBAGiB;EACf,gBAAgB;EAChB,WAAW;;;;;qBAMA,+BAA+B;EAC9B,YAAA;;;qBAOD;;;;;;;;SAQJ,UAAU,QAAQ,oBAAoB,gBAAgB;;;;;;;;;SAYtD,YAAY,UAAU,oBAAoB,QAAQ,GAAG,gBAAgB,KAAK;;;;;;;;;;;SAiB1E,YAAY,KAAK,4BAA4B,WAAW,OAAO"}
@@ -1,8 +1,7 @@
1
- import { DecisionReason, VerificationDecision, VerificationThresholds } from "@arkyc/types";
2
-
1
+ import { DecisionReason, RiskSignal, VerificationDecision, VerificationThresholds } from "@arkyc/types";
3
2
  //#region src/decision.d.ts
4
3
  /** Document signals fed to the decision engine. */
5
- interface DecisionDocumentInput {
4
+ export interface DecisionDocumentInput {
6
5
  /** Document image quality in [0, 1]. */
7
6
  qualityScore: number;
8
7
  /** OCR extraction confidence in [0, 1]. */
@@ -11,7 +10,7 @@ interface DecisionDocumentInput {
11
10
  expired: boolean;
12
11
  }
13
12
  /** Liveness signals fed to the decision engine. */
14
- interface DecisionLivenessInput {
13
+ export interface DecisionLivenessInput {
15
14
  passed: boolean;
16
15
  /** Liveness confidence in [0, 1]. */
17
16
  score: number;
@@ -19,13 +18,13 @@ interface DecisionLivenessInput {
19
18
  multipleFaces?: boolean;
20
19
  }
21
20
  /** Face-match signals fed to the decision engine. */
22
- interface DecisionFaceMatchInput {
21
+ export interface DecisionFaceMatchInput {
23
22
  passed: boolean;
24
23
  /** Similarity between document portrait and selfie in [0, 1]. */
25
24
  similarityScore: number;
26
25
  }
27
26
  /** Address-verification signals fed to the decision engine. */
28
- interface DecisionAddressInput {
27
+ export interface DecisionAddressInput {
29
28
  passed: boolean;
30
29
  /** Overall address-verification confidence in [0, 1]. */
31
30
  score: number;
@@ -43,21 +42,23 @@ interface DecisionAddressInput {
43
42
  threshold?: number;
44
43
  }
45
44
  /** The complete set of signals the decision engine evaluates. */
46
- interface DecisionInput {
45
+ export interface DecisionInput {
47
46
  document: DecisionDocumentInput;
48
47
  liveness: DecisionLivenessInput;
49
48
  faceMatch: DecisionFaceMatchInput;
50
49
  address: DecisionAddressInput;
50
+ /** Fraud and compliance signals, with their policy-resolved severity. */
51
+ signals?: RiskSignal[];
51
52
  }
52
53
  /** The decision engine's verdict for a session. */
53
- interface DecisionResult {
54
+ export interface DecisionResult {
54
55
  decision: VerificationDecision;
55
56
  reason: DecisionReason;
56
57
  /** Aggregate risk score in [0, 1] (higher = riskier). */
57
58
  riskScore: number;
58
59
  }
59
60
  /** Deterministic verification decision engine. */
60
- declare class DecisionEngine {
61
+ export declare class DecisionEngine {
61
62
  /**
62
63
  * Decide a verification outcome from the gathered signals.
63
64
  *
@@ -66,8 +67,9 @@ declare class DecisionEngine {
66
67
  * evaluated in a fixed priority order so the result is deterministic.
67
68
  *
68
69
  * Reject precedence: expired → liveness failed → face-match failed
70
+ * → address failed → reject signals
69
71
  * Review precedence: multiple faces → low quality → low OCR
70
- * → low liveness → low face-match
72
+ * → low liveness → low face-match → address → review signals
71
73
  *
72
74
  * @param input
73
75
  * @param thresholds
@@ -76,5 +78,4 @@ declare class DecisionEngine {
76
78
  static decide(input: DecisionInput, thresholds?: Partial<VerificationThresholds>): DecisionResult;
77
79
  }
78
80
  //#endregion
79
- export { DecisionAddressInput, DecisionDocumentInput, DecisionEngine, DecisionFaceMatchInput, DecisionInput, DecisionLivenessInput, DecisionResult };
80
81
  //# sourceMappingURL=decision.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"decision.d.mts","names":[],"sources":["../src/decision.ts"],"mappings":";;;;UAMiB,qBAAA;EAAA;EAEf,YAAA;;EAEA,aAAA;EAFA;EAIA,OAAA;AAAA;;UAIe,qBAAA;EACf,MAAA;EADoC;EAGpC,KAAA;EAHoC;EAKpC,aAAA;AAAA;;UAIe,sBAAA;EACf,MAAA;EADe;EAGf,eAAe;AAAA;;UAIA,oBAAA;EACf,MAAA;EADmC;EAGnC,KAAA;EAHmC;;;;;EASnC,MAAA;EAMS;AAIX;;;;EAJE,SAAA;AAAA;;UAIe,aAAA;EACf,QAAA,EAAU,qBAAA;EACV,QAAA,EAAU,qBAAA;EACV,SAAA,EAAW,sBAAA;EACX,OAAA,EAAS,oBAAA;AAAA;;UAIM,cAAA;EACf,QAAA,EAAU,oBAAA;EACV,MAAA,EAAQ,cAAc;EANO;EAQ7B,SAAA;AAAA;;cAIW,cAAA;EANW;;;;;;;AAEb;AAIX;;;;;;;EANwB,OAsBf,MAAA,CAAO,KAAA,EAAO,aAAA,EAAe,UAAA,GAAa,OAAA,CAAQ,sBAAA,IAA0B,cAAA;AAAA"}
1
+ {"version":3,"file":"decision.d.mts","names":[],"sources":["../src/decision.ts"],"mappings":";;;iBAOiB;;EAEf;;EAEA;;EAEA;;;iBAIe;EACf;;EAEA;;EAEA;;;iBAIe;EACf;;EAEA;;;iBAIe;EACf;;EAEA;;;;;;EAMA;;;;;;EAMA;;;iBAIe;EACf,UAAU;EACV,UAAU;EACV,WAAW;EACX,SAAS;;EAET,UAAU;;;iBAIK;EACf,UAAU;EACV,QAAQ;;EAER;;;qBAIW;;;;;;;;;;;;;;;;;SAiBJ,OAAO,OAAO,eAAe,aAAa,QAAQ,0BAA0B"}
package/dist/decision.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Thresholds } from "./thresholds.mjs";
2
2
  import { Risk } from "./risk.mjs";
3
+ import { RiskSignals } from "./signals.mjs";
3
4
  //#region src/decision.ts
4
5
  /** Deterministic verification decision engine. */
5
6
  var DecisionEngine = class {
@@ -11,8 +12,9 @@ var DecisionEngine = class {
11
12
  * evaluated in a fixed priority order so the result is deterministic.
12
13
  *
13
14
  * Reject precedence: expired → liveness failed → face-match failed
15
+ * → address failed → reject signals
14
16
  * Review precedence: multiple faces → low quality → low OCR
15
- * → low liveness → low face-match
17
+ * → low liveness → low face-match → address → review signals
16
18
  *
17
19
  * @param input
18
20
  * @param thresholds
@@ -30,6 +32,9 @@ var DecisionEngine = class {
30
32
  if (!input.liveness.passed) return verdict("rejected", "LIVENESS_FAILED");
31
33
  if (!input.faceMatch.passed) return verdict("rejected", "FACE_MATCH_FAILED");
32
34
  if (!input.address.passed && input.address.onFail === "reject") return verdict("rejected", "ADDRESS_VERIFICATION_FAILED");
35
+ const signals = input.signals ?? [];
36
+ const rejectSignal = RiskSignals.reasonFor(signals, "reject");
37
+ if (rejectSignal) return verdict("rejected", rejectSignal);
33
38
  if (input.liveness.multipleFaces === true) return verdict("requires_review", "MULTIPLE_FACES_DETECTED");
34
39
  if (input.document.qualityScore < t.documentQualityThreshold) return verdict("requires_review", "LOW_DOCUMENT_QUALITY");
35
40
  if (input.document.ocrConfidence < t.ocrConfidenceThreshold) return verdict("requires_review", "OCR_LOW_CONFIDENCE");
@@ -37,6 +42,8 @@ var DecisionEngine = class {
37
42
  if (input.faceMatch.similarityScore < t.faceMatchThreshold) return verdict("requires_review", "FACE_MATCH_LOW_CONFIDENCE");
38
43
  if (!input.address.passed) return verdict("requires_review", "ADDRESS_VERIFICATION_FAILED");
39
44
  if (input.address.score < (input.address.threshold ?? t.addressThreshold)) return verdict("requires_review", "ADDRESS_LOW_CONFIDENCE");
45
+ const reviewSignal = RiskSignals.reasonFor(signals, "review");
46
+ if (reviewSignal) return verdict("requires_review", reviewSignal);
40
47
  return verdict("approved", "AUTO_APPROVED");
41
48
  }
42
49
  };
@@ -1 +1 @@
1
- {"version":3,"file":"decision.mjs","names":[],"sources":["../src/decision.ts"],"sourcesContent":["import type { DecisionReason, VerificationDecision, VerificationThresholds } from '@arkyc/types'\n\nimport { Risk } from './risk'\nimport { Thresholds } from './thresholds'\n\n/** Document signals fed to the decision engine. */\nexport interface DecisionDocumentInput {\n /** Document image quality in [0, 1]. */\n qualityScore: number\n /** OCR extraction confidence in [0, 1]. */\n ocrConfidence: number\n /** Whether the document is past its expiry date. */\n expired: boolean\n}\n\n/** Liveness signals fed to the decision engine. */\nexport interface DecisionLivenessInput {\n passed: boolean\n /** Liveness confidence in [0, 1]. */\n score: number\n /** Whether more than one face was detected in the selfie/liveness frame. */\n multipleFaces?: boolean\n}\n\n/** Face-match signals fed to the decision engine. */\nexport interface DecisionFaceMatchInput {\n passed: boolean\n /** Similarity between document portrait and selfie in [0, 1]. */\n similarityScore: number\n}\n\n/** Address-verification signals fed to the decision engine. */\nexport interface DecisionAddressInput {\n passed: boolean\n /** Overall address-verification confidence in [0, 1]. */\n score: number\n /**\n * What a failed result does: flag for `review` (default) or `reject`, driven\n * by the workflow's address config. A low-confidence (but not failed) result\n * is always review, never a reject.\n */\n onFail?: 'review' | 'reject'\n /**\n * Workflow-configured minimum confidence to auto-verify the address. A passed\n * address scoring below this routes to manual review. Falls back to the\n * project `addressThreshold` when omitted.\n */\n threshold?: number\n}\n\n/** The complete set of signals the decision engine evaluates. */\nexport interface DecisionInput {\n document: DecisionDocumentInput\n liveness: DecisionLivenessInput\n faceMatch: DecisionFaceMatchInput\n address: DecisionAddressInput\n}\n\n/** The decision engine's verdict for a session. */\nexport interface DecisionResult {\n decision: VerificationDecision\n reason: DecisionReason\n /** Aggregate risk score in [0, 1] (higher = riskier). */\n riskScore: number\n}\n\n/** Deterministic verification decision engine. */\nexport class DecisionEngine {\n /**\n * Decide a verification outcome from the gathered signals.\n *\n * Hard failures reject outright; borderline/low-confidence signals route to\n * manual review; everything clearing its threshold is auto-approved. Checks are\n * evaluated in a fixed priority order so the result is deterministic.\n *\n * Reject precedence: expired → liveness failed → face-match failed\n * Review precedence: multiple faces → low quality → low OCR\n * → low liveness → low face-match\n *\n * @param input\n * @param thresholds\n * @returns\n */\n static decide(input: DecisionInput, thresholds?: Partial<VerificationThresholds>): DecisionResult {\n const t = Thresholds.resolve(thresholds)\n const riskScore = Risk.score(input)\n const verdict = (decision: VerificationDecision, reason: DecisionReason): DecisionResult => ({\n decision,\n reason,\n riskScore,\n })\n\n // --- Hard rejections (highest priority) ---\n if (input.document.expired) {\n return verdict('rejected', 'DOCUMENT_EXPIRED')\n }\n if (!input.liveness.passed) {\n return verdict('rejected', 'LIVENESS_FAILED')\n }\n if (!input.faceMatch.passed) {\n return verdict('rejected', 'FACE_MATCH_FAILED')\n }\n // Address only rejects when the workflow opts in (`on_fail: 'reject'`);\n // otherwise a failed address falls through to manual review below.\n if (!input.address.passed && input.address.onFail === 'reject') {\n return verdict('rejected', 'ADDRESS_VERIFICATION_FAILED')\n }\n\n // --- Manual review (borderline / ambiguous signals) ---\n if (input.liveness.multipleFaces === true) {\n return verdict('requires_review', 'MULTIPLE_FACES_DETECTED')\n }\n if (input.document.qualityScore < t.documentQualityThreshold) {\n return verdict('requires_review', 'LOW_DOCUMENT_QUALITY')\n }\n if (input.document.ocrConfidence < t.ocrConfidenceThreshold) {\n return verdict('requires_review', 'OCR_LOW_CONFIDENCE')\n }\n if (input.liveness.score < t.livenessThreshold) {\n return verdict('requires_review', 'LIVENESS_LOW_CONFIDENCE')\n }\n if (input.faceMatch.similarityScore < t.faceMatchThreshold) {\n return verdict('requires_review', 'FACE_MATCH_LOW_CONFIDENCE')\n }\n // A failed address that didn't reject (on_fail: 'review') lands here.\n if (!input.address.passed) {\n return verdict('requires_review', 'ADDRESS_VERIFICATION_FAILED')\n }\n // Auto-verify gate: the workflow can set its own minimum address confidence;\n // a passed address below it is held for manual review (never rejected here).\n if (input.address.score < (input.address.threshold ?? t.addressThreshold)) {\n return verdict('requires_review', 'ADDRESS_LOW_CONFIDENCE')\n }\n\n // --- All checks cleared ---\n return verdict('approved', 'AUTO_APPROVED')\n }\n}\n"],"mappings":";;;;AAmEA,IAAa,iBAAb,MAA4B;;;;;;;;;;;;;;;;CAgB1B,OAAO,OAAO,OAAsB,YAA8D;EAChG,MAAM,IAAI,WAAW,QAAQ,UAAU;EACvC,MAAM,YAAY,KAAK,MAAM,KAAK;EAClC,MAAM,WAAW,UAAgC,YAA4C;GAC3F;GACA;GACA;EACF;EAGA,IAAI,MAAM,SAAS,SACjB,OAAO,QAAQ,YAAY,kBAAkB;EAE/C,IAAI,CAAC,MAAM,SAAS,QAClB,OAAO,QAAQ,YAAY,iBAAiB;EAE9C,IAAI,CAAC,MAAM,UAAU,QACnB,OAAO,QAAQ,YAAY,mBAAmB;EAIhD,IAAI,CAAC,MAAM,QAAQ,UAAU,MAAM,QAAQ,WAAW,UACpD,OAAO,QAAQ,YAAY,6BAA6B;EAI1D,IAAI,MAAM,SAAS,kBAAkB,MACnC,OAAO,QAAQ,mBAAmB,yBAAyB;EAE7D,IAAI,MAAM,SAAS,eAAe,EAAE,0BAClC,OAAO,QAAQ,mBAAmB,sBAAsB;EAE1D,IAAI,MAAM,SAAS,gBAAgB,EAAE,wBACnC,OAAO,QAAQ,mBAAmB,oBAAoB;EAExD,IAAI,MAAM,SAAS,QAAQ,EAAE,mBAC3B,OAAO,QAAQ,mBAAmB,yBAAyB;EAE7D,IAAI,MAAM,UAAU,kBAAkB,EAAE,oBACtC,OAAO,QAAQ,mBAAmB,2BAA2B;EAG/D,IAAI,CAAC,MAAM,QAAQ,QACjB,OAAO,QAAQ,mBAAmB,6BAA6B;EAIjE,IAAI,MAAM,QAAQ,SAAS,MAAM,QAAQ,aAAa,EAAE,mBACtD,OAAO,QAAQ,mBAAmB,wBAAwB;EAI5D,OAAO,QAAQ,YAAY,eAAe;CAC5C;AACF"}
1
+ {"version":3,"file":"decision.mjs","names":[],"sources":["../src/decision.ts"],"sourcesContent":["import type { DecisionReason, RiskSignal, VerificationDecision, VerificationThresholds } from '@arkyc/types'\n\nimport { Risk } from './risk'\nimport { RiskSignals } from './signals'\nimport { Thresholds } from './thresholds'\n\n/** Document signals fed to the decision engine. */\nexport interface DecisionDocumentInput {\n /** Document image quality in [0, 1]. */\n qualityScore: number\n /** OCR extraction confidence in [0, 1]. */\n ocrConfidence: number\n /** Whether the document is past its expiry date. */\n expired: boolean\n}\n\n/** Liveness signals fed to the decision engine. */\nexport interface DecisionLivenessInput {\n passed: boolean\n /** Liveness confidence in [0, 1]. */\n score: number\n /** Whether more than one face was detected in the selfie/liveness frame. */\n multipleFaces?: boolean\n}\n\n/** Face-match signals fed to the decision engine. */\nexport interface DecisionFaceMatchInput {\n passed: boolean\n /** Similarity between document portrait and selfie in [0, 1]. */\n similarityScore: number\n}\n\n/** Address-verification signals fed to the decision engine. */\nexport interface DecisionAddressInput {\n passed: boolean\n /** Overall address-verification confidence in [0, 1]. */\n score: number\n /**\n * What a failed result does: flag for `review` (default) or `reject`, driven\n * by the workflow's address config. A low-confidence (but not failed) result\n * is always review, never a reject.\n */\n onFail?: 'review' | 'reject'\n /**\n * Workflow-configured minimum confidence to auto-verify the address. A passed\n * address scoring below this routes to manual review. Falls back to the\n * project `addressThreshold` when omitted.\n */\n threshold?: number\n}\n\n/** The complete set of signals the decision engine evaluates. */\nexport interface DecisionInput {\n document: DecisionDocumentInput\n liveness: DecisionLivenessInput\n faceMatch: DecisionFaceMatchInput\n address: DecisionAddressInput\n /** Fraud and compliance signals, with their policy-resolved severity. */\n signals?: RiskSignal[]\n}\n\n/** The decision engine's verdict for a session. */\nexport interface DecisionResult {\n decision: VerificationDecision\n reason: DecisionReason\n /** Aggregate risk score in [0, 1] (higher = riskier). */\n riskScore: number\n}\n\n/** Deterministic verification decision engine. */\nexport class DecisionEngine {\n /**\n * Decide a verification outcome from the gathered signals.\n *\n * Hard failures reject outright; borderline/low-confidence signals route to\n * manual review; everything clearing its threshold is auto-approved. Checks are\n * evaluated in a fixed priority order so the result is deterministic.\n *\n * Reject precedence: expired → liveness failed → face-match failed\n * → address failed → reject signals\n * Review precedence: multiple faces → low quality → low OCR\n * → low liveness → low face-match → address → review signals\n *\n * @param input\n * @param thresholds\n * @returns\n */\n static decide(input: DecisionInput, thresholds?: Partial<VerificationThresholds>): DecisionResult {\n const t = Thresholds.resolve(thresholds)\n const riskScore = Risk.score(input)\n const verdict = (decision: VerificationDecision, reason: DecisionReason): DecisionResult => ({\n decision,\n reason,\n riskScore,\n })\n\n // --- Hard rejections (highest priority) ---\n if (input.document.expired) {\n return verdict('rejected', 'DOCUMENT_EXPIRED')\n }\n if (!input.liveness.passed) {\n return verdict('rejected', 'LIVENESS_FAILED')\n }\n if (!input.faceMatch.passed) {\n return verdict('rejected', 'FACE_MATCH_FAILED')\n }\n // Address only rejects when the workflow opts in (`on_fail: 'reject'`);\n // otherwise a failed address falls through to manual review below.\n if (!input.address.passed && input.address.onFail === 'reject') {\n return verdict('rejected', 'ADDRESS_VERIFICATION_FAILED')\n }\n\n const signals = input.signals ?? []\n const rejectSignal = RiskSignals.reasonFor(signals, 'reject')\n if (rejectSignal) {\n return verdict('rejected', rejectSignal)\n }\n\n // --- Manual review (borderline / ambiguous signals) ---\n if (input.liveness.multipleFaces === true) {\n return verdict('requires_review', 'MULTIPLE_FACES_DETECTED')\n }\n if (input.document.qualityScore < t.documentQualityThreshold) {\n return verdict('requires_review', 'LOW_DOCUMENT_QUALITY')\n }\n if (input.document.ocrConfidence < t.ocrConfidenceThreshold) {\n return verdict('requires_review', 'OCR_LOW_CONFIDENCE')\n }\n if (input.liveness.score < t.livenessThreshold) {\n return verdict('requires_review', 'LIVENESS_LOW_CONFIDENCE')\n }\n if (input.faceMatch.similarityScore < t.faceMatchThreshold) {\n return verdict('requires_review', 'FACE_MATCH_LOW_CONFIDENCE')\n }\n // A failed address that didn't reject (on_fail: 'review') lands here.\n if (!input.address.passed) {\n return verdict('requires_review', 'ADDRESS_VERIFICATION_FAILED')\n }\n // Auto-verify gate: the workflow can set its own minimum address confidence;\n // a passed address below it is held for manual review (never rejected here).\n if (input.address.score < (input.address.threshold ?? t.addressThreshold)) {\n return verdict('requires_review', 'ADDRESS_LOW_CONFIDENCE')\n }\n\n const reviewSignal = RiskSignals.reasonFor(signals, 'review')\n if (reviewSignal) {\n return verdict('requires_review', reviewSignal)\n }\n\n // --- All checks cleared ---\n return verdict('approved', 'AUTO_APPROVED')\n }\n}\n"],"mappings":";;;;;AAsEA,IAAa,iBAAb,MAA4B;;;;;;;;;;;;;;;;;CAiB1B,OAAO,OAAO,OAAsB,YAA8D;EAChG,MAAM,IAAI,WAAW,QAAQ,UAAU;EACvC,MAAM,YAAY,KAAK,MAAM,KAAK;EAClC,MAAM,WAAW,UAAgC,YAA4C;GAC3F;GACA;GACA;EACF;EAGA,IAAI,MAAM,SAAS,SACjB,OAAO,QAAQ,YAAY,kBAAkB;EAE/C,IAAI,CAAC,MAAM,SAAS,QAClB,OAAO,QAAQ,YAAY,iBAAiB;EAE9C,IAAI,CAAC,MAAM,UAAU,QACnB,OAAO,QAAQ,YAAY,mBAAmB;EAIhD,IAAI,CAAC,MAAM,QAAQ,UAAU,MAAM,QAAQ,WAAW,UACpD,OAAO,QAAQ,YAAY,6BAA6B;EAG1D,MAAM,UAAU,MAAM,WAAW,CAAC;EAClC,MAAM,eAAe,YAAY,UAAU,SAAS,QAAQ;EAC5D,IAAI,cACF,OAAO,QAAQ,YAAY,YAAY;EAIzC,IAAI,MAAM,SAAS,kBAAkB,MACnC,OAAO,QAAQ,mBAAmB,yBAAyB;EAE7D,IAAI,MAAM,SAAS,eAAe,EAAE,0BAClC,OAAO,QAAQ,mBAAmB,sBAAsB;EAE1D,IAAI,MAAM,SAAS,gBAAgB,EAAE,wBACnC,OAAO,QAAQ,mBAAmB,oBAAoB;EAExD,IAAI,MAAM,SAAS,QAAQ,EAAE,mBAC3B,OAAO,QAAQ,mBAAmB,yBAAyB;EAE7D,IAAI,MAAM,UAAU,kBAAkB,EAAE,oBACtC,OAAO,QAAQ,mBAAmB,2BAA2B;EAG/D,IAAI,CAAC,MAAM,QAAQ,QACjB,OAAO,QAAQ,mBAAmB,6BAA6B;EAIjE,IAAI,MAAM,QAAQ,SAAS,MAAM,QAAQ,aAAa,EAAE,mBACtD,OAAO,QAAQ,mBAAmB,wBAAwB;EAG5D,MAAM,eAAe,YAAY,UAAU,SAAS,QAAQ;EAC5D,IAAI,cACF,OAAO,QAAQ,mBAAmB,YAAY;EAIhD,OAAO,QAAQ,YAAY,eAAe;CAC5C;AACF"}
package/dist/index.d.mts CHANGED
@@ -2,7 +2,8 @@ import { Thresholds } from "./thresholds.mjs";
2
2
  import { InvalidStatusTransitionError, StatusMachine } from "./status.mjs";
3
3
  import { DecisionAddressInput, DecisionDocumentInput, DecisionEngine, DecisionFaceMatchInput, DecisionInput, DecisionLivenessInput, DecisionResult } from "./decision.mjs";
4
4
  import { Risk } from "./risk.mjs";
5
+ import { RiskSignals } from "./signals.mjs";
5
6
  import { SessionRules } from "./session.mjs";
6
7
  import { Normalize, SessionSignals } from "./normalize.mjs";
7
8
  import { OrganizationContext, OrganizationProjectContext, OrganizationScopeError } from "./context.mjs";
8
- export { DecisionAddressInput, DecisionDocumentInput, DecisionEngine, DecisionFaceMatchInput, DecisionInput, DecisionLivenessInput, DecisionResult, InvalidStatusTransitionError, Normalize, OrganizationContext, OrganizationProjectContext, OrganizationScopeError, Risk, SessionRules, SessionSignals, StatusMachine, Thresholds };
9
+ export { DecisionAddressInput, DecisionDocumentInput, DecisionEngine, DecisionFaceMatchInput, DecisionInput, DecisionLivenessInput, DecisionResult, InvalidStatusTransitionError, Normalize, OrganizationContext, OrganizationProjectContext, OrganizationScopeError, Risk, RiskSignals, SessionRules, SessionSignals, StatusMachine, Thresholds };
package/dist/index.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Thresholds } from "./thresholds.mjs";
2
2
  import { InvalidStatusTransitionError, StatusMachine } from "./status.mjs";
3
3
  import { Risk } from "./risk.mjs";
4
+ import { RiskSignals } from "./signals.mjs";
4
5
  import { DecisionEngine } from "./decision.mjs";
5
6
  import { SessionRules } from "./session.mjs";
6
7
  import { Normalize } from "./normalize.mjs";
7
8
  import { OrganizationContext, OrganizationScopeError } from "./context.mjs";
8
- export { DecisionEngine, InvalidStatusTransitionError, Normalize, OrganizationContext, OrganizationScopeError, Risk, SessionRules, StatusMachine, Thresholds };
9
+ export { DecisionEngine, InvalidStatusTransitionError, Normalize, OrganizationContext, OrganizationScopeError, Risk, RiskSignals, SessionRules, StatusMachine, Thresholds };
@@ -1,12 +1,11 @@
1
1
  import { DecisionInput } from "./decision.mjs";
2
2
  import { AddressOnFail, AddressResultData, FaceMatchResultData, IsoDateTime, LivenessResultData, OcrResultData, WebhookChecks } from "@arkyc/types";
3
-
4
3
  //#region src/normalize.d.ts
5
4
  /**
6
5
  * The raw provider outputs gathered for a session, plus the document quality.
7
6
  * `address` is only present when the (opt-in) address stage ran.
8
7
  */
9
- interface SessionSignals {
8
+ export interface SessionSignals {
10
9
  documentQualityScore: number;
11
10
  ocr: OcrResultData;
12
11
  liveness: LivenessResultData;
@@ -17,7 +16,7 @@ interface SessionSignals {
17
16
  addressOnFail?: AddressOnFail;
18
17
  }
19
18
  /** Normalisation of gathered provider results into engine/webhook shapes. */
20
- declare class Normalize {
19
+ export declare class Normalize {
21
20
  /**
22
21
  * Normalise gathered provider results into the flat {@link DecisionInput} the
23
22
  * decision engine consumes. Document expiry is derived from the OCR `expiryDate`
@@ -39,5 +38,4 @@ declare class Normalize {
39
38
  static toWebhookChecks(signals: SessionSignals, now: IsoDateTime | Date): WebhookChecks;
40
39
  }
41
40
  //#endregion
42
- export { Normalize, SessionSignals };
43
41
  //# sourceMappingURL=normalize.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"normalize.d.mts","names":[],"sources":["../src/normalize.ts"],"mappings":";;;;;;AAiBA;;UAAiB,cAAA;EACf,oBAAA;EACA,GAAA,EAAK,aAAA;EACL,QAAA,EAAU,kBAAA;EACV,SAAA,EAAW,mBAAA;EAIK;EAFhB,OAAA,GAAU,iBAAA;EAEmB;EAA7B,aAAA,GAAgB,aAAA;AAAA;;cAIL,SAAA;EATD;;;;;;;;AAKmB;EALnB,OAmBH,eAAA,CAAgB,OAAA,EAAS,cAAA,EAAgB,GAAA,EAAK,WAAA,GAAc,IAAA,GAAO,aAAA;EAVtD;;;;;;;;EAAA,OAyCb,eAAA,CAAgB,OAAA,EAAS,cAAA,EAAgB,GAAA,EAAK,WAAA,GAAc,IAAA,GAAO,aAAA;AAAA"}
1
+ {"version":3,"file":"normalize.d.mts","names":[],"sources":["../src/normalize.ts"],"mappings":";;;;;;;iBAiBiB;EACf;EACA,KAAK;EACL,UAAU;EACV,WAAW;;EAEX,UAAU;;EAEV,gBAAgB;;;qBAIL;;;;;;;;;;SAUJ,gBAAgB,SAAS,gBAAgB,KAAK,cAAc,OAAO;;;;;;;;;SA+BnE,gBAAgB,SAAS,gBAAgB,KAAK,cAAc,OAAO"}
package/dist/risk.d.mts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { DecisionInput } from "./decision.mjs";
2
-
3
2
  //#region src/risk.d.ts
4
3
  /** Aggregate risk scoring for a verification session. */
5
- declare class Risk {
4
+ export declare class Risk {
6
5
  /**
7
6
  * Clamp a number into the [0, 1] range.
8
7
  *
@@ -16,7 +15,9 @@ declare class Risk {
16
15
  * The baseline is the inverse of the mean of the positive signals (document
17
16
  * quality, OCR confidence, liveness score, face-match similarity). Hard failure
18
17
  * signals (expired document, failed liveness/face-match, multiple faces) floor
19
- * the risk near the top of the range so risky sessions never look safe.
18
+ * the risk near the top of the range so risky sessions never look safe. Each
19
+ * non-informational signal compounds the remaining headroom by its score, and
20
+ * a rejecting signal floors it like a hard failure.
20
21
  *
21
22
  * @param input
22
23
  * @returns
@@ -24,5 +25,4 @@ declare class Risk {
24
25
  static score(input: DecisionInput): number;
25
26
  }
26
27
  //#endregion
27
- export { Risk };
28
28
  //# sourceMappingURL=risk.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"risk.d.mts","names":[],"sources":["../src/risk.ts"],"mappings":";;;;cASa,IAAA;EAAA;;;;;;EAAA,OAOJ,OAAA,CAAQ,KAAA;EAgBK;;;AAAa;;;;;;;;EAAb,OAAb,KAAA,CAAM,KAAA,EAAO,aAAa;AAAA"}
1
+ {"version":3,"file":"risk.d.mts","names":[],"sources":["../src/risk.ts"],"mappings":";;;qBASa;;;;;;;SAOJ,QAAQ;;;;;;;;;;;;;;SAkBR,MAAM,OAAO"}
package/dist/risk.mjs CHANGED
@@ -22,7 +22,9 @@ var Risk = class Risk {
22
22
  * The baseline is the inverse of the mean of the positive signals (document
23
23
  * quality, OCR confidence, liveness score, face-match similarity). Hard failure
24
24
  * signals (expired document, failed liveness/face-match, multiple faces) floor
25
- * the risk near the top of the range so risky sessions never look safe.
25
+ * the risk near the top of the range so risky sessions never look safe. Each
26
+ * non-informational signal compounds the remaining headroom by its score, and
27
+ * a rejecting signal floors it like a hard failure.
26
28
  *
27
29
  * @param input
28
30
  * @returns
@@ -35,7 +37,10 @@ var Risk = class Risk {
35
37
  input.faceMatch.similarityScore,
36
38
  input.address.score
37
39
  ].map(Risk.clamp01));
38
- if (input.document.expired || !input.liveness.passed || !input.faceMatch.passed || input.liveness.multipleFaces === true || !input.address.passed && input.address.onFail === "reject") risk = Math.max(risk, .9);
40
+ const hardFailure = input.document.expired || !input.liveness.passed || !input.faceMatch.passed || input.liveness.multipleFaces === true || !input.address.passed && input.address.onFail === "reject";
41
+ const signals = (input.signals ?? []).filter((signal) => signal.severity !== "info");
42
+ for (const signal of signals) risk = 1 - (1 - risk) * (1 - Risk.clamp01(signal.score));
43
+ if (hardFailure || signals.some((signal) => signal.severity === "reject")) risk = Math.max(risk, .9);
39
44
  return Risk.clamp01(risk);
40
45
  }
41
46
  };
package/dist/risk.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"risk.mjs","names":[],"sources":["../src/risk.ts"],"sourcesContent":["import type { DecisionInput } from './decision'\n\n/** Average of a list of numbers; 0 for an empty list. */\nfunction mean(values: number[]): number {\n if (values.length === 0) return 0\n return values.reduce((sum, v) => sum + v, 0) / values.length\n}\n\n/** Aggregate risk scoring for a verification session. */\nexport class Risk {\n /**\n * Clamp a number into the [0, 1] range.\n *\n * @param value\n * @returns\n */\n static clamp01(value: number): number {\n if (Number.isNaN(value)) return 0\n return Math.min(1, Math.max(0, value))\n }\n\n /**\n * Compute an aggregate risk score in [0, 1] for a session (higher = riskier).\n *\n * The baseline is the inverse of the mean of the positive signals (document\n * quality, OCR confidence, liveness score, face-match similarity). Hard failure\n * signals (expired document, failed liveness/face-match, multiple faces) floor\n * the risk near the top of the range so risky sessions never look safe.\n *\n * @param input\n * @returns\n */\n static score(input: DecisionInput): number {\n const positives = [\n input.document.qualityScore,\n input.document.ocrConfidence,\n input.liveness.score,\n input.faceMatch.similarityScore,\n input.address.score,\n ].map(Risk.clamp01)\n\n let risk = 1 - mean(positives)\n\n const hardFailure =\n input.document.expired ||\n !input.liveness.passed ||\n !input.faceMatch.passed ||\n input.liveness.multipleFaces === true ||\n // A failed address only floors risk when it's a rejecting failure.\n (!input.address.passed && input.address.onFail === 'reject')\n\n if (hardFailure) {\n risk = Math.max(risk, 0.9)\n }\n\n return Risk.clamp01(risk)\n }\n}\n"],"mappings":";;AAGA,SAAS,KAAK,QAA0B;CACtC,IAAI,OAAO,WAAW,GAAG,OAAO;CAChC,OAAO,OAAO,QAAQ,KAAK,MAAM,MAAM,GAAG,CAAC,IAAI,OAAO;AACxD;;AAGA,IAAa,OAAb,MAAa,KAAK;;;;;;;CAOhB,OAAO,QAAQ,OAAuB;EACpC,IAAI,OAAO,MAAM,KAAK,GAAG,OAAO;EAChC,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;CACvC;;;;;;;;;;;;CAaA,OAAO,MAAM,OAA8B;EASzC,IAAI,OAAO,IAAI,KARG;GAChB,MAAM,SAAS;GACf,MAAM,SAAS;GACf,MAAM,SAAS;GACf,MAAM,UAAU;GAChB,MAAM,QAAQ;EAChB,CAAC,CAAC,IAAI,KAAK,OAEiB,CAAC;EAU7B,IAPE,MAAM,SAAS,WACf,CAAC,MAAM,SAAS,UAChB,CAAC,MAAM,UAAU,UACjB,MAAM,SAAS,kBAAkB,QAEhC,CAAC,MAAM,QAAQ,UAAU,MAAM,QAAQ,WAAW,UAGnD,OAAO,KAAK,IAAI,MAAM,EAAG;EAG3B,OAAO,KAAK,QAAQ,IAAI;CAC1B;AACF"}
1
+ {"version":3,"file":"risk.mjs","names":[],"sources":["../src/risk.ts"],"sourcesContent":["import type { DecisionInput } from './decision'\n\n/** Average of a list of numbers; 0 for an empty list. */\nfunction mean(values: number[]): number {\n if (values.length === 0) return 0\n return values.reduce((sum, v) => sum + v, 0) / values.length\n}\n\n/** Aggregate risk scoring for a verification session. */\nexport class Risk {\n /**\n * Clamp a number into the [0, 1] range.\n *\n * @param value\n * @returns\n */\n static clamp01(value: number): number {\n if (Number.isNaN(value)) return 0\n return Math.min(1, Math.max(0, value))\n }\n\n /**\n * Compute an aggregate risk score in [0, 1] for a session (higher = riskier).\n *\n * The baseline is the inverse of the mean of the positive signals (document\n * quality, OCR confidence, liveness score, face-match similarity). Hard failure\n * signals (expired document, failed liveness/face-match, multiple faces) floor\n * the risk near the top of the range so risky sessions never look safe. Each\n * non-informational signal compounds the remaining headroom by its score, and\n * a rejecting signal floors it like a hard failure.\n *\n * @param input\n * @returns\n */\n static score(input: DecisionInput): number {\n const positives = [\n input.document.qualityScore,\n input.document.ocrConfidence,\n input.liveness.score,\n input.faceMatch.similarityScore,\n input.address.score,\n ].map(Risk.clamp01)\n\n let risk = 1 - mean(positives)\n\n const hardFailure =\n input.document.expired ||\n !input.liveness.passed ||\n !input.faceMatch.passed ||\n input.liveness.multipleFaces === true ||\n // A failed address only floors risk when it's a rejecting failure.\n (!input.address.passed && input.address.onFail === 'reject')\n\n const signals = (input.signals ?? []).filter((signal) => signal.severity !== 'info')\n for (const signal of signals) {\n risk = 1 - (1 - risk) * (1 - Risk.clamp01(signal.score))\n }\n\n if (hardFailure || signals.some((signal) => signal.severity === 'reject')) {\n risk = Math.max(risk, 0.9)\n }\n\n return Risk.clamp01(risk)\n }\n}\n"],"mappings":";;AAGA,SAAS,KAAK,QAA0B;CACtC,IAAI,OAAO,WAAW,GAAG,OAAO;CAChC,OAAO,OAAO,QAAQ,KAAK,MAAM,MAAM,GAAG,CAAC,IAAI,OAAO;AACxD;;AAGA,IAAa,OAAb,MAAa,KAAK;;;;;;;CAOhB,OAAO,QAAQ,OAAuB;EACpC,IAAI,OAAO,MAAM,KAAK,GAAG,OAAO;EAChC,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;CACvC;;;;;;;;;;;;;;CAeA,OAAO,MAAM,OAA8B;EASzC,IAAI,OAAO,IAAI,KARG;GAChB,MAAM,SAAS;GACf,MAAM,SAAS;GACf,MAAM,SAAS;GACf,MAAM,UAAU;GAChB,MAAM,QAAQ;EAChB,CAAC,CAAC,IAAI,KAAK,OAEiB,CAAC;EAE7B,MAAM,cACJ,MAAM,SAAS,WACf,CAAC,MAAM,SAAS,UAChB,CAAC,MAAM,UAAU,UACjB,MAAM,SAAS,kBAAkB,QAEhC,CAAC,MAAM,QAAQ,UAAU,MAAM,QAAQ,WAAW;EAErD,MAAM,WAAW,MAAM,WAAW,CAAC,EAAA,CAAG,QAAQ,WAAW,OAAO,aAAa,MAAM;EACnF,KAAK,MAAM,UAAU,SACnB,OAAO,KAAK,IAAI,SAAS,IAAI,KAAK,QAAQ,OAAO,KAAK;EAGxD,IAAI,eAAe,QAAQ,MAAM,WAAW,OAAO,aAAa,QAAQ,GACtE,OAAO,KAAK,IAAI,MAAM,EAAG;EAG3B,OAAO,KAAK,QAAQ,IAAI;CAC1B;AACF"}
@@ -1,8 +1,7 @@
1
1
  import { IsoDate, IsoDateTime, VerificationStatus } from "@arkyc/types";
2
-
3
2
  //#region src/session.d.ts
4
3
  /** Pure session/document expiry rules. */
5
- declare class SessionRules {
4
+ export declare class SessionRules {
6
5
  /**
7
6
  * Whether a session has passed its `expires_at` instant.
8
7
  *
@@ -36,5 +35,4 @@ declare class SessionRules {
36
35
  static shouldExpire(status: VerificationStatus, expiresAt: IsoDateTime | Date, now: IsoDateTime | Date): boolean;
37
36
  }
38
37
  //#endregion
39
- export { SessionRules };
40
38
  //# sourceMappingURL=session.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.mts","names":[],"sources":["../src/session.ts"],"mappings":";;;;cAUa,YAAA;EAAA;;;;;;;;;EAAA,OAUJ,gBAAA,CAAiB,SAAA,EAAW,WAAA,GAAc,IAAA,EAAM,GAAA,EAAK,WAAA,GAAc,IAAA;EA8B9C;;;;;;;;;;EAAA,OAhBrB,iBAAA,CAAkB,UAAA,EAAY,OAAA,qBAA4B,GAAA,EAAK,WAAA,GAAc,IAAA;EAdxB;;;;;;;;;EAAA,OA8BrD,YAAA,CAAa,MAAA,EAAQ,kBAAA,EAAoB,SAAA,EAAW,WAAA,GAAc,IAAA,EAAM,GAAA,EAAK,WAAA,GAAc,IAAA;AAAA"}
1
+ {"version":3,"file":"session.d.mts","names":[],"sources":["../src/session.ts"],"mappings":";;;qBAUa;;;;;;;;;;SAUJ,iBAAiB,WAAW,cAAc,MAAM,KAAK,cAAc;;;;;;;;;;;SAcnE,kBAAkB,YAAY,4BAA4B,KAAK,cAAc;;;;;;;;;;SAgB7E,aAAa,QAAQ,oBAAoB,WAAW,cAAc,MAAM,KAAK,cAAc"}
@@ -0,0 +1,24 @@
1
+ import { DecisionReason, RiskPolicyConfig, RiskSeverity, RiskSignal } from "@arkyc/types";
2
+ //#region src/signals.d.ts
3
+ /** Policy resolution and precedence for risk signals. */
4
+ export declare class RiskSignals {
5
+ /**
6
+ * Resolve each signal's severity from the workflow policy. `info` signals stay
7
+ * informational, advisory and review-only signals never exceed review.
8
+ *
9
+ * @param signals
10
+ * @param policy
11
+ * @returns
12
+ */
13
+ static applyPolicy<T extends RiskSignal>(signals: T[], policy?: RiskPolicyConfig): T[];
14
+ /**
15
+ * The reason of the highest-precedence signal at `severity`, or `null`.
16
+ *
17
+ * @param signals
18
+ * @param severity
19
+ * @returns
20
+ */
21
+ static reasonFor(signals: RiskSignal[], severity: Exclude<RiskSeverity, 'info'>): DecisionReason | null;
22
+ }
23
+ //#endregion
24
+ //# sourceMappingURL=signals.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signals.d.mts","names":[],"sources":["../src/signals.ts"],"mappings":";;;qBAmBa;;;;;;;;;SASJ,YAAY,UAAU,YAAY,SAAS,KAAK,SAAQ,mBAAwB;;;;;;;;SAmBhF,UAAU,SAAS,cAAc,UAAU,QAAQ,wBAAwB"}
@@ -0,0 +1,46 @@
1
+ import { DEFAULT_RISK_SIGNAL_POLICY, REVIEW_ONLY_RISK_SIGNALS, RISK_SIGNAL_KEYS, RISK_SIGNAL_REASONS } from "@arkyc/types";
2
+ //#region src/signals.ts
3
+ const POLICY_SEVERITY = {
4
+ ignore: "info",
5
+ review: "review",
6
+ reject: "reject"
7
+ };
8
+ /** Policy resolution and precedence for risk signals. */
9
+ var RiskSignals = class {
10
+ /**
11
+ * Resolve each signal's severity from the workflow policy. `info` signals stay
12
+ * informational, advisory and review-only signals never exceed review.
13
+ *
14
+ * @param signals
15
+ * @param policy
16
+ * @returns
17
+ */
18
+ static applyPolicy(signals, policy = {}) {
19
+ return signals.map((signal) => {
20
+ if (signal.severity === "info") return signal;
21
+ let severity = POLICY_SEVERITY[policy[signal.key] ?? DEFAULT_RISK_SIGNAL_POLICY];
22
+ const capped = signal.advisory === true || REVIEW_ONLY_RISK_SIGNALS.includes(signal.key);
23
+ if (severity === "reject" && capped) severity = "review";
24
+ return {
25
+ ...signal,
26
+ severity
27
+ };
28
+ });
29
+ }
30
+ /**
31
+ * The reason of the highest-precedence signal at `severity`, or `null`.
32
+ *
33
+ * @param signals
34
+ * @param severity
35
+ * @returns
36
+ */
37
+ static reasonFor(signals, severity) {
38
+ const keys = new Set(signals.filter((signal) => signal.severity === severity).map((signal) => signal.key));
39
+ const key = RISK_SIGNAL_KEYS.find((candidate) => keys.has(candidate));
40
+ return key ? RISK_SIGNAL_REASONS[key] : null;
41
+ }
42
+ };
43
+ //#endregion
44
+ export { RiskSignals };
45
+
46
+ //# sourceMappingURL=signals.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signals.mjs","names":[],"sources":["../src/signals.ts"],"sourcesContent":["import {\n DEFAULT_RISK_SIGNAL_POLICY,\n REVIEW_ONLY_RISK_SIGNALS,\n RISK_SIGNAL_KEYS,\n RISK_SIGNAL_REASONS,\n type DecisionReason,\n type RiskPolicyConfig,\n type RiskSeverity,\n type RiskSignal,\n type RiskSignalPolicy,\n} from '@arkyc/types'\n\nconst POLICY_SEVERITY: Record<RiskSignalPolicy, RiskSeverity> = {\n ignore: 'info',\n review: 'review',\n reject: 'reject',\n}\n\n/** Policy resolution and precedence for risk signals. */\nexport class RiskSignals {\n /**\n * Resolve each signal's severity from the workflow policy. `info` signals stay\n * informational, advisory and review-only signals never exceed review.\n *\n * @param signals\n * @param policy\n * @returns\n */\n static applyPolicy<T extends RiskSignal>(signals: T[], policy: RiskPolicyConfig = {}): T[] {\n return signals.map((signal) => {\n if (signal.severity === 'info') return signal\n\n let severity = POLICY_SEVERITY[policy[signal.key] ?? DEFAULT_RISK_SIGNAL_POLICY]\n const capped = signal.advisory === true || REVIEW_ONLY_RISK_SIGNALS.includes(signal.key)\n if (severity === 'reject' && capped) severity = 'review'\n\n return { ...signal, severity }\n })\n }\n\n /**\n * The reason of the highest-precedence signal at `severity`, or `null`.\n *\n * @param signals\n * @param severity\n * @returns\n */\n static reasonFor(signals: RiskSignal[], severity: Exclude<RiskSeverity, 'info'>): DecisionReason | null {\n const keys = new Set(signals.filter((signal) => signal.severity === severity).map((signal) => signal.key))\n const key = RISK_SIGNAL_KEYS.find((candidate) => keys.has(candidate))\n\n return key ? RISK_SIGNAL_REASONS[key] : null\n }\n}\n"],"mappings":";;AAYA,MAAM,kBAA0D;CAC9D,QAAQ;CACR,QAAQ;CACR,QAAQ;AACV;;AAGA,IAAa,cAAb,MAAyB;;;;;;;;;CASvB,OAAO,YAAkC,SAAc,SAA2B,CAAC,GAAQ;EACzF,OAAO,QAAQ,KAAK,WAAW;GAC7B,IAAI,OAAO,aAAa,QAAQ,OAAO;GAEvC,IAAI,WAAW,gBAAgB,OAAO,OAAO,QAAQ;GACrD,MAAM,SAAS,OAAO,aAAa,QAAQ,yBAAyB,SAAS,OAAO,GAAG;GACvF,IAAI,aAAa,YAAY,QAAQ,WAAW;GAEhD,OAAO;IAAE,GAAG;IAAQ;GAAS;EAC/B,CAAC;CACH;;;;;;;;CASA,OAAO,UAAU,SAAuB,UAAgE;EACtG,MAAM,OAAO,IAAI,IAAI,QAAQ,QAAQ,WAAW,OAAO,aAAa,QAAQ,CAAC,CAAC,KAAK,WAAW,OAAO,GAAG,CAAC;EACzG,MAAM,MAAM,iBAAiB,MAAM,cAAc,KAAK,IAAI,SAAS,CAAC;EAEpE,OAAO,MAAM,oBAAoB,OAAO;CAC1C;AACF"}
package/dist/status.d.mts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { VerificationStatus } from "@arkyc/types";
2
-
3
2
  //#region src/status.d.ts
4
3
  /**
5
4
  * Thrown when an illegal status transition is attempted.
6
5
  */
7
- declare class InvalidStatusTransitionError extends Error {
6
+ export declare class InvalidStatusTransitionError extends Error {
8
7
  readonly from: VerificationStatus;
9
8
  readonly to: VerificationStatus;
10
9
  constructor(from: VerificationStatus, to: VerificationStatus);
@@ -12,7 +11,7 @@ declare class InvalidStatusTransitionError extends Error {
12
11
  /**
13
12
  * Verification session status-transition rules.
14
13
  */
15
- declare class StatusMachine {
14
+ export declare class StatusMachine {
16
15
  /**
17
16
  * Allowed status transitions for a verification session.
18
17
  *
@@ -52,5 +51,4 @@ declare class StatusMachine {
52
51
  static assert(from: VerificationStatus, to: VerificationStatus): VerificationStatus;
53
52
  }
54
53
  //#endregion
55
- export { InvalidStatusTransitionError, StatusMachine };
56
54
  //# sourceMappingURL=status.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.mts","names":[],"sources":["../src/status.ts"],"mappings":";;;;;AAKA;cAAa,4BAAA,SAAqC,KAAA;EAAA,SAE9B,IAAA,EAAM,kBAAA;EAAA,SACN,EAAA,EAAI,kBAAA;cADJ,IAAA,EAAM,kBAAA,EACN,EAAA,EAAI,kBAAA;AAAA;;;;cAUX,aAAA;EAbqC;;;;;;;;;;EAAA,gBAwBhC,WAAA,EAAa,QAAA,CAAS,MAAA,CAAO,kBAAA,WAA6B,kBAAA;EArBlC;EAAA,gBAwCxB,QAAA,WAAmB,kBAAA;EA9BX;;;;;;EAAA,OAsCjB,UAAA,CAAW,MAAA,EAAQ,kBAAA;EAAA;;;;;;;EAAA,OAWnB,aAAA,CAAc,IAAA,EAAM,kBAAA,EAAoB,EAAA,EAAI,kBAAA;EAtCnC;;;;;;;;EAAA,OAkDT,MAAA,CAAO,IAAA,EAAM,kBAAA,EAAoB,EAAA,EAAI,kBAAA,GAAqB,kBAAA;AAAA"}
1
+ {"version":3,"file":"status.d.mts","names":[],"sources":["../src/status.ts"],"mappings":";;;;;qBAKa,qCAAqC;WAE9B,MAAM;WACN,IAAI;EADJ,YAAA,MAAM,oBACN,IAAI;;;;;qBAUX;;;;;;;;;;;kBAWK,aAAa,SAAS,OAAO,6BAA6B;;kBAmB1D,mBAAmB;;;;;;;SAQ5B,WAAW,QAAQ;;;;;;;;SAWnB,cAAc,MAAM,oBAAoB,IAAI;;;;;;;;;SAY5C,OAAO,MAAM,oBAAoB,IAAI,qBAAqB"}
@@ -1,8 +1,7 @@
1
1
  import { VerificationThresholds } from "@arkyc/types";
2
-
3
2
  //#region src/thresholds.d.ts
4
3
  /** Verification threshold defaults and override resolution. */
5
- declare class Thresholds {
4
+ export declare class Thresholds {
6
5
  /**
7
6
  * Platform default verification thresholds. Projects may override any subset
8
7
  * via `ProjectSettings.thresholds`; {@link Thresholds.resolve} merges them.
@@ -17,5 +16,4 @@ declare class Thresholds {
17
16
  static resolve(overrides?: Partial<VerificationThresholds>): VerificationThresholds;
18
17
  }
19
18
  //#endregion
20
- export { Thresholds };
21
19
  //# sourceMappingURL=thresholds.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"thresholds.d.mts","names":[],"sources":["../src/thresholds.ts"],"mappings":";;;;cAGa,UAAA;EAAA;;;;EAAA,gBAKK,QAAA,EAAU,sBAAA;EAcC;;;;;;EAAA,OAApB,OAAA,CAAQ,SAAA,GAAY,OAAA,CAAQ,sBAAA,IAA0B,sBAAA;AAAA"}
1
+ {"version":3,"file":"thresholds.d.mts","names":[],"sources":["../src/thresholds.ts"],"mappings":";;;qBAGa;;;;;kBAKK,UAAU;;;;;;;SAQnB,QAAQ,YAAY,QAAQ,0BAA0B"}
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_VERIFICATION_THRESHOLDS } from "@arkyc/types";
1
2
  //#region src/thresholds.ts
2
3
  /** Verification threshold defaults and override resolution. */
3
4
  var Thresholds = class Thresholds {
@@ -5,13 +6,7 @@ var Thresholds = class Thresholds {
5
6
  * Platform default verification thresholds. Projects may override any subset
6
7
  * via `ProjectSettings.thresholds`; {@link Thresholds.resolve} merges them.
7
8
  */
8
- static DEFAULTS = {
9
- documentQualityThreshold: .75,
10
- ocrConfidenceThreshold: .8,
11
- livenessThreshold: .85,
12
- faceMatchThreshold: .75,
13
- addressThreshold: .6
14
- };
9
+ static DEFAULTS = DEFAULT_VERIFICATION_THRESHOLDS;
15
10
  /**
16
11
  * Merge a partial set of project overrides over the platform defaults.
17
12
  *
@@ -1 +1 @@
1
- {"version":3,"file":"thresholds.mjs","names":[],"sources":["../src/thresholds.ts"],"sourcesContent":["import type { VerificationThresholds } from '@arkyc/types'\n\n/** Verification threshold defaults and override resolution. */\nexport class Thresholds {\n /**\n * Platform default verification thresholds. Projects may override any subset\n * via `ProjectSettings.thresholds`; {@link Thresholds.resolve} merges them.\n */\n static readonly DEFAULTS: VerificationThresholds = {\n documentQualityThreshold: 0.75,\n ocrConfidenceThreshold: 0.8,\n livenessThreshold: 0.85,\n faceMatchThreshold: 0.75,\n addressThreshold: 0.6,\n }\n\n /**\n * Merge a partial set of project overrides over the platform defaults.\n *\n * @param overrides\n * @returns\n */\n static resolve(overrides?: Partial<VerificationThresholds>): VerificationThresholds {\n return { ...Thresholds.DEFAULTS, ...(overrides ?? {}) }\n }\n}\n"],"mappings":";;AAGA,IAAa,aAAb,MAAa,WAAW;;;;;CAKtB,OAAgB,WAAmC;EACjD,0BAA0B;EAC1B,wBAAwB;EACxB,mBAAmB;EACnB,oBAAoB;EACpB,kBAAkB;CACpB;;;;;;;CAQA,OAAO,QAAQ,WAAqE;EAClF,OAAO;GAAE,GAAG,WAAW;GAAU,GAAI,aAAa,CAAC;EAAG;CACxD;AACF"}
1
+ {"version":3,"file":"thresholds.mjs","names":[],"sources":["../src/thresholds.ts"],"sourcesContent":["import { DEFAULT_VERIFICATION_THRESHOLDS, type VerificationThresholds } from '@arkyc/types'\n\n/** Verification threshold defaults and override resolution. */\nexport class Thresholds {\n /**\n * Platform default verification thresholds. Projects may override any subset\n * via `ProjectSettings.thresholds`; {@link Thresholds.resolve} merges them.\n */\n static readonly DEFAULTS: VerificationThresholds = DEFAULT_VERIFICATION_THRESHOLDS\n\n /**\n * Merge a partial set of project overrides over the platform defaults.\n *\n * @param overrides\n * @returns\n */\n static resolve(overrides?: Partial<VerificationThresholds>): VerificationThresholds {\n return { ...Thresholds.DEFAULTS, ...(overrides ?? {}) }\n }\n}\n"],"mappings":";;;AAGA,IAAa,aAAb,MAAa,WAAW;;;;;CAKtB,OAAgB,WAAmC;;;;;;;CAQnD,OAAO,QAAQ,WAAqE;EAClF,OAAO;GAAE,GAAG,WAAW;GAAU,GAAI,aAAa,CAAC;EAAG;CACxD;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkyc/core",
3
- "version": "1.2.11",
3
+ "version": "2.1.0",
4
4
  "description": "Decision engine, status transitions, and scoring logic",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -36,7 +36,7 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@arkyc/types": "^1.2.11"
39
+ "@arkyc/types": "^2.1.0"
40
40
  },
41
41
  "scripts": {
42
42
  "typecheck": "tsc --noEmit",