Initial commit: AS4/411 directory and discovery service for Sankofa Marketplace
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
18
packages/core/package.json
Normal file
18
packages/core/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@as4-411/core",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"description": "Domain model, validation, and policy types for as4-411",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "node --test dist/**/*.test.js 2>/dev/null || true"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
78
packages/core/src/adapter-interface.ts
Normal file
78
packages/core/src/adapter-interface.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Protocol adapter plugin interface. Each rail implements this contract.
|
||||
* See ADR-001 (adapter-interface-and-versioning).
|
||||
*/
|
||||
import type {
|
||||
Participant,
|
||||
Endpoint,
|
||||
Capability,
|
||||
Identifier,
|
||||
RouteDirective,
|
||||
ResolveRequest,
|
||||
ServiceContext,
|
||||
} from "./types.js";
|
||||
|
||||
/** Minimal read-only view supplied by the resolver (e.g. DirectoryStore). */
|
||||
export interface AdapterContext {
|
||||
findParticipantsByIdentifiers(
|
||||
identifiers: Array<{ type: string; value: string }>,
|
||||
options?: { tenantId?: string }
|
||||
): Promise<Participant[]>;
|
||||
getEndpointsByParticipantId(
|
||||
participantId: string,
|
||||
options?: { protocol?: string; status?: string }
|
||||
): Promise<Endpoint[]>;
|
||||
getCapabilitiesByParticipantId(participantId: string): Promise<Capability[]>;
|
||||
}
|
||||
|
||||
/** A single candidate (participant + endpoint) produced by an adapter. */
|
||||
export interface AdapterCandidate {
|
||||
participant: Participant;
|
||||
endpoint: Endpoint;
|
||||
capability?: Capability;
|
||||
identifier?: Identifier;
|
||||
}
|
||||
|
||||
/** Result of optional ingest. */
|
||||
export interface IngestResult {
|
||||
participants: number;
|
||||
identifiers: number;
|
||||
endpoints?: number;
|
||||
}
|
||||
|
||||
/** Options when rendering a directive (e.g. default TTL). */
|
||||
export interface RenderDirectiveOptions {
|
||||
defaultTtlSeconds?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protocol adapter: one per rail. Registry discovers/loads by protocol or identifier type.
|
||||
* Semantic versioning of this interface: backward-compatible additions only (new optional methods or optional fields).
|
||||
*/
|
||||
export interface ProtocolAdapter {
|
||||
/** Adapter semantic version (e.g. "1.0.0"). */
|
||||
readonly version: string;
|
||||
/** Protocol or rail name (e.g. "as4", "ss7", "iso8583"). */
|
||||
readonly protocol: string;
|
||||
|
||||
validateIdentifier(type: string, value: string): boolean;
|
||||
|
||||
/** Return normalized value for storage/lookup, or null if invalid. */
|
||||
normalizeIdentifier(type: string, value: string): string | null;
|
||||
|
||||
/** Return candidates for the request using the provided context. */
|
||||
resolveCandidates(
|
||||
ctx: AdapterContext,
|
||||
request: ResolveRequest,
|
||||
options?: { tenantId?: string }
|
||||
): Promise<AdapterCandidate[]>;
|
||||
|
||||
/** Whether the candidate matches the service context (e.g. capability). */
|
||||
evaluateCapabilities(candidate: AdapterCandidate, serviceContext?: ServiceContext): boolean;
|
||||
|
||||
/** Build a RouteDirective from a candidate. */
|
||||
renderRouteDirective(candidate: AdapterCandidate, options?: RenderDirectiveOptions): RouteDirective;
|
||||
|
||||
/** Optional: ingest from external source (SMP, file, etc.). */
|
||||
ingestSource?(config: unknown): Promise<IngestResult>;
|
||||
}
|
||||
4
packages/core/src/index.ts
Normal file
4
packages/core/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./types.js";
|
||||
export * from "./validation.js";
|
||||
export * from "./protocol_registry/index.js";
|
||||
export * from "./adapter-interface.js";
|
||||
97
packages/core/src/protocol_registry/artifacts.ts
Normal file
97
packages/core/src/protocol_registry/artifacts.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Routing artifact type definitions and payload shapes.
|
||||
* Versioned, optionally signed.
|
||||
*/
|
||||
|
||||
import type { RoutingArtifactType, RoutingArtifactPayload } from "./types.js";
|
||||
|
||||
export interface BinTableEntry {
|
||||
binPrefix: string;
|
||||
binLength: number;
|
||||
brand?: string;
|
||||
region?: string;
|
||||
routingTarget: string;
|
||||
tenantId?: string;
|
||||
}
|
||||
|
||||
export interface BinTablePayload {
|
||||
version: string;
|
||||
data: { entries: BinTableEntry[] };
|
||||
signature?: string;
|
||||
fingerprint?: string;
|
||||
}
|
||||
|
||||
export interface GttTableEntry {
|
||||
globalTitle: string;
|
||||
pointCode?: string;
|
||||
ssn?: string;
|
||||
translationType?: string;
|
||||
}
|
||||
|
||||
export interface GttTablePayload {
|
||||
version: string;
|
||||
data: { entries: GttTableEntry[] };
|
||||
signature?: string;
|
||||
fingerprint?: string;
|
||||
}
|
||||
|
||||
export interface ParticipantMapEntry {
|
||||
identifierType: string;
|
||||
identifierValue: string;
|
||||
participantId: string;
|
||||
endpointId?: string;
|
||||
}
|
||||
|
||||
export interface ParticipantMapPayload {
|
||||
version: string;
|
||||
data: { entries: ParticipantMapEntry[] };
|
||||
signature?: string;
|
||||
fingerprint?: string;
|
||||
}
|
||||
|
||||
export interface FallbackRule {
|
||||
match: Record<string, unknown>;
|
||||
targetParticipantId?: string;
|
||||
targetEndpointId?: string;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface FallbackRulesPayload {
|
||||
version: string;
|
||||
data: { rules: FallbackRule[] };
|
||||
signature?: string;
|
||||
fingerprint?: string;
|
||||
}
|
||||
|
||||
export const ARTIFACT_TYPES: RoutingArtifactType[] = [
|
||||
"bin_table",
|
||||
"gtt_table",
|
||||
"participant_map",
|
||||
"fallback_rules",
|
||||
];
|
||||
|
||||
export function isKnownArtifactType(t: string): t is RoutingArtifactType {
|
||||
return ARTIFACT_TYPES.includes(t as RoutingArtifactType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate payload shape for artifact type. Returns true if valid or type unknown.
|
||||
*/
|
||||
export function validateArtifactPayload(
|
||||
artifactType: RoutingArtifactType,
|
||||
payload: RoutingArtifactPayload
|
||||
): boolean {
|
||||
if (!payload?.version || typeof payload.data !== "object") return false;
|
||||
switch (artifactType) {
|
||||
case "bin_table":
|
||||
return Array.isArray((payload.data as { entries?: unknown }).entries);
|
||||
case "gtt_table":
|
||||
return Array.isArray((payload.data as { entries?: unknown }).entries);
|
||||
case "participant_map":
|
||||
return Array.isArray((payload.data as { entries?: unknown }).entries);
|
||||
case "fallback_rules":
|
||||
return Array.isArray((payload.data as { rules?: unknown }).rules);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
3
packages/core/src/protocol_registry/index.ts
Normal file
3
packages/core/src/protocol_registry/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./types.js";
|
||||
export * from "./validators.js";
|
||||
export * from "./artifacts.js";
|
||||
39
packages/core/src/protocol_registry/types.ts
Normal file
39
packages/core/src/protocol_registry/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/** Rail profile and routing artifact types. */
|
||||
|
||||
export type ProtocolFamily =
|
||||
| "as4"
|
||||
| "ss7"
|
||||
| "iso8583"
|
||||
| "mq"
|
||||
| "sftp"
|
||||
| "api"
|
||||
| "https";
|
||||
|
||||
export interface RailProfile {
|
||||
name: string;
|
||||
protocolFamily: ProtocolFamily;
|
||||
addressPattern?: string | RegExp;
|
||||
allowedIdentifierTypes?: string[];
|
||||
}
|
||||
|
||||
export type RoutingArtifactType =
|
||||
| "bin_table"
|
||||
| "gtt_table"
|
||||
| "participant_map"
|
||||
| "fallback_rules";
|
||||
|
||||
export interface RoutingArtifactPayload {
|
||||
version: string;
|
||||
data: unknown;
|
||||
signature?: string;
|
||||
fingerprint?: string;
|
||||
}
|
||||
|
||||
export interface RoutingArtifact {
|
||||
id: string;
|
||||
tenantId?: string;
|
||||
artifactType: RoutingArtifactType;
|
||||
payload: RoutingArtifactPayload;
|
||||
effectiveFrom: string;
|
||||
effectiveTo?: string;
|
||||
}
|
||||
36
packages/core/src/protocol_registry/validators.ts
Normal file
36
packages/core/src/protocol_registry/validators.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { RailProfile } from "./types.js";
|
||||
|
||||
const BUILTIN: RailProfile[] = [
|
||||
{ name: "peppol-as4", protocolFamily: "as4", allowedIdentifierTypes: ["as4.partyId", "peppol.participantId"] },
|
||||
{ name: "visa-base1", protocolFamily: "iso8583", allowedIdentifierTypes: ["pan.bin", "mid", "tid"] },
|
||||
{ name: "dtcc-mq", protocolFamily: "mq", allowedIdentifierTypes: ["lei", "bic", "dtc.participantId"] },
|
||||
{ name: "as4", protocolFamily: "as4", allowedIdentifierTypes: ["as4.partyId"] },
|
||||
{ name: "ss7", protocolFamily: "ss7", allowedIdentifierTypes: ["e164", "gt", "pc", "ssn"] },
|
||||
];
|
||||
|
||||
const registry = new Map<string, RailProfile>(BUILTIN.map((p) => [p.name, p]));
|
||||
|
||||
export function registerProfile(profile: RailProfile): void {
|
||||
registry.set(profile.name, profile);
|
||||
}
|
||||
|
||||
export function getProfile(name: string): RailProfile | undefined {
|
||||
return registry.get(name);
|
||||
}
|
||||
|
||||
export function listProfiles(): RailProfile[] {
|
||||
return Array.from(registry.values());
|
||||
}
|
||||
|
||||
export function validateAddressForProfile(profileName: string, address: string): boolean {
|
||||
const profile = registry.get(profileName);
|
||||
if (!profile?.addressPattern) return true;
|
||||
const re = typeof profile.addressPattern === "string" ? new RegExp(profile.addressPattern) : profile.addressPattern;
|
||||
return re.test(address);
|
||||
}
|
||||
|
||||
export function isIdentifierTypeAllowed(profileName: string, identifierType: string): boolean {
|
||||
const profile = registry.get(profileName);
|
||||
if (!profile?.allowedIdentifierTypes) return true;
|
||||
return profile.allowedIdentifierTypes.includes(identifierType);
|
||||
}
|
||||
181
packages/core/src/types.ts
Normal file
181
packages/core/src/types.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Domain types aligned with OpenAPI and data-model.
|
||||
* No I/O; pure domain and validation.
|
||||
*/
|
||||
|
||||
export interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
name: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Identifier {
|
||||
id: string;
|
||||
participantId: string;
|
||||
identifier_type: string;
|
||||
value: string;
|
||||
scope?: string;
|
||||
priority: number;
|
||||
verified_at?: string;
|
||||
}
|
||||
|
||||
export interface Endpoint {
|
||||
id: string;
|
||||
participantId: string;
|
||||
protocol: string;
|
||||
address: string;
|
||||
profile?: string;
|
||||
priority: number;
|
||||
status: "active" | "inactive" | "draining";
|
||||
}
|
||||
|
||||
export interface Capability {
|
||||
id: string;
|
||||
participantId: string;
|
||||
service?: string;
|
||||
action?: string;
|
||||
process?: string;
|
||||
document_type?: string;
|
||||
constraints_json?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CredentialRef {
|
||||
id: string;
|
||||
participantId: string;
|
||||
credential_type: "tls" | "sign" | "encrypt";
|
||||
vault_ref: string;
|
||||
fingerprint?: string;
|
||||
valid_from?: string;
|
||||
valid_to?: string;
|
||||
}
|
||||
|
||||
export interface Policy {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
rule_json: Record<string, unknown>;
|
||||
effect: "allow" | "deny";
|
||||
priority: number;
|
||||
}
|
||||
|
||||
/** Input identifier for resolve request */
|
||||
export interface IdentifierInput {
|
||||
type: string;
|
||||
value: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export interface ServiceContext {
|
||||
service?: string;
|
||||
action?: string;
|
||||
process?: string;
|
||||
documentType?: string;
|
||||
}
|
||||
|
||||
export interface ResolveConstraints {
|
||||
trustDomain?: string;
|
||||
region?: string;
|
||||
jurisdiction?: string;
|
||||
maxResults?: number;
|
||||
/** Card network brand: visa, mastercard, amex, discover, diners */
|
||||
networkBrand?: string;
|
||||
/** Tenant contract or connectivity group for per-tenant/contract routing */
|
||||
tenantContract?: string;
|
||||
connectivityGroup?: string;
|
||||
/** Explicit required capability filter */
|
||||
requiredCapability?: string;
|
||||
/** Message type (e.g. ISO8583 MTI or AS4 service/action) */
|
||||
messageType?: string;
|
||||
}
|
||||
|
||||
export interface ResolveRequest {
|
||||
identifiers: IdentifierInput[];
|
||||
serviceContext?: ServiceContext;
|
||||
constraints?: ResolveConstraints;
|
||||
tenant?: string;
|
||||
desiredProtocols?: string[];
|
||||
}
|
||||
|
||||
/** Security block in RouteDirective */
|
||||
export interface RouteDirectiveSecurity {
|
||||
signRequired?: boolean;
|
||||
encryptRequired?: boolean;
|
||||
keyRefs?: string[];
|
||||
algorithms?: Record<string, string>;
|
||||
}
|
||||
|
||||
/** Service context in RouteDirective */
|
||||
export interface RouteDirectiveServiceContext {
|
||||
service?: string;
|
||||
action?: string;
|
||||
serviceIndicator?: string;
|
||||
}
|
||||
|
||||
/** QoS in RouteDirective */
|
||||
export interface RouteDirectiveQos {
|
||||
retries?: number;
|
||||
receiptsRequired?: boolean;
|
||||
ordering?: string;
|
||||
}
|
||||
|
||||
/** Evidence in RouteDirective (single or array for multiple sources) */
|
||||
export interface RouteDirectiveEvidence {
|
||||
source?: string;
|
||||
lastVerified?: string;
|
||||
confidenceScore?: number;
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export interface RouteDirective {
|
||||
target_protocol: string;
|
||||
target_address: string;
|
||||
transport_profile?: string;
|
||||
security?: RouteDirectiveSecurity;
|
||||
service_context?: RouteDirectiveServiceContext;
|
||||
qos?: RouteDirectiveQos;
|
||||
ttl_seconds?: number;
|
||||
evidence?: RouteDirectiveEvidence | RouteDirectiveEvidence[];
|
||||
}
|
||||
|
||||
/** Directive with optional reason (for alternates) */
|
||||
export interface DirectiveWithReason {
|
||||
directive: RouteDirective;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** Advisory failure policy for gateway */
|
||||
export interface FailurePolicy {
|
||||
retry?: boolean;
|
||||
backoff?: string;
|
||||
circuitBreak?: boolean;
|
||||
}
|
||||
|
||||
/** Entry in resolution trace (which source contributed) */
|
||||
export interface ResolutionTraceEntry {
|
||||
source: string;
|
||||
directiveIndex?: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ResolveResponse {
|
||||
/** Best match; when set, directives[0] should equal primary for backward compat */
|
||||
primary?: RouteDirective;
|
||||
/** Ordered fallback directives with reason */
|
||||
alternates?: DirectiveWithReason[];
|
||||
directives: RouteDirective[];
|
||||
ttl?: number;
|
||||
traceId?: string;
|
||||
correlationId?: string;
|
||||
failure_policy?: FailurePolicy;
|
||||
/** TTL for negative (no-match) cache in seconds */
|
||||
negative_cache_ttl?: number;
|
||||
resolution_trace?: ResolutionTraceEntry[];
|
||||
}
|
||||
107
packages/core/src/validation.ts
Normal file
107
packages/core/src/validation.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Identifier validation helpers (stubs / minimal regex-based).
|
||||
* Aligned with identifier types in data-model.md.
|
||||
*/
|
||||
|
||||
/** E.164: optional +, digits only, typically up to 15 digits */
|
||||
const E164_REGEX = /^\+?[1-9]\d{1,14}$/;
|
||||
|
||||
/** AS4 PartyId: common pattern scheme:value (e.g. 0088:123456789) */
|
||||
const PARTY_ID_REGEX = /^[^:]+:[^:]+$/;
|
||||
|
||||
/** PEPPOL participant ID: same format as PartyId (ISO 6523) */
|
||||
const PEPPOL_PARTICIPANT_REGEX = /^[0-9]{4}:[a-zA-Z0-9]+$/;
|
||||
|
||||
/** Point code: numeric, format depends on variant (ITU 14-bit, ANSI 24-bit); accept digits and dots */
|
||||
const PC_REGEX = /^[\d.]+$/;
|
||||
|
||||
/** SSN: 1-255 */
|
||||
const SSN_REGEX = /^(?:25[0-5]|2[0-4]\d|1?\d{1,2})$/;
|
||||
|
||||
/** KTT (placeholder rail): alphanumeric, optional separators */
|
||||
const KTT_ID_REGEX = /^[a-zA-Z0-9._-]+$/;
|
||||
|
||||
/** BIN/IIN: 6-8 digits only (never full PAN) */
|
||||
const PAN_BIN_REGEX = /^\d{6,8}$/;
|
||||
|
||||
/** Merchant ID / Terminal ID: alphanumeric, tenant-scoped format */
|
||||
const MID_TID_REGEX = /^[a-zA-Z0-9]+$/;
|
||||
|
||||
/** LEI: 20 alphanumeric */
|
||||
const LEI_REGEX = /^[A-Z0-9]{20}$/;
|
||||
|
||||
/** BIC: 8 or 11 alphanumeric */
|
||||
const BIC_REGEX = /^[A-Za-z0-9]{8}([A-Za-z0-9]{3})?$/;
|
||||
|
||||
/** DTC participant/account ID: alphanumeric, tenant-scoped */
|
||||
const DTC_ID_REGEX = /^[a-zA-Z0-9._-]+$/;
|
||||
|
||||
export function validateE164(value: string): boolean {
|
||||
return typeof value === "string" && E164_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
export function validateAs4PartyId(value: string): boolean {
|
||||
return typeof value === "string" && value.length > 0 && PARTY_ID_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
export function validatePeppolParticipantId(value: string): boolean {
|
||||
return typeof value === "string" && PEPPOL_PARTICIPANT_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
export function validatePointCode(value: string): boolean {
|
||||
return typeof value === "string" && PC_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
export function validateSsn(value: string): boolean {
|
||||
return typeof value === "string" && SSN_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
export function validateKttId(value: string): boolean {
|
||||
return typeof value === "string" && value.length > 0 && KTT_ID_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
export function validatePanBin(value: string): boolean {
|
||||
return typeof value === "string" && PAN_BIN_REGEX.test(value.replace(/\D/g, ""));
|
||||
}
|
||||
|
||||
export function validateMidOrTid(value: string): boolean {
|
||||
return typeof value === "string" && value.length > 0 && MID_TID_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
export function validateLei(value: string): boolean {
|
||||
return typeof value === "string" && LEI_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
export function validateBic(value: string): boolean {
|
||||
return typeof value === "string" && BIC_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
export function validateDtcId(value: string): boolean {
|
||||
return typeof value === "string" && value.length > 0 && DTC_ID_REGEX.test(value.trim());
|
||||
}
|
||||
|
||||
const VALIDATORS: Record<string, (v: string) => boolean> = {
|
||||
e164: validateE164,
|
||||
"as4.partyId": validateAs4PartyId,
|
||||
"peppol.participantId": validatePeppolParticipantId,
|
||||
pc: validatePointCode,
|
||||
ssn: validateSsn,
|
||||
"ktt.id": validateKttId,
|
||||
"ktt.participantId": validateKttId,
|
||||
"pan.bin": validatePanBin,
|
||||
mid: validateMidOrTid,
|
||||
tid: validateMidOrTid,
|
||||
lei: validateLei,
|
||||
bic: validateBic,
|
||||
"dtc.participantId": validateDtcId,
|
||||
"dtc.accountId": validateDtcId,
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate an identifier by type. Returns true if valid or type has no validator (permissive).
|
||||
*/
|
||||
export function validateIdentifier(type: string, value: string): boolean {
|
||||
const fn = VALIDATORS[type];
|
||||
if (!fn) return typeof value === "string" && value.length > 0;
|
||||
return fn(value);
|
||||
}
|
||||
16
packages/core/tsconfig.json
Normal file
16
packages/core/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user