@eqxjs/transporter-diameter
v1.0.0
Published
TypeScript library for the Diameter protocol (RFC 6733) – encode/decode, socket management, and custom dictionaries
Downloads
92
Readme
@eqxjs/transporter-diameter
A TypeScript library for the Diameter protocol (RFC 6733), providing:
- Binary encode / decode of Diameter messages and AVPs
- TCP socket management with automatic message framing
- Capability exchange (CER/CEA), device watchdog (DWR/DWA), and graceful disconnect (DPR/DPA)
- Extensible dictionary system (base RFC 6733 + 3GPP out of the box)
- Full JSDoc on every public API
Table of contents
- Installation
- Quick start
- Examples
- Architecture overview
- Implementation flows
- API reference
- Pre-compiled dictionaries
- Custom dictionary
- Redis pub/sub routing adapter
- Running tests
Installation
npm install @eqxjs/transporter-diameterRequires Node.js ≥ 18.
Quick start
Encode / decode
import { DiameterCodec, DiameterDictionary, DiameterMessage, DiameterAVP } from '@eqxjs/transporter-diameter';
const dict = new DiameterDictionary().loadBase();
const codec = new DiameterCodec(dict);
// Build a Capabilities-Exchange-Request
const cer = new DiameterMessage({
commandCode: 257,
applicationId: 0,
flags: { request: true, proxiable: true },
});
cer.addAVPByName('Origin-Host', 'client.example.com', dict);
cer.addAVPByName('Origin-Realm', 'example.com', dict);
cer.addAVPByName('Vendor-Id', 0, dict);
cer.addAVPByName('Product-Name', 'my-app', dict);
const wireBuffer = codec.encodeMessage(cer);
console.log('Encoded', wireBuffer.length, 'bytes');
const decoded = codec.decodeMessage(wireBuffer);
console.log(decoded.getCommandName(dict)); // Capabilities-Exchange-RequestClient
import { DiameterClient, DiameterDictionary, DiameterMessage, ResultCode } from '@eqxjs/transporter-diameter';
const dict = new DiameterDictionary().loadBase();
const client = new DiameterClient(
{ originHost: 'client.example.com', originRealm: 'example.com', applicationIds: [3] },
dict,
);
const peer = await client.connect('diameter.example.com', 3868);
const acr = new DiameterMessage({ commandCode: 271, applicationId: 3,
flags: { request: true, proxiable: true } });
acr.addAVPByName('Session-Id', 'client.example.com;1;1', dict);
acr.addAVPByName('Origin-Host', 'client.example.com', dict);
acr.addAVPByName('Origin-Realm', 'example.com', dict);
acr.addAVPByName('Destination-Realm', 'example.com', dict);
acr.addAVPByName('Accounting-Record-Type', 1, dict);
acr.addAVPByName('Accounting-Record-Number', 1, dict);
acr.addAVPByName('Acct-Application-Id', 3, dict);
const aca = await client.request(peer, acr);
console.log('Result-Code:', aca.getAVPValue('Result-Code', dict)); // 2001
await client.close();Server
import { DiameterServer, DiameterDictionary, ResultCode } from '@eqxjs/transporter-diameter';
const dict = new DiameterDictionary().loadBase();
const server = new DiameterServer(
{ originHost: 'server.example.com', originRealm: 'example.com', applicationIds: [3] },
dict,
);
// Register a handler for Accounting-Request (ACR)
server.route(271, async (request) => {
const answer = request.toAnswer(ResultCode.DIAMETER_SUCCESS);
answer.addAVPByName('Origin-Host', 'server.example.com', dict);
answer.addAVPByName('Origin-Realm', 'example.com', dict);
return answer;
});
await server.listen(3868);
console.log('Listening on port 3868');Examples
For practical working examples, see the example/ folder:
- server.ts — Basic Diameter server handling Accounting-Request messages
- client.ts — Basic Diameter client sending requests and receiving answers
- client-server.ts — Integrated example with both client and server
- server-initiated-request.ts — Server-initiated request to a connected client
- custom-dictionary.ts — Custom dictionary extension example
- redis-routing.ts — ioredis routing adapter setup for both client and server
- nestjs/ — NestJS client and server-mode integration example
To run the examples:
cd example
npm install # install dependencies
npm run integrated # run the integrated client-server example
npm run server # or run server in one terminal
npm run client # and client in another
npm run server-request # server-initiated request demo
npm run custom-dict # or explore custom dictionaries
npm run redis-routing # ioredis adapter setup demoRedis pub/sub routing adapter
For microservice deployments, you can share Diameter routing events via Redis Pub/Sub (powered by ioredis).
When routingAdapter is configured on server instances, unmatched inbound requests are forwarded internally over Redis to other instances with matching handlers. You only configure adapters and routes; publish/subscribe plumbing is managed by the library.
Recommended flow:
- Create a single
RedisPubSubAdapterinstance (shared by your app components). - Call
connect()once during startup. - Pass the adapter in
DiameterServeroptions asroutingAdapter. - Register routes normally on worker/server instances.
- Start servers with
listen().
Routing flow:
sequenceDiagram
participant App
participant Redis as RedisPubSubAdapter
participant Edge as Edge DiameterServer
participant Worker as Worker DiameterServer
App->>Redis: connect()
App->>Edge: new DiameterServer({ routingAdapter: Redis })
App->>Worker: new DiameterServer({ routingAdapter: Redis })
App->>Worker: route(271, handler)
App->>Edge: listen(3868)
App->>Worker: listen(3869)
Note over Edge,Worker: Incoming request has no local handler on Edge
Edge->>Redis: publish(routing:request, encoded request)
Redis-->>Worker: subscribed request event
Worker->>Worker: execute route handler
Worker->>Redis: publish(routing:response, encoded answer)
Redis-->>Edge: subscribed response event
Edge-->>App: send Diameter answer to requester
App->>Edge: close()
App->>Worker: close()
App->>Redis: close()import {
DiameterServer,
DiameterDictionary,
RedisPubSubAdapter,
ResultCode,
} from '@eqxjs/transporter-diameter';
const dict = new DiameterDictionary().loadBase();
const bus = new RedisPubSubAdapter({
url: 'redis://127.0.0.1:6379',
channelPrefix: 'diameter:prod:',
});
await bus.connect();
const edgeServer = new DiameterServer(
{
originHost: 'edge-a.example.com',
originRealm: 'example.com',
applicationIds: [3],
routingAdapter: bus,
},
dict,
);
const workerServer = new DiameterServer(
{
originHost: 'worker-b.example.com',
originRealm: 'example.com',
applicationIds: [3],
routingAdapter: bus,
},
dict,
);
workerServer.route(271, async (request) => {
const answer = request.toAnswer(ResultCode.DIAMETER_SUCCESS);
answer.addAVPByName('Origin-Host', 'worker-b.example.com', dict);
answer.addAVPByName('Origin-Realm', 'example.com', dict);
return answer;
});
await edgeServer.listen(3868);
await workerServer.listen(3869);
// Requests arriving at edgeServer are internally routed to workerServer.
// On shutdown:
await edgeServer.close();
await workerServer.close();
await bus.close();Redis adapter API:
connect()opens publisher/subscriber clientspublish(channel, message)publishes a string payloadsubscribe(channel, handler)listens and returns an async unsubscribe functionunsubscribe(channel)removes a single subscriptionclose()unsubscribes all channels and closes Redis clients
Architecture overview
graph TB
subgraph Public API
Client[DiameterClient]
Server[DiameterServer]
end
subgraph Transport
Peer[DiameterPeer<br/>State machine + Watchdog]
Conn[DiameterConnection<br/>TCP framing]
end
subgraph Core
Codec[DiameterCodec<br/>Encode / Decode]
Message[DiameterMessage]
AVP[DiameterAVP]
end
subgraph Dictionary
Dict[DiameterDictionary]
Base[base-dictionary<br/>RFC 6733]
GTHPP[3gpp<br/>Cx · Sh · Gx · Rx]
Tmpl[template<br/>Pre-compiled]
end
Client --> Peer
Server --> Conn
Peer --> Conn
Conn --> Codec
Codec --> Message
Codec --> AVP
Client --> Codec
Server --> Codec
Peer --> Dict
Conn --> Dict
Dict --> Base
Dict --> GTHPP
Dict --> TmplImplementation flows
Encoding a message
sequenceDiagram
participant App
participant DiameterMessage
participant DiameterAVP
participant DiameterCodec
participant Buffer
App->>DiameterMessage: new DiameterMessage({ commandCode, flags, … })
App->>DiameterMessage: addAVPByName("Session-Id", "…", dict)
DiameterMessage->>DiameterAVP: DiameterAVP.fromDictionary(263, value, dict)
DiameterAVP-->>DiameterMessage: avp (value encoded to Buffer)
App->>DiameterCodec: encodeMessage(message)
DiameterCodec->>Buffer: write 20-byte header
loop for each AVP
DiameterCodec->>DiameterCodec: encodeAVP(avp)
DiameterCodec->>Buffer: write AVP header + padded data
end
DiameterCodec-->>App: wire BufferDecoding a raw buffer (stream)
sequenceDiagram
participant TCPSocket
participant DiameterConnection
participant DiameterCodec
participant DiameterMessage
participant App
TCPSocket->>DiameterConnection: data event (chunk)
DiameterConnection->>DiameterConnection: append chunk to rxBuf
DiameterConnection->>DiameterCodec: extractMessages(rxBuf)
loop while buffer has complete message
DiameterCodec->>DiameterCodec: peek declared length (bytes 1-3)
DiameterCodec->>DiameterCodec: decodeMessage(msgBuf)
DiameterCodec->>DiameterMessage: parse header + AVPs
DiameterCodec-->>DiameterConnection: DiameterMessage
end
DiameterCodec-->>DiameterConnection: remainder (partial bytes)
DiameterConnection-->>App: emit 'message' eventClient connection lifecycle
sequenceDiagram
participant App
participant DiameterClient
participant DiameterPeer
participant DiameterConnection
participant RemoteServer
App->>DiameterClient: connect("host", 3868)
DiameterClient->>DiameterPeer: new DiameterPeer(options, dict)
DiameterPeer->>DiameterConnection: new DiameterConnection(socket)
DiameterConnection->>RemoteServer: TCP connect
RemoteServer-->>DiameterConnection: TCP ACK
DiameterConnection-->>DiameterPeer: 'connect' event
DiameterPeer->>RemoteServer: send CER (cmd=257, R=1)
RemoteServer-->>DiameterPeer: receive CEA (cmd=257, R=0)
DiameterPeer->>DiameterPeer: state → Open
DiameterPeer->>DiameterPeer: start watchdog timer
DiameterClient-->>App: resolved DiameterPeer
App->>DiameterClient: request(peer, message)
DiameterClient->>DiameterPeer: sendRequest(message)
DiameterPeer->>RemoteServer: send request
RemoteServer-->>DiameterPeer: receive answer (matched by Hop-by-Hop ID)
DiameterPeer-->>App: resolved answer message
App->>DiameterClient: close()
DiameterClient->>DiameterPeer: disconnect()
DiameterPeer->>RemoteServer: send DPR (cmd=282)
RemoteServer-->>DiameterPeer: receive DPA
DiameterPeer->>DiameterConnection: close()
DiameterPeer->>DiameterPeer: state → ClosedServer request lifecycle
sequenceDiagram
participant RemoteClient
participant TCPServer
participant DiameterServer
participant DiameterConnection
participant Handler
RemoteClient->>TCPServer: TCP connect
TCPServer->>DiameterServer: 'connection' event
DiameterServer->>DiameterConnection: wrap socket
RemoteClient->>DiameterConnection: send CER (cmd=257)
DiameterConnection-->>DiameterServer: 'message' CER
DiameterServer->>RemoteClient: send CEA (Result-Code=2001)
RemoteClient->>DiameterConnection: send Request (e.g. ACR, cmd=271)
DiameterConnection-->>DiameterServer: 'message' Request
DiameterServer->>DiameterServer: _findHandler(271)
DiameterServer->>Handler: handler(request)
Handler-->>DiameterServer: answer message
DiameterServer->>RemoteClient: send Answer (ACA)
RemoteClient->>DiameterConnection: send DPR (cmd=282)
DiameterServer->>RemoteClient: send DPA (Result-Code=2001)
DiameterServer->>DiameterConnection: close()Peer state machine
stateDiagram-v2
[*] --> Closed
Closed --> Wait_Conn_Ack : connect() called<br/>TCP connect initiated
Wait_Conn_Ack --> Wait_I_CEA : socket 'connect'<br/>Send CER
Wait_Conn_Ack --> Closed : socket error<br/>or timeout
Wait_I_CEA --> Open : CEA received<br/>Result-Code = 2001<br/>Start watchdog
Wait_I_CEA --> Closed : CEA error / timeout
Open --> Open : DWR sent & DWA received<br/>(watchdog cycle)
Open --> Open : application request ↔ answer
Open --> Closing : disconnect() called<br/>Send DPR
Open --> Closed : socket closed by peer<br/>or watchdog timeout
Closing --> Closed : DPA received<br/>or socket closed
Closed --> [*]API reference
DiameterDictionary
| Method | Description |
|--------|-------------|
| loadBase() | Load the RFC 6733 base definitions. Chainable. |
| load(data) | Merge additional dictionary data. Chainable. |
| loadFromDict(xml) | Parse and load a legacy .dict XML dictionary. Chainable. |
| addAVP(def) | Register a single AVP definition. |
| addCommand(def) | Register a command definition. |
| addApplication(def) | Register an application definition. |
| getAVPByCode(code, vendorId?) | Look up an AVP by numeric code. |
| getAVPByName(name) | Look up an AVP by name (case-insensitive). |
| getAVPType(code, vendorId?) | Get the data type for an AVP code. |
| getCommandByCode(code, applicationId?) | Look up a command by numeric code. |
| getCommandByName(name) | Look up a command by name. |
| getCommandName(code, applicationId?, isRequest?) | Resolve a human-readable command name. |
| getApplicationById(id) | Look up an application definition by ID. |
| resolveEnum(avpName, symbol) | Resolve an enum symbol to its integer value. |
| getAllAVPs() | Return all registered AVP definitions. |
| getAllCommands() | Return all registered command definitions. |
DiameterCodec
| Method | Description |
|--------|-------------|
| encodeMessage(msg) | Encode a DiameterMessage to a Buffer. |
| decodeMessage(buf) | Decode a complete message from a Buffer. |
| encodeAVP(avp) | Encode a single DiameterAVP (with padding). |
| decodeAVP(buf, offset?) | Decode one AVP from a buffer at the given offset. |
| extractMessages(buf) | Extract all complete messages from a TCP stream chunk. |
DiameterMessage
| Method / Property | Description |
|-------------------|-------------|
| commandCode | 24-bit command code. |
| applicationId | Application ID. |
| flags | MessageFlags — R, P, E, T bits. |
| version | Protocol version (usually 1). |
| hopByHopId | Hop-by-Hop identifier. |
| endToEndId | End-to-End identifier. |
| avps | Ordered AVP list. |
| DiameterMessage.fromData(data) | Rebuild a message from decoded wire data. |
| addAVP(avp) | Append a pre-built AVP. |
| addAVPByName(name, value, dict) | Build and append an AVP via the dictionary. |
| getAVP(code, vendorId?) | Get first matching AVP. |
| getAVPs(code, vendorId?) | Get all matching AVPs. |
| getAVPByName(name, dict) | Get first AVP by dictionary name. |
| getAVPValue(name, dict) | Get decoded value of first matching AVP. |
| isRequest() | Returns true when the R flag is set. |
| toAnswer(resultCode?) | Create a pre-populated answer message. |
| getCommandName(dict?) | Resolve human-readable command name. |
| toString() | Return a short debug-friendly summary string. |
| clone() | Shallow clone. |
| DiameterMessage.createDWR(host, realm) | Build a Device-Watchdog-Request. |
| DiameterMessage.createDWA(req, host, realm) | Build a Device-Watchdog-Answer. |
| DiameterMessage.createDPR(host, realm, cause?) | Build a Disconnect-Peer-Request. |
DiameterAVP
| Method / Property | Description |
|-------------------|-------------|
| code | AVP code. |
| flags | AVPFlags — V, M, P bits. |
| vendorId | Vendor ID (when V flag set). |
| value | Raw Buffer or DiameterAVP[] for Grouped. |
| name | Human-readable name (from dictionary). |
| type | Data type (AVPDataType). |
| getValue() | Decode and return a typed JavaScript value. |
| setValue(value, type?) | Encode a typed value to wire format. |
| annotate(dict) | Populate name and type from dictionary. |
| toString() | Return a short debug-friendly summary string. |
| DiameterAVP.fromValue(code, value, type) | Factory — encode from typed value. |
| DiameterAVP.fromDictionary(code, value, dict, vendorId?) | Factory — resolve type from dictionary. |
DiameterConnection
| Method / Property | Description |
|-------------------|-------------|
| state | Current connection state (idle/open/closing/closed). |
| socket | Underlying TCP/TLS socket. |
| isSecure | true when the socket is TLS-enabled. |
| remoteAddress | Peer address as host:port. |
| send(msg) | Encode and send a Diameter message. |
| close() | Gracefully close the connection. |
| destroy(error?) | Force-close the socket immediately. |
Events: message, connect, close, error
DiameterPeer
| Method / Property | Description |
|-------------------|-------------|
| connect() | TCP connect + CER/CEA handshake. Returns a Promise. |
| sendRequest(req, timeout?) | Send request, wait for answer by Hop-by-Hop ID. |
| send(msg) | Send a message without waiting for an answer. |
| disconnect(cause?) | Send DPR, wait for DPA, close socket. |
| state | Current PeerState. |
| host | Remote host name/IP for the peer. |
| port | Remote TCP port for the peer. |
| remoteOriginHost | Peer identity learned from CER/CEA Origin-Host. |
| remoteOriginRealm | Peer identity learned from CER/CEA Origin-Realm. |
Events: connect, close, error, request, answer, stateChange
DiameterClient
| Method | Description |
|--------|-------------|
| connect(host, port?) | Connect to a peer (reuses existing Open connections). |
| request(peer, req, timeout?) | Send request via a specific peer. |
| request(req, timeout?) | Auto-route request by Destination-Host → Destination-Realm. |
| setRoutingAdapter(adapter?) | Set or replace the client routing adapter. Returns the client instance. |
| getRoutingAdapter() | Return the currently configured client routing adapter. |
| close() | Disconnect all peers. |
| getPeers() | Return all active peers. |
| getPeer(host, port?) | Return a specific peer. |
ClientOptions includes routingAdapter?: DiameterRoutingAdapter.
DiameterServer
| Method | Description |
|--------|-------------|
| listen(port?, host?) | Start TCP server. |
| route(codeOrName, handler) | Register a request handler. |
| request(conn, req, timeout?) | Send a server-initiated request to a specific connection. |
| request(req, timeout?) | Auto-route server-initiated request by Destination-Host → Destination-Realm. |
| setRoutingAdapter(adapter?) | Set or replace the server routing adapter. Returns the server instance. |
| getRoutingAdapter() | Return the currently configured server routing adapter. |
| close() | Stop server and close all connections. |
| address() | Return bound AddressInfo. |
Events: listening, connection, disconnect, request, answer, error
ServerOptions includes routingAdapter?: DiameterRoutingAdapter.
DiameterRoutingAdapter
| Method | Description |
|--------|-------------|
| connect() | Initialize adapter resources and connections. |
| publish(channel, message) | Publish a message payload to a logical channel. |
| subscribe(channel, handler) | Subscribe to a logical channel and return an async unsubscribe function. |
| unsubscribe(channel) | Remove an existing logical channel subscription. |
| close() | Close adapter resources and active subscriptions. |
RedisPubSubAdapter
| API | Description |
|-----|-------------|
| new RedisPubSubAdapter(options?) | Create an ioredis-based implementation of DiameterRoutingAdapter. |
| options.url | Redis connection URL, for example redis://127.0.0.1:6379. |
| options.channelPrefix | Optional prefix prepended to every channel. |
| options.redisOptions | Optional raw ioredis RedisOptions. |
Message routing
DiameterClient and DiameterServer can automatically select the correct peer / client connection without the caller needing to hold a reference to it.
Routing priority:
- Match a connected peer/client whose CER/CEA
Origin-Hostequals the request'sDestination-Host. - If no host match, match by
Origin-RealmvsDestination-Realm.
Peer identity is learned automatically from the capability exchange (CER/CEA).
Client auto-routing
import { DiameterClient, DiameterDictionary, DiameterMessage } from '@eqxjs/transporter-diameter';
const dict = new DiameterDictionary().loadBase();
const client = new DiameterClient(
{ originHost: 'client.example.com', originRealm: 'example.com', applicationIds: [3] },
dict,
);
// Connect to multiple peers
await client.connect('peer-a.example.com', 3868);
await client.connect('peer-b.example.com', 3868);
// Build request — no need to specify which peer to use
const acr = new DiameterMessage({ commandCode: 271, applicationId: 3,
flags: { request: true, proxiable: true } });
acr.addAVPByName('Destination-Host', 'peer-b.example.com', dict); // exact host match
acr.addAVPByName('Destination-Realm', 'example.com', dict); // realm fallback
// request() selects peer-b automatically
const aca = await client.request(acr);
console.log('Result-Code:', aca.getAVPValue('Result-Code', dict));Server auto-routing (server-initiated requests)
import { DiameterServer, DiameterDictionary, DiameterMessage } from '@eqxjs/transporter-diameter';
const dict = new DiameterDictionary().loadBase();
const server = new DiameterServer(
{ originHost: 'server.example.com', originRealm: 'example.com', applicationIds: [3] },
dict,
);
server.route(271, async (request) => {
const answer = request.toAnswer();
answer.addAVPByName('Origin-Host', 'server.example.com', dict);
answer.addAVPByName('Origin-Realm', 'example.com', dict);
return answer;
});
await server.listen(3868);
// Later — push a server-initiated request to a connected client:
const push = new DiameterMessage({ commandCode: 258, applicationId: 3,
flags: { request: true, proxiable: true } });
push.addAVPByName('Destination-Host', 'client-a.example.com', dict);
push.addAVPByName('Destination-Realm', 'example.com', dict);
// server.request() selects the matching connected client automatically
const answer = await server.request(push);
console.log('Result-Code:', answer.getAVPValue('Result-Code', dict));Constants and Helpers
| Export | Description |
|--------|-------------|
| MSG_HEADER_SIZE | Diameter message header size (20 bytes). |
| nextHopByHopId() | Generate a new Hop-by-Hop identifier. |
| nextEndToEndId() | Generate a new End-to-End identifier. |
| parseDictXml(xml) | Parse a .dict XML dictionary into DictionaryData. |
Pre-compiled dictionaries
The library provides pre-compiled dictionary data for common Diameter implementations, avoiding runtime XML parsing overhead:
Template dictionary
The template dictionary includes vendor-specific extensions (Ericsson SCAP, AIS TORO, Huawei, Convergys) and applications (DCCA, 3GPP Re/Rf, etc.):
import { DiameterDictionary, templateDictionary } from '@eqxjs/transporter-diameter';
const dict = new DiameterDictionary()
.loadBase() // RFC 6733 base
.load(templateDictionary); // + ~280 extension AVPsThis is useful when you need support for multiple vendors and applications without parsing a .dict file at runtime.
Parsing .dict files at runtime
For custom dictionary formats or ad-hoc extensions, use parseDictXml() to load XML dictionary files:
import * as fs from 'fs';
import { DiameterDictionary, parseDictXml } from '@eqxjs/transporter-diameter';
const dict = new DiameterDictionary()
.loadBase()
.loadFromDict(fs.readFileSync('my-dictionary.dict', 'utf8'));Custom dictionary
import {
DiameterDictionary,
DictionaryData,
VENDOR_3GPP,
} from '@eqxjs/transporter-diameter';
const myExtension: DictionaryData = {
applications: [
{ id: 16777251, name: 'My Custom App' },
],
commands: [
{
code: 999,
name: 'My-Custom-Command',
applicationId: 16777251,
requestAVPs: ['Session-Id', 'Origin-Host', 'Origin-Realm', 'Custom-Data'],
answerAVPs: ['Session-Id', 'Result-Code', 'Origin-Host', 'Origin-Realm'],
},
],
avps: [
{
code: 9999,
name: 'Custom-Data',
type: 'UTF8String',
vendorId: 99999, // your vendor ID
mandatory: true,
},
],
};
const dict = new DiameterDictionary()
.loadBase() // RFC 6733
.load(myExtension); // your extensionRunning tests
npm test # run all tests
npm run test:cov # run with coverage report
npm run build # compile TypeScript to dist/Protocol reference
| RFC / Spec | Description | |------------|-------------| | RFC 6733 | Diameter base protocol | | RFC 7155 | NASREQ application | | 3GPP TS 29.229 | Cx interface (IMS) | | 3GPP TS 29.329 | Sh interface (IMS) | | 3GPP TS 29.214 | Rx interface (Policy) |
