From afe14c33d60c5db74e260abe0d6c1bbff3c3f4eb Mon Sep 17 00:00:00 2001 From: Joshua Date: Tue, 17 Feb 2026 12:18:01 -0800 Subject: [PATCH] Add deterministic member app channel API and conformance docs --- README.md | 4 + docs/api/examples/member-channel.examples.md | 103 ++++++ docs/api/member-channel.openapi.yaml | 311 ++++++++++++++++++ docs/app-channel-spec.md | 8 + docs/conformance/member-channel-vectors.md | 22 ++ .../member-channel-backend-checklist.md | 53 +++ docs/implementation-mapping.md | 1 + docs/release-gate.md | 21 +- docs/roadmap-membership-platform.md | 7 + docs/roadmap-status.md | 2 + 10 files changed, 523 insertions(+), 9 deletions(-) create mode 100644 docs/api/examples/member-channel.examples.md create mode 100644 docs/api/member-channel.openapi.yaml create mode 100644 docs/conformance/member-channel-vectors.md create mode 100644 docs/handoff/member-channel-backend-checklist.md diff --git a/README.md b/README.md index 8e015bc..8566e24 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ docs/ membership-platform-interfaces.md conformance/ membership-gating-vectors.md + member-channel-vectors.md deployment/ README.md chain-config.template.json @@ -63,12 +64,15 @@ docs/ api/ secret-system.openapi.yaml marketplace.openapi.yaml + member-channel.openapi.yaml examples/ secret-system.examples.md marketplace.examples.md + member-channel.examples.md handoff/ membership-backend-checklist.md marketplace-backend-checklist.md + member-channel-backend-checklist.md schemas/ offer.v1.schema.json entitlement.v1.schema.json diff --git a/docs/api/examples/member-channel.examples.md b/docs/api/examples/member-channel.examples.md new file mode 100644 index 0000000..62cb1c5 --- /dev/null +++ b/docs/api/examples/member-channel.examples.md @@ -0,0 +1,103 @@ +# Member App Channel API Examples + +## Register Device Channel + +### Request + +```http +POST /member/channel/device/register +Authorization: Bearer +Content-Type: application/json + +{ + "wallet": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + "chain_id": 8453, + "device_id": "desktop-7f6f3a9b", + "platform": "desktop", + "app_version": "0.1.0", + "push_provider": "none" +} +``` + +### Response + +```json +{ + "channel_binding_id": "ch_01J9A4MQQ6V7H1S0A7R7ME7D4J", + "status": "active", + "poll_interval_seconds": 30, + "server_time": "2026-02-17T20:43:18Z" +} +``` + +## Poll Events + +### Request + +```http +GET /member/channel/events?wallet=0x742d35Cc6634C0532925a3b844Bc454e4438f44e&device_id=desktop-7f6f3a9b&cursor=evt_1030&limit=25 +Authorization: Bearer +``` + +### Response + +```json +{ + "wallet": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + "device_id": "desktop-7f6f3a9b", + "events": [ + { + "event_id": "evt_1031", + "class": "platform_update", + "created_at": "2026-02-17T20:42:11Z", + "title": "Platform update available", + "body": "A deterministic runtime update is ready for your current platform channel.", + "dedupe_key": "platform_update:0.1.1", + "requires_ack": true, + "policy_hash": "sha256:8b903f0d3a...", + "payload": { + "version": "0.1.1", + "channel": "stable" + } + } + ], + "next_cursor": "evt_1031", + "server_time": "2026-02-17T20:43:20Z" +} +``` + +## Acknowledge Event + +### Request + +```http +POST /member/channel/events/evt_1031/ack +Authorization: Bearer +Content-Type: application/json + +{ + "wallet": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + "device_id": "desktop-7f6f3a9b", + "acknowledged_at": "2026-02-17T20:43:25Z" +} +``` + +### Response + +```json +{ + "status": "acknowledged", + "event_id": "evt_1031", + "acknowledged_at": "2026-02-17T20:43:25Z" +} +``` + +## Membership Inactive Error + +```json +{ + "error": "membership_inactive", + "code": "membership_inactive", + "correlation_id": "req_01J9A4Q9GPDXDNZEWJ2FJS6F5R" +} +``` diff --git a/docs/api/member-channel.openapi.yaml b/docs/api/member-channel.openapi.yaml new file mode 100644 index 0000000..961101d --- /dev/null +++ b/docs/api/member-channel.openapi.yaml @@ -0,0 +1,311 @@ +openapi: 3.1.0 +info: + title: EDUT Member App Channel API + version: 1.0.0 + description: | + Deterministic member communication channel contract. + App notifications are derived from wallet-authenticated membership and entitlement state. +servers: + - url: https://api.edut.ai +security: + - WalletSession: [] +paths: + /member/channel/device/register: + post: + summary: Register or refresh a member app device channel binding. + operationId: registerMemberDevice + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceRegisterRequest' + responses: + '200': + description: Device channel binding active. + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceRegisterResponse' + '403': + description: Membership is not active for this wallet. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /member/channel/device/unregister: + post: + summary: Remove a device channel binding. + operationId: unregisterMemberDevice + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceUnregisterRequest' + responses: + '200': + description: Device channel binding removed. + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceUnregisterResponse' + '404': + description: Device channel binding not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /member/channel/events: + get: + summary: Poll deterministic member channel events. + operationId: listMemberChannelEvents + parameters: + - in: query + name: wallet + required: true + schema: + type: string + description: Wallet address tied to session. + - in: query + name: device_id + required: true + schema: + type: string + - in: query + name: cursor + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 25 + responses: + '200': + description: Event page for in-app inbox rendering. + content: + application/json: + schema: + $ref: '#/components/schemas/EventListResponse' + '403': + description: Membership inactive or suspended. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /member/channel/events/{event_id}/ack: + post: + summary: Acknowledge event delivery/read state. + operationId: acknowledgeMemberChannelEvent + parameters: + - in: path + name: event_id + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EventAckRequest' + responses: + '200': + description: Event acknowledged. + content: + application/json: + schema: + $ref: '#/components/schemas/EventAckResponse' + '404': + description: Event not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' +components: + securitySchemes: + WalletSession: + type: http + scheme: bearer + bearerFormat: EDUT-WALLET-SESSION + schemas: + DeviceRegisterRequest: + type: object + required: + - wallet + - chain_id + - device_id + - platform + - app_version + properties: + wallet: + type: string + description: Hex wallet address. + chain_id: + type: integer + description: Active chain id in app session. + device_id: + type: string + description: Stable app-install identifier. + platform: + type: string + enum: [ios, android, desktop] + app_version: + type: string + push_provider: + type: string + enum: [apns, fcm, webpush, none] + default: none + push_token: + type: string + description: Provider token if push is enabled. + DeviceRegisterResponse: + type: object + required: + - channel_binding_id + - status + - poll_interval_seconds + - server_time + properties: + channel_binding_id: + type: string + status: + type: string + enum: [active] + poll_interval_seconds: + type: integer + minimum: 10 + server_time: + type: string + format: date-time + DeviceUnregisterRequest: + type: object + required: + - wallet + - device_id + properties: + wallet: + type: string + device_id: + type: string + DeviceUnregisterResponse: + type: object + required: + - status + - wallet + - device_id + properties: + status: + type: string + enum: [removed] + wallet: + type: string + device_id: + type: string + EventListResponse: + type: object + required: + - wallet + - device_id + - events + - next_cursor + - server_time + properties: + wallet: + type: string + device_id: + type: string + events: + type: array + items: + $ref: '#/components/schemas/MemberEvent' + next_cursor: + type: string + nullable: true + server_time: + type: string + format: date-time + MemberEvent: + type: object + required: + - event_id + - class + - created_at + - title + - body + - dedupe_key + - policy_hash + properties: + event_id: + type: string + class: + type: string + enum: + - offer_available + - entitlement_activated + - platform_update + - publisher_update + - membership_policy + created_at: + type: string + format: date-time + title: + type: string + body: + type: string + dedupe_key: + type: string + requires_ack: + type: boolean + default: true + policy_hash: + type: string + payload: + type: object + additionalProperties: true + EventAckRequest: + type: object + required: + - wallet + - device_id + - acknowledged_at + properties: + wallet: + type: string + device_id: + type: string + acknowledged_at: + type: string + format: date-time + EventAckResponse: + type: object + required: + - status + - event_id + - acknowledged_at + properties: + status: + type: string + enum: [acknowledged] + event_id: + type: string + acknowledged_at: + type: string + format: date-time + ErrorResponse: + type: object + required: + - error + - code + properties: + error: + type: string + code: + type: string + correlation_id: + type: string diff --git a/docs/app-channel-spec.md b/docs/app-channel-spec.md index b74f9e4..ccfc71c 100644 --- a/docs/app-channel-spec.md +++ b/docs/app-channel-spec.md @@ -35,6 +35,14 @@ No active membership -> no app access channel. 2. Delivery payloads must include deterministic event ids for dedupe and audit. 3. Membership revocation/suspension immediately suppresses member-channel delivery. 4. No marketing list fan-out disconnected from entitlement state. +5. Event polling inbox is canonical; push transport is optional acceleration. + +## Contract References + +1. `docs/api/member-channel.openapi.yaml` +2. `docs/api/examples/member-channel.examples.md` +3. `docs/handoff/member-channel-backend-checklist.md` +4. `docs/conformance/member-channel-vectors.md` ## Non-Goals diff --git a/docs/conformance/member-channel-vectors.md b/docs/conformance/member-channel-vectors.md new file mode 100644 index 0000000..31d88d6 --- /dev/null +++ b/docs/conformance/member-channel-vectors.md @@ -0,0 +1,22 @@ +# Conformance Vectors: Member App Channel v1 + +These vectors verify deterministic member communication behavior. + +## Vector Set + +1. `MC-001` Register device with active membership -> returns `status=active`. +2. `MC-002` Register device with inactive membership -> returns `membership_inactive`. +3. `MC-003` Poll events with valid cursor -> returns monotonic events + next cursor. +4. `MC-004` Poll events with tampered cursor -> fail closed with validation error. +5. `MC-005` Ack existing event -> returns `status=acknowledged`. +6. `MC-006` Ack same event again -> idempotent success, no duplicate side effects. +7. `MC-007` Membership revoked after registration -> subsequent event poll blocked. +8. `MC-008` Push unavailable -> event still available in poll inbox. +9. `MC-009` Dedupe key collision attempt -> backend rejects duplicate event insertion for same member stream. +10. `MC-010` Wallet mismatch between session and payload -> request rejected. + +## Pass Criteria + +1. All vectors pass in CI and staging. +2. Any failure blocks release per `docs/release-gate.md`. +3. Evidence artifact includes vector id, input, output, and correlation id. diff --git a/docs/handoff/member-channel-backend-checklist.md b/docs/handoff/member-channel-backend-checklist.md new file mode 100644 index 0000000..d6130c2 --- /dev/null +++ b/docs/handoff/member-channel-backend-checklist.md @@ -0,0 +1,53 @@ +# Backend Handoff Checklist: Member App Channel + +This checklist defines backend requirements for app-native member communication. + +## Required Endpoints + +1. `POST /member/channel/device/register` +2. `POST /member/channel/device/unregister` +3. `GET /member/channel/events` +4. `POST /member/channel/events/{event_id}/ack` + +## Contract Source + +1. `docs/api/member-channel.openapi.yaml` +2. `docs/api/examples/member-channel.examples.md` + +## Deterministic Rules + +1. Wallet session is mandatory for all channel calls. +2. Membership status must be active for event delivery. +3. Event ids and dedupe keys are immutable once emitted. +4. Event ordering is monotonic by server sequence. +5. `ack` is idempotent per `event_id` + `device_id`. + +## Data Requirements + +Persist at minimum: + +1. Device channel binding (`wallet`, `device_id`, `platform`, `push_provider`, `push_token`) +2. Membership status snapshot at event emission time +3. Event envelope (`event_id`, class, title/body, payload, policy_hash, dedupe_key) +4. Delivery and ack timestamps + +## Security Requirements + +1. Rate limit register/events/ack paths. +2. Validate wallet in request matches authenticated session wallet. +3. Reject event polling for suspended/revoked memberships. +4. Protect against cursor tampering and replay. + +## Observability Requirements + +1. Counter metrics per event class emitted. +2. Counter metrics for successful/failed ack calls. +3. Channel registration churn metrics by platform. +4. Correlation id in all responses and logs. + +## Done Criteria + +1. App can register channel and poll events deterministically. +2. Push loss does not lose events because inbox polling is canonical. +3. Membership status changes immediately gate event visibility. +4. API behavior matches OpenAPI contract and examples. diff --git a/docs/implementation-mapping.md b/docs/implementation-mapping.md index 752dd80..40b13b5 100644 --- a/docs/implementation-mapping.md +++ b/docs/implementation-mapping.md @@ -11,6 +11,7 @@ 1. Intent/verify/quote/confirm/status endpoints. 2. Deterministic state transitions and persistence. 3. Chain verification and policy hash enforcement. +4. Member app channel endpoints for device registration and event polling. ## Runtime/Kernel Responsibilities diff --git a/docs/release-gate.md b/docs/release-gate.md index 1d6acbd..52b053e 100644 --- a/docs/release-gate.md +++ b/docs/release-gate.md @@ -13,21 +13,24 @@ This gate controls deploy/no-deploy decisions for membership-gated commerce chan ## Deploy Criteria (All Required) 1. `docs/conformance/membership-gating-vectors.md`: all vectors pass. -2. OpenAPI and implementation remain compatible. -3. Signature replay tests pass. -4. Quote expiry tests pass. -5. Tx mismatch tests pass. -6. Membership gate blocks non-members in all checkout paths. -7. Terms/privacy copy still match utility-access framing. -8. Structured logs and metrics are emitted for each state transition. +2. `docs/conformance/member-channel-vectors.md`: all vectors pass. +3. OpenAPI and implementation remain compatible. +4. Signature replay tests pass. +5. Quote expiry tests pass. +6. Tx mismatch tests pass. +7. Membership gate blocks non-members in all checkout paths. +8. Member channel blocks inactive memberships. +9. Terms/privacy copy still match utility-access framing. +10. Structured logs and metrics are emitted for each state transition. ## No-Deploy Triggers 1. Any conformance vector failure. 2. Any path that allows purchase without active membership. 3. Any activation path that proceeds with non-active entitlement. -4. Any missing audit evidence on successful purchase. -5. Any breaking API change without version bump and migration note. +4. Any member channel path serving events to suspended/revoked memberships. +5. Any missing audit evidence on successful purchase. +6. Any breaking API change without version bump and migration note. ## Evidence Bundle Required for Release diff --git a/docs/roadmap-membership-platform.md b/docs/roadmap-membership-platform.md index c0f5e0e..dbb42c9 100644 --- a/docs/roadmap-membership-platform.md +++ b/docs/roadmap-membership-platform.md @@ -89,3 +89,10 @@ This roadmap is intentionally step-based and dependency-ordered. No timeline com - visit -> signature -> membership mint -> first entitlement purchase. 2. Tune copy, wallet guidance, and pricing policy using deterministic metrics. 3. Expand issuer ecosystem only after quality and support controls are stable. + +## Step 14: Lock Member App Channel + +1. Register device channels with wallet-authenticated session. +2. Deliver deterministic event inbox (polling canonical, push optional). +3. Gate event visibility by active membership status. +4. Track ack receipts with immutable event ids and dedupe keys. diff --git a/docs/roadmap-status.md b/docs/roadmap-status.md index cb2f4dd..d6d0e27 100644 --- a/docs/roadmap-status.md +++ b/docs/roadmap-status.md @@ -40,6 +40,7 @@ Implemented now: 13. Issuer onboarding pack, migration policy, trust page spec, and integration mapping docs. 14. Public `/trust` page scaffold aligned with trust-page spec. 15. Dedicated marketplace OpenAPI contract and examples. +16. Member app channel contract, examples, backend handoff checklist, and conformance vectors. Remaining in this repo: @@ -52,3 +53,4 @@ Cross-repo dependencies (kernel/backend/contracts): 2. Implement membership contract and membership status reads. 3. Implement checkout APIs and entitlement mint pipeline. 4. Implement runtime entitlement gate and evidence receipts. +5. Implement member app channel APIs and deterministic event stream storage.