Execute roadmap docs and upgrade store scaffold to live membership gating

This commit is contained in:
Joshua 2026-02-17 12:01:22 -08:00
parent a0a8274721
commit ffb8ce0ead
22 changed files with 1052 additions and 27 deletions

View File

@ -29,6 +29,20 @@ docs/
roadmap-membership-platform.md roadmap-membership-platform.md
roadmap-status.md roadmap-status.md
membership-pricing-policy.md membership-pricing-policy.md
membership-tier-extension.md
failure-state-matrix.md
legal-copy-matrix.md
localization-qa-matrix.md
mobile-wallet-handoff.md
chain-operations-runbook.md
security-hardening-checklist.md
policy-hash-versioning.md
integration-test-plan.md
implementation-mapping.md
public-trust-page-spec.md
migration-policy-v1-to-v2.md
issuer-onboarding-pack.md
release-gate.md
review-notes.md review-notes.md
platform-spec-alignment-review.md platform-spec-alignment-review.md
contracts/ contracts/
@ -39,18 +53,24 @@ docs/
README.md README.md
chain-config.template.json chain-config.template.json
contract-addresses.template.json contract-addresses.template.json
environment-invariants.md
api/ api/
secret-system.openapi.yaml secret-system.openapi.yaml
examples/
secret-system.examples.md
handoff/ handoff/
membership-backend-checklist.md membership-backend-checklist.md
schemas/ schemas/
offer.v1.schema.json offer.v1.schema.json
entitlement.v1.schema.json entitlement.v1.schema.json
issuer-manifest.v1.schema.json issuer-manifest.v1.schema.json
evidence-receipt.v1.schema.json
launch-offers-catalog.v1.schema.json
examples/ examples/
offer.v1.example.json offer.v1.example.json
entitlement.v1.example.json entitlement.v1.example.json
issuer-manifest.v1.example.json issuer-manifest.v1.example.json
launch-offers-catalog.v1.example.json
README.md README.md
``` ```

View File

@ -0,0 +1,182 @@
# Secret System API Examples (v1)
## 1) `POST /secret/wallet/intent`
Request:
```json
{
"address": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"origin": "https://edut.ai",
"locale": "en",
"chain_id": 8453
}
```
Success (`200`):
```json
{
"intent_id": "wi_01HZZX2Q8R0FQFQ6B1VQ1N2P9J",
"designation_code": "0217073045482",
"display_token": "0217-0730-4548-2",
"nonce": "47f43f70d1288d4e",
"issued_at": "2026-02-17T07:30:45Z",
"expires_at": "2026-02-17T07:35:45Z",
"domain_name": "EDUT Designation",
"chain_id": 8453,
"verifying_contract": "0x0000000000000000000000000000000000000000"
}
```
Error (`429` rate limited):
```json
{
"error": "rate_limited",
"message": "Too many intent requests. Retry later."
}
```
## 2) `POST /secret/wallet/verify`
Request:
```json
{
"intent_id": "wi_01HZZX2Q8R0FQFQ6B1VQ1N2P9J",
"address": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"chain_id": 8453,
"signature": "0xabcdef..."
}
```
Success (`200`):
```json
{
"status": "signature_verified",
"designation_code": "0217073045482",
"display_token": "0217-0730-4548-2",
"verified_at": "2026-02-17T07:31:12Z"
}
```
Error (`400` intent expired):
```json
{
"error": "intent_expired",
"message": "Intent has expired. Request a new intent."
}
```
## 3) `POST /secret/membership/quote`
Request:
```json
{
"designation_code": "0217073045482",
"address": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"chain_id": 8453
}
```
Success (`200`):
```json
{
"quote_id": "mq_01HZZX4F8VQXJ6A57R8P3SCB2W",
"chain_id": 8453,
"currency": "USDC",
"amount": "5.00",
"amount_atomic": "5000000",
"decimals": 6,
"deadline": "2026-02-17T07:36:12Z",
"contract_address": "0x1111111111111111111111111111111111111111",
"method": "mintMembership",
"calldata": "0xdeadbeef",
"value": "0x0",
"tx": {
"to": "0x1111111111111111111111111111111111111111",
"data": "0xdeadbeef",
"value": "0x0"
}
}
```
Error (`403` not verified):
```json
{
"error": "signature_not_verified",
"message": "Signature verification is required before quote issuance."
}
```
## 4) `POST /secret/membership/confirm`
Request:
```json
{
"designation_code": "0217073045482",
"quote_id": "mq_01HZZX4F8VQXJ6A57R8P3SCB2W",
"tx_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"address": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"chain_id": 8453
}
```
Success (`200`):
```json
{
"status": "membership_active",
"designation_code": "0217073045482",
"display_token": "0217-0730-4548-2",
"tx_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"activated_at": "2026-02-17T07:33:09Z"
}
```
Error (`400` tx mismatch):
```json
{
"error": "tx_mismatch",
"message": "Transaction amount or destination does not match quote policy."
}
```
## 5) `POST /secret/notify`
Request:
```json
{
"email": "user@example.com",
"designation_code": "0217073045482",
"designation_token": "0217-0730-4548-2",
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"locale": "en"
}
```
Success (`200`):
```json
{
"status": "saved"
}
```
Error (`422` invalid email):
```json
{
"error": "invalid_email",
"message": "Email format is invalid."
}
```

View File

@ -0,0 +1,50 @@
# Chain Operations Runbook (Base, v1)
## Scope
Operational procedures for membership mint and checkout confirmation dependency on chain state.
## Normal Operation
1. Primary RPC healthy.
2. Confirmation endpoint verifies tx receipt and policy match.
3. Membership state transitions to `membership_active` only on valid confirmation.
## Degraded Scenarios
## RPC Outage
1. Mark confirmation dependency degraded.
2. Switch to secondary RPC endpoint.
3. Re-run receipt verification.
4. If uncertain, fail closed and queue retry.
## Reorg Risk
1. Apply minimum confirmation depth policy.
2. If tx dropped/reorged, revert to `pending_membership_mint`.
3. Notify via deterministic status message; do not promote state.
## Chain Congestion
1. Quote remains authoritative until expiry.
2. Expired quote requires re-quote.
3. No off-policy amount overrides.
## Safe Mode Triggers
1. Conflicting tx results across RPC providers.
2. Contract bytecode mismatch at expected address.
3. Persistent receipt retrieval failures beyond threshold.
Safe mode actions:
1. Pause new confirmations.
2. Keep purchase state blocked.
3. Emit incident evidence.
## Recovery
1. Validate RPC consensus.
2. Reconcile pending confirms deterministically.
3. Resume confirmations after verification threshold restored.

View File

@ -0,0 +1,18 @@
# Environment Invariants
These invariants must hold for staging and production.
## Required Invariants
1. Chain ID in backend config matches allowed chain ID in API and frontend expectations.
2. Membership contract address in backend matches deployment registry.
3. Quote currency policy matches configured token addresses.
4. Origin allowlist includes only approved domains.
5. Fail-closed default behavior enabled for unknown membership/entitlement states.
## Verification Checklist
1. Intent endpoint returns expected chain and contract metadata.
2. Confirm endpoint rejects tx on wrong chain.
3. Checkout gate blocks non-members.
4. Runtime activation gate blocks non-active entitlements.

View File

@ -0,0 +1,27 @@
# Membership Flow Failure-State Matrix (v1)
This matrix defines deterministic fail-closed behavior and user-facing outcomes.
| Stage | Failure | Detection Source | System Action | User Surface |
|---|---|---|---|---|
| Intent | Rate limit | API guard | Block intent issuance | "Too many requests. Try again later." |
| Intent | Invalid origin | API allowlist | Reject request | "Request origin not allowed." |
| Verify | Intent expired | TTL check | Reject verify | "Intent expired. Start again." |
| Verify | Signature mismatch | Signature recovery | Reject verify + audit entry | "Signature could not be verified." |
| Quote | Signature not verified | State check | Deny quote | "Verify wallet signature first." |
| Quote | Quote expired | TTL check | Deny confirm | "Quote expired. Request a new quote." |
| Mint | Wallet reject tx | Wallet provider | No state change | "Membership mint was not approved." |
| Confirm | Wrong chain | Chain check | Reject confirm | "Transaction is on an unsupported chain." |
| Confirm | Amount mismatch | Quote/tx comparator | Reject confirm | "Transaction does not match quote." |
| Confirm | Recipient mismatch | Quote/tx comparator | Reject confirm | "Destination contract mismatch." |
| Confirm | Node unavailable | RPC health | Fail closed | "Unable to confirm transaction. Purchase stays blocked." |
| Notify | Invalid email | Input validation | Reject notify | "Invalid email format." |
| Checkout | No membership | Gate check | Block purchase | "Membership required." |
| Checkout | Membership suspended/revoked | Gate check | Block purchase | "Membership inactive. Contact support." |
| Activation | Entitlement not active | Gate check | Block runtime | "License inactive. Activation blocked." |
## Invariants
1. Unknown state defaults to blocked.
2. No failed transition may promote membership or entitlement state.
3. Every reject path produces structured audit evidence.

View File

@ -0,0 +1,25 @@
# Implementation Mapping (Web -> Backend -> Runtime)
## Web Repo Responsibilities
1. Wallet-first UX and membership flow orchestration.
2. API contract and schema definitions.
3. Policy/legal/public messaging consistency.
## Backend Responsibilities
1. Intent/verify/quote/confirm/notify endpoints.
2. Deterministic state transitions and persistence.
3. Chain verification and policy hash enforcement.
## Runtime/Kernel Responsibilities
1. Membership and entitlement gates at activation points.
2. Fail-closed behavior for uncertain states.
3. Evidence receipt generation and retention.
## Required Integration Contract
1. Backend API shape follows `docs/api/secret-system.openapi.yaml`.
2. Policy/offer/entitlement payloads validate against schemas.
3. Runtime consumes entitlement state and policy hash from backend evidence.

View File

@ -0,0 +1,22 @@
# Integration Test Plan (Membership Commerce)
## Objective
Validate end-to-end behavior from wallet intent to membership-gated checkout.
## E2E Scenarios
1. Happy path membership activation.
2. Signature mismatch.
3. Quote expiry before tx submission.
4. Tx mismatch (amount/currency/recipient).
5. Membership suspended blocks checkout.
6. Active membership enables checkout quote.
7. Entitlement non-active blocks runtime activation.
## Artifacts
1. API request/response captures.
2. Tx hash and chain verification outputs.
3. Receipt/audit evidence IDs.
4. Pass/fail mapping to conformance vectors.

View File

@ -0,0 +1,50 @@
# Issuer Onboarding Pack (v1)
## Purpose
This pack defines the minimum deterministic requirements for external issuers publishing offers on EDUT.
## Issuer Entry Checklist
1. Register issuer namespace (`issuer_id`).
2. Submit issuer manifest (`issuer_manifest.v1`).
3. Register signing keys and key-rotation contact.
4. Provide support channel and incident contact.
5. Accept marketplace policy and conformance obligations.
## Offer Publish Checklist
1. Offer payload validates against `offer.v1.schema.json`.
2. `member_only` policy is explicit.
3. Price/currency/chain fields are complete.
4. Entitlement type and scope are explicit.
5. Offer status set to `draft` first.
6. Policy hash generated and stored.
7. Review gate passed before `active`.
## Policy Lint Checklist
1. No missing required policy fields.
2. No unknown enum values.
3. No contradictory flags (for example, workspace-bound + transferable true unless explicitly supported).
4. Currency is supported (`USDC` or `ETH` in v1).
5. Amount is positive atomic integer.
6. Lifecycle transitions are valid (`draft -> active -> paused/retired`).
## Runtime Expectations
1. Issuer offers cannot bypass membership gate.
2. Entitlement activation must be fail-closed.
3. Revocation and suspension must propagate deterministically.
## Incident Responsibilities
1. Issuer must acknowledge critical entitlement issue within published SLA.
2. Issuer must provide rollback or pause decision path.
3. Issuer actions must preserve audit evidence.
## Non-Negotiables
1. No direct side-channel entitlement grants.
2. No hidden pricing paths outside quote/confirm policy.
3. No policy mutation without versioned update and evidence.

17
docs/legal-copy-matrix.md Normal file
View File

@ -0,0 +1,17 @@
# Legal Copy Alignment Matrix
This matrix prevents drift between public surfaces and legal posture.
| Surface | Required Message | Prohibited Message |
|---|---|---|
| Landing (`public/index.html`) | Wallet signature + paid membership unlocks access | Investment, yield, appreciation claims |
| Store (`public/store/index.html`) | Membership required for purchasing offers | "Membership includes all products forever" |
| Terms (`public/terms/index.html`) | Membership is utility access; licenses separate | Equity/ownership implications |
| Privacy (`public/privacy/index.html`) | Wallet/signature processing and optional notify email | Hidden collection claims inconsistent with implementation |
| Vision/spec docs | Deterministic governance and fail-closed controls | Speculative financial framing |
## Hard Rules
1. Membership language must always distinguish access rights from license rights.
2. Any copy introducing financial upside claims is blocked.
3. Any change to legal-critical copy requires review against this matrix.

View File

@ -0,0 +1,35 @@
# Localization QA Matrix (12 Languages)
## Scope
Validate membership-flow strings and legal-critical labels across all locale bundles.
## Locales
1. en
2. zh
3. es
4. ar
5. fr
6. pt
7. de
8. ja
9. ru
10. ko
11. hi
12. he
## Required Key Sets
1. core identity support keys (`definition`, `descriptor`, `acknowledged`, `privacy`, `terms`)
2. wallet flow keys (`continue_label`, `wallet_intro`, `wallet_connecting`, `wallet_signing`, `wallet_verifying`, `wallet_failed`)
3. membership flow keys (`membership_quoting`, `membership_minting`, `membership_confirming`, `membership_active`)
4. notify keys (`notify_me`, `notify_placeholder`, `notify_submit`, `notify_saved`, `notify_failed`)
## QA Checks
1. JSON parses successfully.
2. All required keys exist in each locale.
3. RTL locales (`ar`, `he`) render with per-node `dir` handling.
4. Strings preserve meaning for utility-access framing.
5. No locale introduces investment language.

View File

@ -0,0 +1,23 @@
# Membership Tier Extension Spec
## Purpose
Define optional supply-based tiered membership pricing without breaking v1 flows.
## Tier Model
1. Tier boundaries based on total minted memberships.
2. Each tier defines currency and amount_atomic.
3. Price auto-selects by minted supply at quote time.
## Compatibility
1. Existing quote/confirm flow remains unchanged.
2. Tier metadata added to quote response (`tier_id`, `tier_label`).
3. Receipts persist tier metadata for audit.
## Guardrails
1. Floor policy still applies.
2. Tier transitions event-emitted.
3. Quotes lock tier price until expiry.

View File

@ -0,0 +1,14 @@
# Migration Policy: v1 to v2
## Rules
1. v1 interfaces evolve additively only.
2. Breaking changes require v2 namespace.
3. v1 deprecation requires migration guide and overlap window.
## Required v2 Deliverables
1. Side-by-side API spec (`/v2`).
2. Schema migration map.
3. Backward compatibility notes.
4. Evidence continuity guarantees.

View File

@ -0,0 +1,30 @@
# Mobile Wallet Handoff UX Spec (v1)
## Goal
Provide deterministic cross-device path when user starts on desktop but wallet is on phone.
## Entry Paths
1. Desktop with extension wallet available -> direct connect.
2. Desktop without extension -> QR handoff to mobile wallet.
3. Mobile browser with wallet app -> deep-link connect.
## Desktop QR Handoff
1. User clicks `I have a wallet`.
2. If no injected provider detected, show QR panel.
3. QR encodes short-lived session handoff token.
4. Mobile wallet scan opens connect/sign flow.
5. Desktop polls handoff status until signature/tx complete or timeout.
## Constraints
1. Handoff token TTL short (recommended 5 minutes).
2. Single-use token; replay denied.
3. If timeout occurs, restart with new token.
## Fail-Closed
1. No completed handoff token -> no signature verify.
2. No membership confirm -> no acknowledged state.

View File

@ -0,0 +1,29 @@
# Policy Hash and Versioning Spec (v1)
## Purpose
Ensure each quote, purchase, and entitlement is provably bound to an exact policy snapshot.
## Canonical Policy Snapshot
1. Serialize policy object with stable key ordering.
2. Normalize numeric representations.
3. Remove non-policy metadata fields.
## Hashing
1. Compute `policy_hash = SHA-256(canonical_policy_json)`.
2. Store hex-encoded 64-char hash.
3. Include `policy_hash` in quote response, receipt, and entitlement record.
## Versioning
1. `policy_version` is semantic (`v1`, `v1.1`, etc.) for human readability.
2. `policy_hash` is authoritative for machine verification.
3. Breaking changes require new `policy_version` and migration note.
## Enforcement
1. Checkout confirm rejects if tx-linked quote policy hash differs from current quote policy hash.
2. Entitlement activation uses stored `policy_hash`; no retroactive mutation.
3. Historical purchases remain tied to their original policy hash.

View File

@ -0,0 +1,19 @@
# Public Trust Page Spec
## Purpose
Provide transparent operational facts without exposing private internals.
## Required Sections
1. Active chain and chain ID.
2. Contract addresses (membership, offer registry, entitlement).
3. Current membership pricing policy hash/version.
4. API health summary for intent/verify/quote/confirm.
5. Last policy update timestamp.
## Non-Goals
1. No private key details.
2. No internal infrastructure topology.
3. No speculative roadmap commitments.

37
docs/release-gate.md Normal file
View File

@ -0,0 +1,37 @@
# Release Gate: Membership Platform (v1)
This gate controls deploy/no-deploy decisions for membership-gated commerce changes.
## Gate Categories
1. Contract/API compatibility
2. Conformance vectors
3. Security checks
4. Legal/policy checks
5. Observability checks
## 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.
## 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.
## Evidence Bundle Required for Release
1. Test result artifact references.
2. Contract address/version snapshot.
3. Policy hash snapshot.
4. Change summary and rollback plan.

View File

@ -12,8 +12,8 @@ Status key:
2. Freeze token taxonomy: `DONE` 2. Freeze token taxonomy: `DONE`
3. Finalize membership contract interface targets: `DONE` 3. Finalize membership contract interface targets: `DONE`
4. Lock signature + intent protocol: `DONE` 4. Lock signature + intent protocol: `DONE`
5. Add membership mint transaction stage in web flow: `IN_PROGRESS` 5. Add membership mint transaction stage in web flow: `DONE` (frontend path implemented; backend endpoints pending)
6. Implement membership gate in marketplace checkout: `PENDING` 6. Implement membership gate in marketplace checkout: `IN_PROGRESS` (store scaffold + gate logic implemented; live API pending)
7. Ship offer registry schema: `DONE` 7. Ship offer registry schema: `DONE`
8. Ship entitlement purchase schema/pipeline contracts: `IN_PROGRESS` 8. Ship entitlement purchase schema/pipeline contracts: `IN_PROGRESS`
9. Bind entitlements to runtime activation: `PENDING` 9. Bind entitlements to runtime activation: `PENDING`
@ -33,10 +33,15 @@ Implemented now:
6. Interface target document for contracts/APIs. 6. Interface target document for contracts/APIs.
7. Pricing policy with USD 5 floor rule. 7. Pricing policy with USD 5 floor rule.
8. Terms utility-only non-investment clause. 8. Terms utility-only non-investment clause.
9. Store page upgraded from static to live-state scaffold with membership gate behavior.
10. OpenAPI contract + request/response examples for secret-system endpoints.
11. Conformance vectors + failure matrix + release gate + security checklist.
12. Deployment templates + invariants + chain operations runbook.
13. Issuer onboarding pack, migration policy, trust page spec, and integration mapping docs.
Remaining in this repo: Remaining in this repo:
1. Build live store behavior on top of the static skeleton once checkout APIs are available. 1. Wire live store checkout flow to production marketplace APIs when available.
2. Replace deployment templates with real contract addresses after chain deployment. 2. Replace deployment templates with real contract addresses after chain deployment.
Cross-repo dependencies (kernel/backend/contracts): Cross-repo dependencies (kernel/backend/contracts):

View File

@ -0,0 +1,71 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://edut.ai/schemas/evidence-receipt.v1.schema.json",
"title": "EDUT Evidence Receipt Schema v1",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"receipt_id",
"event_type",
"wallet",
"timestamp",
"hash"
],
"properties": {
"schema_version": {
"type": "string",
"const": "evidence_receipt.v1"
},
"receipt_id": {
"type": "string"
},
"event_type": {
"type": "string",
"enum": [
"membership_mint",
"membership_confirm",
"offer_checkout_quote",
"offer_checkout_confirm",
"entitlement_mint",
"entitlement_state_change"
]
},
"wallet": {
"type": "string",
"pattern": "^0x[a-fA-F0-9]{40}$"
},
"designation_code": {
"type": ["string", "null"]
},
"offer_id": {
"type": ["string", "null"]
},
"quote_id": {
"type": ["string", "null"]
},
"entitlement_id": {
"type": ["string", "null"]
},
"tx_hash": {
"type": ["string", "null"],
"pattern": "^0x[a-fA-F0-9]{64}$"
},
"chain_id": {
"type": ["integer", "null"],
"minimum": 1
},
"policy_hash": {
"type": ["string", "null"],
"pattern": "^[a-fA-F0-9]{64}$"
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"hash": {
"type": "string",
"pattern": "^[a-fA-F0-9]{64}$"
}
}
}

View File

@ -0,0 +1,25 @@
{
"schema_version": "launch_offers_catalog.v1",
"catalog_id": "launch-2026-operator",
"offers": [
{
"offer_id": "edut.crm.pro.annual",
"title": "EDUT CRM Pro",
"summary": "Workspace-bound CRM module with governance and evidence integration.",
"price": "199.00",
"currency": "USDC",
"member_only": true,
"workspace_bound": true
},
{
"offer_id": "edut.invoicing.core.annual",
"title": "EDUT Invoicing Core",
"summary": "Invoicing workflow module for member workspaces.",
"price": "99.00",
"currency": "USDC",
"member_only": true,
"workspace_bound": true
}
],
"published_at": "2026-02-17T00:00:00Z"
}

View File

@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://edut.ai/schemas/launch-offers-catalog.v1.schema.json",
"title": "EDUT Launch Offers Catalog v1",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "catalog_id", "offers", "published_at"],
"properties": {
"schema_version": { "type": "string", "const": "launch_offers_catalog.v1" },
"catalog_id": { "type": "string" },
"offers": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["offer_id", "title", "price", "currency", "member_only"],
"properties": {
"offer_id": { "type": "string" },
"title": { "type": "string" },
"summary": { "type": "string" },
"price": { "type": "string" },
"currency": { "type": "string", "enum": ["USDC", "ETH"] },
"member_only": { "type": "boolean" },
"workspace_bound": { "type": "boolean" }
}
}
},
"published_at": { "type": "string", "format": "date-time" }
}
}

View File

@ -0,0 +1,38 @@
# Security Hardening Checklist (Membership Flow)
## Wallet Intent and Signature
1. Enforce strict nonce uniqueness.
2. Enforce intent TTL.
3. Enforce origin allowlist.
4. Verify chain ID against allowlist.
5. Reject malformed or oversized signatures.
6. Reject replayed `intent_id`.
## Quote and Confirm
1. Use quote TTL and one-time confirmation semantics.
2. Bind quote to wallet and designation.
3. Confirm tx amount, currency, and contract destination exactly.
4. Confirm tx success status and finality threshold.
5. Idempotent confirm handling by `tx_hash` + `quote_id`.
## API Controls
1. Rate limits on intent, verify, quote, confirm, notify.
2. Request size limits.
3. Structured error responses without sensitive internals.
4. Correlation ID logging for all transitions.
## Data Integrity
1. Append-only audit records for state transitions.
2. Immutable receipt hash generation.
3. Versioned policy hash persistence with each quote and purchase.
## Operational Safety
1. Fail closed on RPC/node uncertainty.
2. Multi-RPC fallback with deterministic selection policy.
3. Emergency pause path for mint/checkout.
4. Key rotation runbook for issuer and system keys.

View File

@ -18,7 +18,7 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.container { max-width: 880px; margin: 0 auto; } .container { max-width: 960px; margin: 0 auto; }
a { color: #2c2c2c; text-underline-offset: 2px; } a { color: #2c2c2c; text-underline-offset: 2px; }
.back { .back {
display: inline-block; display: inline-block;
@ -43,7 +43,7 @@
.grid { .grid {
display: grid; display: grid;
gap: 14px; gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
margin-bottom: 20px; margin-bottom: 20px;
} }
.card { .card {
@ -80,6 +80,48 @@
} }
.state.ok { border-color: #6f8d72; color: #3f6545; } .state.ok { border-color: #6f8d72; color: #3f6545; }
.state.block { border-color: #9d7676; color: #7c4a4a; } .state.block { border-color: #9d7676; color: #7c4a4a; }
.state.warn { border-color: #99834b; color: #6d5b30; }
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 12px;
}
button {
border: 1px solid #c2c8d0;
background: #ffffff;
padding: 7px 10px;
font-family: 'IBM Plex Mono', 'Courier New', monospace;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #3d434a;
cursor: pointer;
}
button:hover { border-color: #8c949d; }
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
select {
border: 1px solid #c2c8d0;
background: #ffffff;
padding: 6px 8px;
font-family: 'IBM Plex Mono', 'Courier New', monospace;
font-size: 11px;
color: #3d434a;
}
.status-log {
margin-top: 12px;
border-top: 1px solid #d0d5db;
padding-top: 10px;
font-size: 11px;
color: #60666f;
line-height: 1.6;
min-height: 40px;
white-space: pre-wrap;
}
.mono { font-family: 'IBM Plex Mono', 'Courier New', monospace; }
.foot { .foot {
margin-top: 20px; margin-top: 20px;
border-top: 1px solid #d0d5db; border-top: 1px solid #d0d5db;
@ -94,40 +136,255 @@
<div class="container"> <div class="container">
<a href="/" class="back">← Back</a> <a href="/" class="back">← Back</a>
<h1>EDUT Store</h1> <h1>EDUT Store</h1>
<p class="sub">Membership-gated checkout states (preview skeleton)</p> <p class="sub">Membership-gated checkout behavior (live-state scaffold)</p>
<div class="grid"> <div class="grid">
<section class="card"> <section class="card">
<p class="label">State A</p> <p class="label">Wallet + Membership</p>
<p class="title">Wallet connected, no membership</p> <p class="line">Wallet: <span class="mono" id="wallet-label">not connected</span></p>
<p class="line">Checkout is blocked. User is prompted to mint membership first.</p> <p class="line">Membership status: <span class="mono" id="membership-label">unknown</span></p>
<span class="state block">membership required</span> <p class="line">Gate decision: <span class="mono" id="gate-label">blocked</span></p>
</section> <span class="state block" id="gate-pill">membership required</span>
<div class="actions">
<section class="card"> <button id="connect-btn" type="button">connect wallet</button>
<p class="label">State B</p> <button id="refresh-btn" type="button">refresh state</button>
<p class="title">Membership active</p> <label class="line" for="mock-select">mock mode:</label>
<p class="line">Offers can be quoted and purchased. Entitlement mint becomes available.</p> <select id="mock-select" aria-label="Mock state override">
<span class="state ok">checkout enabled</span> <option value="">live</option>
</section> <option value="active">active</option>
<option value="none">none</option>
<section class="card"> <option value="suspended">suspended</option>
<p class="label">State C</p> <option value="revoked">revoked</option>
<p class="title">Membership suspended or revoked</p> </select>
<p class="line">Checkout and activation both fail closed until state returns to active.</p>
<span class="state block">fail-closed</span>
</section>
</div> </div>
<div class="status-log" id="status-log">No checks run yet.</div>
</section>
<div class="card"> <section class="card">
<p class="label">Offer Skeleton</p> <p class="label">Offer Skeleton</p>
<p class="title">EDUT CRM Pro</p> <p class="title">EDUT CRM Pro</p>
<p class="line">Price: 199.00 USDC</p> <p class="line">Price: 199.00 USDC</p>
<p class="line">Policy: member-only, workspace-bound, non-transferable</p> <p class="line">Policy: member-only, workspace-bound, non-transferable</p>
<p class="line">Action: membership check -> quote -> wallet confirm -> entitlement receipt</p> <p class="line">Action chain: membership check -> quote -> wallet confirm -> entitlement receipt</p>
<div class="actions">
<button id="checkout-btn" type="button" disabled>request checkout quote</button>
</div>
<div class="status-log" id="checkout-log">Checkout is blocked until membership is active.</div>
</section>
<section class="card">
<p class="label">Fail-Closed States</p>
<p class="line">No membership: checkout blocked.</p>
<p class="line">Suspended/revoked: checkout and activation blocked.</p>
<p class="line">Unknown state or API error: blocked by default.</p>
<span class="state warn">default deny</span>
</section>
</div> </div>
<p class="foot">This page is a static contract between UX and policy: membership gates purchasing; entitlement gates runtime.</p> <p class="foot">This page is intentionally deterministic: if membership cannot be confirmed, purchase remains blocked.</p>
</div> </div>
<script>
(function () {
'use strict';
const state = {
wallet: null,
membership: 'unknown',
gate: false,
source: 'live',
};
const walletLabel = document.getElementById('wallet-label');
const membershipLabel = document.getElementById('membership-label');
const gateLabel = document.getElementById('gate-label');
const gatePill = document.getElementById('gate-pill');
const statusLog = document.getElementById('status-log');
const checkoutLog = document.getElementById('checkout-log');
const connectBtn = document.getElementById('connect-btn');
const refreshBtn = document.getElementById('refresh-btn');
const checkoutBtn = document.getElementById('checkout-btn');
const mockSelect = document.getElementById('mock-select');
function abbreviateWallet(wallet) {
if (!wallet || wallet.length < 10) return wallet || 'not connected';
return wallet.slice(0, 6) + '...' + wallet.slice(-4);
}
function setLog(message) {
statusLog.textContent = message;
}
function setCheckoutLog(message) {
checkoutLog.textContent = message;
}
function normalizeMembership(raw) {
const value = String(raw || '').toLowerCase();
if (value === 'active') return 'active';
if (value === 'suspended') return 'suspended';
if (value === 'revoked') return 'revoked';
if (value === 'none' || value === 'inactive') return 'none';
return 'unknown';
}
function applyGateState() {
state.gate = state.membership === 'active';
walletLabel.textContent = abbreviateWallet(state.wallet);
membershipLabel.textContent = state.membership;
gateLabel.textContent = state.gate ? 'enabled' : 'blocked';
if (state.gate) {
gatePill.className = 'state ok';
gatePill.textContent = 'checkout enabled';
} else if (state.membership === 'unknown') {
gatePill.className = 'state warn';
gatePill.textContent = 'status unknown';
} else {
gatePill.className = 'state block';
gatePill.textContent = 'membership required';
}
checkoutBtn.disabled = !state.gate;
}
function getMockFromQuery() {
const params = new URLSearchParams(window.location.search);
return normalizeMembership(params.get('mock'));
}
function getStoredDesignationCode() {
const raw = localStorage.getItem('edut_ack_state');
if (!raw) return null;
try {
const parsed = JSON.parse(raw);
return parsed && parsed.code ? parsed.code : null;
} catch (err) {
return null;
}
}
async function fetchJson(url) {
const response = await fetch(url, { method: 'GET' });
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
return response.json();
}
async function fetchLiveMembershipStatus() {
if (!state.wallet) {
throw new Error('Connect wallet first.');
}
const walletUrl = '/secret/membership/status?wallet=' + encodeURIComponent(state.wallet);
try {
const payload = await fetchJson(walletUrl);
return normalizeMembership(payload.status || payload.membership_status);
} catch (walletErr) {
const designationCode = getStoredDesignationCode();
if (!designationCode) {
throw walletErr;
}
const codeUrl = '/secret/membership/status?designation_code=' + encodeURIComponent(designationCode);
const payload = await fetchJson(codeUrl);
return normalizeMembership(payload.status || payload.membership_status);
}
}
async function refreshMembershipState() {
const mock = normalizeMembership(mockSelect.value || getMockFromQuery());
if (mock !== 'unknown' && mock !== '') {
state.source = 'mock';
state.membership = mock;
applyGateState();
setLog('Mock mode active: ' + mock + '.');
return;
}
state.source = 'live';
setLog('Checking live membership status...');
try {
const membership = await fetchLiveMembershipStatus();
state.membership = membership;
applyGateState();
setLog('Live status resolved: ' + membership + '.');
} catch (err) {
state.membership = 'unknown';
applyGateState();
setLog('Live status check failed: ' + err.message + '. Purchase remains blocked.');
}
}
async function connectWallet() {
if (!window.ethereum) {
setLog('No wallet provider detected on this device.');
return;
}
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
if (!Array.isArray(accounts) || accounts.length === 0) {
throw new Error('Wallet connection not approved.');
}
state.wallet = accounts[0];
setLog('Wallet connected: ' + abbreviateWallet(state.wallet) + '.');
await refreshMembershipState();
} catch (err) {
setLog('Wallet connection failed: ' + err.message + '.');
}
}
async function requestCheckoutQuote() {
if (!state.gate) {
setCheckoutLog('Checkout blocked: membership is not active.');
return;
}
if (!state.wallet) {
setCheckoutLog('Checkout blocked: wallet not connected.');
return;
}
setCheckoutLog('Requesting quote...');
try {
const response = await fetch('/marketplace/checkout/quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wallet: state.wallet,
offer_id: 'edut.crm.pro.annual',
}),
});
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
const payload = await response.json();
const quoteId = payload.quote_id || 'unknown';
const amount = payload.amount || payload.amount_atomic || 'unknown';
const currency = payload.currency || 'unknown';
setCheckoutLog('Quote ready: ' + quoteId + ' (' + amount + ' ' + currency + ').');
} catch (err) {
setCheckoutLog('Quote request failed: ' + err.message + '. API wiring pending.');
}
}
connectBtn.addEventListener('click', connectWallet);
refreshBtn.addEventListener('click', refreshMembershipState);
checkoutBtn.addEventListener('click', requestCheckoutQuote);
mockSelect.addEventListener('change', refreshMembershipState);
applyGateState();
const initialMock = getMockFromQuery();
if (initialMock !== 'unknown') {
mockSelect.value = initialMock;
}
})();
</script>
</body> </body>
</html> </html>