@vixor/contracts 0.2.0 → 0.2.2
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/LICENSE +61 -0
- package/README.md +39 -37
- package/dist/envelope.d.ts +27 -0
- package/dist/envelope.js +68 -0
- package/dist/fixtures.d.ts +25 -0
- package/dist/fixtures.js +81 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +17 -0
- package/dist/opportunity.d.ts +35 -0
- package/dist/opportunity.js +124 -0
- package/dist/publication.d.ts +30 -0
- package/dist/publication.js +124 -0
- package/dist/risk.d.ts +30 -0
- package/dist/risk.js +73 -0
- package/dist/validate.d.ts +40 -0
- package/dist/validate.js +80 -0
- package/package.json +43 -42
package/LICENSE
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
VIXOR PROPRIETARY INTERNAL USE LICENSE
|
|
2
|
+
Version 1.0 — 2026-11-05
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 VIXOR. All rights reserved.
|
|
5
|
+
|
|
6
|
+
This license applies to the @vixor/contracts package, including its source
|
|
7
|
+
code, compiled files, declarations, schemas, types, documentation, and related
|
|
8
|
+
materials (the "Package").
|
|
9
|
+
|
|
10
|
+
1. Ownership
|
|
11
|
+
The Package is proprietary to VIXOR. All rights, title, and interest in the
|
|
12
|
+
Package remain with VIXOR. No ownership is transferred by this license.
|
|
13
|
+
|
|
14
|
+
2. Limited internal permission
|
|
15
|
+
VIXOR grants a limited, non-exclusive, non-transferable, revocable permission
|
|
16
|
+
to use, reproduce, modify, and integrate the Package solely within products,
|
|
17
|
+
applications, and services that form part of the VIXOR ecosystem, and only
|
|
18
|
+
by VIXOR personnel and other persons or service providers expressly
|
|
19
|
+
authorized by VIXOR who are performing work on VIXOR's behalf.
|
|
20
|
+
|
|
21
|
+
3. Restrictions
|
|
22
|
+
Without prior written permission from an authorized representative of VIXOR,
|
|
23
|
+
you may not:
|
|
24
|
+
- reuse the Package in an external or independent product or service;
|
|
25
|
+
- redistribute, publish, or make the Package available as a standalone
|
|
26
|
+
package or library;
|
|
27
|
+
- sell, sublicense, rent, lease, or otherwise commercially license the
|
|
28
|
+
Package to any third party;
|
|
29
|
+
- copy, incorporate, or merge the Package into a project outside the VIXOR
|
|
30
|
+
ecosystem; or
|
|
31
|
+
- grant any third party independent rights to use, copy, modify, or
|
|
32
|
+
redistribute the Package.
|
|
33
|
+
|
|
34
|
+
4. Service providers
|
|
35
|
+
A service provider authorized by VIXOR may access and use the Package only
|
|
36
|
+
to perform services for VIXOR, subject to confidentiality obligations and
|
|
37
|
+
the restrictions in this license. Such access does not grant the service
|
|
38
|
+
provider any independent right to reuse or redistribute the Package.
|
|
39
|
+
|
|
40
|
+
5. No open-source grant
|
|
41
|
+
The Package is not offered under MIT, Apache-2.0, GPL, or another
|
|
42
|
+
open-source license. No rights are granted except the limited internal
|
|
43
|
+
permission stated here or a separate written authorization from VIXOR.
|
|
44
|
+
|
|
45
|
+
6. Termination
|
|
46
|
+
VIXOR may revoke an authorization granted under this license. Upon
|
|
47
|
+
termination, the authorized user must stop using the Package and follow
|
|
48
|
+
VIXOR's written instructions regarding copies in their possession or
|
|
49
|
+
control, subject to applicable law and mandatory record-retention duties.
|
|
50
|
+
|
|
51
|
+
7. Written exceptions
|
|
52
|
+
Any exception to this license must be expressly approved in writing by an
|
|
53
|
+
authorized representative of VIXOR.
|
|
54
|
+
|
|
55
|
+
8. Disclaimer
|
|
56
|
+
To the maximum extent permitted by applicable law, the Package is provided
|
|
57
|
+
"as is", without warranties of any kind. VIXOR will not be liable for
|
|
58
|
+
indirect, incidental, special, or consequential damages arising from use
|
|
59
|
+
of the Package, except where liability cannot lawfully be excluded.
|
|
60
|
+
|
|
61
|
+
All rights reserved.
|
package/README.md
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
1
|
-
# @vixor/contracts
|
|
2
|
-
|
|
3
|
-
Zero-runtime-dependency TypeScript contracts for VIXOR integration boundaries.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
npm install @vixor/contracts
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Runtime
|
|
12
|
-
|
|
13
|
-
Node.js 20 or newer is supported. The published package contains compiled
|
|
14
|
-
ES modules and TypeScript declaration files under `dist/`; consumers do not
|
|
15
|
-
need a TypeScript runtime loader.
|
|
16
|
-
|
|
17
|
-
## Example
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
import { validateEnvelope, validateOpportunityPayload, fixtureEnvelope } from "@vixor/contracts";
|
|
21
|
-
|
|
22
|
-
const result = validateEnvelope(fixtureEnvelope(), validateOpportunityPayload);
|
|
23
|
-
if (!result.ok) throw new Error(result.errors.join("; "));
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Package SemVer (currently 0.2.
|
|
27
|
-
(currently `0.1`). Consumers must validate the wire schema version independently.
|
|
28
|
-
|
|
29
|
-
This package validates VIXOR event envelopes, opportunity payloads, Shield risk
|
|
30
|
-
assessments, and publication candidates. It does not authenticate requests,
|
|
31
|
-
verify HMAC signatures, authorize channels, or replace persistence/idempotency.
|
|
32
|
-
|
|
33
|
-
## License
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
# @vixor/contracts
|
|
2
|
+
|
|
3
|
+
Zero-runtime-dependency TypeScript contracts for VIXOR integration boundaries.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @vixor/contracts
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Runtime
|
|
12
|
+
|
|
13
|
+
Node.js 20 or newer is supported. The published package contains compiled
|
|
14
|
+
ES modules and TypeScript declaration files under `dist/`; consumers do not
|
|
15
|
+
need a TypeScript runtime loader.
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { validateEnvelope, validateOpportunityPayload, fixtureEnvelope } from "@vixor/contracts";
|
|
21
|
+
|
|
22
|
+
const result = validateEnvelope(fixtureEnvelope(), validateOpportunityPayload);
|
|
23
|
+
if (!result.ok) throw new Error(result.errors.join("; "));
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Package SemVer (currently 0.2.2) is independent of the wire `SCHEMA_VERSION`
|
|
27
|
+
(currently `0.1`). Consumers must validate the wire schema version independently.
|
|
28
|
+
|
|
29
|
+
This package validates VIXOR event envelopes, opportunity payloads, Shield risk
|
|
30
|
+
assessments, and publication candidates. It does not authenticate requests,
|
|
31
|
+
verify HMAC signatures, authorize channels, or replace persistence/idempotency.
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
This package is proprietary to VIXOR and is provided under the
|
|
36
|
+
[VIXOR Proprietary Internal Use License](./LICENSE). Use is limited to the
|
|
37
|
+
VIXOR ecosystem by persons and service providers authorized by VIXOR.
|
|
38
|
+
External reuse, standalone redistribution, sale, sublicensing, or integration
|
|
39
|
+
into projects outside VIXOR requires prior written authorization from VIXOR.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ValidationResult } from "./validate.ts";
|
|
2
|
+
/** Contract version — consumers must reject unsupported versions. */
|
|
3
|
+
export declare const SCHEMA_VERSION = "0.1";
|
|
4
|
+
export declare const PRODUCED_BY: readonly ["hunt", "shield", "core", "trade", "tg-content-bot"];
|
|
5
|
+
export type ProducedBy = (typeof PRODUCED_BY)[number];
|
|
6
|
+
/** The only event type on the minimal proof path (§2). */
|
|
7
|
+
export declare const EVENT_OPPORTUNITY_DETECTED = "opportunity.detected";
|
|
8
|
+
export interface EventEnvelope<P = unknown> {
|
|
9
|
+
schema_version: string;
|
|
10
|
+
event_id: string;
|
|
11
|
+
event_type: string;
|
|
12
|
+
occurred_at: string;
|
|
13
|
+
produced_by: ProducedBy;
|
|
14
|
+
correlation_id: string;
|
|
15
|
+
idempotency_key: string;
|
|
16
|
+
payload: P;
|
|
17
|
+
}
|
|
18
|
+
export interface EnvelopeOptions {
|
|
19
|
+
/** Restrict `event_type`; defaults to the minimal-path event only. */
|
|
20
|
+
allowedEventTypes?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate a §1 envelope. `validatePayload` validates the event-specific
|
|
24
|
+
* payload against its schema (e.g. §2 opportunity). Envelope identity fields
|
|
25
|
+
* are length-capped for log hygiene.
|
|
26
|
+
*/
|
|
27
|
+
export declare function validateEnvelope<P>(raw: unknown, validatePayload: (payload: unknown) => ValidationResult<P>, opts?: EnvelopeOptions): ValidationResult<EventEnvelope<P>>;
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// VIXOR Contracts — §1 Common envelope (VIXOR_INTEGRATION_CONTRACTS_DRAFT §1)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { cappedId, cappedTimestamp, field, invalid, isOneOf, isRecord, } from "./validate.js";
|
|
5
|
+
/** Contract version — consumers must reject unsupported versions. */
|
|
6
|
+
export const SCHEMA_VERSION = "0.1";
|
|
7
|
+
export const PRODUCED_BY = [
|
|
8
|
+
"hunt",
|
|
9
|
+
"shield",
|
|
10
|
+
"core",
|
|
11
|
+
"trade",
|
|
12
|
+
"tg-content-bot",
|
|
13
|
+
];
|
|
14
|
+
/** The only event type on the minimal proof path (§2). */
|
|
15
|
+
export const EVENT_OPPORTUNITY_DETECTED = "opportunity.detected";
|
|
16
|
+
/**
|
|
17
|
+
* Validate a §1 envelope. `validatePayload` validates the event-specific
|
|
18
|
+
* payload against its schema (e.g. §2 opportunity). Envelope identity fields
|
|
19
|
+
* are length-capped for log hygiene.
|
|
20
|
+
*/
|
|
21
|
+
export function validateEnvelope(raw, validatePayload, opts = {}) {
|
|
22
|
+
if (!isRecord(raw))
|
|
23
|
+
return invalid("envelope: must be a JSON object");
|
|
24
|
+
const sv = field(raw, "schema_version", (v) => v === SCHEMA_VERSION
|
|
25
|
+
? { ok: true, value: SCHEMA_VERSION }
|
|
26
|
+
: { ok: false, errors: [`schema_version: unsupported version (expected "${SCHEMA_VERSION}")`] });
|
|
27
|
+
if (!sv.ok)
|
|
28
|
+
return sv;
|
|
29
|
+
const allowed = opts.allowedEventTypes ?? [EVENT_OPPORTUNITY_DETECTED];
|
|
30
|
+
const eventId = field(raw, "event_id", (v) => cappedId(v, "event_id"));
|
|
31
|
+
if (!eventId.ok)
|
|
32
|
+
return eventId;
|
|
33
|
+
const eventType = field(raw, "event_type", (v) => isOneOf(v, allowed)
|
|
34
|
+
? { ok: true, value: v }
|
|
35
|
+
: { ok: false, errors: [`event_type: unknown event type "${String(v).slice(0, 50)}"`] });
|
|
36
|
+
if (!eventType.ok)
|
|
37
|
+
return eventType;
|
|
38
|
+
const occurredAt = field(raw, "occurred_at", (v) => cappedTimestamp(v, "occurred_at"));
|
|
39
|
+
if (!occurredAt.ok)
|
|
40
|
+
return occurredAt;
|
|
41
|
+
const producedBy = field(raw, "produced_by", (v) => isOneOf(v, PRODUCED_BY)
|
|
42
|
+
? { ok: true, value: v }
|
|
43
|
+
: { ok: false, errors: ["produced_by: must be one of hunt|shield|core|trade|tg-content-bot"] });
|
|
44
|
+
if (!producedBy.ok)
|
|
45
|
+
return producedBy;
|
|
46
|
+
const correlationId = field(raw, "correlation_id", (v) => cappedId(v, "correlation_id"));
|
|
47
|
+
if (!correlationId.ok)
|
|
48
|
+
return correlationId;
|
|
49
|
+
const idempotencyKey = field(raw, "idempotency_key", (v) => cappedId(v, "idempotency_key"));
|
|
50
|
+
if (!idempotencyKey.ok)
|
|
51
|
+
return idempotencyKey;
|
|
52
|
+
const payload = validatePayload(raw.payload);
|
|
53
|
+
if (!payload.ok)
|
|
54
|
+
return payload;
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
value: {
|
|
58
|
+
schema_version: sv.value,
|
|
59
|
+
event_id: eventId.value,
|
|
60
|
+
event_type: eventType.value,
|
|
61
|
+
occurred_at: occurredAt.value,
|
|
62
|
+
produced_by: producedBy.value,
|
|
63
|
+
correlation_id: correlationId.value,
|
|
64
|
+
idempotency_key: idempotencyKey.value,
|
|
65
|
+
payload: payload.value,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EventEnvelope } from "./envelope.ts";
|
|
2
|
+
import type { OpportunityPayload } from "./opportunity.ts";
|
|
3
|
+
import type { RiskAssessment } from "./risk.ts";
|
|
4
|
+
import type { PublicationCandidate } from "./publication.ts";
|
|
5
|
+
/**
|
|
6
|
+
* All fixture values are constant (no Date.now, no randomness) so that two
|
|
7
|
+
* runs always produce deep-equal output and identical idempotency keys —
|
|
8
|
+
* the property T2 (duplicate → 409) depends on.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FIXTURE_EVENT_ID = "evt-e2e-test-0001";
|
|
11
|
+
export declare const FIXTURE_CORRELATION_ID = "corr-e2e-test-0001";
|
|
12
|
+
export declare const FIXTURE_OPPORTUNITY_ID = "vixor-e2e-test-001";
|
|
13
|
+
export declare const FIXTURE_ASSESSMENT_ID = "asmt-e2e-test-0001";
|
|
14
|
+
export declare const FIXTURE_PUBLICATION_ID = "pub-e2e-test-0001";
|
|
15
|
+
/** Fixed instants (UTC) — far future expiry keeps fixtures time-stable. */
|
|
16
|
+
export declare const FIXTURE_OCCURRED_AT = "2026-01-01T00:00:00.000Z";
|
|
17
|
+
export declare const FIXTURE_OBSERVED_AT = "2026-01-01T00:00:00.000Z";
|
|
18
|
+
export declare const FIXTURE_CHECKED_AT = "2026-01-01T00:01:00.000Z";
|
|
19
|
+
export declare const FIXTURE_EXPIRES_AT = "2099-01-01T00:00:00.000Z";
|
|
20
|
+
/** Canonical asset identity: chain:address (Solana test fixture). */
|
|
21
|
+
export declare const FIXTURE_ASSET_ID = "solana:So11111111111111111111111111111111111111112";
|
|
22
|
+
export declare function fixtureOpportunityPayload(): OpportunityPayload;
|
|
23
|
+
export declare function fixtureRiskAssessment(): RiskAssessment;
|
|
24
|
+
export declare function fixtureEnvelope(): EventEnvelope<OpportunityPayload>;
|
|
25
|
+
export declare function fixturePublicationCandidate(): PublicationCandidate;
|
package/dist/fixtures.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// VIXOR Contracts — deterministic fixtures (M1 acceptance: one fixture
|
|
3
|
+
// traverses every schema: envelope → opportunity → risk → publication)
|
|
4
|
+
// ============================================================================
|
|
5
|
+
import { EVENT_OPPORTUNITY_DETECTED } from "./envelope.js";
|
|
6
|
+
/**
|
|
7
|
+
* All fixture values are constant (no Date.now, no randomness) so that two
|
|
8
|
+
* runs always produce deep-equal output and identical idempotency keys —
|
|
9
|
+
* the property T2 (duplicate → 409) depends on.
|
|
10
|
+
*/
|
|
11
|
+
export const FIXTURE_EVENT_ID = "evt-e2e-test-0001";
|
|
12
|
+
export const FIXTURE_CORRELATION_ID = "corr-e2e-test-0001";
|
|
13
|
+
export const FIXTURE_OPPORTUNITY_ID = "vixor-e2e-test-001";
|
|
14
|
+
export const FIXTURE_ASSESSMENT_ID = "asmt-e2e-test-0001";
|
|
15
|
+
export const FIXTURE_PUBLICATION_ID = "pub-e2e-test-0001";
|
|
16
|
+
/** Fixed instants (UTC) — far future expiry keeps fixtures time-stable. */
|
|
17
|
+
export const FIXTURE_OCCURRED_AT = "2026-01-01T00:00:00.000Z";
|
|
18
|
+
export const FIXTURE_OBSERVED_AT = "2026-01-01T00:00:00.000Z";
|
|
19
|
+
export const FIXTURE_CHECKED_AT = "2026-01-01T00:01:00.000Z";
|
|
20
|
+
export const FIXTURE_EXPIRES_AT = "2099-01-01T00:00:00.000Z";
|
|
21
|
+
/** Canonical asset identity: chain:address (Solana test fixture). */
|
|
22
|
+
export const FIXTURE_ASSET_ID = "solana:So11111111111111111111111111111111111111112";
|
|
23
|
+
export function fixtureOpportunityPayload() {
|
|
24
|
+
return {
|
|
25
|
+
opportunity_id: FIXTURE_OPPORTUNITY_ID,
|
|
26
|
+
asset_id: FIXTURE_ASSET_ID,
|
|
27
|
+
symbol: "TEST",
|
|
28
|
+
source: { name: "dexscreener", record_ref: "pair:e2e-test-pair-001" },
|
|
29
|
+
observed_at: FIXTURE_OBSERVED_AT,
|
|
30
|
+
market_data: {
|
|
31
|
+
liq_usd: { value: 25000, currency: "USD", at: FIXTURE_OBSERVED_AT },
|
|
32
|
+
accel_score: { value: 78, at: FIXTURE_OBSERVED_AT },
|
|
33
|
+
price_chg_m5: { value: 4.2, unit: "pct", at: FIXTURE_OBSERVED_AT },
|
|
34
|
+
},
|
|
35
|
+
evidence_refs: ["geckoterminal:e2e-test-pool", "hunt:scanner:1"],
|
|
36
|
+
quality_flags: [],
|
|
37
|
+
risk_assessment: fixtureRiskAssessment(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function fixtureRiskAssessment() {
|
|
41
|
+
return {
|
|
42
|
+
assessment_id: FIXTURE_ASSESSMENT_ID,
|
|
43
|
+
opportunity_id: FIXTURE_OPPORTUNITY_ID,
|
|
44
|
+
decision: "allow",
|
|
45
|
+
reason_codes: ["S03", "S07"],
|
|
46
|
+
checked_at: FIXTURE_CHECKED_AT,
|
|
47
|
+
policy_version: "shield-assess-v1",
|
|
48
|
+
data_quality: "valid",
|
|
49
|
+
expires_at: FIXTURE_EXPIRES_AT,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function fixtureEnvelope() {
|
|
53
|
+
return {
|
|
54
|
+
schema_version: "0.1",
|
|
55
|
+
event_id: FIXTURE_EVENT_ID,
|
|
56
|
+
event_type: EVENT_OPPORTUNITY_DETECTED,
|
|
57
|
+
occurred_at: FIXTURE_OCCURRED_AT,
|
|
58
|
+
produced_by: "hunt",
|
|
59
|
+
correlation_id: FIXTURE_CORRELATION_ID,
|
|
60
|
+
idempotency_key: `publish:test:${FIXTURE_OPPORTUNITY_ID}`,
|
|
61
|
+
payload: fixtureOpportunityPayload(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function fixturePublicationCandidate() {
|
|
65
|
+
return {
|
|
66
|
+
publication_id: FIXTURE_PUBLICATION_ID,
|
|
67
|
+
opportunity_id: FIXTURE_OPPORTUNITY_ID,
|
|
68
|
+
assessment_id: FIXTURE_ASSESSMENT_ID,
|
|
69
|
+
channel_id: "test-channel-001",
|
|
70
|
+
template_id: "signal_card",
|
|
71
|
+
template_version: "1",
|
|
72
|
+
content: {
|
|
73
|
+
text: "[TEST] TEST (solana) — accel 78, liq 25000 USD. " +
|
|
74
|
+
"assessment asmt-e2e-test-0001 allow/valid until 2099.",
|
|
75
|
+
},
|
|
76
|
+
buttons: [{ type: "url", label: "Chart", url: "https://example.com/chart" }],
|
|
77
|
+
approval_state: "pending_human",
|
|
78
|
+
expires_at: FIXTURE_EXPIRES_AT,
|
|
79
|
+
idempotency_key: `publish:test:${FIXTURE_OPPORTUNITY_ID}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { valid, invalid, flatMap, isRecord, isString, isNonEmptyString, isStringArray, isTimestamp, isFiniteNumber, isOneOf, cappedId, cappedText, cappedStringList, cappedTimestamp, field, optionalField, MAX_ID_LEN, MAX_TEXT_LEN, MAX_LIST_LEN, } from "./validate.ts";
|
|
2
|
+
export type { Valid, Invalid, ValidationResult } from "./validate.ts";
|
|
3
|
+
export { SCHEMA_VERSION, PRODUCED_BY, EVENT_OPPORTUNITY_DETECTED, validateEnvelope, } from "./envelope.ts";
|
|
4
|
+
export type { EventEnvelope, ProducedBy, EnvelopeOptions } from "./envelope.ts";
|
|
5
|
+
export { validateOpportunityPayload } from "./opportunity.ts";
|
|
6
|
+
export type { OpportunityPayload, OpportunitySource, MarketValue, } from "./opportunity.ts";
|
|
7
|
+
export { RISK_DECISIONS, DATA_QUALITIES, validateRiskAssessment, isAssessmentUsableForPublication, } from "./risk.ts";
|
|
8
|
+
export type { RiskAssessment, RiskDecision, DataQuality, } from "./risk.ts";
|
|
9
|
+
export { validatePublicationCandidate } from "./publication.ts";
|
|
10
|
+
export type { PublicationCandidate, PublicationButton, PublicationContent, } from "./publication.ts";
|
|
11
|
+
export { FIXTURE_EVENT_ID, FIXTURE_CORRELATION_ID, FIXTURE_OPPORTUNITY_ID, FIXTURE_ASSESSMENT_ID, FIXTURE_PUBLICATION_ID, FIXTURE_ASSET_ID, FIXTURE_OCCURRED_AT, FIXTURE_OBSERVED_AT, FIXTURE_CHECKED_AT, FIXTURE_EXPIRES_AT, fixtureEnvelope, fixtureOpportunityPayload, fixtureRiskAssessment, fixturePublicationCandidate, } from "./fixtures.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// VIXOR Contracts Package — Public Exports
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Zero-dependency runtime contracts for the VIXOR integration proof path
|
|
5
|
+
// (mirrors integration/VIXOR_INTEGRATION_CONTRACTS_DRAFT.md §1–§4).
|
|
6
|
+
// --- validation primitives -------------------------------------------------
|
|
7
|
+
export { valid, invalid, flatMap, isRecord, isString, isNonEmptyString, isStringArray, isTimestamp, isFiniteNumber, isOneOf, cappedId, cappedText, cappedStringList, cappedTimestamp, field, optionalField, MAX_ID_LEN, MAX_TEXT_LEN, MAX_LIST_LEN, } from "./validate.js";
|
|
8
|
+
// --- §1 common envelope -----------------------------------------------------
|
|
9
|
+
export { SCHEMA_VERSION, PRODUCED_BY, EVENT_OPPORTUNITY_DETECTED, validateEnvelope, } from "./envelope.js";
|
|
10
|
+
// --- §2 opportunity detected ------------------------------------------------
|
|
11
|
+
export { validateOpportunityPayload } from "./opportunity.js";
|
|
12
|
+
// --- §3 risk assessment ------------------------------------------------------
|
|
13
|
+
export { RISK_DECISIONS, DATA_QUALITIES, validateRiskAssessment, isAssessmentUsableForPublication, } from "./risk.js";
|
|
14
|
+
// --- §4 publication candidate ------------------------------------------------
|
|
15
|
+
export { validatePublicationCandidate } from "./publication.js";
|
|
16
|
+
// --- deterministic fixtures ---------------------------------------------------
|
|
17
|
+
export { FIXTURE_EVENT_ID, FIXTURE_CORRELATION_ID, FIXTURE_OPPORTUNITY_ID, FIXTURE_ASSESSMENT_ID, FIXTURE_PUBLICATION_ID, FIXTURE_ASSET_ID, FIXTURE_OCCURRED_AT, FIXTURE_OBSERVED_AT, FIXTURE_CHECKED_AT, FIXTURE_EXPIRES_AT, fixtureEnvelope, fixtureOpportunityPayload, fixtureRiskAssessment, fixturePublicationCandidate, } from "./fixtures.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ValidationResult } from "./validate.ts";
|
|
2
|
+
import { type RiskAssessment } from "./risk.ts";
|
|
3
|
+
/** One measured value with its units/currency and observation timestamp (§2 market_data). */
|
|
4
|
+
export interface MarketValue {
|
|
5
|
+
value: number;
|
|
6
|
+
unit?: string;
|
|
7
|
+
currency?: string;
|
|
8
|
+
at?: string;
|
|
9
|
+
}
|
|
10
|
+
/** §2: "source: source/provider name and source record reference". */
|
|
11
|
+
export interface OpportunitySource {
|
|
12
|
+
name: string;
|
|
13
|
+
record_ref: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* §2 minimum payload. Field names mirror the draft verbatim.
|
|
17
|
+
*
|
|
18
|
+
* `risk_assessment` is a VIXOR extension used on the minimal proof path: the
|
|
19
|
+
* producer (Hunt emitter) fetches Shield's §3 assessment and embeds it so the
|
|
20
|
+
* publication ledger can fail closed without an outbound call. Consumers that
|
|
21
|
+
* require stronger guarantees should verify the assessment against Shield
|
|
22
|
+
* directly (documented limitation of the proof phase).
|
|
23
|
+
*/
|
|
24
|
+
export interface OpportunityPayload {
|
|
25
|
+
opportunity_id: string;
|
|
26
|
+
asset_id: string;
|
|
27
|
+
symbol: string;
|
|
28
|
+
source: OpportunitySource;
|
|
29
|
+
observed_at: string;
|
|
30
|
+
market_data: Record<string, MarketValue>;
|
|
31
|
+
evidence_refs: string[];
|
|
32
|
+
quality_flags: string[];
|
|
33
|
+
risk_assessment?: RiskAssessment;
|
|
34
|
+
}
|
|
35
|
+
export declare function validateOpportunityPayload(raw: unknown): ValidationResult<OpportunityPayload>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// VIXOR Contracts — §2 Hunt → Core: opportunity detected
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { cappedId, cappedStringList, cappedTimestamp, field, invalid, isFiniteNumber, isRecord, optionalField, } from "./validate.js";
|
|
5
|
+
import { validateRiskAssessment, } from "./risk.js";
|
|
6
|
+
function validateMarketValue(v, name) {
|
|
7
|
+
if (!isRecord(v))
|
|
8
|
+
return invalid(`market_data.${name}: must be an object`);
|
|
9
|
+
const value = field(v, "value", (x) => isFiniteNumber(x)
|
|
10
|
+
? { ok: true, value: x }
|
|
11
|
+
: { ok: false, errors: [`market_data.${name}.value: must be a finite number`] });
|
|
12
|
+
if (!value.ok)
|
|
13
|
+
return value;
|
|
14
|
+
const unit = optionalField(v, "unit", (x) => typeof x === "string" && x.length <= 50
|
|
15
|
+
? { ok: true, value: x }
|
|
16
|
+
: { ok: false, errors: [`market_data.${name}.unit: must be a string ≤ 50 chars`] });
|
|
17
|
+
if (!unit.ok)
|
|
18
|
+
return unit;
|
|
19
|
+
const currency = optionalField(v, "currency", (x) => typeof x === "string" && x.length <= 10
|
|
20
|
+
? { ok: true, value: x }
|
|
21
|
+
: { ok: false, errors: [`market_data.${name}.currency: must be a string ≤ 10 chars`] });
|
|
22
|
+
if (!currency.ok)
|
|
23
|
+
return currency;
|
|
24
|
+
const at = optionalField(v, "at", (x) => cappedTimestamp(x, `market_data.${name}.at`));
|
|
25
|
+
if (!at.ok)
|
|
26
|
+
return at;
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
value: { value: value.value, unit: unit.value, currency: currency.value, at: at.value },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function validateMarketData(v) {
|
|
33
|
+
if (!isRecord(v))
|
|
34
|
+
return invalid("market_data: must be an object");
|
|
35
|
+
const keys = Object.keys(v);
|
|
36
|
+
if (keys.length === 0)
|
|
37
|
+
return invalid("market_data: must contain at least one metric");
|
|
38
|
+
if (keys.length > 50)
|
|
39
|
+
return invalid("market_data: exceeds 50 metrics");
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const k of keys) {
|
|
42
|
+
if (!/^[a-z0-9_]{1,50}$/.test(k)) {
|
|
43
|
+
return invalid(`market_data: metric name "${k.slice(0, 50)}" must match [a-z0-9_]{1,50}`);
|
|
44
|
+
}
|
|
45
|
+
const mv = validateMarketValue(v[k], k);
|
|
46
|
+
if (!mv.ok)
|
|
47
|
+
return mv;
|
|
48
|
+
out[k] = mv.value;
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, value: out };
|
|
51
|
+
}
|
|
52
|
+
export function validateOpportunityPayload(raw) {
|
|
53
|
+
if (!isRecord(raw))
|
|
54
|
+
return invalid("payload: must be a JSON object");
|
|
55
|
+
const opportunityId = field(raw, "opportunity_id", (v) => cappedId(v, "opportunity_id"));
|
|
56
|
+
if (!opportunityId.ok)
|
|
57
|
+
return opportunityId;
|
|
58
|
+
// §2: "asset_id: canonical asset identity, preferably chain/network +
|
|
59
|
+
// contract or provider ID" — enforce the canonical "chain:address" shape.
|
|
60
|
+
const assetId = field(raw, "asset_id", (v) => {
|
|
61
|
+
const id = cappedId(v, "asset_id");
|
|
62
|
+
if (!id.ok)
|
|
63
|
+
return id;
|
|
64
|
+
const colon = id.value.indexOf(":");
|
|
65
|
+
if (colon <= 0 || colon === id.value.length - 1) {
|
|
66
|
+
return { ok: false, errors: ["asset_id: must be canonical \"chain:address\""] };
|
|
67
|
+
}
|
|
68
|
+
return id;
|
|
69
|
+
});
|
|
70
|
+
if (!assetId.ok)
|
|
71
|
+
return assetId;
|
|
72
|
+
const symbol = field(raw, "symbol", (v) => {
|
|
73
|
+
// §2: display only; never sole identity — cap length, allow empty marginally? No: must exist.
|
|
74
|
+
if (typeof v !== "string" || v.length === 0 || v.length > 50) {
|
|
75
|
+
return { ok: false, errors: ["symbol: must be a string of 1..50 chars (display only)"] };
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, value: v };
|
|
78
|
+
});
|
|
79
|
+
if (!symbol.ok)
|
|
80
|
+
return symbol;
|
|
81
|
+
const sourceRaw = raw.source;
|
|
82
|
+
if (!isRecord(sourceRaw))
|
|
83
|
+
return invalid("source: must be an object");
|
|
84
|
+
const sourceName = field(sourceRaw, "name", (v) => cappedId(v, "source.name"));
|
|
85
|
+
if (!sourceName.ok)
|
|
86
|
+
return sourceName;
|
|
87
|
+
const sourceRef = field(sourceRaw, "record_ref", (v) => cappedId(v, "source.record_ref"));
|
|
88
|
+
if (!sourceRef.ok)
|
|
89
|
+
return sourceRef;
|
|
90
|
+
const observedAt = field(raw, "observed_at", (v) => cappedTimestamp(v, "observed_at"));
|
|
91
|
+
if (!observedAt.ok)
|
|
92
|
+
return observedAt;
|
|
93
|
+
const marketData = field(raw, "market_data", validateMarketData);
|
|
94
|
+
if (!marketData.ok)
|
|
95
|
+
return marketData;
|
|
96
|
+
const evidenceRefs = field(raw, "evidence_refs", (v) => cappedStringList(v, "evidence_refs"));
|
|
97
|
+
if (!evidenceRefs.ok)
|
|
98
|
+
return evidenceRefs;
|
|
99
|
+
const qualityFlags = field(raw, "quality_flags", (v) => cappedStringList(v, "quality_flags"));
|
|
100
|
+
if (!qualityFlags.ok)
|
|
101
|
+
return qualityFlags;
|
|
102
|
+
// Notes must never smuggle credentials — generic cap, content-agnostic.
|
|
103
|
+
const riskAssessment = optionalField(raw, "risk_assessment", validateRiskAssessment);
|
|
104
|
+
if (!riskAssessment.ok)
|
|
105
|
+
return riskAssessment;
|
|
106
|
+
if (riskAssessment.value &&
|
|
107
|
+
riskAssessment.value.opportunity_id !== opportunityId.value) {
|
|
108
|
+
return invalid("risk_assessment.opportunity_id: must match payload.opportunity_id");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
value: {
|
|
113
|
+
opportunity_id: opportunityId.value,
|
|
114
|
+
asset_id: assetId.value,
|
|
115
|
+
symbol: symbol.value,
|
|
116
|
+
source: { name: sourceName.value, record_ref: sourceRef.value },
|
|
117
|
+
observed_at: observedAt.value,
|
|
118
|
+
market_data: marketData.value,
|
|
119
|
+
evidence_refs: evidenceRefs.value,
|
|
120
|
+
quality_flags: qualityFlags.value,
|
|
121
|
+
risk_assessment: riskAssessment.value,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ValidationResult } from "./validate.ts";
|
|
2
|
+
/** §4: "buttons: allow-listed action types and validated destinations". */
|
|
3
|
+
export interface PublicationButton {
|
|
4
|
+
type: string;
|
|
5
|
+
label: string;
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
8
|
+
/** §4: "content: rendered text/media references". */
|
|
9
|
+
export interface PublicationContent {
|
|
10
|
+
text: string;
|
|
11
|
+
media_refs?: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* §4 minimum payload, mirrored field-for-field. `approval_state` starts at
|
|
15
|
+
* `pending_human` — a candidate can never enter the ledger pre-approved.
|
|
16
|
+
*/
|
|
17
|
+
export interface PublicationCandidate {
|
|
18
|
+
publication_id: string;
|
|
19
|
+
opportunity_id: string;
|
|
20
|
+
assessment_id: string;
|
|
21
|
+
channel_id: string;
|
|
22
|
+
template_id: string;
|
|
23
|
+
template_version: string;
|
|
24
|
+
content: PublicationContent;
|
|
25
|
+
buttons?: PublicationButton[];
|
|
26
|
+
approval_state: "pending_human";
|
|
27
|
+
expires_at: string;
|
|
28
|
+
idempotency_key: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function validatePublicationCandidate(raw: unknown): ValidationResult<PublicationCandidate>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// VIXOR Contracts — §4 Core → tg-content-bot: publication candidate
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { cappedId, cappedText, cappedTimestamp, field, invalid, isRecord, optionalField, } from "./validate.js";
|
|
5
|
+
import { validateRiskAssessment } from "./risk.js";
|
|
6
|
+
const BUTTON_TYPES = ["url"];
|
|
7
|
+
function validateButton(v, idx) {
|
|
8
|
+
if (!isRecord(v))
|
|
9
|
+
return invalid(`buttons[${idx}]: must be an object`);
|
|
10
|
+
const type = field(v, "type", (x) => typeof x === "string" && BUTTON_TYPES.includes(x)
|
|
11
|
+
? { ok: true, value: x }
|
|
12
|
+
: { ok: false, errors: [`buttons[${idx}].type: not an allow-listed action type`] });
|
|
13
|
+
if (!type.ok)
|
|
14
|
+
return type;
|
|
15
|
+
const label = field(v, "label", (x) => cappedText(x, `buttons[${idx}].label`));
|
|
16
|
+
if (!label.ok)
|
|
17
|
+
return label;
|
|
18
|
+
const url = field(v, "url", (x) => {
|
|
19
|
+
if (typeof x !== "string" || x.length > 500) {
|
|
20
|
+
return { ok: false, errors: [`buttons[${idx}].url: must be a string of at most 500 chars`] };
|
|
21
|
+
}
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = new URL(x);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { ok: false, errors: [`buttons[${idx}].url: must be a valid absolute https URL`] };
|
|
28
|
+
}
|
|
29
|
+
if (parsed.protocol !== "https:" || !parsed.hostname || parsed.username || parsed.password) {
|
|
30
|
+
return { ok: false, errors: [`buttons[${idx}].url: must be https and cannot contain credentials`] };
|
|
31
|
+
}
|
|
32
|
+
return { ok: true, value: parsed.toString() };
|
|
33
|
+
});
|
|
34
|
+
if (!url.ok)
|
|
35
|
+
return url;
|
|
36
|
+
return { ok: true, value: { type: type.value, label: label.value, url: url.value } };
|
|
37
|
+
}
|
|
38
|
+
export function validatePublicationCandidate(raw) {
|
|
39
|
+
if (!isRecord(raw))
|
|
40
|
+
return invalid("publication_candidate: must be a JSON object");
|
|
41
|
+
const publicationId = field(raw, "publication_id", (v) => cappedId(v, "publication_id"));
|
|
42
|
+
if (!publicationId.ok)
|
|
43
|
+
return publicationId;
|
|
44
|
+
const opportunityId = field(raw, "opportunity_id", (v) => cappedId(v, "opportunity_id"));
|
|
45
|
+
if (!opportunityId.ok)
|
|
46
|
+
return opportunityId;
|
|
47
|
+
const assessmentId = field(raw, "assessment_id", (v) => cappedId(v, "assessment_id"));
|
|
48
|
+
if (!assessmentId.ok)
|
|
49
|
+
return assessmentId;
|
|
50
|
+
// §4: "channel_id: approved internal identifier, not an arbitrary
|
|
51
|
+
// destination supplied by an event" — shape-level check only; the ledger
|
|
52
|
+
// must additionally verify it exists internally before use.
|
|
53
|
+
const channelId = field(raw, "channel_id", (v) => cappedId(v, "channel_id"));
|
|
54
|
+
if (!channelId.ok)
|
|
55
|
+
return channelId;
|
|
56
|
+
const templateId = field(raw, "template_id", (v) => cappedId(v, "template_id"));
|
|
57
|
+
if (!templateId.ok)
|
|
58
|
+
return templateId;
|
|
59
|
+
const templateVersion = field(raw, "template_version", (v) => cappedId(v, "template_version"));
|
|
60
|
+
if (!templateVersion.ok)
|
|
61
|
+
return templateVersion;
|
|
62
|
+
const contentRaw = raw.content;
|
|
63
|
+
if (!isRecord(contentRaw))
|
|
64
|
+
return invalid("content: must be an object");
|
|
65
|
+
const text = field(contentRaw, "text", (v) => cappedText(v, "content.text"));
|
|
66
|
+
if (!text.ok)
|
|
67
|
+
return text;
|
|
68
|
+
const mediaRefs = optionalField(contentRaw, "media_refs", (v) => {
|
|
69
|
+
if (!Array.isArray(v) || v.length > 10 || !v.every((x) => typeof x === "string")) {
|
|
70
|
+
return { ok: false, errors: ["content.media_refs: must be ≤ 10 strings"] };
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, value: v };
|
|
73
|
+
});
|
|
74
|
+
if (!mediaRefs.ok)
|
|
75
|
+
return mediaRefs;
|
|
76
|
+
const buttons = optionalField(raw, "buttons", (v) => {
|
|
77
|
+
if (!Array.isArray(v))
|
|
78
|
+
return { ok: false, errors: ["buttons: must be an array"] };
|
|
79
|
+
if (v.length > 8)
|
|
80
|
+
return { ok: false, errors: ["buttons: exceeds 8 buttons"] };
|
|
81
|
+
const out = [];
|
|
82
|
+
for (let i = 0; i < v.length; i++) {
|
|
83
|
+
const b = validateButton(v[i], i);
|
|
84
|
+
if (!b.ok)
|
|
85
|
+
return b;
|
|
86
|
+
out.push(b.value);
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, value: out };
|
|
89
|
+
});
|
|
90
|
+
if (!buttons.ok)
|
|
91
|
+
return buttons;
|
|
92
|
+
// §4: "approval_state: initially pending_human" — literally enforced.
|
|
93
|
+
const approvalState = field(raw, "approval_state", (v) => v === "pending_human"
|
|
94
|
+
? { ok: true, value: "pending_human" }
|
|
95
|
+
: { ok: false, errors: ["approval_state: must be exactly \"pending_human\""] });
|
|
96
|
+
if (!approvalState.ok)
|
|
97
|
+
return approvalState;
|
|
98
|
+
const expiresAt = field(raw, "expires_at", (v) => cappedTimestamp(v, "expires_at"));
|
|
99
|
+
if (!expiresAt.ok)
|
|
100
|
+
return expiresAt;
|
|
101
|
+
const idempotencyKey = field(raw, "idempotency_key", (v) => cappedId(v, "idempotency_key"));
|
|
102
|
+
if (!idempotencyKey.ok)
|
|
103
|
+
return idempotencyKey;
|
|
104
|
+
// risk_assessment presence is not part of §4; validate if supplied (helper reuse).
|
|
105
|
+
const extraAssessment = optionalField(raw, "risk_assessment", validateRiskAssessment);
|
|
106
|
+
if (!extraAssessment.ok)
|
|
107
|
+
return extraAssessment;
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
value: {
|
|
111
|
+
publication_id: publicationId.value,
|
|
112
|
+
opportunity_id: opportunityId.value,
|
|
113
|
+
assessment_id: assessmentId.value,
|
|
114
|
+
channel_id: channelId.value,
|
|
115
|
+
template_id: templateId.value,
|
|
116
|
+
template_version: templateVersion.value,
|
|
117
|
+
content: { text: text.value, media_refs: mediaRefs.value },
|
|
118
|
+
buttons: buttons.value,
|
|
119
|
+
approval_state: "pending_human",
|
|
120
|
+
expires_at: expiresAt.value,
|
|
121
|
+
idempotency_key: idempotencyKey.value,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
package/dist/risk.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ValidationResult } from "./validate.ts";
|
|
2
|
+
export declare const RISK_DECISIONS: readonly ["allow", "reject", "review"];
|
|
3
|
+
export type RiskDecision = (typeof RISK_DECISIONS)[number];
|
|
4
|
+
export declare const DATA_QUALITIES: readonly ["valid", "stale", "incomplete", "conflicting"];
|
|
5
|
+
export type DataQuality = (typeof DATA_QUALITIES)[number];
|
|
6
|
+
/**
|
|
7
|
+
* Shield's /assess response (§3): "Shield response must include" — mirrored
|
|
8
|
+
* field-for-field. Any decision other than a verbatim match is rejected.
|
|
9
|
+
*/
|
|
10
|
+
export interface RiskAssessment {
|
|
11
|
+
assessment_id: string;
|
|
12
|
+
opportunity_id: string;
|
|
13
|
+
decision: RiskDecision;
|
|
14
|
+
reason_codes: string[];
|
|
15
|
+
checked_at: string;
|
|
16
|
+
policy_version: string;
|
|
17
|
+
data_quality: DataQuality;
|
|
18
|
+
expires_at: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function validateRiskAssessment(raw: unknown): ValidationResult<RiskAssessment>;
|
|
21
|
+
/**
|
|
22
|
+
* §3 gate: "A missing response, unknown decision, expired assessment, or
|
|
23
|
+
* non-valid data quality must block publication."
|
|
24
|
+
*
|
|
25
|
+
* Publication is allowed only when the decision is `allow`, the data quality
|
|
26
|
+
* is exactly `valid`, and the assessment has not expired. Anything else
|
|
27
|
+
* blocks — including `review` (human triage may override later by explicit
|
|
28
|
+
* owner action, never by default).
|
|
29
|
+
*/
|
|
30
|
+
export declare function isAssessmentUsableForPublication(a: RiskAssessment, nowMs: number): boolean;
|
package/dist/risk.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// VIXOR Contracts — §3 Shield risk assessment (DRAFT §3 response shape)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { cappedId, cappedStringList, cappedTimestamp, field, invalid, isOneOf, isRecord, } from "./validate.js";
|
|
5
|
+
export const RISK_DECISIONS = ["allow", "reject", "review"];
|
|
6
|
+
export const DATA_QUALITIES = [
|
|
7
|
+
"valid",
|
|
8
|
+
"stale",
|
|
9
|
+
"incomplete",
|
|
10
|
+
"conflicting",
|
|
11
|
+
];
|
|
12
|
+
export function validateRiskAssessment(raw) {
|
|
13
|
+
if (!isRecord(raw))
|
|
14
|
+
return invalid("risk_assessment: must be a JSON object");
|
|
15
|
+
const assessmentId = field(raw, "assessment_id", (v) => cappedId(v, "assessment_id"));
|
|
16
|
+
if (!assessmentId.ok)
|
|
17
|
+
return assessmentId;
|
|
18
|
+
const opportunityId = field(raw, "opportunity_id", (v) => cappedId(v, "opportunity_id"));
|
|
19
|
+
if (!opportunityId.ok)
|
|
20
|
+
return opportunityId;
|
|
21
|
+
const decision = field(raw, "decision", (v) => isOneOf(v, RISK_DECISIONS)
|
|
22
|
+
? { ok: true, value: v }
|
|
23
|
+
: { ok: false, errors: ["decision: must be one of allow|reject|review"] });
|
|
24
|
+
if (!decision.ok)
|
|
25
|
+
return decision;
|
|
26
|
+
const reasonCodes = field(raw, "reason_codes", (v) => cappedStringList(v, "reason_codes"));
|
|
27
|
+
if (!reasonCodes.ok)
|
|
28
|
+
return reasonCodes;
|
|
29
|
+
const checkedAt = field(raw, "checked_at", (v) => cappedTimestamp(v, "checked_at"));
|
|
30
|
+
if (!checkedAt.ok)
|
|
31
|
+
return checkedAt;
|
|
32
|
+
const policyVersion = field(raw, "policy_version", (v) => cappedId(v, "policy_version"));
|
|
33
|
+
if (!policyVersion.ok)
|
|
34
|
+
return policyVersion;
|
|
35
|
+
const dataQuality = field(raw, "data_quality", (v) => isOneOf(v, DATA_QUALITIES)
|
|
36
|
+
? { ok: true, value: v }
|
|
37
|
+
: { ok: false, errors: ["data_quality: must be one of valid|stale|incomplete|conflicting"] });
|
|
38
|
+
if (!dataQuality.ok)
|
|
39
|
+
return dataQuality;
|
|
40
|
+
const expiresAt = field(raw, "expires_at", (v) => cappedTimestamp(v, "expires_at"));
|
|
41
|
+
if (!expiresAt.ok)
|
|
42
|
+
return expiresAt;
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
value: {
|
|
46
|
+
assessment_id: assessmentId.value,
|
|
47
|
+
opportunity_id: opportunityId.value,
|
|
48
|
+
decision: decision.value,
|
|
49
|
+
reason_codes: reasonCodes.value,
|
|
50
|
+
checked_at: checkedAt.value,
|
|
51
|
+
policy_version: policyVersion.value,
|
|
52
|
+
data_quality: dataQuality.value,
|
|
53
|
+
expires_at: expiresAt.value,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* §3 gate: "A missing response, unknown decision, expired assessment, or
|
|
59
|
+
* non-valid data quality must block publication."
|
|
60
|
+
*
|
|
61
|
+
* Publication is allowed only when the decision is `allow`, the data quality
|
|
62
|
+
* is exactly `valid`, and the assessment has not expired. Anything else
|
|
63
|
+
* blocks — including `review` (human triage may override later by explicit
|
|
64
|
+
* owner action, never by default).
|
|
65
|
+
*/
|
|
66
|
+
export function isAssessmentUsableForPublication(a, nowMs) {
|
|
67
|
+
if (a.decision !== "allow")
|
|
68
|
+
return false;
|
|
69
|
+
if (a.data_quality !== "valid")
|
|
70
|
+
return false;
|
|
71
|
+
const exp = Date.parse(a.expires_at);
|
|
72
|
+
return Number.isFinite(exp) && exp > nowMs;
|
|
73
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-rolled validation helpers (house style: the sibling engines validate
|
|
3
|
+
* by hand, no zod at runtime). Everything here is pure and deterministic.
|
|
4
|
+
*
|
|
5
|
+
* Result shape: `{ ok: true, value }` or `{ ok: false, errors: string[] }`.
|
|
6
|
+
* Errors are safe, redacted messages — never echo payload secrets.
|
|
7
|
+
*/
|
|
8
|
+
export type Valid<T> = {
|
|
9
|
+
ok: true;
|
|
10
|
+
value: T;
|
|
11
|
+
};
|
|
12
|
+
export type Invalid = {
|
|
13
|
+
ok: false;
|
|
14
|
+
errors: string[];
|
|
15
|
+
};
|
|
16
|
+
export type ValidationResult<T> = Valid<T> | Invalid;
|
|
17
|
+
export declare function valid<T>(value: T): Valid<T>;
|
|
18
|
+
export declare function invalid(...errors: string[]): Invalid;
|
|
19
|
+
/** Merge two results; short-circuits on the first invalid one. */
|
|
20
|
+
export declare function flatMap<T, U>(r: ValidationResult<T>, f: (value: T) => ValidationResult<U>): ValidationResult<U>;
|
|
21
|
+
export declare function isRecord(v: unknown): v is Record<string, unknown>;
|
|
22
|
+
export declare function isString(v: unknown): v is string;
|
|
23
|
+
export declare function isNonEmptyString(v: unknown): v is string;
|
|
24
|
+
export declare function isStringArray(v: unknown): v is string[];
|
|
25
|
+
/** Any string that `Date.parse` accepts (ISO-8601 UTC expected in practice). */
|
|
26
|
+
export declare function isTimestamp(v: unknown): v is string;
|
|
27
|
+
export declare function isFiniteNumber(v: unknown): v is number;
|
|
28
|
+
export declare function isOneOf<T extends string>(v: unknown, allowed: readonly T[]): v is T;
|
|
29
|
+
/** Hard length caps — hygiene against oversized/abusive fields. */
|
|
30
|
+
export declare const MAX_ID_LEN = 200;
|
|
31
|
+
export declare const MAX_TEXT_LEN = 8000;
|
|
32
|
+
export declare const MAX_LIST_LEN = 50;
|
|
33
|
+
export declare function cappedId(v: unknown, field: string): ValidationResult<string>;
|
|
34
|
+
export declare function cappedText(v: unknown, field: string): ValidationResult<string>;
|
|
35
|
+
export declare function cappedStringList(v: unknown, field: string): ValidationResult<string[]>;
|
|
36
|
+
export declare function cappedTimestamp(v: unknown, field: string): ValidationResult<string>;
|
|
37
|
+
/** Read a required field from a record and validate it. */
|
|
38
|
+
export declare function field<T>(rec: Record<string, unknown>, name: string, validate: (v: unknown) => ValidationResult<T>): ValidationResult<T>;
|
|
39
|
+
/** Read an optional field; absent → undefined, present-but-invalid → error. */
|
|
40
|
+
export declare function optionalField<T>(rec: Record<string, unknown>, name: string, validate: (v: unknown) => ValidationResult<T>): ValidationResult<T | undefined>;
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// VIXOR Contracts — minimal zero-dependency runtime validators
|
|
3
|
+
// ============================================================================
|
|
4
|
+
export function valid(value) {
|
|
5
|
+
return { ok: true, value };
|
|
6
|
+
}
|
|
7
|
+
export function invalid(...errors) {
|
|
8
|
+
return { ok: false, errors };
|
|
9
|
+
}
|
|
10
|
+
/** Merge two results; short-circuits on the first invalid one. */
|
|
11
|
+
export function flatMap(r, f) {
|
|
12
|
+
return r.ok ? f(r.value) : r;
|
|
13
|
+
}
|
|
14
|
+
export function isRecord(v) {
|
|
15
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16
|
+
}
|
|
17
|
+
export function isString(v) {
|
|
18
|
+
return typeof v === "string";
|
|
19
|
+
}
|
|
20
|
+
export function isNonEmptyString(v) {
|
|
21
|
+
return typeof v === "string" && v.length > 0;
|
|
22
|
+
}
|
|
23
|
+
export function isStringArray(v) {
|
|
24
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
25
|
+
}
|
|
26
|
+
/** Any string that `Date.parse` accepts (ISO-8601 UTC expected in practice). */
|
|
27
|
+
export function isTimestamp(v) {
|
|
28
|
+
if (typeof v !== "string")
|
|
29
|
+
return false;
|
|
30
|
+
const t = Date.parse(v);
|
|
31
|
+
return Number.isFinite(t);
|
|
32
|
+
}
|
|
33
|
+
export function isFiniteNumber(v) {
|
|
34
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
35
|
+
}
|
|
36
|
+
export function isOneOf(v, allowed) {
|
|
37
|
+
return typeof v === "string" && allowed.includes(v);
|
|
38
|
+
}
|
|
39
|
+
/** Hard length caps — hygiene against oversized/abusive fields. */
|
|
40
|
+
export const MAX_ID_LEN = 200;
|
|
41
|
+
export const MAX_TEXT_LEN = 8000;
|
|
42
|
+
export const MAX_LIST_LEN = 50;
|
|
43
|
+
export function cappedId(v, field) {
|
|
44
|
+
if (!isNonEmptyString(v))
|
|
45
|
+
return invalid(`${field}: must be a non-empty string`);
|
|
46
|
+
if (v.length > MAX_ID_LEN)
|
|
47
|
+
return invalid(`${field}: exceeds ${MAX_ID_LEN} chars`);
|
|
48
|
+
return valid(v);
|
|
49
|
+
}
|
|
50
|
+
export function cappedText(v, field) {
|
|
51
|
+
if (!isString(v))
|
|
52
|
+
return invalid(`${field}: must be a string`);
|
|
53
|
+
if (v.length > MAX_TEXT_LEN)
|
|
54
|
+
return invalid(`${field}: exceeds ${MAX_TEXT_LEN} chars`);
|
|
55
|
+
return valid(v);
|
|
56
|
+
}
|
|
57
|
+
export function cappedStringList(v, field) {
|
|
58
|
+
if (!isStringArray(v))
|
|
59
|
+
return invalid(`${field}: must be an array of strings`);
|
|
60
|
+
if (v.length > MAX_LIST_LEN)
|
|
61
|
+
return invalid(`${field}: exceeds ${MAX_LIST_LEN} items`);
|
|
62
|
+
return valid(v);
|
|
63
|
+
}
|
|
64
|
+
export function cappedTimestamp(v, field) {
|
|
65
|
+
if (!isTimestamp(v))
|
|
66
|
+
return invalid(`${field}: must be a parseable timestamp`);
|
|
67
|
+
return valid(v);
|
|
68
|
+
}
|
|
69
|
+
/** Read a required field from a record and validate it. */
|
|
70
|
+
export function field(rec, name, validate) {
|
|
71
|
+
if (!(name in rec))
|
|
72
|
+
return invalid(`${name}: missing required field`);
|
|
73
|
+
return validate(rec[name]);
|
|
74
|
+
}
|
|
75
|
+
/** Read an optional field; absent → undefined, present-but-invalid → error. */
|
|
76
|
+
export function optionalField(rec, name, validate) {
|
|
77
|
+
if (!(name in rec) || rec[name] === undefined)
|
|
78
|
+
return valid(undefined);
|
|
79
|
+
return validate(rec[name]);
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,42 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@vixor/contracts",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Zero-dependency runtime contracts and validators for VIXOR crypto-intelligence integrations.",
|
|
5
|
-
"license": "
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./dist/index.js",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/index.js",
|
|
13
|
-
"default": "./dist/index.js"
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist",
|
|
18
|
-
"README.md"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@vixor/contracts",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Zero-dependency runtime contracts and validators for VIXOR crypto-intelligence integrations.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "node --experimental-strip-types --test test/contracts.test.ts",
|
|
32
|
+
"pack:check": "node scripts/verify-pack.mjs"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.16.5",
|
|
36
|
+
"typescript": "^5.8.3"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/kam65624-cmd/vixor-packages.git",
|
|
41
|
+
"directory": "packages/vixor-contracts"
|
|
42
|
+
}
|
|
43
|
+
}
|