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-status.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
|
||||
platform-spec-alignment-review.md
|
||||
contracts/
|
||||
@ -39,18 +53,24 @@ docs/
|
||||
README.md
|
||||
chain-config.template.json
|
||||
contract-addresses.template.json
|
||||
environment-invariants.md
|
||||
api/
|
||||
secret-system.openapi.yaml
|
||||
examples/
|
||||
secret-system.examples.md
|
||||
handoff/
|
||||
membership-backend-checklist.md
|
||||
schemas/
|
||||
offer.v1.schema.json
|
||||
entitlement.v1.schema.json
|
||||
issuer-manifest.v1.schema.json
|
||||
evidence-receipt.v1.schema.json
|
||||
launch-offers-catalog.v1.schema.json
|
||||
examples/
|
||||
offer.v1.example.json
|
||||
entitlement.v1.example.json
|
||||
issuer-manifest.v1.example.json
|
||||
launch-offers-catalog.v1.example.json
|
||||
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`
|
||||
3. Finalize membership contract interface targets: `DONE`
|
||||
4. Lock signature + intent protocol: `DONE`
|
||||
5. Add membership mint transaction stage in web flow: `IN_PROGRESS`
|
||||
6. Implement membership gate in marketplace checkout: `PENDING`
|
||||
5. Add membership mint transaction stage in web flow: `DONE` (frontend path implemented; backend endpoints pending)
|
||||
6. Implement membership gate in marketplace checkout: `IN_PROGRESS` (store scaffold + gate logic implemented; live API pending)
|
||||
7. Ship offer registry schema: `DONE`
|
||||
8. Ship entitlement purchase schema/pipeline contracts: `IN_PROGRESS`
|
||||
9. Bind entitlements to runtime activation: `PENDING`
|
||||
@ -33,10 +33,15 @@ Implemented now:
|
||||
6. Interface target document for contracts/APIs.
|
||||
7. Pricing policy with USD 5 floor rule.
|
||||
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:
|
||||
|
||||
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.
|
||||
|
||||
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;
|
||||
-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; }
|
||||
.back {
|
||||
display: inline-block;
|
||||
@ -43,7 +43,7 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card {
|
||||
@ -80,6 +80,48 @@
|
||||
}
|
||||
.state.ok { border-color: #6f8d72; color: #3f6545; }
|
||||
.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 {
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid #d0d5db;
|
||||
@ -94,40 +136,255 @@
|
||||
<div class="container">
|
||||
<a href="/" class="back">← Back</a>
|
||||
<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">
|
||||
<section class="card">
|
||||
<p class="label">State A</p>
|
||||
<p class="title">Wallet connected, no membership</p>
|
||||
<p class="line">Checkout is blocked. User is prompted to mint membership first.</p>
|
||||
<span class="state block">membership required</span>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<p class="label">State B</p>
|
||||
<p class="title">Membership active</p>
|
||||
<p class="line">Offers can be quoted and purchased. Entitlement mint becomes available.</p>
|
||||
<span class="state ok">checkout enabled</span>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<p class="label">State C</p>
|
||||
<p class="title">Membership suspended or revoked</p>
|
||||
<p class="line">Checkout and activation both fail closed until state returns to active.</p>
|
||||
<span class="state block">fail-closed</span>
|
||||
</section>
|
||||
<p class="label">Wallet + Membership</p>
|
||||
<p class="line">Wallet: <span class="mono" id="wallet-label">not connected</span></p>
|
||||
<p class="line">Membership status: <span class="mono" id="membership-label">unknown</span></p>
|
||||
<p class="line">Gate decision: <span class="mono" id="gate-label">blocked</span></p>
|
||||
<span class="state block" id="gate-pill">membership required</span>
|
||||
<div class="actions">
|
||||
<button id="connect-btn" type="button">connect wallet</button>
|
||||
<button id="refresh-btn" type="button">refresh state</button>
|
||||
<label class="line" for="mock-select">mock mode:</label>
|
||||
<select id="mock-select" aria-label="Mock state override">
|
||||
<option value="">live</option>
|
||||
<option value="active">active</option>
|
||||
<option value="none">none</option>
|
||||
<option value="suspended">suspended</option>
|
||||
<option value="revoked">revoked</option>
|
||||
</select>
|
||||
</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="title">EDUT CRM Pro</p>
|
||||
<p class="line">Price: 199.00 USDC</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user