Execute roadmap docs and upgrade store scaffold to live membership gating
This commit is contained in:
parent
a0a8274721
commit
ffb8ce0ead
20
README.md
20
README.md
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
182
docs/api/examples/secret-system.examples.md
Normal file
182
docs/api/examples/secret-system.examples.md
Normal 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."
|
||||||
|
}
|
||||||
|
```
|
||||||
50
docs/chain-operations-runbook.md
Normal file
50
docs/chain-operations-runbook.md
Normal 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.
|
||||||
18
docs/deployment/environment-invariants.md
Normal file
18
docs/deployment/environment-invariants.md
Normal 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.
|
||||||
27
docs/failure-state-matrix.md
Normal file
27
docs/failure-state-matrix.md
Normal 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.
|
||||||
25
docs/implementation-mapping.md
Normal file
25
docs/implementation-mapping.md
Normal 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.
|
||||||
22
docs/integration-test-plan.md
Normal file
22
docs/integration-test-plan.md
Normal 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.
|
||||||
50
docs/issuer-onboarding-pack.md
Normal file
50
docs/issuer-onboarding-pack.md
Normal 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
17
docs/legal-copy-matrix.md
Normal 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.
|
||||||
35
docs/localization-qa-matrix.md
Normal file
35
docs/localization-qa-matrix.md
Normal 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.
|
||||||
23
docs/membership-tier-extension.md
Normal file
23
docs/membership-tier-extension.md
Normal 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.
|
||||||
14
docs/migration-policy-v1-to-v2.md
Normal file
14
docs/migration-policy-v1-to-v2.md
Normal 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.
|
||||||
30
docs/mobile-wallet-handoff.md
Normal file
30
docs/mobile-wallet-handoff.md
Normal 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.
|
||||||
29
docs/policy-hash-versioning.md
Normal file
29
docs/policy-hash-versioning.md
Normal 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.
|
||||||
19
docs/public-trust-page-spec.md
Normal file
19
docs/public-trust-page-spec.md
Normal 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
37
docs/release-gate.md
Normal 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.
|
||||||
@ -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):
|
||||||
|
|||||||
71
docs/schemas/evidence-receipt.v1.schema.json
Normal file
71
docs/schemas/evidence-receipt.v1.schema.json
Normal 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}$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
docs/schemas/examples/launch-offers-catalog.v1.example.json
Normal file
25
docs/schemas/examples/launch-offers-catalog.v1.example.json
Normal 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"
|
||||||
|
}
|
||||||
31
docs/schemas/launch-offers-catalog.v1.schema.json
Normal file
31
docs/schemas/launch-offers-catalog.v1.schema.json
Normal 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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
38
docs/security-hardening-checklist.md
Normal file
38
docs/security-hardening-checklist.md
Normal 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.
|
||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user