From e456b415e50f47eee610ba23930ea9e8bb8c67c0 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Fri, 11 Sep 2026 10:28:35 +0800 Subject: [PATCH 1/2] Expand FMSG-002 address and quota service standard --- IMPLEMENTATIONS.md | 2 +- STANDARDS.md | 2 +- standards/fmsg-002-id.md | 456 +++++++++++++++++++++++++++++++++++---- 3 files changed, 422 insertions(+), 38 deletions(-) diff --git a/IMPLEMENTATIONS.md b/IMPLEMENTATIONS.md index 02a99db..48b2e62 100644 --- a/IMPLEMENTATIONS.md +++ b/IMPLEMENTATIONS.md @@ -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) | Address and quota HTTP service for [FMSG-002](standards/fmsg-002-id.md), with usage reporting and address provisioning; see the standard’s compatibility notes. | | [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 | diff --git a/STANDARDS.md b/STANDARDS.md index 3fb6dbd..5fff19c 100644 --- a/STANDARDS.md +++ b/STANDARDS.md @@ -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 | diff --git a/standards/fmsg-002-id.md b/standards/fmsg-002-id.md index b178144..1407a65 100644 --- a/standards/fmsg-002-id.md +++ b/standards/fmsg-002-id.md @@ -1,55 +1,439 @@ # 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 | Expanded draft: address lookup, quota and usage semantics, accounting, and optional provisioning; replaces the unversioned `/addr` sketch | +This standard defines the HTTP address and quota service used by an fmsg host +and its client API. It is independent of the identity provider used by a +deployment. The [implementation compatibility notes](#implementation-compatibility) +distinguish this contract from limitations in existing implementations. -TODO move address from query string to body +## Requirements Language -GET `/addr/{address}` +The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, +**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **NOT RECOMMENDED**, **MAY**, and +**OPTIONAL** in this document are to be interpreted as described in BCP 14 +([RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and +[RFC 8174](https://www.rfc-editor.org/rfc/rfc8174)) when, and only when, they +appear in all capitals. + +## Scope and Roles + +An **address service** answers whether an address is known, whether it accepts +new messages, its configured limits, and its recorded usage. It accepts +accounting reports from trusted host components. It MAY also provide the +address-provisioning operation defined here. + +A **caller** is a host component authorized to use the service, such as a +host-to-host daemon, a [FMSG-003 Web API](fmsg-003-webapi.md), or a provisioning +process. A caller's authority MUST be restricted to the domains and operations +it is allowed to manage. + +An **identity provider** authenticates users through a deployment's chosen +login mechanism. An address service does not perform that login, issue user +JWTs, or prove that a person owns an address. In FMSG-003, JWT validation and +selection of the effective fmsg address precede the address-service check; +a successful lookup MUST NOT substitute for authenticating the API caller. + +This standard does not specify a database schema, a shared message store, +identity-provider synchronization, an administrative UI, or a public directory +of every address. CSV import and other provisioning integrations are +implementation choices. Hosts MAY use other mechanisms for address and quota +checks without implementing FMSG-002; the core fmsg protocol does not mandate +this HTTP service. + +## References + +- [fmsg Specification](../SPECIFICATION.md), especially [Address](../SPECIFICATION.md#address) and per-recipient acceptance rules. +- [FMSG-003 Web API Standard](fmsg-003-webapi.md), for authenticated client access and API-access grants. +- [RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format](https://www.rfc-editor.org/rfc/rfc8259). + +## Transport and Trust Boundary + +The service base URL is configured by the deployment. It MUST NOT be derived +from a user-supplied URL, an identity-provider brand, or the address being +looked up. FMSG-001's `fmsg.` discovery and TCP port 4930 apply to +host-to-host messaging, not to this service. + +Routes use the `/fmsgid` base path. For a configured base URL of +`https://directory.example.com`, the lookup route is +`https://directory.example.com/fmsgid/{address}`. + +Connections crossing an untrusted network MUST use HTTPS with server +certificate verification. Plain HTTP MAY be used on a protected local network +or behind a trusted TLS terminator. The service MUST restrict access to +trusted callers; network isolation, mutually authenticated TLS, a gateway, or +service credentials MAY provide that boundary. This revision does not specify +a common service-credential format or require forwarding an end user's JWT. + +Provisioning and accounting writes MUST NOT be exposed to unauthenticated, +untrusted callers. Lookup also exposes address existence, activity and policy; +deployments SHOULD restrict it rather than make it publicly enumerable. +Addresses in URL paths can appear in access logs even with HTTPS. Operators +SHOULD restrict or redact those logs. This revision retains GET lookup and +does not define a request-body alternative. + +## Common Conventions + +### Addresses + +Every `address` is a complete fmsg address such as `@alice@example.com`, not a +bare username or email address. Validation MUST follow the core +[Address](../SPECIFICATION.md#address) rules, including UTF-8 syntax and the +whole-address byte-length limit. + +Identity comparison MUST use Unicode default case folding, as defined by the +core specification, consistently for lookup, provisioning, and accounting. +ASCII lowercasing alone is insufficient. For example, `@Alice@EXAMPLE.COM` +and `@alice@example.com` refer to one identity. A service MAY preserve the +registered spelling in the response's `address`; clients MUST compare it by +identity rather than byte-for-byte spelling. + +A lookup caller MUST encode the address as one URL path segment. For example, +`GET /fmsgid/%40alice%40example.com` addresses the same identity as +`GET /fmsgid/@alice@example.com`. The decoded address is validated and folded; +URL escaping is not an identity-normalization operation. + +### JSON and Numbers + +JSON request and response bodies MUST use `application/json`. Field names are +case-sensitive. The lookup representation uses camelCase, while provisioning +uses `display_name` and accounting uses `ts`; these spellings are intentional. + +Size and count fields are JSON integers. Limits MUST be either `-1` (unlimited) +or non-negative integers; usage MUST be non-negative. `0` is a real limit, +not an alias for unlimited. Clients MUST preserve integer precision and MUST +NOT interpret missing or malformed quota fields as unlimited. Implementations +MUST document any supported numeric bounds and reject unsupported request +values rather than wrap or truncate them. + +Clients MUST ignore unrecognized response properties. Servers MAY accept +additional request properties, but clients MUST NOT rely on an extension's +behavior unless it is supported by their configured service. Required request +properties MUST be present with the documented type; an omitted value is not +the same as an explicit zero. + +### Errors and Availability + +An error MAY have an empty body. Clients MUST use the HTTP status, rather than +relying on an implementation-specific JSON error shape. Services MUST NOT +return database errors, credentials, or internal connection details to callers. + +| Status | Meaning | +|--------|---------| +| `400 Bad Request` | Invalid address, malformed JSON, missing required property, or invalid field value. | +| `401 Unauthorized` / `403 Forbidden` | The deployment's access-control layer rejected the caller. These do not describe the target address's message-acceptance status. | +| `404 Not Found` | On lookup or accounting, the valid address is not registered. | +| `409 Conflict` | An accounting implementation detected a conflicting or duplicate report; see [Retries](#retries). | +| `413 Content Too Large` | The request exceeds the service's supported request size. | +| `429 Too Many Requests` | The caller exceeded an API rate limit; this is distinct from an address's message quota. | +| `500 Internal Server Error` / `503 Service Unavailable` | The operation could not be completed. | + +A timeout, invalid success payload, access-control failure, or server error +MUST NOT be interpreted as "unknown address" or "accepting messages". Callers +SHOULD use bounded timeouts and explicit retry policies. A failed lookup must +remain a service failure rather than a fabricated recipient-policy result. + +## Operations + +| Method | Route | Purpose | Successful response | +|--------|-------|---------|---------------------| +| `GET` | `/fmsgid/{address}` | Lookup policy, metadata, and usage. | `200` with an address-detail object. | +| `POST` | `/fmsgid/send` | Record one send accounting event. | `200` with no required body. | +| `POST` | `/fmsgid/recv` | Record one receive accounting event. | `200` with no required body. | +| `POST` | `/fmsgid` | Optionally provision an address without modifying an existing identity. | `201` if created; `200` if already present. | + +The first three operations form the core service. Provisioning is OPTIONAL; +a service that does not expose it MAY return `404` or `405` on that route. +There is no standardized list, update, delete, quota-reset, or usage-reversal +operation in this revision. + +## Address Lookup + +`GET /fmsgid/{address}` returns `200 OK` for a registered identity, including +one with `acceptingNew: false`, or `404` for a valid but unknown identity. +Lookup MUST NOT create an address or modify its usage. + +The response MUST include `address`, `displayName`, `acceptingNew`, all eight +limit fields, and all six usage fields below. An address with no accounting +history has zero usage, not missing or null counters. + +| Field | Type | Meaning | +|-------|------|---------| +| `address` | string | Registered spelling of the requested identity. | +| `displayName` | string | Human-readable label; an empty string means no label is set. It is not an identity key. | +| `acceptingNew` | boolean | Whether the address is enabled to accept new messages. `true` does not override quota checks. | +| `tags` | array of strings or null, optional | Deployment-defined metadata. Omitted, null, and an empty array all mean no tags. No authorization semantics are standardized. | + +| Receive limit | Send limit | Meaning | +|---------------|------------|---------| +| `limitRecvSizeTotal` | `limitSendSizeTotal` | Maximum cumulative accounted bytes. | +| `limitRecvSizePerMsg` | `limitSendSizePerMsg` | Maximum accounted bytes for one chargeable message event. | +| `limitRecvSizePer1d` | `limitSendSizePer1d` | Maximum accounted bytes in the rolling one-day window. | +| `limitRecvCountPer1d` | `limitSendCountPer1d` | Maximum event count in that window. | + +| Receive usage | Send usage | Meaning | +|---------------|------------|---------| +| `recvSizeTotal` | `sendSizeTotal` | Sum of recorded sizes for that direction over the retained accounting history. | +| `recvSizePer1d` | `sendSizePer1d` | Sum of recorded sizes for that direction in the one-day window. | +| `recvCountPer1d` | `sendCountPer1d` | Number of recorded events for that direction in the one-day window. | + +Example: + +```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": [] +} +``` + +`acceptingNew` is not a statement about credentials, domain ownership, or +retention of existing messages. FMSG-003 also uses this status when admitting +client requests; its authorization rules remain defined by FMSG-003. + +### Accounting Windows and Storage + +For a lookup evaluated at service time `T`, `Per1d` means the preceding +86,400 seconds, with `T - 86400 < ts <= T`. It is a rolling window, not a +calendar date or a reset at local midnight. All daily counters in a response +MUST use the same window. A report at the lower boundary is excluded; a +future-dated report MUST NOT enter daily usage before its timestamp. + +Total usage is cumulative accounting, not a measurement of files currently +on disk. Deleting a message through FMSG-003 does not itself reverse an +accounting report. Services MUST document retention, resets, or other +administrative adjustments that can change totals; they MUST NOT silently +present a truncated history as lifetime usage. Deployments needing live +storage quotas must define how storage changes are reconciled with their +accounting policy. + +### Evaluating Limits + +For a proposed event of size `s`, a host using this service MUST check the +applicable direction's limits. A limit `L` permits a candidate value `x` iff +`L == -1` or `x <= L`. + +| Dimension | Candidate value | +|-----------|-----------------| +| Per-message size | `s` | +| Total size | reported total size + `s` | +| Daily size | reported daily size + `s` | +| Daily count | reported daily count + `1` | + +Equality with a limit is allowed; exceeding any finite limit fails the quota +check. Unlimited on one dimension does not disable the others. A zero-byte +event still adds one to the count. Callers MUST avoid overflow when comparing +usage with limits. + +Lookup is a snapshot, not an atomic quota reservation. Concurrent callers, +cached responses, or delayed accounting can admit more work than a later +snapshot permits. Hosts needing strict enforcement MUST coordinate admission +and reporting, or provide a separately agreed reservation mechanism. No such +mechanism is defined here. Callers MAY cache lookups for a bounded, +deployment-defined interval, but MUST account for the resulting delay in +status and quota changes; cached data is not a reservation. + +### Host-to-Host Results + +When deciding a recipient's disposition, a host MUST follow the core +specification's order of checks and response-code rules. Relevant outcomes +include: + +| Address-service result | Recipient outcome | +|------------------------|-------------------| +| Unknown address (`404`) | `100` (user unknown), or `105` (user undisclosed). | +| A finite receive quota would be exceeded | `101` (user full), or `105`. | +| Known address with `acceptingNew: false` | `102` (user not accepting), or `105`. | +| Known, enabled address within quota | Eligible for `200` (accept), subject to every other protocol check. | + +HTTP statuses and fmsg response bytes are different namespaces. Service +failure is not evidence for any of the recipient-policy rejections above. +This standard does not bypass duplicate, participant, terminal-message, +message-validation, or transport checks in the core specification. + +## Usage Reporting + +`POST /fmsgid/send` and `POST /fmsgid/recv` use the same request shape. The +route selects the direction; there is no `type` request field. ```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; a valid, registered fmsg identity, compared using the same case folding as lookup. | +| `ts` | number | REQUIRED; finite Unix epoch seconds in UTC, with fractional seconds allowed. This is not milliseconds or an RFC 3339 string. | +| `size` | integer | REQUIRED; non-negative accounted bytes, under the deployment's documented size policy. | + +A successful report MUST be recorded before the service responds `200`. +It contributes its size to that identity's direction-specific total, and its +size and one event to daily usage while its timestamp is in the window. +Reports MUST NOT change the other direction's usage or another identity's +usage. Recording usage MUST NOT implicitly provision an unknown identity. + +Accounting records actual chargeable activity. A report MUST NOT be silently +discarded merely because it makes a quota exceed its limit or the address has +since been disabled. Admission checks happen before the activity; reporting +is not a request to reserve permission to send or receive. + +### What to Report + +The host and address service MUST agree on a byte-accounting policy, and use +it consistently for limits, admission checks, and reports. Following the core +specification's quota guidance, the RECOMMENDED size is the sum of message +data and attachment data after decompression, excluding message headers and +transport overhead. Alternative policies MUST be documented; compressed +wire bytes MUST NOT be compared against limits defined in expanded bytes. + +A receive event represents successful acceptance for one recipient identity; +a failed attempt or duplicate delivery is not a new receive event. A send +event SHOULD represent acceptance of one logical outgoing message by the +sender's host, rather than one event per remote domain or retry. Hosts MUST +define how add-to batches and other chargeable operations fit their policy, +including local-only delivery, and avoid double reporting when multiple +components share the message store. A notification that adds no new storage +for a recipient does not by itself imply a full-message storage charge. + +Callers SHOULD timestamp the chargeable operation using a trusted host clock, +not the later HTTP retry time or an untrusted timestamp supplied by a client. +A delayed report retains the original event timestamp. Services SHOULD bound +acceptable timestamp skew and reject unreasonable future timestamps. Reporting +failures SHOULD be retained for reconciliation rather than silently lost. + +### Retries + +This revision defines no event identifier, idempotency key, deduplication +algorithm, or exactly-once guarantee for accounting. Two reports can share an +address, timestamp, and size without proving that they are the same event. + +Callers MUST NOT assume that repeating a POST after a timeout is safe: the +first request may have been committed even if its response was lost. Services +MAY provide separately documented deduplication or idempotency extensions. +If a service rejects a recognized duplicate or conflicting report with `409`, +the caller MUST resolve it according to that service's documented policy; +`409` alone does not prove the intended event was successfully accounted for. + +## Optional Address Provisioning + +`POST /fmsgid` provisions 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 an empty +string. The operation MUST be idempotent under the address's case-folded +identity: + +- If the identity is absent, create it and return `201 Created`. +- If the identity already exists, return `200 OK` without changing its + spelling, display name, enabled status, quotas, or usage. +- Invalid input returns `400`; callers lacking provisioning authority are + rejected by the deployment's access-control layer. + +No response body is required. Callers SHOULD perform a lookup after creation +to obtain the effective defaults, rather than assume specific quotas or an +enabled status. This route MUST NOT treat unrecognized quota or status +properties as authority to change policy. + +Provisioning is not proof of ownership and MUST NOT be offered as unrestricted +self-service registration. A component provisioning a derived address for a +FMSG-003 API-access grant must first establish its authority to do so. Policy +updates, disabling, deletion, and synchronization with an identity provider +are outside this route and this revision. + +## Implementation Compatibility + +This section is non-normative. The public +[fmsgid implementation](https://github.com/markmnl/fmsgid) and its host callers +informed the paths and field names in this draft; listing an implementation +is not a claim of full conformance. + +The old unversioned sketch used `/addr/{address}`, `/addr/recv`, `/addr/sent`, +`timestamp`, `recvSizePerPer1d`, and an undefined `name` response property. +Those are not aliases defined by this revision. The implemented names are +`/fmsgid/{address}`, `/fmsgid/recv`, `/fmsgid/send`, `ts`, and +`recvSizePer1d`; no separate `name` property is required. Null `tags` from +existing implementations remains acceptable. + +The following gaps were identified in the public implementations during the +2026-09-11 review and require implementation work, not alternative meanings +for the fields above: + +- In fmsgid revision `5a74176`, the order of aggregate columns in + [`sqlActuals`](https://github.com/markmnl/fmsgid/blob/5a74176/src/sql.go) + differs from the order of destinations in + [`getAddressDetail`](https://github.com/markmnl/fmsgid/blob/5a74176/src/fmsgid.go). + Consequently several send/receive size and count values are assigned to + the wrong response fields. Daily aggregation also lacks the upper time + boundary specified here. +- In that revision, accounting writes do not apply the lookup/provisioning + case folding or validate all required fields. Address validation on the + other routes is also weaker than the core address grammar. Unknown + identities and duplicate timestamps can surface as database `500` errors. + The database key `(address_lower, ts)` is an implementation constraint, + not a standard event identifier or idempotency guarantee. +- The [daemon at revision `13d6fc8`](https://github.com/markmnl/fmsgd/blob/13d6fc8/cmd/fmsgd/host.go) + charges message data size only for receive accounting; + per-message limits, expanded attachment accounting, and send reporting + need alignment with the chosen policy. The + [Web API's local delivery path at revision `1a0cff1`](https://github.com/markmnl/fmsg-webapi/blob/1a0cff1/internal/handlers/messages.go) + checks existence and enabled status but does not implement + the complete quota and accounting flow. Service lookup alone therefore + does not establish end-to-end quota enforcement. + +## Conformance Checks + +Implementations should exercise at least these cases when claiming support +for this revision: +1. Valid, malformed, and unknown addresses; mixed-case and non-ASCII + case-fold-equivalent identities across all supported operations. +2. A known disabled address returns a complete `200` lookup with + `acceptingNew: false`, not `404`. +3. A newly provisioned address has zero usage. Re-provisioning it, including + with different casing or display metadata, preserves its existing state. +4. Every limit and usage field has the documented type and direction. Test + with asymmetric sizes and counts so a swapped aggregate cannot pass. +5. For a lookup at `T`, reports of send 100 bytes at `T - 90000`, send 200 + bytes at `T - 3600`, and receive 300 bytes at `T - 60` produce the six + usage values shown in the lookup example. Also test the exact lower + boundary and future timestamps. +6. `-1`, `0`, exact-limit admission, one-over-limit rejection, and independent + count and size limits, including a zero-byte event. +7. Missing, null, negative, fractional-size, malformed, and out-of-range + accounting inputs are rejected without modifying usage. +8. Send and receive reports affect only the correct address and direction; + reporting never creates an address or overwrites policy. +9. Duplicate/conflicting reports, lost responses, and reconciliation follow + the documented retry policy without assuming timestamps are unique IDs. +10. Unavailable or malformed lookup responses cannot become a successful + identity or quota check. Unauthorized callers cannot provision addresses + or forge accounting, and caches respect their documented bounds. From fbc642d33da508d02a3405475ff2191875e049c0 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Fri, 11 Sep 2026 13:48:26 +0800 Subject: [PATCH 2/2] Condense FMSG-002 and simplify the implementation reference --- IMPLEMENTATIONS.md | 2 +- standards/fmsg-002-id.md | 532 ++++++++++++--------------------------- 2 files changed, 163 insertions(+), 371 deletions(-) diff --git a/IMPLEMENTATIONS.md b/IMPLEMENTATIONS.md index 48b2e62..1d482c6 100644 --- a/IMPLEMENTATIONS.md +++ b/IMPLEMENTATIONS.md @@ -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) | Address and quota HTTP service for [FMSG-002](standards/fmsg-002-id.md), with usage reporting and address provisioning; see the standard’s compatibility notes. | +| [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 | diff --git a/standards/fmsg-002-id.md b/standards/fmsg-002-id.md index 1407a65..4a1aa74 100644 --- a/standards/fmsg-002-id.md +++ b/standards/fmsg-002-id.md @@ -2,186 +2,93 @@ ## Status -| Revision | Date | Summary | -|----------|------------|---------| -| v0.1.0 | 2026-09-11 | Expanded draft: address lookup, quota and usage semantics, accounting, and optional provisioning; replaces the unversioned `/addr` sketch | - -This standard defines the HTTP address and quota service used by an fmsg host -and its client API. It is independent of the identity provider used by a -deployment. The [implementation compatibility notes](#implementation-compatibility) -distinguish this contract from limitations in existing implementations. - -## Requirements Language - -The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, -**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **NOT RECOMMENDED**, **MAY**, and -**OPTIONAL** in this document are to be interpreted as described in BCP 14 -([RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and -[RFC 8174](https://www.rfc-editor.org/rfc/rfc8174)) when, and only when, they -appear in all capitals. - -## Scope and Roles - -An **address service** answers whether an address is known, whether it accepts -new messages, its configured limits, and its recorded usage. It accepts -accounting reports from trusted host components. It MAY also provide the -address-provisioning operation defined here. - -A **caller** is a host component authorized to use the service, such as a -host-to-host daemon, a [FMSG-003 Web API](fmsg-003-webapi.md), or a provisioning -process. A caller's authority MUST be restricted to the domains and operations -it is allowed to manage. - -An **identity provider** authenticates users through a deployment's chosen -login mechanism. An address service does not perform that login, issue user -JWTs, or prove that a person owns an address. In FMSG-003, JWT validation and -selection of the effective fmsg address precede the address-service check; -a successful lookup MUST NOT substitute for authenticating the API caller. - -This standard does not specify a database schema, a shared message store, -identity-provider synchronization, an administrative UI, or a public directory -of every address. CSV import and other provisioning integrations are -implementation choices. Hosts MAY use other mechanisms for address and quota -checks without implementing FMSG-002; the core fmsg protocol does not mandate -this HTTP service. - -## References - -- [fmsg Specification](../SPECIFICATION.md), especially [Address](../SPECIFICATION.md#address) and per-recipient acceptance rules. -- [FMSG-003 Web API Standard](fmsg-003-webapi.md), for authenticated client access and API-access grants. -- [RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format](https://www.rfc-editor.org/rfc/rfc8259). - -## Transport and Trust Boundary - -The service base URL is configured by the deployment. It MUST NOT be derived -from a user-supplied URL, an identity-provider brand, or the address being -looked up. FMSG-001's `fmsg.` discovery and TCP port 4930 apply to -host-to-host messaging, not to this service. - -Routes use the `/fmsgid` base path. For a configured base URL of -`https://directory.example.com`, the lookup route is -`https://directory.example.com/fmsgid/{address}`. - -Connections crossing an untrusted network MUST use HTTPS with server -certificate verification. Plain HTTP MAY be used on a protected local network -or behind a trusted TLS terminator. The service MUST restrict access to -trusted callers; network isolation, mutually authenticated TLS, a gateway, or -service credentials MAY provide that boundary. This revision does not specify -a common service-credential format or require forwarding an end user's JWT. - -Provisioning and accounting writes MUST NOT be exposed to unauthenticated, -untrusted callers. Lookup also exposes address existence, activity and policy; -deployments SHOULD restrict it rather than make it publicly enumerable. -Addresses in URL paths can appear in access logs even with HTTPS. Operators -SHOULD restrict or redact those logs. This revision retains GET lookup and -does not define a request-body alternative. - -## Common Conventions - -### Addresses - -Every `address` is a complete fmsg address such as `@alice@example.com`, not a -bare username or email address. Validation MUST follow the core -[Address](../SPECIFICATION.md#address) rules, including UTF-8 syntax and the -whole-address byte-length limit. - -Identity comparison MUST use Unicode default case folding, as defined by the -core specification, consistently for lookup, provisioning, and accounting. -ASCII lowercasing alone is insufficient. For example, `@Alice@EXAMPLE.COM` -and `@alice@example.com` refer to one identity. A service MAY preserve the -registered spelling in the response's `address`; clients MUST compare it by -identity rather than byte-for-byte spelling. - -A lookup caller MUST encode the address as one URL path segment. For example, -`GET /fmsgid/%40alice%40example.com` addresses the same identity as -`GET /fmsgid/@alice@example.com`. The decoded address is validated and folded; -URL escaping is not an identity-normalization operation. - -### JSON and Numbers - -JSON request and response bodies MUST use `application/json`. Field names are -case-sensitive. The lookup representation uses camelCase, while provisioning -uses `display_name` and accounting uses `ts`; these spellings are intentional. - -Size and count fields are JSON integers. Limits MUST be either `-1` (unlimited) -or non-negative integers; usage MUST be non-negative. `0` is a real limit, -not an alias for unlimited. Clients MUST preserve integer precision and MUST -NOT interpret missing or malformed quota fields as unlimited. Implementations -MUST document any supported numeric bounds and reject unsupported request -values rather than wrap or truncate them. - -Clients MUST ignore unrecognized response properties. Servers MAY accept -additional request properties, but clients MUST NOT rely on an extension's -behavior unless it is supported by their configured service. Required request -properties MUST be present with the documented type; an omitted value is not -the same as an explicit zero. - -### Errors and Availability - -An error MAY have an empty body. Clients MUST use the HTTP status, rather than -relying on an implementation-specific JSON error shape. Services MUST NOT -return database errors, credentials, or internal connection details to callers. - -| Status | Meaning | -|--------|---------| -| `400 Bad Request` | Invalid address, malformed JSON, missing required property, or invalid field value. | -| `401 Unauthorized` / `403 Forbidden` | The deployment's access-control layer rejected the caller. These do not describe the target address's message-acceptance status. | -| `404 Not Found` | On lookup or accounting, the valid address is not registered. | -| `409 Conflict` | An accounting implementation detected a conflicting or duplicate report; see [Retries](#retries). | -| `413 Content Too Large` | The request exceeds the service's supported request size. | -| `429 Too Many Requests` | The caller exceeded an API rate limit; this is distinct from an address's message quota. | -| `500 Internal Server Error` / `503 Service Unavailable` | The operation could not be completed. | - -A timeout, invalid success payload, access-control failure, or server error -MUST NOT be interpreted as "unknown address" or "accepting messages". Callers -SHOULD use bounded timeouts and explicit retry policies. A failed lookup must -remain a service failure rather than a fabricated recipient-policy result. +| 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. + +[fmsgid](https://github.com/markmnl/fmsgid) is an existing example implementation. + +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 | Successful response | -|--------|-------|---------|---------------------| -| `GET` | `/fmsgid/{address}` | Lookup policy, metadata, and usage. | `200` with an address-detail object. | -| `POST` | `/fmsgid/send` | Record one send accounting event. | `200` with no required body. | -| `POST` | `/fmsgid/recv` | Record one receive accounting event. | `200` with no required body. | -| `POST` | `/fmsgid` | Optionally provision an address without modifying an existing identity. | `201` if created; `200` if already present. | +| 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. | -The first three operations form the core service. Provisioning is OPTIONAL; -a service that does not expose it MAY return `404` or `405` on that route. -There is no standardized list, update, delete, quota-reset, or usage-reversal -operation in this revision. +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 OK` for a registered identity, including -one with `acceptingNew: false`, or `404` for a valid but unknown identity. -Lookup MUST NOT create an address or modify its usage. - -The response MUST include `address`, `displayName`, `acceptingNew`, all eight -limit fields, and all six usage fields below. An address with no accounting -history has zero usage, not missing or null counters. +`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 requested identity. | -| `displayName` | string | Human-readable label; an empty string means no label is set. It is not an identity key. | -| `acceptingNew` | boolean | Whether the address is enabled to accept new messages. `true` does not override quota checks. | -| `tags` | array of strings or null, optional | Deployment-defined metadata. Omitted, null, and an empty array all mean no tags. No authorization semantics are standardized. | - -| Receive limit | Send limit | Meaning | -|---------------|------------|---------| -| `limitRecvSizeTotal` | `limitSendSizeTotal` | Maximum cumulative accounted bytes. | -| `limitRecvSizePerMsg` | `limitSendSizePerMsg` | Maximum accounted bytes for one chargeable message event. | -| `limitRecvSizePer1d` | `limitSendSizePer1d` | Maximum accounted bytes in the rolling one-day window. | -| `limitRecvCountPer1d` | `limitSendCountPer1d` | Maximum event count in that window. | +| `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 usage | Send usage | Meaning | +| Receive limit | Send limit | Maximum | |---------------|------------|---------| -| `recvSizeTotal` | `sendSizeTotal` | Sum of recorded sizes for that direction over the retained accounting history. | -| `recvSizePer1d` | `sendSizePer1d` | Sum of recorded sizes for that direction in the one-day window. | -| `recvCountPer1d` | `sendCountPer1d` | Number of recorded events for that direction in the one-day window. | +| `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. | -Example: +| 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 { @@ -206,74 +113,45 @@ Example: } ``` -`acceptingNew` is not a statement about credentials, domain ownership, or -retention of existing messages. FMSG-003 also uses this status when admitting -client requests; its authorization rules remain defined by FMSG-003. +## Quota Semantics -### Accounting Windows and Storage +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 lookup evaluated at service time `T`, `Per1d` means the preceding -86,400 seconds, with `T - 86400 < ts <= T`. It is a rolling window, not a -calendar date or a reset at local midnight. All daily counters in a response -MUST use the same window. A report at the lower boundary is excluded; a -future-dated report MUST NOT enter daily usage before its timestamp. +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`: -Total usage is cumulative accounting, not a measurement of files currently -on disk. Deleting a message through FMSG-003 does not itself reverse an -accounting report. Services MUST document retention, resets, or other -administrative adjustments that can change totals; they MUST NOT silently -present a truncated history as lifetime usage. Deployments needing live -storage quotas must define how storage changes are reconciled with their -accounting policy. +| Limit | Candidate value | +|-------|-----------------| +| Per-message size | `s` | +| Total size | Total usage + `s` | +| Daily size | Daily usage + `s` | +| Daily count | Daily count + `1` | -### Evaluating Limits +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. -For a proposed event of size `s`, a host using this service MUST check the -applicable direction's limits. A limit `L` permits a candidate value `x` iff -`L == -1` or `x <= L`. +Hosts MUST follow the core specification's acceptance rules and order of +checks. Address-service results inform these per-recipient outcomes: -| Dimension | Candidate value | -|-----------|-----------------| -| Per-message size | `s` | -| Total size | reported total size + `s` | -| Daily size | reported daily size + `s` | -| Daily count | reported daily count + `1` | - -Equality with a limit is allowed; exceeding any finite limit fails the quota -check. Unlimited on one dimension does not disable the others. A zero-byte -event still adds one to the count. Callers MUST avoid overflow when comparing -usage with limits. - -Lookup is a snapshot, not an atomic quota reservation. Concurrent callers, -cached responses, or delayed accounting can admit more work than a later -snapshot permits. Hosts needing strict enforcement MUST coordinate admission -and reporting, or provide a separately agreed reservation mechanism. No such -mechanism is defined here. Callers MAY cache lookups for a bounded, -deployment-defined interval, but MUST account for the resulting delay in -status and quota changes; cached data is not a reservation. - -### Host-to-Host Results - -When deciding a recipient's disposition, a host MUST follow the core -specification's order of checks and response-code rules. Relevant outcomes -include: - -| Address-service result | Recipient outcome | -|------------------------|-------------------| -| Unknown address (`404`) | `100` (user unknown), or `105` (user undisclosed). | -| A finite receive quota would be exceeded | `101` (user full), or `105`. | -| Known address with `acceptingNew: false` | `102` (user not accepting), or `105`. | -| Known, enabled address within quota | Eligible for `200` (accept), subject to every other protocol check. | - -HTTP statuses and fmsg response bytes are different namespaces. Service -failure is not evidence for any of the recipient-policy rejections above. -This standard does not bypass duplicate, participant, terminal-message, -message-validation, or transport checks in the core specification. +| 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` use the same request shape. The -route selects the direction; there is no `type` request field. +`POST /fmsgid/send` and `POST /fmsgid/recv` accept the same JSON body; the route +selects the direction: ```json { @@ -285,61 +163,39 @@ route selects the direction; there is no `type` request field. | Field | Type | Requirement | |-------|------|-------------| -| `address` | string | REQUIRED; a valid, registered fmsg identity, compared using the same case folding as lookup. | -| `ts` | number | REQUIRED; finite Unix epoch seconds in UTC, with fractional seconds allowed. This is not milliseconds or an RFC 3339 string. | -| `size` | integer | REQUIRED; non-negative accounted bytes, under the deployment's documented size policy. | - -A successful report MUST be recorded before the service responds `200`. -It contributes its size to that identity's direction-specific total, and its -size and one event to daily usage while its timestamp is in the window. -Reports MUST NOT change the other direction's usage or another identity's -usage. Recording usage MUST NOT implicitly provision an unknown identity. - -Accounting records actual chargeable activity. A report MUST NOT be silently -discarded merely because it makes a quota exceed its limit or the address has -since been disabled. Admission checks happen before the activity; reporting -is not a request to reserve permission to send or receive. - -### What to Report - -The host and address service MUST agree on a byte-accounting policy, and use -it consistently for limits, admission checks, and reports. Following the core -specification's quota guidance, the RECOMMENDED size is the sum of message -data and attachment data after decompression, excluding message headers and -transport overhead. Alternative policies MUST be documented; compressed -wire bytes MUST NOT be compared against limits defined in expanded bytes. - -A receive event represents successful acceptance for one recipient identity; -a failed attempt or duplicate delivery is not a new receive event. A send -event SHOULD represent acceptance of one logical outgoing message by the -sender's host, rather than one event per remote domain or retry. Hosts MUST -define how add-to batches and other chargeable operations fit their policy, -including local-only delivery, and avoid double reporting when multiple -components share the message store. A notification that adds no new storage -for a recipient does not by itself imply a full-message storage charge. - -Callers SHOULD timestamp the chargeable operation using a trusted host clock, -not the later HTTP retry time or an untrusted timestamp supplied by a client. -A delayed report retains the original event timestamp. Services SHOULD bound -acceptable timestamp skew and reject unreasonable future timestamps. Reporting -failures SHOULD be retained for reconciliation rather than silently lost. - -### Retries - -This revision defines no event identifier, idempotency key, deduplication -algorithm, or exactly-once guarantee for accounting. Two reports can share an -address, timestamp, and size without proving that they are the same event. - -Callers MUST NOT assume that repeating a POST after a timeout is safe: the -first request may have been committed even if its response was lost. Services -MAY provide separately documented deduplication or idempotency extensions. -If a service rejects a recognized duplicate or conflicting report with `409`, -the caller MUST resolve it according to that service's documented policy; -`409` alone does not prove the intended event was successfully accounted for. - -## Optional Address Provisioning - -`POST /fmsgid` provisions an address with service-configured defaults: +| `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 { @@ -348,92 +204,28 @@ the caller MUST resolve it according to that service's documented policy; } ``` -`address` is REQUIRED. `display_name` is OPTIONAL and defaults to an empty -string. The operation MUST be idempotent under the address's case-folded -identity: - -- If the identity is absent, create it and return `201 Created`. -- If the identity already exists, return `200 OK` without changing its - spelling, display name, enabled status, quotas, or usage. -- Invalid input returns `400`; callers lacking provisioning authority are - rejected by the deployment's access-control layer. - -No response body is required. Callers SHOULD perform a lookup after creation -to obtain the effective defaults, rather than assume specific quotas or an -enabled status. This route MUST NOT treat unrecognized quota or status -properties as authority to change policy. - -Provisioning is not proof of ownership and MUST NOT be offered as unrestricted -self-service registration. A component provisioning a derived address for a -FMSG-003 API-access grant must first establish its authority to do so. Policy -updates, disabling, deletion, and synchronization with an identity provider -are outside this route and this revision. - -## Implementation Compatibility - -This section is non-normative. The public -[fmsgid implementation](https://github.com/markmnl/fmsgid) and its host callers -informed the paths and field names in this draft; listing an implementation -is not a claim of full conformance. - -The old unversioned sketch used `/addr/{address}`, `/addr/recv`, `/addr/sent`, -`timestamp`, `recvSizePerPer1d`, and an undefined `name` response property. -Those are not aliases defined by this revision. The implemented names are -`/fmsgid/{address}`, `/fmsgid/recv`, `/fmsgid/send`, `ts`, and -`recvSizePer1d`; no separate `name` property is required. Null `tags` from -existing implementations remains acceptable. - -The following gaps were identified in the public implementations during the -2026-09-11 review and require implementation work, not alternative meanings -for the fields above: - -- In fmsgid revision `5a74176`, the order of aggregate columns in - [`sqlActuals`](https://github.com/markmnl/fmsgid/blob/5a74176/src/sql.go) - differs from the order of destinations in - [`getAddressDetail`](https://github.com/markmnl/fmsgid/blob/5a74176/src/fmsgid.go). - Consequently several send/receive size and count values are assigned to - the wrong response fields. Daily aggregation also lacks the upper time - boundary specified here. -- In that revision, accounting writes do not apply the lookup/provisioning - case folding or validate all required fields. Address validation on the - other routes is also weaker than the core address grammar. Unknown - identities and duplicate timestamps can surface as database `500` errors. - The database key `(address_lower, ts)` is an implementation constraint, - not a standard event identifier or idempotency guarantee. -- The [daemon at revision `13d6fc8`](https://github.com/markmnl/fmsgd/blob/13d6fc8/cmd/fmsgd/host.go) - charges message data size only for receive accounting; - per-message limits, expanded attachment accounting, and send reporting - need alignment with the chosen policy. The - [Web API's local delivery path at revision `1a0cff1`](https://github.com/markmnl/fmsg-webapi/blob/1a0cff1/internal/handlers/messages.go) - checks existence and enabled status but does not implement - the complete quota and accounting flow. Service lookup alone therefore - does not establish end-to-end quota enforcement. - -## Conformance Checks - -Implementations should exercise at least these cases when claiming support -for this revision: - -1. Valid, malformed, and unknown addresses; mixed-case and non-ASCII - case-fold-equivalent identities across all supported operations. -2. A known disabled address returns a complete `200` lookup with - `acceptingNew: false`, not `404`. -3. A newly provisioned address has zero usage. Re-provisioning it, including - with different casing or display metadata, preserves its existing state. -4. Every limit and usage field has the documented type and direction. Test - with asymmetric sizes and counts so a swapped aggregate cannot pass. -5. For a lookup at `T`, reports of send 100 bytes at `T - 90000`, send 200 - bytes at `T - 3600`, and receive 300 bytes at `T - 60` produce the six - usage values shown in the lookup example. Also test the exact lower - boundary and future timestamps. -6. `-1`, `0`, exact-limit admission, one-over-limit rejection, and independent - count and size limits, including a zero-byte event. -7. Missing, null, negative, fractional-size, malformed, and out-of-range - accounting inputs are rejected without modifying usage. -8. Send and receive reports affect only the correct address and direction; - reporting never creates an address or overwrites policy. -9. Duplicate/conflicting reports, lost responses, and reconciliation follow - the documented retry policy without assuming timestamps are unique IDs. -10. Unavailable or malformed lookup responses cannot become a successful - identity or quota check. Unauthorized callers cannot provision addresses - or forge accounting, and caches respect their documented bounds. +`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.