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

View File

@@ -0,0 +1,91 @@
-- Initial schema for as4-411 directory (data-model.md)
-- Run with psql or migration runner; uses snake_case for columns.
CREATE TABLE IF NOT EXISTS tenants (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS participants (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_participants_tenant_id ON participants(tenant_id);
CREATE TABLE IF NOT EXISTS identifiers (
id TEXT PRIMARY KEY,
participant_id TEXT NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
identifier_type TEXT NOT NULL,
value TEXT NOT NULL,
scope TEXT,
priority INTEGER NOT NULL DEFAULT 0,
verified_at TIMESTAMPTZ
);
CREATE INDEX idx_identifiers_lookup ON identifiers(identifier_type, value);
CREATE INDEX idx_identifiers_participant_id ON identifiers(participant_id);
CREATE TABLE IF NOT EXISTS endpoints (
id TEXT PRIMARY KEY,
participant_id TEXT NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
protocol TEXT NOT NULL,
address TEXT NOT NULL,
profile TEXT,
priority INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'draining'))
);
CREATE INDEX idx_endpoints_participant_id ON endpoints(participant_id);
CREATE TABLE IF NOT EXISTS capabilities (
id TEXT PRIMARY KEY,
participant_id TEXT NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
service TEXT,
action TEXT,
process TEXT,
document_type TEXT,
constraints_json JSONB
);
CREATE INDEX idx_capabilities_participant_id ON capabilities(participant_id);
CREATE TABLE IF NOT EXISTS credentials (
id TEXT PRIMARY KEY,
participant_id TEXT NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
credential_type TEXT NOT NULL CHECK (credential_type IN ('tls', 'sign', 'encrypt')),
vault_ref TEXT NOT NULL,
fingerprint TEXT,
valid_from TIMESTAMPTZ,
valid_to TIMESTAMPTZ
);
CREATE INDEX idx_credentials_participant_id ON credentials(participant_id);
CREATE TABLE IF NOT EXISTS policies (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
rule_json JSONB NOT NULL DEFAULT '{}',
effect TEXT NOT NULL CHECK (effect IN ('allow', 'deny')),
priority INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_policies_tenant_id ON policies(tenant_id);
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
actor TEXT,
action TEXT NOT NULL,
resource TEXT NOT NULL,
resource_id TEXT NOT NULL,
payload JSONB,
hash_prev TEXT
);
CREATE INDEX idx_audit_log_resource ON audit_log(resource, resource_id);

View File

@@ -0,0 +1,16 @@
-- Routing artifacts: BIN tables, GTT tables, participant maps, fallback rules.
-- See docs/architecture/data-model and protocol_registry.
CREATE TABLE IF NOT EXISTS routing_artifacts (
id TEXT PRIMARY KEY,
tenant_id TEXT REFERENCES tenants(id) ON DELETE CASCADE,
artifact_type TEXT NOT NULL CHECK (artifact_type IN ('bin_table', 'gtt_table', 'participant_map', 'fallback_rules')),
artifact_payload JSONB NOT NULL,
effective_from TIMESTAMPTZ NOT NULL,
effective_to TIMESTAMPTZ,
signature TEXT,
fingerprint TEXT
);
CREATE INDEX idx_routing_artifacts_tenant_type ON routing_artifacts(tenant_id, artifact_type);
CREATE INDEX idx_routing_artifacts_effective ON routing_artifacts(effective_from, effective_to);

View File

@@ -0,0 +1,20 @@
-- Graph layer: edges with provenance and validity (see data-model.md).
-- Optional: used when explicit graph and conflict resolution are needed.
CREATE TABLE IF NOT EXISTS edges (
id TEXT PRIMARY KEY,
from_type TEXT NOT NULL,
from_id TEXT NOT NULL,
to_type TEXT NOT NULL,
to_id TEXT NOT NULL,
relation TEXT NOT NULL,
confidence REAL,
source TEXT,
valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
valid_to TIMESTAMPTZ
);
CREATE INDEX idx_edges_from ON edges(from_type, from_id);
CREATE INDEX idx_edges_to ON edges(to_type, to_id);
CREATE INDEX idx_edges_relation ON edges(relation);
CREATE INDEX idx_edges_valid ON edges(valid_from, valid_to);

View File

@@ -0,0 +1,23 @@
{
"name": "@as4-411/storage",
"type": "module",
"version": "0.1.0",
"description": "Directory store port and implementations 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"
},
"dependencies": {
"@as4-411/core": "workspace:*",
"pg": "^8.11.3"
},
"devDependencies": {
"@types/pg": "^8.10.9",
"typescript": "^5.3.0"
},
"engines": {
"node": ">=18"
}
}

View File

View File

View File

@@ -0,0 +1,54 @@
import type {
Tenant,
Participant,
Identifier,
Endpoint,
Capability,
CredentialRef,
Policy,
} from "@as4-411/core";
import type { DirectoryStore } from "./port.js";
/**
* Admin store: DirectoryStore read methods plus full CRUD for directory entities.
* Used by Admin API; Postgres and InMemory can implement.
*/
export interface AdminStore extends DirectoryStore {
// Tenants
listTenants(): Promise<Tenant[]>;
getTenant(id: string): Promise<Tenant | null>;
createTenant(tenant: Tenant): Promise<void>;
updateTenant(id: string, tenant: Omit<Tenant, "id">): Promise<boolean>;
deleteTenant(id: string): Promise<boolean>;
// Participants
listParticipants(options?: { tenantId?: string }): Promise<Participant[]>;
getParticipant(id: string): Promise<Participant | null>;
createParticipant(participant: Participant): Promise<void>;
updateParticipant(id: string, participant: Omit<Participant, "id" | "tenantId">): Promise<boolean>;
deleteParticipant(id: string): Promise<boolean>;
// Identifiers (create/list; list is getIdentifiersByParticipantId)
createIdentifier(identifier: Identifier): Promise<void>;
deleteIdentifier(id: string): Promise<boolean>;
// Endpoints
createEndpoint(endpoint: Endpoint): Promise<void>;
updateEndpoint(id: string, endpoint: Omit<Endpoint, "id" | "participantId">): Promise<boolean>;
deleteEndpoint(id: string): Promise<boolean>;
// Capabilities
createCapability(capability: Capability): Promise<void>;
deleteCapability(id: string): Promise<boolean>;
// Credentials
createCredential(credential: CredentialRef): Promise<void>;
deleteCredential(id: string): Promise<boolean>;
// Policies
listPolicies(options?: { tenantId?: string }): Promise<Policy[]>;
getPolicy(id: string): Promise<Policy | null>;
createPolicy(policy: Policy): Promise<void>;
updatePolicy(id: string, policy: Omit<Policy, "id" | "tenantId">): Promise<boolean>;
deletePolicy(id: string): Promise<boolean>;
}

View File

@@ -0,0 +1,8 @@
export type { DirectoryStore } from "./port.js";
export type { AdminStore } from "./admin-port.js";
export type { RoutingArtifactStore } from "./routing-artifact-port.js";
export { InMemoryDirectoryStore } from "./memory-store.js";
export { InMemoryRoutingArtifactStore } from "./routing-artifact-memory.js";
export { PostgresDirectoryStore } from "./postgres/postgres-store.js";
export { PostgresRoutingArtifactStore } from "./postgres/routing-artifact-store.js";
export type { PostgresStoreConfig } from "./postgres/postgres-store.js";

View File

@@ -0,0 +1,242 @@
import type {
Tenant,
Participant,
Identifier,
Endpoint,
Capability,
CredentialRef,
Policy,
} from "@as4-411/core";
import type { AdminStore } from "./admin-port.js";
/**
* In-memory directory store for development and tests.
* Implements AdminStore (read + write). Not persistent; no migrations.
*/
export class InMemoryDirectoryStore implements AdminStore {
private tenants: Map<string, Tenant> = new Map();
private participants: Map<string, Participant> = new Map();
private identifiers: Identifier[] = [];
private endpoints: Endpoint[] = [];
private capabilities: Capability[] = [];
private credentials: CredentialRef[] = [];
private policies: Policy[] = [];
addParticipant(p: Participant): void {
this.participants.set(p.id, p);
}
addIdentifier(i: Identifier): void {
this.identifiers.push(i);
}
addEndpoint(e: Endpoint): void {
this.endpoints.push(e);
}
addCapability(c: Capability): void {
this.capabilities.push(c);
}
addCredential(c: CredentialRef): void {
this.credentials.push(c);
}
addPolicy(p: Policy): void {
this.policies.push(p);
}
addTenant(t: Tenant): void {
this.tenants.set(t.id, t);
}
async listTenants(): Promise<Tenant[]> {
return Array.from(this.tenants.values());
}
async getTenant(id: string): Promise<Tenant | null> {
return this.tenants.get(id) ?? null;
}
async createTenant(tenant: Tenant): Promise<void> {
this.tenants.set(tenant.id, tenant);
}
async updateTenant(id: string, tenant: Omit<Tenant, "id">): Promise<boolean> {
const existing = this.tenants.get(id);
if (!existing) return false;
this.tenants.set(id, { ...existing, ...tenant });
return true;
}
async deleteTenant(id: string): Promise<boolean> {
return this.tenants.delete(id);
}
async listParticipants(options?: { tenantId?: string }): Promise<Participant[]> {
let list = Array.from(this.participants.values());
if (options?.tenantId)
list = list.filter((p) => p.tenantId === options.tenantId);
return list;
}
async getParticipant(id: string): Promise<Participant | null> {
return this.participants.get(id) ?? null;
}
async createParticipant(participant: Participant): Promise<void> {
this.participants.set(participant.id, participant);
}
async updateParticipant(
id: string,
participant: Omit<Participant, "id" | "tenantId">
): Promise<boolean> {
const existing = this.participants.get(id);
if (!existing) return false;
this.participants.set(id, { ...existing, ...participant });
return true;
}
async deleteParticipant(id: string): Promise<boolean> {
if (!this.participants.has(id)) return false;
this.participants.delete(id);
this.identifiers = this.identifiers.filter((i) => i.participantId !== id);
this.endpoints = this.endpoints.filter((e) => e.participantId !== id);
this.capabilities = this.capabilities.filter((c) => c.participantId !== id);
this.credentials = this.credentials.filter((c) => c.participantId !== id);
return true;
}
async createIdentifier(identifier: Identifier): Promise<void> {
this.identifiers.push(identifier);
}
async deleteIdentifier(id: string): Promise<boolean> {
const idx = this.identifiers.findIndex((i) => i.id === id);
if (idx === -1) return false;
this.identifiers.splice(idx, 1);
return true;
}
async createEndpoint(endpoint: Endpoint): Promise<void> {
this.endpoints.push(endpoint);
}
async updateEndpoint(
id: string,
endpoint: Omit<Endpoint, "id" | "participantId">
): Promise<boolean> {
const idx = this.endpoints.findIndex((e) => e.id === id);
if (idx === -1) return false;
this.endpoints[idx] = { ...this.endpoints[idx], ...endpoint };
return true;
}
async deleteEndpoint(id: string): Promise<boolean> {
const idx = this.endpoints.findIndex((e) => e.id === id);
if (idx === -1) return false;
this.endpoints.splice(idx, 1);
return true;
}
async createCapability(capability: Capability): Promise<void> {
this.capabilities.push(capability);
}
async deleteCapability(id: string): Promise<boolean> {
const idx = this.capabilities.findIndex((c) => c.id === id);
if (idx === -1) return false;
this.capabilities.splice(idx, 1);
return true;
}
async createCredential(credential: CredentialRef): Promise<void> {
this.credentials.push(credential);
}
async deleteCredential(id: string): Promise<boolean> {
const idx = this.credentials.findIndex((c) => c.id === id);
if (idx === -1) return false;
this.credentials.splice(idx, 1);
return true;
}
async listPolicies(options?: { tenantId?: string }): Promise<Policy[]> {
let list = Array.from(this.policies);
if (options?.tenantId)
list = list.filter((p) => p.tenantId === options.tenantId);
return list.sort((a, b) => b.priority - a.priority);
}
async getPolicy(id: string): Promise<Policy | null> {
return this.policies.find((p) => p.id === id) ?? null;
}
async createPolicy(policy: Policy): Promise<void> {
this.policies.push(policy);
}
async updatePolicy(id: string, policy: Omit<Policy, "id" | "tenantId">): Promise<boolean> {
const idx = this.policies.findIndex((p) => p.id === id);
if (idx === -1) return false;
this.policies[idx] = { ...this.policies[idx], ...policy };
return true;
}
async deletePolicy(id: string): Promise<boolean> {
const idx = this.policies.findIndex((p) => p.id === id);
if (idx === -1) return false;
this.policies.splice(idx, 1);
return true;
}
async findParticipantsByIdentifiers(
identifiers: Array<{ type: string; value: string }>,
options?: { tenantId?: string }
): Promise<Participant[]> {
const byKey = new Set<string>();
for (const id of identifiers) {
const matching = this.identifiers.filter(
(i) =>
i.identifier_type === id.type &&
i.value === id.value &&
(options?.tenantId == null ||
this.participants.get(i.participantId)?.tenantId === options.tenantId)
);
for (const m of matching) byKey.add(m.participantId);
}
const out: Participant[] = [];
for (const pid of byKey) {
const p = this.participants.get(pid);
if (p) out.push(p);
}
return out;
}
async getIdentifiersByParticipantId(participantId: string): Promise<Identifier[]> {
return this.identifiers.filter((i) => i.participantId === participantId);
}
async getEndpointsByParticipantId(
participantId: string,
options?: { protocol?: string; status?: string }
): Promise<Endpoint[]> {
let list = this.endpoints.filter((e) => e.participantId === participantId);
if (options?.protocol) list = list.filter((e) => e.protocol === options.protocol);
if (options?.status) list = list.filter((e) => e.status === options.status);
return list;
}
async getCapabilitiesByParticipantId(participantId: string): Promise<Capability[]> {
return this.capabilities.filter((c) => c.participantId === participantId);
}
async getCredentialsByParticipantId(participantId: string): Promise<CredentialRef[]> {
return this.credentials.filter((c) => c.participantId === participantId);
}
async getPoliciesByTenantId(tenantId: string): Promise<Policy[]> {
return this.policies.filter((p) => p.tenantId === tenantId);
}
}

View File

@@ -0,0 +1,38 @@
import type {
Participant,
Identifier,
Endpoint,
Capability,
CredentialRef,
Policy,
} from "@as4-411/core";
/**
* Directory store port: enough for the resolver to find participants and endpoints.
* Implementations: in-memory, Postgres, SQLite.
*/
export interface DirectoryStore {
/** Find participants that have any of the given identifier (type, value) pairs, optionally scoped by tenant */
findParticipantsByIdentifiers(
identifiers: Array<{ type: string; value: string }>,
options?: { tenantId?: string }
): Promise<Participant[]>;
/** Get all identifiers for a participant */
getIdentifiersByParticipantId(participantId: string): Promise<Identifier[]>;
/** Get all endpoints for a participant, optionally filter by protocol */
getEndpointsByParticipantId(
participantId: string,
options?: { protocol?: string; status?: string }
): Promise<Endpoint[]>;
/** Get capabilities for a participant */
getCapabilitiesByParticipantId(participantId: string): Promise<Capability[]>;
/** Get credential refs for a participant */
getCredentialsByParticipantId(participantId: string): Promise<CredentialRef[]>;
/** Get policies for a tenant (for policy filter step) */
getPoliciesByTenantId(tenantId: string): Promise<Policy[]>;
}

View File

@@ -0,0 +1,388 @@
import type { Pool } from "pg";
import type {
Tenant,
Participant,
Identifier,
Endpoint,
Capability,
CredentialRef,
Policy,
} from "@as4-411/core";
import type { AdminStore } from "../admin-port.js";
function rowToTenant(r: Record<string, unknown>): Tenant {
return {
id: r.id as string,
name: r.name as string,
createdAt: (r.created_at as Date)?.toISOString?.(),
updatedAt: (r.updated_at as Date)?.toISOString?.(),
};
}
function rowToParticipant(r: Record<string, unknown>): Participant {
return {
id: r.id as string,
tenantId: r.tenant_id as string,
name: r.name as string,
createdAt: (r.created_at as Date)?.toISOString?.(),
updatedAt: (r.updated_at as Date)?.toISOString?.(),
};
}
function rowToIdentifier(r: Record<string, unknown>): Identifier {
return {
id: r.id as string,
participantId: r.participant_id as string,
identifier_type: r.identifier_type as string,
value: r.value as string,
scope: r.scope as string | undefined,
priority: Number(r.priority) ?? 0,
verified_at: (r.verified_at as Date)?.toISOString?.(),
};
}
function rowToEndpoint(r: Record<string, unknown>): Endpoint {
return {
id: r.id as string,
participantId: r.participant_id as string,
protocol: r.protocol as string,
address: r.address as string,
profile: r.profile as string | undefined,
priority: Number(r.priority) ?? 0,
status: r.status as "active" | "inactive" | "draining",
};
}
function rowToCapability(r: Record<string, unknown>): Capability {
return {
id: r.id as string,
participantId: r.participant_id as string,
service: r.service as string | undefined,
action: r.action as string | undefined,
process: r.process as string | undefined,
document_type: r.document_type as string | undefined,
constraints_json: r.constraints_json as Record<string, unknown> | undefined,
};
}
function rowToCredential(r: Record<string, unknown>): CredentialRef {
return {
id: r.id as string,
participantId: r.participant_id as string,
credential_type: r.credential_type as "tls" | "sign" | "encrypt",
vault_ref: r.vault_ref as string,
fingerprint: r.fingerprint as string | undefined,
valid_from: (r.valid_from as Date)?.toISOString?.(),
valid_to: (r.valid_to as Date)?.toISOString?.(),
};
}
function rowToPolicy(r: Record<string, unknown>): Policy {
return {
id: r.id as string,
tenantId: r.tenant_id as string,
rule_json: (r.rule_json as Record<string, unknown>) ?? {},
effect: r.effect as "allow" | "deny",
priority: Number(r.priority) ?? 0,
};
}
export interface PostgresStoreConfig {
pool: Pool;
}
/**
* Postgres implementation of AdminStore (read + write).
* Run migrations (001_initial.sql) before use.
*/
export class PostgresDirectoryStore implements AdminStore {
constructor(private readonly config: PostgresStoreConfig) {}
private get pool(): Pool {
return this.config.pool;
}
async findParticipantsByIdentifiers(
identifiers: Array<{ type: string; value: string }>,
options?: { tenantId?: string }
): Promise<Participant[]> {
if (identifiers.length === 0) return [];
const values: unknown[] = identifiers.flatMap((id) => [id.type, id.value]);
const conditions = identifiers
.map((_, i) => `(i.identifier_type = $${2 * i + 1} AND i.value = $${2 * i + 2})`)
.join(" OR ");
let sql = `
SELECT DISTINCT p.id, p.tenant_id, p.name, p.created_at, p.updated_at
FROM participants p
JOIN identifiers i ON i.participant_id = p.id
WHERE ${conditions}
`;
if (options?.tenantId) {
values.push(options.tenantId);
sql += ` AND p.tenant_id = $${values.length}`;
}
const result = await this.pool.query(sql, values);
return result.rows.map(rowToParticipant);
}
async getIdentifiersByParticipantId(participantId: string): Promise<Identifier[]> {
const result = await this.pool.query(
"SELECT * FROM identifiers WHERE participant_id = $1",
[participantId]
);
return result.rows.map(rowToIdentifier);
}
async getEndpointsByParticipantId(
participantId: string,
options?: { protocol?: string; status?: string }
): Promise<Endpoint[]> {
let sql = "SELECT * FROM endpoints WHERE participant_id = $1";
const values: unknown[] = [participantId];
if (options?.protocol) {
values.push(options.protocol);
sql += ` AND protocol = $${values.length}`;
}
if (options?.status) {
values.push(options.status);
sql += ` AND status = $${values.length}`;
}
const result = await this.pool.query(sql, values);
return result.rows.map(rowToEndpoint);
}
async getCapabilitiesByParticipantId(participantId: string): Promise<Capability[]> {
const result = await this.pool.query(
"SELECT * FROM capabilities WHERE participant_id = $1",
[participantId]
);
return result.rows.map(rowToCapability);
}
async getCredentialsByParticipantId(participantId: string): Promise<CredentialRef[]> {
const result = await this.pool.query(
"SELECT * FROM credentials WHERE participant_id = $1",
[participantId]
);
return result.rows.map(rowToCredential);
}
async getPoliciesByTenantId(tenantId: string): Promise<Policy[]> {
const result = await this.pool.query(
"SELECT * FROM policies WHERE tenant_id = $1 ORDER BY priority DESC",
[tenantId]
);
return result.rows.map(rowToPolicy);
}
async listTenants(): Promise<Tenant[]> {
const result = await this.pool.query("SELECT * FROM tenants ORDER BY id");
return result.rows.map(rowToTenant);
}
async getTenant(id: string): Promise<Tenant | null> {
const result = await this.pool.query("SELECT * FROM tenants WHERE id = $1", [id]);
return result.rows[0] ? rowToTenant(result.rows[0]) : null;
}
async createTenant(tenant: Tenant): Promise<void> {
await this.pool.query(
"INSERT INTO tenants (id, name) VALUES ($1, $2)",
[tenant.id, tenant.name]
);
}
async updateTenant(id: string, tenant: Omit<Tenant, "id">): Promise<boolean> {
const result = await this.pool.query(
"UPDATE tenants SET name = $1, updated_at = NOW() WHERE id = $2",
[tenant.name, id]
);
return (result.rowCount ?? 0) > 0;
}
async deleteTenant(id: string): Promise<boolean> {
const result = await this.pool.query("DELETE FROM tenants WHERE id = $1", [id]);
return (result.rowCount ?? 0) > 0;
}
async listParticipants(options?: { tenantId?: string }): Promise<Participant[]> {
let sql = "SELECT * FROM participants";
const values: unknown[] = [];
if (options?.tenantId) {
values.push(options.tenantId);
sql += " WHERE tenant_id = $1";
}
sql += " ORDER BY id";
const result = await this.pool.query(sql, values);
return result.rows.map(rowToParticipant);
}
async getParticipant(id: string): Promise<Participant | null> {
const result = await this.pool.query("SELECT * FROM participants WHERE id = $1", [id]);
return result.rows[0] ? rowToParticipant(result.rows[0]) : null;
}
async createParticipant(participant: Participant): Promise<void> {
await this.pool.query(
"INSERT INTO participants (id, tenant_id, name) VALUES ($1, $2, $3)",
[participant.id, participant.tenantId, participant.name]
);
}
async updateParticipant(
id: string,
participant: Omit<Participant, "id" | "tenantId">
): Promise<boolean> {
const result = await this.pool.query(
"UPDATE participants SET name = $1, updated_at = NOW() WHERE id = $2",
[participant.name, id]
);
return (result.rowCount ?? 0) > 0;
}
async deleteParticipant(id: string): Promise<boolean> {
const result = await this.pool.query("DELETE FROM participants WHERE id = $1", [id]);
return (result.rowCount ?? 0) > 0;
}
async createIdentifier(identifier: Identifier): Promise<void> {
await this.pool.query(
"INSERT INTO identifiers (id, participant_id, identifier_type, value, scope, priority, verified_at) VALUES ($1, $2, $3, $4, $5, $6, $7)",
[
identifier.id,
identifier.participantId,
identifier.identifier_type,
identifier.value,
identifier.scope ?? null,
identifier.priority ?? 0,
identifier.verified_at ? new Date(identifier.verified_at) : null,
]
);
}
async deleteIdentifier(id: string): Promise<boolean> {
const result = await this.pool.query("DELETE FROM identifiers WHERE id = $1", [id]);
return (result.rowCount ?? 0) > 0;
}
async createEndpoint(endpoint: Endpoint): Promise<void> {
await this.pool.query(
"INSERT INTO endpoints (id, participant_id, protocol, address, profile, priority, status) VALUES ($1, $2, $3, $4, $5, $6, $7)",
[
endpoint.id,
endpoint.participantId,
endpoint.protocol,
endpoint.address,
endpoint.profile ?? null,
endpoint.priority ?? 0,
endpoint.status,
]
);
}
async updateEndpoint(
id: string,
endpoint: Omit<Endpoint, "id" | "participantId">
): Promise<boolean> {
const result = await this.pool.query(
"UPDATE endpoints SET protocol = $1, address = $2, profile = $3, priority = $4, status = $5 WHERE id = $6",
[
endpoint.protocol,
endpoint.address,
endpoint.profile ?? null,
endpoint.priority ?? 0,
endpoint.status,
id,
]
);
return (result.rowCount ?? 0) > 0;
}
async deleteEndpoint(id: string): Promise<boolean> {
const result = await this.pool.query("DELETE FROM endpoints WHERE id = $1", [id]);
return (result.rowCount ?? 0) > 0;
}
async createCapability(capability: Capability): Promise<void> {
await this.pool.query(
"INSERT INTO capabilities (id, participant_id, service, action, process, document_type, constraints_json) VALUES ($1, $2, $3, $4, $5, $6, $7)",
[
capability.id,
capability.participantId,
capability.service ?? null,
capability.action ?? null,
capability.process ?? null,
capability.document_type ?? null,
capability.constraints_json ? JSON.stringify(capability.constraints_json) : null,
]
);
}
async deleteCapability(id: string): Promise<boolean> {
const result = await this.pool.query("DELETE FROM capabilities WHERE id = $1", [id]);
return (result.rowCount ?? 0) > 0;
}
async createCredential(credential: CredentialRef): Promise<void> {
await this.pool.query(
"INSERT INTO credentials (id, participant_id, credential_type, vault_ref, fingerprint, valid_from, valid_to) VALUES ($1, $2, $3, $4, $5, $6, $7)",
[
credential.id,
credential.participantId,
credential.credential_type,
credential.vault_ref,
credential.fingerprint ?? null,
credential.valid_from ? new Date(credential.valid_from) : null,
credential.valid_to ? new Date(credential.valid_to) : null,
]
);
}
async deleteCredential(id: string): Promise<boolean> {
const result = await this.pool.query("DELETE FROM credentials WHERE id = $1", [id]);
return (result.rowCount ?? 0) > 0;
}
async listPolicies(options?: { tenantId?: string }): Promise<Policy[]> {
let sql = "SELECT * FROM policies";
const values: unknown[] = [];
if (options?.tenantId) {
values.push(options.tenantId);
sql += " WHERE tenant_id = $1";
}
sql += " ORDER BY priority DESC, id";
const result = await this.pool.query(sql, values);
return result.rows.map(rowToPolicy);
}
async getPolicy(id: string): Promise<Policy | null> {
const result = await this.pool.query("SELECT * FROM policies WHERE id = $1", [id]);
return result.rows[0] ? rowToPolicy(result.rows[0]) : null;
}
async createPolicy(policy: Policy): Promise<void> {
await this.pool.query(
"INSERT INTO policies (id, tenant_id, rule_json, effect, priority) VALUES ($1, $2, $3, $4, $5)",
[
policy.id,
policy.tenantId,
JSON.stringify(policy.rule_json ?? {}),
policy.effect,
policy.priority ?? 0,
]
);
}
async updatePolicy(id: string, policy: Omit<Policy, "id" | "tenantId">): Promise<boolean> {
const result = await this.pool.query(
"UPDATE policies SET rule_json = $1, effect = $2, priority = $3 WHERE id = $4",
[JSON.stringify(policy.rule_json ?? {}), policy.effect, policy.priority ?? 0, id]
);
return (result.rowCount ?? 0) > 0;
}
async deletePolicy(id: string): Promise<boolean> {
const result = await this.pool.query("DELETE FROM policies WHERE id = $1", [id]);
return (result.rowCount ?? 0) > 0;
}
}

View File

@@ -0,0 +1,79 @@
import type { Pool } from "pg";
import type { RoutingArtifact } from "@as4-411/core";
import type { RoutingArtifactStore } from "../routing-artifact-port.js";
function rowToArtifact(r: Record<string, unknown>): RoutingArtifact {
return {
id: r.id as string,
tenantId: r.tenant_id as string | undefined,
artifactType: r.artifact_type as RoutingArtifact["artifactType"],
payload: r.artifact_payload as RoutingArtifact["payload"],
effectiveFrom: (r.effective_from as Date)?.toISOString?.() ?? "",
effectiveTo: (r.effective_to as Date)?.toISOString?.(),
};
}
export class PostgresRoutingArtifactStore implements RoutingArtifactStore {
constructor(private readonly pool: Pool) {}
async get(
artifactType: RoutingArtifact["artifactType"],
options?: { tenantId?: string; atTime?: Date }
): Promise<RoutingArtifact | null> {
const at = options?.atTime ?? new Date();
let sql =
"SELECT * FROM routing_artifacts WHERE artifact_type = $1 AND effective_from <= $2 AND (effective_to IS NULL OR effective_to >= $2)";
const values: unknown[] = [artifactType, at];
if (options?.tenantId) {
values.push(options.tenantId);
sql += " AND tenant_id = $3";
}
sql += " ORDER BY effective_from DESC LIMIT 1";
const result = await this.pool.query(sql, values);
return result.rows[0] ? rowToArtifact(result.rows[0]) : null;
}
async put(artifact: RoutingArtifact): Promise<void> {
await this.pool.query(
`INSERT INTO routing_artifacts (id, tenant_id, artifact_type, artifact_payload, effective_from, effective_to, signature, fingerprint)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (id) DO UPDATE SET
tenant_id = EXCLUDED.tenant_id,
artifact_type = EXCLUDED.artifact_type,
artifact_payload = EXCLUDED.artifact_payload,
effective_from = EXCLUDED.effective_from,
effective_to = EXCLUDED.effective_to,
signature = EXCLUDED.signature,
fingerprint = EXCLUDED.fingerprint`,
[
artifact.id,
artifact.tenantId ?? null,
artifact.artifactType,
JSON.stringify(artifact.payload),
new Date(artifact.effectiveFrom),
artifact.effectiveTo ? new Date(artifact.effectiveTo) : null,
artifact.payload.signature ?? null,
artifact.payload.fingerprint ?? null,
]
);
}
async list(
options?: { tenantId?: string; artifactType?: RoutingArtifact["artifactType"] }
): Promise<RoutingArtifact[]> {
let sql = "SELECT * FROM routing_artifacts WHERE 1=1";
const values: unknown[] = [];
let n = 1;
if (options?.tenantId) {
values.push(options.tenantId);
sql += ` AND tenant_id = $${n++}`;
}
if (options?.artifactType) {
values.push(options.artifactType);
sql += ` AND artifact_type = $${n++}`;
}
sql += " ORDER BY effective_from DESC";
const result = await this.pool.query(sql, values);
return result.rows.map(rowToArtifact);
}
}

View File

@@ -0,0 +1,37 @@
import type { RoutingArtifact } from "@as4-411/core";
import type { RoutingArtifactStore } from "./routing-artifact-port.js";
export class InMemoryRoutingArtifactStore implements RoutingArtifactStore {
private artifacts: RoutingArtifact[] = [];
async get(
artifactType: RoutingArtifact["artifactType"],
options?: { tenantId?: string; atTime?: Date }
): Promise<RoutingArtifact | null> {
const at = options?.atTime ?? new Date();
const list = this.artifacts.filter(
(a) =>
a.artifactType === artifactType &&
(options?.tenantId == null || a.tenantId === options.tenantId) &&
new Date(a.effectiveFrom) <= at &&
(a.effectiveTo == null || new Date(a.effectiveTo) >= at)
);
list.sort((a, b) => new Date(b.effectiveFrom).getTime() - new Date(a.effectiveFrom).getTime());
return list[0] ?? null;
}
async put(artifact: RoutingArtifact): Promise<void> {
const idx = this.artifacts.findIndex((a) => a.id === artifact.id);
if (idx >= 0) this.artifacts[idx] = artifact;
else this.artifacts.push(artifact);
}
async list(
options?: { tenantId?: string; artifactType?: RoutingArtifact["artifactType"] }
): Promise<RoutingArtifact[]> {
let list = [...this.artifacts];
if (options?.tenantId) list = list.filter((a) => a.tenantId === options.tenantId);
if (options?.artifactType) list = list.filter((a) => a.artifactType === options.artifactType);
return list;
}
}

View File

@@ -0,0 +1,18 @@
import type { RoutingArtifact } from "@as4-411/core";
/**
* Store for routing artifacts (BIN table, GTT table, participant_map, fallback_rules).
* Optional: resolver can use this when resolving by BIN or GTT.
*/
export interface RoutingArtifactStore {
get(
artifactType: RoutingArtifact["artifactType"],
options?: { tenantId?: string; atTime?: Date }
): Promise<RoutingArtifact | null>;
put(artifact: RoutingArtifact): Promise<void>;
list(
options?: { tenantId?: string; artifactType?: RoutingArtifact["artifactType"] }
): Promise<RoutingArtifact[]>;
}

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