Initial commit: AS4/411 directory and discovery service for Sankofa Marketplace
Some checks failed
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-08 08:44:20 -08:00
commit c24ae925cf
109 changed files with 7222 additions and 0 deletions

View 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"
}
}

View 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>;
}

View File

@@ -0,0 +1,4 @@
export * from "./types.js";
export * from "./validation.js";
export * from "./protocol_registry/index.js";
export * from "./adapter-interface.js";

View 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;
}
}

View File

@@ -0,0 +1,3 @@
export * from "./types.js";
export * from "./validators.js";
export * from "./artifacts.js";

View 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;
}

View 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
View 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[];
}

View 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);
}

View 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"]
}