From b6a5011b206af6976eaf399852059e1a5af185cf Mon Sep 17 00:00:00 2001 From: Proxicon Date: Sun, 4 Oct 2026 18:31:19 +0200 Subject: [PATCH] Mirror authoritative RatelDesk incident receiver contract and golden fixtures --- .../rateldesk-incident-create.v1.SHA256SUMS | 3 + ...rateldesk-incident-create.v1.fixtures.json | 276 +++++++++ .../rateldesk-incident-create.v1.json | 585 ++++++++++++++++++ .../contracts/rateldesk-incident-create.v1.md | 64 ++ 4 files changed, 928 insertions(+) create mode 100644 docs/contracts/rateldesk-incident-create.v1.SHA256SUMS create mode 100644 docs/contracts/rateldesk-incident-create.v1.fixtures.json create mode 100644 docs/contracts/rateldesk-incident-create.v1.json create mode 100644 docs/contracts/rateldesk-incident-create.v1.md diff --git a/docs/contracts/rateldesk-incident-create.v1.SHA256SUMS b/docs/contracts/rateldesk-incident-create.v1.SHA256SUMS new file mode 100644 index 00000000..88378354 --- /dev/null +++ b/docs/contracts/rateldesk-incident-create.v1.SHA256SUMS @@ -0,0 +1,3 @@ +87f534716b4d69619cb9e15f913e896402956e561916f79b1a283dcf635a74b4 rateldesk-incident-create.v1.json +bb6b49ed69ca00bbc6cfe5b226c6ea8858879785eb94945f6a11edf06c947e99 rateldesk-incident-create.v1.fixtures.json +f8b67d3b96988cd265d27af7bc4da64fc600589b2cb4ba0a3d4b7a2f8b52b7c5 rateldesk-incident-create.v1.md diff --git a/docs/contracts/rateldesk-incident-create.v1.fixtures.json b/docs/contracts/rateldesk-incident-create.v1.fixtures.json new file mode 100644 index 00000000..4ccce63c --- /dev/null +++ b/docs/contracts/rateldesk-incident-create.v1.fixtures.json @@ -0,0 +1,276 @@ +{ + "contractVersion": "rateldesk.incident-create.v1", + "fingerprintProjection": "rateldesk.incident-create.v1.semantic-json", + "vectors": [ + { + "name": "minimal", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222" + }, + "fingerprint": "46e4bf2a6b2e9ccc1b3e862b7c02b9444e2b22db284f76eea9e18640581345bd" + }, + { + "name": "reordered-equivalent-defaults", + "request": { + "categoryIds": null, + "requesterEmail": "ignored@example.test", + "ccRecipients": [], + "impact": null, + "dueDate": null, + "attachments": null, + "linkedAssetIds": [], + "assignedToId": " ", + "priority": 0, + "customerId": "22222222-2222-2222-2222-222222222222", + "organizationId": "11111111-1111-1111-1111-111111111111", + "description": "Description", + "title": "Incident" + }, + "fingerprint": "46e4bf2a6b2e9ccc1b3e862b7c02b9444e2b22db284f76eea9e18640581345bd" + }, + { + "name": "sanitized-description-equivalent", + "request": { + "title": "Incident", + "description": "\u003Cb\u003EDescription\u003C/b\u003E", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222" + }, + "fingerprint": "46e4bf2a6b2e9ccc1b3e862b7c02b9444e2b22db284f76eea9e18640581345bd" + }, + { + "name": "material-title", + "request": { + "title": "Changed", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222" + }, + "fingerprint": "497ce61c67f0c216c67e4bd7c51acbde36afd17ab2d0df48d077acd00e2f1334" + }, + { + "name": "material-description", + "request": { + "title": "Incident", + "description": "Changed", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222" + }, + "fingerprint": "d0c22434483764a59722dd3fbbb491a4c4bdd70fc26df2b607531bcf1ed4df0c" + }, + { + "name": "material-priority", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "priority": 3 + }, + "fingerprint": "57efb455a5526cd9a7fdc320d440c2e413d4f50eb3fd0eea7d729d12fca812b4" + }, + { + "name": "material-organizationId", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "33333333-3333-3333-3333-333333333333", + "customerId": "22222222-2222-2222-2222-222222222222" + }, + "fingerprint": "4f030a9259e56ce80680b4269df74f8c1973af9972a8da6f295f4c1a8a349ac9" + }, + { + "name": "material-customerId", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "44444444-4444-4444-4444-444444444444" + }, + "fingerprint": "48113cf08a3cba19a7998ddc093afcc7e01f64aec962abbcf7832bd551912b6a" + }, + { + "name": "material-assignedToId", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "assignedToId": "55555555-5555-5555-5555-555555555555" + }, + "fingerprint": "8a32169354c0962a484af880aa4e15a1b1b3db5cf9a384f038551614bd056e1f" + }, + { + "name": "material-linkedAssetIds", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "linkedAssetIds": [ + "asset-1" + ] + }, + "fingerprint": "1a648c3be8e844336824a34cfaf66eb1eca8f1e5c535d33979132306aa1cba0f" + }, + { + "name": "material-attachments", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "attachments": [ + "attachment-1" + ] + }, + "fingerprint": "2f65f5146ba676d3793278169255dc1e93ea11b03b1d16b092c158240e8501b0" + }, + { + "name": "material-dueDate", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "dueDate": "2026-10-01T00:00:00Z" + }, + "fingerprint": "15001f6213752c927c1fa2d32a29856f433e80b693960db96a2f20501a64b8e5" + }, + { + "name": "material-impact", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "impact": "Changed" + }, + "fingerprint": "aa7d714a6e631edb6a6cd1cc90ff0c2ce5012b65dafbddd85a2f9f8328cfc64b" + }, + { + "name": "material-ccRecipients", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "ccRecipients": [ + "cc@example.test" + ] + }, + "fingerprint": "f911dc0d6a0c3ac2c05715efbd6c69131031ad98f444b0efad12873fdb24e126" + }, + { + "name": "material-categoryIds", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "categoryIds": [ + "66666666-6666-6666-6666-666666666666" + ] + }, + "fingerprint": "fe8daa0229abc4673d0721514ed5ac177fcd05cbb7a55c246ac5475fb65e5c53" + }, + { + "name": "cc-set-equivalence", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "ccRecipients": [ + " CC@example.test ", + "cc@example.test" + ] + }, + "fingerprint": "f911dc0d6a0c3ac2c05715efbd6c69131031ad98f444b0efad12873fdb24e126" + }, + { + "name": "utc-offset-equivalence", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "dueDate": "2026-10-01T02:00:00\u002B02:00" + }, + "fingerprint": "15001f6213752c927c1fa2d32a29856f433e80b693960db96a2f20501a64b8e5" + }, + { + "name": "category-set-equivalence", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "categoryIds": [ + "66666666-6666-6666-6666-666666666666", + "66666666-6666-6666-6666-666666666666" + ] + }, + "fingerprint": "fe8daa0229abc4673d0721514ed5ac177fcd05cbb7a55c246ac5475fb65e5c53" + }, + { + "name": "ordered-assets", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "linkedAssetIds": [ + "a", + "b" + ] + }, + "fingerprint": "60e73824f19cdb1cb237a99853c890d704f4c80d0978f712f454e392b0fa90aa" + }, + { + "name": "reordered-assets-material", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "linkedAssetIds": [ + "b", + "a" + ] + }, + "fingerprint": "701dbb93ecc870b04433099460eb575eb9f5eb2d34f7fb568714f6a8f5080bac" + }, + { + "name": "ordered-attachments", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "attachments": [ + "a", + "b" + ] + }, + "fingerprint": "c6dc006e945e530a674ff241be50bf965262222826dd6c5738a2ca0566300cc8" + }, + { + "name": "reordered-attachments-material", + "request": { + "title": "Incident", + "description": "Description", + "organizationId": "11111111-1111-1111-1111-111111111111", + "customerId": "22222222-2222-2222-2222-222222222222", + "attachments": [ + "b", + "a" + ] + }, + "fingerprint": "0abba082a801d8afa4553a4c8358f0e077a4d3d4fb2416f2c5cf55013ba6a7bf" + } + ] +} diff --git a/docs/contracts/rateldesk-incident-create.v1.json b/docs/contracts/rateldesk-incident-create.v1.json new file mode 100644 index 00000000..1ef0c48b --- /dev/null +++ b/docs/contracts/rateldesk-incident-create.v1.json @@ -0,0 +1,585 @@ +{ + "contractVersion": "rateldesk.incident-create.v1", + "owner": "RatelDesk issue #116", + "authenticationModes": [ + "api_bearer" + ], + "headers": { + "key": "Idempotency-Key", + "source": "X-NetRatel-Source-Instance", + "sourceFormat": "lowercase nonempty GUID D; identifier only; administrator-approved membership required" + }, + "key": { + "maxLength": 256, + "pattern": "^[A-Za-z0-9._~-]{1,256}$", + "caseSensitive": true, + "receiptPathEncoding": "URI unreserved, no percent escapes; preserve dot segments verbatim" + }, + "endpoints": { + "create": { + "method": "POST", + "path": "/api/v1/incidents/", + "body": "CreateIncidentDto", + "createdStatus": 201, + "replayStatus": 200, + "conflictStatus": 409, + "conflictCode": "idempotency-payload-conflict", + "response": "IncidentDto plus integrationReceipt for keyed requests only" + }, + "capabilities": { + "method": "GET", + "path": "/api/v1/integrations/netratel/capabilities" + }, + "receiptLookup": { + "method": "GET", + "path": "/api/v1/integrations/netratel/incident-receipts/{key}", + "successStatus": 200, + "missingStatus": 404, + "response": "immutable accepted keyed create body and captured Location" + }, + "targetValidation": { + "method": "POST", + "path": "/api/v1/integrations/netratel/targets/validate", + "readOnly": true, + "successStatus": 200, + "malformedStatus": 400, + "malformedCode": "invalid-target-request", + "unauthorizedStatus": 403, + "unauthorizedCode": "target-not-authorized", + "invalidSelectionStatus": 422, + "invalidSelectionCode": "invalid-incident-target" + } + }, + "integrationReceiptSchema": { + "type": "object", + "required": [ + "contractVersion", + "receiverInstanceId", + "sourceNamespaceId", + "sourceInstanceId", + "key", + "fingerprint", + "outcome", + "incidentId", + "trackingId", + "organizationId", + "customerId", + "committedAtUtc", + "location" + ], + "properties": { + "contractVersion": { + "type": "string", + "const": "rateldesk.incident-create.v1" + }, + "receiverInstanceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "sourceNamespaceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "sourceInstanceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "key": { + "type": "string", + "pattern": "^[A-Za-z0-9._~-]{1,256}$", + "maxLength": 256 + }, + "fingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "outcome": { + "type": "string", + "const": "committed" + }, + "incidentId": { + "type": "string" + }, + "trackingId": { + "type": "string" + }, + "organizationId": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "customerId": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "committedAtUtc": { + "type": "string", + "format": "date-time" + }, + "location": { + "type": "string", + "pattern": "^/", + "maxLength": 256 + } + } + }, + "capabilitySchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": [ + "contractVersion", + "receiverInstanceId", + "sourceInstanceId", + "sourceNamespaceId", + "createEndpoint", + "receiptEndpointTemplate", + "targetValidationEndpoint", + "keyHeader", + "sourceHeader", + "maxKeyLength", + "keyPattern", + "minimumReceiptRetentionSeconds", + "maximumAutomaticReplaySeconds", + "receiptEvictionEnabled", + "atomicIncidentReceiptAndEffects", + "supportsReceiptLookup", + "supportsSafeSameKeyReplay", + "authenticationModes" + ], + "properties": { + "contractVersion": { + "type": "string", + "const": "rateldesk.incident-create.v1" + }, + "receiverInstanceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "sourceInstanceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "sourceNamespaceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "createEndpoint": { + "type": "string", + "format": "uri", + "description": "absolute StorageOptions.PublicApiBaseUrl + /api/v1/incidents/" + }, + "receiptEndpointTemplate": { + "type": "string", + "pattern": "^[^{}]*\\{key\\}[^{}]*$", + "description": "absolute StorageOptions.PublicApiBaseUrl + /api/v1/integrations/netratel/incident-receipts/{key}" + }, + "targetValidationEndpoint": { + "type": "string", + "format": "uri", + "description": "absolute StorageOptions.PublicApiBaseUrl + /api/v1/integrations/netratel/targets/validate" + }, + "keyHeader": { + "type": "string", + "const": "Idempotency-Key" + }, + "sourceHeader": { + "type": "string", + "const": "X-NetRatel-Source-Instance" + }, + "maxKeyLength": { + "type": "integer", + "const": 256 + }, + "keyPattern": { + "type": "string", + "const": "^[A-Za-z0-9._~-]{1,256}$" + }, + "minimumReceiptRetentionSeconds": { + "type": "integer", + "const": 7776000 + }, + "maximumAutomaticReplaySeconds": { + "type": "integer", + "const": 2592000 + }, + "receiptEvictionEnabled": { + "type": "boolean", + "const": false + }, + "atomicIncidentReceiptAndEffects": { + "type": "boolean", + "const": true + }, + "supportsReceiptLookup": { + "type": "boolean", + "const": true + }, + "supportsSafeSameKeyReplay": { + "type": "boolean", + "const": true + }, + "authenticationModes": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "api_bearer", + "oauth_client_credentials" + ] + }, + "description": "V1 receiver initially advertises only api_bearer; OAuth requires the separately verified managed-principal implementation." + } + }, + "additionalProperties": true + }, + "targetRequestSchema": { + "type": "object", + "required": [ + "organizationId", + "customerId" + ], + "properties": { + "organizationId": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "customerId": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "assignedToId": { + "type": [ + "string", + "null" + ], + "maxLength": 64, + "default": null + }, + "categoryIds": { + "type": [ + "array", + "null" + ], + "maxItems": 128, + "items": { + "type": "string", + "format": "uuid" + }, + "default": [] + } + } + }, + "targetResponseSchema": { + "type": "object", + "required": [ + "contractVersion", + "receiverInstanceId", + "sourceInstanceId", + "sourceNamespaceId", + "valid", + "mapping" + ], + "properties": { + "contractVersion": { + "type": "string", + "const": "rateldesk.incident-create.v1" + }, + "receiverInstanceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "sourceInstanceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "sourceNamespaceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "valid": { + "type": "boolean", + "const": true + }, + "mapping": { + "type": "object", + "required": [ + "organizationId", + "customerId", + "assignedToId", + "categoryIds" + ], + "properties": { + "organizationId": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "customerId": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "assignedToId": { + "type": [ + "string", + "null" + ], + "maxLength": 64 + }, + "categoryIds": { + "type": "array", + "uniqueItems": true, + "maxItems": 128, + "items": { + "type": "string", + "format": "uuid" + } + } + } + } + } + }, + "sourceAdministration": { + "authorization": "enabled interactive local/OIDC instance administrator; integration bearer cannot manage sources", + "register": { + "method": "POST", + "path": "/api/v1/integrations/netratel/sources/", + "body": { + "sourceInstanceId": "GUID D", + "organizationId": "local ID", + "customerId": "local ID", + "credentialId": "API-purpose credential GUID" + }, + "status": 201 + }, + "bindOrDisableCredential": { + "method": "PUT", + "path": "/api/v1/integrations/netratel/sources/{sourceNamespaceId}/principal-bindings/{credentialId}", + "body": { + "expectedRevision": 1, + "isEnabled": true + } + }, + "enableOrDisableSource": { + "method": "PATCH", + "path": "/api/v1/integrations/netratel/sources/{sourceNamespaceId}", + "body": { + "expectedRevision": 1, + "isEnabled": false + } + }, + "list": { + "method": "GET", + "path": "/api/v1/integrations/netratel/sources/", + "maximumResults": 256 + }, + "mappingImmutable": true, + "physicalDeletionSupported": false, + "audit": "actor, time, operation, scope, principal identifier, enabled state, revision; no secret" + }, + "futureManagedScopes": { + "rateldesk.incidents.create": "approved source/mapping keyed creation only", + "rateldesk.incident-receipts.read": "capability and receipt reconciliation only", + "rateldesk.incident-targets.read": "read-only approved mapping validation only" + }, + "authorizationPrecedence": [ + "HTTP authentication", + "header syntax", + "current credential purpose/revocation/expiry/owner and source binding", + "enabled approved organization/customer and effective operation permission", + "submitted/accepted mapping", + "bounds and semantic fingerprint", + "prior receipt/conflict", + "fresh creation category/assignee rules only" + ], + "fingerprint": { + "algorithm": "SHA-256 lowercase hex", + "projection": "rateldesk.incident-create.v1.semantic-json", + "encoding": "System.Text.Json Web defaults UTF-8, compact, fixed property order; no Unicode NFC folding", + "fieldsInOrder": [ + "title", + "description", + "priority", + "customerId", + "organizationId", + "assignedToId", + "linkedAssetIds", + "attachments", + "dueDate", + "impact", + "ccRecipients", + "categoryIds" + ], + "normalization": { + "title": "exact string; nonblank <=256", + "description": "existing HtmlSanitizerService then HtmlToPlainTextConverter; exact resulting plain text; missing => empty; null invalid", + "priority": "integer 0-3; missing=Low=0; null/string invalid", + "customerId": "exact submitted ID, must equal approved canonical ID", + "organizationId": "exact submitted ID, must equal approved canonical ID", + "assignedToId": "missing/null/whitespace => null; otherwise exact string", + "linkedAssetIds": "ordered strings; missing/null=empty; no sorting or deduplication", + "attachments": "ordered strings; missing/null=empty; no sorting or deduplication", + "dueDate": "missing/null=null; unspecified interpreted UTC; offsets converted UTC; invariant roundtrip 7 fractional digits and Z", + "impact": "missing/null=null; empty is material", + "ccRecipients": "missing/null=empty; trim/lowercase invariant; omit empty; distinct set sorted ordinally; projection independent of current customer email", + "categoryIds": "missing/null=empty; omit empty GUID; distinct set of canonical GUID D sorted ordinally", + "requesterEmail": "ignored and excluded; creation derives requester from approved customer.Email; never trusted" + } + }, + "bounds": { + "maximumJsonBodyBytes": 131072, + "maximumDescriptionCharacters": 65536, + "maximumImpactCharacters": 4096, + "maximumListItems": 128, + "maximumAssetOrAttachmentCharacters": 512, + "maximumCcCharacters": 254, + "maximumMappingIdCharacters": 64 + }, + "retention": { + "minimumReceiptRetentionSeconds": 7776000, + "maximumAutomaticReplaySeconds": 2592000, + "fullReceipts": "indefinite", + "evictionEnabled": false, + "futureEviction": "requires durable non-recreation barrier/tombstone; never turns accepted key into a fresh create opportunity" + }, + "atomicity": { + "uniqueDatabaseKey": [ + "sourceNamespaceId", + "key" + ], + "sameTransaction": [ + "incident", + "immutable accepted receipt", + "category links", + "SLA state", + "notifications/support delivery records", + "deduplicated ingress outbox effects" + ], + "confirmationGuarantee": "one durable logical enqueue; external SMTP is not exactly-once", + "cacheControl": "no-store" + }, + "rollout": { + "requiresDeployedCapability": true, + "beta8Compatible": false, + "receiverSourceBaseline": "9a9a8abd2578dc31d1ec500dd3893478f8fa2749", + "inspectedProducerMain": "af6a7ed84de4463097258f44199cbd1c6fbbc3c9", + "inspectedProducerConnectorHead": "f283e6af810209b8e7b6b2ef6a78428d4da0a28f" + }, + "acceptedResponseSchema": { + "type": "object", + "required": [ + "id", + "trackingId", + "organizationId", + "customerId", + "integrationReceipt" + ], + "properties": { + "id": { + "type": "string" + }, + "trackingId": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "customerId": { + "type": "string" + }, + "integrationReceipt": { + "type": "object", + "required": [ + "contractVersion", + "receiverInstanceId", + "sourceNamespaceId", + "sourceInstanceId", + "key", + "fingerprint", + "outcome", + "incidentId", + "trackingId", + "organizationId", + "customerId", + "committedAtUtc", + "location" + ], + "properties": { + "contractVersion": { + "type": "string", + "const": "rateldesk.incident-create.v1" + }, + "receiverInstanceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "sourceNamespaceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "sourceInstanceId": { + "type": "string", + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "key": { + "type": "string", + "pattern": "^[A-Za-z0-9._~-]{1,256}$", + "maxLength": 256 + }, + "fingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "outcome": { + "type": "string", + "const": "committed" + }, + "incidentId": { + "type": "string" + }, + "trackingId": { + "type": "string" + }, + "organizationId": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "customerId": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "committedAtUtc": { + "type": "string", + "format": "date-time" + }, + "location": { + "type": "string", + "pattern": "^/", + "maxLength": 256 + } + } + } + }, + "additionalProperties": true, + "description": "The remaining existing IncidentDto fields are preserved. ID/tracking/organization/customer must agree with integrationReceipt." + } +} diff --git a/docs/contracts/rateldesk-incident-create.v1.md b/docs/contracts/rateldesk-incident-create.v1.md new file mode 100644 index 00000000..731e408b --- /dev/null +++ b/docs/contracts/rateldesk-incident-create.v1.md @@ -0,0 +1,64 @@ +# RatelDesk reliable incident receiver v1 + +The authoritative contract is [rateldesk-incident-create.v1.json](rateldesk-incident-create.v1.json). The frozen semantic SHA-256 vectors are [rateldesk-incident-create.v1.fixtures.json](rateldesk-incident-create.v1.fixtures.json). Copy these exact files and this document into the producer repository before enabling delivery. Compare the file SHA-256 digests; neither a connector proposal nor successful authentication establishes receiver support. + +This contract is owned by RatelDesk issue #116 and consumed by NetRatel epic #141 and connector PR #152. The separate M2M interoperability work is tracked in RatelDesk issue #89. V1 advertises only `api_bearer`. It adds no reciprocal pairing UX or OAuth service credentials. + +## Routes and responses + +All routes are relative to the configured API path prefix. Configure `StorageOptions:PublicApiBaseUrl` to the approved external API URL, including its prefix. Capabilities never derive credential destinations from Host or forwarded request headers. Missing/invalid configuration returns 503. Source identity comes from the approved registry, and receiver identity reuses the durable first-run `InstanceInitialization.InstanceId`. + +| Operation | Method and route | Result | +| --- | --- | --- | +| Create | `POST /api/v1/incidents/` | 201 on commit; 200 on identical replay; immutable top-level IncidentDto plus integrationReceipt | +| Capabilities | `GET /api/v1/integrations/netratel/capabilities` | 200 with the required capability fields in the JSON contract | +| Reconcile | `GET /api/v1/integrations/netratel/incident-receipts/{key}` | 200 accepted create body and Location; 404 absent key in the authorized namespace | +| Validate target | `POST /api/v1/integrations/netratel/targets/validate` | 200 authorized normalized mapping; read-only | + +Keyed create requires exactly one `Idempotency-Key` and one `X-NetRatel-Source-Instance`. The source is a lowercase nonempty GUID in D form. The key is case-sensitive ASCII URI-unreserved, 1–256 characters, matching `^[A-Za-z0-9._~-]{1,256}$`. Both headers are identifiers; neither grants authority. One missing header, empty/multiple values or invalid encoding returns 400 with `invalid-integration-headers`. A valid but unknown/foreign/unbound source returns 403 `source-not-authorized`. There is no fallback to ordinary create. Existing callers with neither header retain their ordinary DTO/result and creation behavior. A managed machine identity must always use both headers through the bounded authorization seam; no machine authentication mode is enabled in this implementation. + +Use the key verbatim as one unreserved path segment. Do not percent-escape, case-fold or normalize it, including the rare `.`/`..` keys. Clients must preserve dot segments instead of allowing their URI builder to resolve them. New producers should use 64-character SHA-256 keys. Encoded path variants, invalid keys and receipt query strings return 400 `invalid-idempotency-key`; an invalid source header returns 400 `invalid-source-header`. A nonexistent key's 404 does not imply an in-flight create cannot still commit. Retry with the original key. + +Creation, replay and lookup use `Cache-Control: no-store`. Successful responses include the captured resource Location. Replays also include `Idempotency-Replayed: true`. Later incident edits, renamed customers or changed customer email do not alter the stored accepted body. IntegrationReceipt has exactly the fixed string fields `contractVersion`, `receiverInstanceId`, `sourceNamespaceId`, `sourceInstanceId`, `key`, `fingerprint`, `outcome`, `incidentId`, `trackingId`, `organizationId`, `customerId`, `committedAtUtc` and `location`. `outcome` remains `committed`; the UTC timestamp uses RFC3339. Receipt incident/tracking/organization/customer identifiers agree with the top-level result. + +The same authorized namespace/key with different submitted semantics returns 409 ProblemDetails, extension `code: idempotency-payload-conflict`. Authentication, live source membership, enabled organization/customer, operation permissions and approved mapping are checked before any receipt or conflict is exposed. A changed organization/customer outside the approved mapping returns 403 `target-not-authorized`, even when the key exists. Replay/lookup do not rerun mutable category/assignee selection rules. Fresh create and target validation do. + +Target validation accepts required organizationId/customerId, optional assignedToId (omitted/null means null), and optional categoryIds (omitted/null means empty). Its 200 response has contractVersion, receiverInstanceId, sourceInstanceId, sourceNamespaceId, valid=true and mapping. The mapping preserves explicit null assignment and distinct canonical GUID D category IDs sorted ordinally. Malformed/oversized input returns 400 `invalid-target-request`; unauthorized mapping returns 403 `target-not-authorized`; invalid assignee or inactive/incompatible/foreign categories return 422 `invalid-incident-target` with bounded field errors and no entity details. It creates no ticket, job, customer or notification. Validation is an observation and does not replace checks at creation. + +## Register and rotate a source + +Use an authenticated interactive local/OIDC instance administrator. Integration credentials cannot manage this registry. Create the API-purpose credential with current incident permissions for an enabled owner and the intended organization using the existing credential management flow. Never place the opaque secret in source registration, audit logs or contract files. + +1. Register with `POST /api/v1/integrations/netratel/sources/` and `{sourceInstanceId, organizationId, customerId, credentialId}`. IDs must refer to approved enabled local data; the customer must belong to the organization. The receiver returns 201 with its generated permanent sourceNamespaceId, approved mapping and revision=1. Unknown headers cannot self-register. A duplicate source instance returns 409 `source-already-registered`. +2. Inspect registrations with `GET /api/v1/integrations/netratel/sources/` (bounded to 256 results). Mapping and source identity are immutable. Each namespace has one approved organization/customer mapping. +3. For rotation, create a replacement API credential in the same organization. Bind it with `PUT /api/v1/integrations/netratel/sources/{sourceNamespaceId}/principal-bindings/{credentialId}` and `{expectedRevision, isEnabled:true}`. The source namespace remains unchanged, and the replacement can reconcile prior keys. A fresh unaffiliated credential cannot take over a source by copying its header. +4. Revoke the predecessor through existing credential management, or disable its binding with the same PUT and `isEnabled:false`. Disabling/revoking stops its access immediately on subsequent requests. The new source revision is returned; stale revisions return 409 `source-revision-conflict`. +5. Disable/enable the whole source with `PATCH /api/v1/integrations/netratel/sources/{sourceNamespaceId}` and `{expectedRevision, isEnabled}`. The namespace and accepted keys survive. No physical deletion or remapping operation exists. + +Registration/grant changes commit source state, binding and audit together. Audit records contain actor, UTC time, operation, approved mapping, principal identifier, state and revision, without secret values. Every replica reads current persisted state. Credential validity, enabled owner, API purpose and the current owner's effective permission intersection remain mandatory. Registry membership cannot expand credential scope. + +`IIncidentReceiverAuthorization` is the application boundary for future approved service principals. Its operation enum separates Create, ReadReceipt and ValidateTarget; binding keys distinguish principal kind from principal identity. A future implementation must map the three managed scopes in the JSON contract, require headers, enforce current service/source/mapping grants, and advertise OAuth only after real acceptance testing. The create route uses an explicit authenticated boundary plus per-request application authorization, so a later narrow machine grant need not inherit the human IncidentAccess role policy. Ordinary create still explicitly checks IncidentAccess. Other incident routes retain their existing policies. Token subject/client_id never becomes a requester or user FK. + +## Semantic fingerprint and bounds + +The receiver hashes a frozen typed projection using UTF-8 System.Text.Json Web defaults, compact JSON and the fixed property order in the JSON contract. The conformance vectors lock the exact serialized representation and hash. Property order in submitted JSON is irrelevant. Numeric priority uses the existing enum values 0–3. Strings are not NFC-folded or generally trimmed. Title/impact and ordered asset/attachment lists retain meaningful differences. Null/missing ordered lists equal empty lists. Category IDs are a distinct sorted set; empty GUIDs are omitted. CC values are trimmed, lowercased, distinct and sorted, independently of current customer email. Creation then omits the selected requester's email from delivery recipients. + +Description uses the existing HTML sanitizer and plain-text converter before hashing and creation. Missing description defaults to empty; explicit null is invalid. Missing priority defaults to Low; null/string priority is invalid. Missing/null/whitespace assignment equals null. Missing/null impact equals null; an empty impact is distinct. Due dates normalize to UTC, with unspecified dates interpreted as UTC and an invariant roundtrip representation with seven fractional digits. The accepted requester's email always comes from the approved customer. DTO requesterEmail is ignored and excluded from the fingerprint. + +Bodies are streamed into a bounded 131,072-byte buffer for keyed create, target validation and registry writes, including chunked bodies. Keyed input requires a nonblank title up to 256 characters, description up to 65,536, impact up to 4,096, mapping IDs up to 64, lists up to 128, asset/attachment strings up to 512 and CC strings up to 254. Violations fail before business effects. Ordinary no-header create retains its existing handling. + +## Transaction, effects and retention + +A real unique database index on (SourceNamespaceId, Key) reserves a receipt before calling CreateIncidentCommand. The incident, categories, SLA, notification/support delivery rows, immutable accepted JSON and ingress outbox effects commit in one database transaction. Existing `IIngressEffectContext` captures creation effects and confirmation, and `MailboxOutboxStore.FlushAsync` runs once before commit. Receipt GUIDs share the existing unrestricted outbox receipt namespace. The HTTP path binds captured email to the approved organization without entering background-only ingress routing authority. + +A losing concurrent writer rolls back, disposes the failed context, and reads the winner in a clean scope. Provider execution strategies also create fresh operation scopes. SQLite competing-writer retries are bounded and cancellation-aware. Rollback leaves no accepted receipt or externally visible effect. Replay never calls the create command or captures effects again. One logical confirmation enqueue is guaranteed; existing outbox leases, retry and uncertain SMTP submission handling govern transport delivery. External SMTP is not exactly-once. + +V1 retains full receipts indefinitely, advertises a 90-day minimum and disables eviction. Sender automatic retry must stop within 30 days of the original durable action creation time, with age persisted across restarts. Manual reconciliation can continue afterward. Future cleanup requires a durable tombstone or equivalent non-recreation barrier; removing a source/binding or rotating credentials must never make an accepted key new again. + +## Validation and rollout + +The receiver tests use the actual endpoint maps, real credential handler, current authorization service, application handlers and ingress decorators. Both provider migration chains are generated by normal EF tooling. Functional tests use file-backed SQLite and the repository's Testcontainers PostgreSQL harness without skips or mocks for persistence/authorization. They cover two independent API hosts and connections, response loss after commit, precommit fault/rollback, database reopen, separate real API process launches, source rotation, current authorization loss, immutable replay, validation, bounds and durable outbox claims. Golden vectors cover defaults/order equivalence and every consumed field's material change. Foreign scope changes are rejected by authorization before conflict disclosure. + +The producer was inspected at main `af6a7ed84de4463097258f44199cbd1c6fbbc3c9` and connector #152 head `f283e6af810209b8e7b6b2ef6a78428d4da0a28f`. Its proposal has null capability/header definitions and intentionally blocks automatic delivery. Replace that proposal with these exact reviewed contract/fixture files in the companion work. Receiver and producer semantic fingerprints belong to different projections and must not be substituted for each other. + +Published RatelDesk beta.8 is not compatible. Deploy a subsequent reviewed build containing the receiver merge and provider migrations, configure the public API URL, register/bind the approved source, and verify the authenticated live capability. Keep NetRatel delivery/retry capability-gated until that deployed build reports this contract and its guarantees. This receiver PR does not publish an intermediate release, retag beta.8, promote latest or create production tickets.