Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion IMPLEMENTATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
| Implementation | Description |
|--------------------------------------------------------|--------------------------------------|
| [fmsg-docker](https://github.com/markmnl/fmsg-docker) | Docker compose all-in-one fmsg stack |
| [fmsgid](https://github.com/markmnl/fmsgid) | Implementation of the [fmsg Id Standard](standards/fmsg-002-id.md) - HTTP API providing address and qutoas lookup. |
| [fmsgid](https://github.com/markmnl/fmsgid) | Example implementation of [FMSG-002](standards/fmsg-002-id.md). |
| [fmsg-webapi](https://github.com/markmnl/fmsg-webapi) | Implementation of the [fmsg Web API Standard](standards/fmsg-003-webapi.md) - HTTP API providing message sending and retrival via an integrated fmsgd host |
| [fmsg-cli](https://github.com/markmnl/fmsg-cli) | Command line interface to fmsg Web API |
| [fmsg-groot](https://github.com/markmnl/fmsg-groot) | Demo bot that replies “I am Groot” to every message |
Expand Down
2 changes: 1 addition & 1 deletion STANDARDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Each standard is prefixed with a unique identifer following the format: "FMSG-##
| Standard | Short Description |
|-----------|--------------------------------------------------------|
| [FMSG-001](standards/fmsg-001-transport-and-binding.md) | TCP+TLS Transport and Binding Standard |
| [FMSG-002](standards/fmsg-002-id.md) | HTTP API spec for recipient lookup and quota limits |
| [FMSG-002](standards/fmsg-002-id.md) | Address lookup, quota and usage accounting, and optional provisioning HTTP API |
| [FMSG-003](standards/fmsg-003-webapi.md) | HTTP API spec client to perform CRUD operations on messages for a specific address |
| [FMSG-004](standards/fmsg-004-a2a-binding.md) | A2A protocol binding over fmsg |
| [FMSG-005](standards/fmsg-005-reactions.md) | Emoji reactions to messages, carried as terminal fmsg messages |
Expand Down
248 changes: 212 additions & 36 deletions standards/fmsg-002-id.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,231 @@
# FMSG-002 Id Standard

FMSG-002 Id Standard describes an HTTP API for a fmsg host to lookup the identity of a participant to:
## Status

1. Verify the user indeed exists
2. Check user is accepting messages
3. Get user's daily and total message count and data limits, and actuals.
4. Get user metadata such as display name and any host specific tags
| Revision | Date | Summary |
|----------|------|---------|
| v0.1.0 | 2026-09-11 | Address lookup, quotas, usage reporting, and optional provisioning |

This standard defines an HTTP address and quota service for fmsg hosts and
[FMSG-003 client APIs](fmsg-003-webapi.md), independent of the configured
identity provider.

TODO move address from query string to body
[fmsgid](https://github.com/markmnl/fmsgid) is an existing example implementation.

GET `/addr/{address}`
The capitalized requirement words **MUST**, **MUST NOT**, **SHOULD**,
**SHOULD NOT**, **RECOMMENDED**, **MAY**, **REQUIRED**, and **OPTIONAL** have
the meanings defined by [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119)
and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).

## Transport and Access

The deployment configures the service base URL. All routes use `/fmsgid`.
Access MUST be restricted to trusted host components and provisioning callers,
with authority limited to their permitted domains and operations. The
service-credential mechanism is deployment-specific.

Connections over untrusted networks MUST use HTTPS with certificate
verification. Plain HTTP MAY be used on a protected local network or behind
a trusted TLS terminator. Lookup addresses can appear in URL access logs;
operators SHOULD restrict or redact those logs.

Address lookup and provisioning do not authenticate a user or establish
ownership. FMSG-003 callers MUST validate credentials and establish the
effective address before consulting this service.

## Conventions

- Addresses MUST follow the core [Address](../SPECIFICATION.md#address)
syntax and byte-length limit. Lookup, provisioning, and accounting MUST
compare identities using Unicode default case folding. Responses MAY
preserve registered spelling. Lookup addresses MUST be URL-encoded as
one path segment.
- JSON bodies MUST use `application/json`. Field names are case-sensitive;
clients MUST ignore unrecognized response properties.
- Sizes and counts are JSON integers. Usage MUST be non-negative; limits
MUST be `-1` for unlimited or non-negative. Zero is a real limit. Callers
MUST preserve integer precision and avoid overflow in quota calculations.
- Required fields MUST be present with the documented types. Services MUST
reject unsupported numeric values rather than truncate or wrap them.
Missing or malformed quota fields MUST NOT be interpreted as unlimited.

## Operations

| Method | Route | Purpose | Success |
|--------|-------|---------|---------|
| `GET` | `/fmsgid/{address}` | Lookup address details. | `200` with JSON. |
| `POST` | `/fmsgid/send` | Record a send event. | `200`. |
| `POST` | `/fmsgid/recv` | Record a receive event. | `200`. |
| `POST` | `/fmsgid` | Optionally provision an address. | `201` if created; `200` if already present. |

Provisioning is OPTIONAL; an unsupported provisioning route MAY return `404`
or `405`. Other operations are REQUIRED. Successful writes require no
response body. Administrative updates, deletion, and usage reversal are
outside this revision.

## Address Lookup

`GET /fmsgid/{address}` returns `200` for a known identity, including one with
`acceptingNew: false`, or `404` for an unknown identity. Lookup MUST NOT change
state. The response MUST contain all fields below except the optional `tags`.
An address without accounting history has zero usage.

| Field | Type | Meaning |
|-------|------|---------|
| `address` | string | Registered spelling of the identity. |
| `displayName` | string | Display label; empty when unset. |
| `acceptingNew` | boolean | Whether the address accepts new messages, subject to quotas. |
| `tags` | array of strings or null, optional | Deployment-defined metadata; omitted, null, and empty mean no tags. |

| Receive limit | Send limit | Maximum |
|---------------|------------|---------|
| `limitRecvSizeTotal` | `limitSendSizeTotal` | Cumulative accounted bytes. |
| `limitRecvSizePerMsg` | `limitSendSizePerMsg` | Accounted bytes per message event. |
| `limitRecvSizePer1d` | `limitSendSizePer1d` | Accounted bytes in the daily window. |
| `limitRecvCountPer1d` | `limitSendCountPer1d` | Event count in the daily window. |

| Receive usage | Send usage | Value |
|---------------|------------|-------|
| `recvSizeTotal` | `sendSizeTotal` | Recorded bytes over the retained accounting history. |
| `recvSizePer1d` | `sendSizePer1d` | Recorded bytes in the daily window. |
| `recvCountPer1d` | `sendCountPer1d` | Recorded event count in the daily window. |

```json
{
"address": "@alice@example.com",
"displayName": "Alice",
"acceptingNew": true,
"limitRecvSizeTotal": -1,
"limitRecvSizePerMsg": 10240,
"limitRecvSizePer1d": 102400,
"limitRecvCountPer1d": 1000,
"limitSendSizeTotal": -1,
"limitSendSizePerMsg": 10240,
"limitSendSizePer1d": 102400,
"limitSendCountPer1d": 1000,
"recvSizeTotal": 300,
"recvSizePer1d": 300,
"recvCountPer1d": 1,
"sendSizeTotal": 300,
"sendSizePer1d": 200,
"sendCountPer1d": 1,
"tags": []
}
```

## Quota Semantics

At service time `T`, `Per1d` covers the rolling 86,400-second window
`T - 86400 < ts <= T`. All daily counters in a response MUST use that same
window. Totals accumulate recorded usage; deleting a message does not reverse
its report. Services MUST document retention, resets, and adjustments that
can change totals. Live storage accounting requires a documented
reconciliation policy.

For a proposed event of size `s`, the host MUST check every applicable limit
in the event's direction. A limit `L` permits a candidate `x` when
`L == -1` or `x <= L`:

| Limit | Candidate value |
|-------|-----------------|
| Per-message size | `s` |
| Total size | Total usage + `s` |
| Daily size | Daily usage + `s` |
| Daily count | Daily count + `1` |

A zero-byte event still increments the count. Lookup does not reserve quota;
hosts requiring strict limits MUST coordinate concurrent admission and
reporting. Cached lookups MUST have bounded lifetimes, with their staleness
accounted for in enforcement.

Hosts MUST follow the core specification's acceptance rules and order of
checks. Address-service results inform these per-recipient outcomes:

| Result | fmsg response code |
|--------|--------------------|
| Unknown address | `100` (user unknown), or `105` (user undisclosed). |
| Receive quota exceeded | `101` (user full), or `105`. |
| `acceptingNew: false` | `102` (user not accepting), or `105`. |
| Known, enabled, and within quota | Eligible for `200` (accept), subject to all other protocol checks. |

## Usage Reporting

`POST /fmsgid/send` and `POST /fmsgid/recv` accept the same JSON body; the route
selects the direction:

```json
{
"name": "jsmith",
"address": "@jsmith@example.com",
"displayName": "John Smith",
"acceptingNew": true,

"limitRecvSizeTotal": -1,
"limitRecvSizePerMsg": 1024,
"limitRecvSizePer1d": 102400,
"limitRecvCountPer1d": 500,

"limitSendSizeTotal": -1,
"limitSendSizePerMsg": -1,
"limitSendSizePer1d": -1,
"limitSendCountPer1d": -1,

"recvSizeTotal": 92835135,
"recvSizePerPer1d": 0,
"recvCountPer1d": 0,

"sendSizeTotal": 23459872,
"sendSizePer1d": 23423,
"sendCountPer1d": 4,

"tags": []
"address": "@alice@example.com",
"ts": 1789084800.125,
"size": 456
}
```

POST /addr/recv
POST /addr/sent
| Field | Type | Requirement |
|-------|------|-------------|
| `address` | string | REQUIRED; valid, registered fmsg identity. |
| `ts` | number | REQUIRED; finite Unix epoch seconds, with fractional seconds allowed. |
| `size` | integer | REQUIRED; non-negative accounted bytes. |

A report MUST be recorded before returning `200`. It adds bytes to that
identity's total for the selected direction, and bytes plus one event to
daily usage while `ts` is in the window. Reporting MUST NOT create an address,
change policy, or affect another identity or direction. Actual usage MUST NOT
be discarded merely because a limit was exceeded or the address was disabled.

Hosts and services MUST use a consistent byte-accounting policy for limits,
admission checks, and reports. The RECOMMENDED size is message data plus
attachments after decompression, excluding headers and transport overhead.
Alternative policies MUST be documented.

Receive events account for successful acceptance per recipient, excluding
failed or duplicate delivery attempts. A send event SHOULD account for one
logical outgoing message, rather than each remote domain or retry. Hosts MUST
define charging for add-to batches and local delivery, and avoid double
reporting between components.

Callers SHOULD timestamp events using a trusted host clock and preserve that
timestamp on delayed reports. Services SHOULD reject unreasonable future
timestamps. Failed reports SHOULD be retained for reconciliation.

Accounting POSTs have no standardized idempotency guarantee or event ID.
Callers MUST NOT blindly repeat a request after an ambiguous failure: it may
already have been recorded. Any deduplication or retry mechanism MUST be
separately documented; timestamps alone do not establish event identity.

## Optional Provisioning

`POST /fmsgid` creates an address with service-configured defaults:

```json
{
"address": "@alice@example.com",
"timestamp": 1776482588.5446732,
"size": 456
"address": "@alice@example.com",
"display_name": "Alice"
}
```

`address` is REQUIRED; `display_name` is OPTIONAL and defaults to empty.
Provisioning MUST be idempotent under case-folded identity: return `201` when
created, or `200` when already present. An existing identity's spelling,
display name, enabled status, quotas, and usage MUST remain unchanged.
Callers SHOULD look up the resulting defaults. This route MUST NOT accept
extra quota or status properties as authority to modify policy.

## Errors

Error bodies MAY be empty. Clients MUST use the HTTP status rather than
assume a particular error-body format.

| Status | Meaning |
|--------|---------|
| `400` | Invalid address, JSON, required field, or value. |
| `401` / `403` | Caller authentication or authorization failed. |
| `404` | Unknown address on lookup or reporting. |
| `409` | Conflicting report under a documented deduplication policy. |
| `413` / `429` | Request-size or API-rate limit exceeded. |
| `500` / `503` | Service failure or unavailability. |

Callers SHOULD use bounded timeouts. A timeout, malformed success payload,
or service error MUST NOT become an "unknown address" or successful
acceptance check. Services MUST NOT expose credentials or internal database
and connection details in error responses.