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