Compare commits

...

10 Commits

47 changed files with 1470 additions and 200 deletions

View File

@ -29,6 +29,8 @@ Copy `.env.example` in this folder and set contract/runtime values before deploy
- `POST /secret/wallet/intent`
- `POST /secret/wallet/verify`
- `POST /secret/wallet/session/refresh`
- `POST /secret/wallet/session/revoke`
- `POST /secret/membership/quote`
- `POST /secret/membership/confirm`
- `GET /secret/membership/status`
@ -71,6 +73,11 @@ When `SECRET_API_REQUIRE_WALLET_SESSION=true`, wallet-scoped control-plane endpo
Covered endpoints include marketplace checkout/entitlements, governance install/lease actions, and member-channel calls.
Session lifecycle endpoints:
1. `POST /secret/wallet/session/refresh`: rotates the current session token and revokes the prior token.
2. `POST /secret/wallet/session/revoke`: revokes the current token immediately.
## Sponsorship Behavior
Membership quote supports ownership wallet and distinct payer wallet:
@ -132,7 +139,7 @@ Policy gates:
- `SECRET_API_DOMAIN_NAME`
- `SECRET_API_VERIFYING_CONTRACT`
- `SECRET_API_MEMBERSHIP_CONTRACT`
- `SECRET_API_MINT_CURRENCY` (default `USDC`)
- `SECRET_API_MINT_CURRENCY` (must be `USDC` in v1)
- `SECRET_API_MINT_AMOUNT_ATOMIC` (default `100000000`)
- `SECRET_API_MINT_DECIMALS` (default `6`)

View File

@ -34,6 +34,8 @@ func (a *app) routes() http.Handler {
mux.HandleFunc("/healthz", a.withCORS(a.handleHealth))
mux.HandleFunc("/secret/wallet/intent", a.withCORS(a.handleWalletIntent))
mux.HandleFunc("/secret/wallet/verify", a.withCORS(a.handleWalletVerify))
mux.HandleFunc("/secret/wallet/session/refresh", a.withCORS(a.handleWalletSessionRefresh))
mux.HandleFunc("/secret/wallet/session/revoke", a.withCORS(a.handleWalletSessionRevoke))
mux.HandleFunc("/secret/membership/quote", a.withCORS(a.handleMembershipQuote))
mux.HandleFunc("/secret/membership/confirm", a.withCORS(a.handleMembershipConfirm))
mux.HandleFunc("/secret/membership/status", a.withCORS(a.handleMembershipStatus))
@ -230,6 +232,97 @@ func (a *app) handleWalletVerify(w http.ResponseWriter, r *http.Request) {
})
}
func (a *app) handleWalletSessionRefresh(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req walletSessionRefreshRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorCode(w, http.StatusBadRequest, "invalid_request", "invalid request body")
return
}
wallet, err := normalizeAddress(req.Wallet)
if err != nil {
writeErrorCode(w, http.StatusBadRequest, "invalid_wallet", err.Error())
return
}
oldToken := sessionTokenFromRequest(r)
if strings.TrimSpace(oldToken) == "" {
writeErrorCode(w, http.StatusUnauthorized, "wallet_session_required", "wallet session required")
return
}
if !a.enforceWalletSession(w, r, wallet) {
return
}
oldSession, err := a.store.getWalletSession(r.Context(), oldToken)
if err != nil {
if err == errNotFound {
writeErrorCode(w, http.StatusUnauthorized, "wallet_session_invalid", "wallet session not found")
return
}
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve wallet session")
return
}
now := time.Now().UTC()
if err := a.store.revokeWalletSession(r.Context(), oldToken, now); err != nil && err != errNotFound {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to revoke old wallet session")
return
}
newSession, err := a.issueWalletSession(r.Context(), wallet, oldSession.DesignationCode)
if err != nil {
writeErrorCode(w, http.StatusInternalServerError, "session_issue_failed", "failed to issue wallet session")
return
}
w.Header().Set(sessionHeaderToken, newSession.SessionToken)
w.Header().Set(sessionHeaderExpiresAt, newSession.ExpiresAt.UTC().Format(time.RFC3339Nano))
writeJSON(w, http.StatusOK, walletSessionRefreshResponse{
Status: "session_refreshed",
Wallet: wallet,
SessionToken: newSession.SessionToken,
SessionExpire: newSession.ExpiresAt.UTC().Format(time.RFC3339Nano),
})
}
func (a *app) handleWalletSessionRevoke(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req walletSessionRevokeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorCode(w, http.StatusBadRequest, "invalid_request", "invalid request body")
return
}
wallet, err := normalizeAddress(req.Wallet)
if err != nil {
writeErrorCode(w, http.StatusBadRequest, "invalid_wallet", err.Error())
return
}
sessionToken := sessionTokenFromRequest(r)
if strings.TrimSpace(sessionToken) == "" {
writeErrorCode(w, http.StatusUnauthorized, "wallet_session_required", "wallet session required")
return
}
if !a.enforceWalletSession(w, r, wallet) {
return
}
now := time.Now().UTC()
if err := a.store.revokeWalletSession(r.Context(), sessionToken, now); err != nil {
if err == errNotFound {
writeErrorCode(w, http.StatusUnauthorized, "wallet_session_invalid", "wallet session not found")
return
}
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to revoke wallet session")
return
}
writeJSON(w, http.StatusOK, walletSessionRevokeResponse{
Status: "session_revoked",
Wallet: wallet,
RevokedAt: now.Format(time.RFC3339Nano),
})
}
func (a *app) handleMembershipQuote(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
@ -1476,4 +1569,7 @@ func isTxHash(value string) bool {
func logConfig(cfg Config) {
log.Printf("secret api listening on %s chain_id=%d contract=%s currency=%s amount_atomic=%s", cfg.ListenAddr, cfg.ChainID, cfg.MembershipContract, cfg.MintCurrency, cfg.MintAmountAtomic)
if !cfg.RequireWalletSession {
log.Printf("warning: wallet session enforcement is disabled (SECRET_API_REQUIRE_WALLET_SESSION=false)")
}
}

View File

@ -1210,6 +1210,314 @@ func TestWalletSessionMismatchBlocked(t *testing.T) {
}
}
func TestWalletVerifyIssuesSessionToken(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr,
Origin: "https://edut.ai",
Locale: "en",
ChainID: cfg.ChainID,
}, http.StatusOK)
issuedAt, err := time.Parse(time.RFC3339Nano, intentRes.IssuedAt)
if err != nil {
t.Fatalf("parse issued_at: %v", err)
}
td := buildTypedData(cfg, designationRecord{
Code: intentRes.DesignationCode,
DisplayToken: intentRes.DisplayToken,
Nonce: intentRes.Nonce,
IssuedAt: issuedAt,
Origin: "https://edut.ai",
})
sig := signTypedData(t, ownerKey, td)
verifyRes := postJSONExpect[tWalletVerifyResponse](t, a, "/secret/wallet/verify", walletVerifyRequest{
IntentID: intentRes.IntentID,
Address: ownerAddr,
ChainID: cfg.ChainID,
Signature: sig,
}, http.StatusOK)
if strings.TrimSpace(verifyRes.SessionToken) == "" {
t.Fatalf("expected wallet verify to issue session token: %+v", verifyRes)
}
if strings.TrimSpace(verifyRes.SessionExpiresAt) == "" {
t.Fatalf("expected wallet verify to return session expiry: %+v", verifyRes)
}
}
func TestWalletSessionInvalidBlocked(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
errResp := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized, map[string]string{
sessionHeaderToken: "deadbeef",
})
if code := errResp["code"]; code != "wallet_session_invalid" {
t.Fatalf("expected wallet_session_invalid, got %+v", errResp)
}
}
func TestWalletSessionExpiredBlocked(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
now := time.Now().UTC()
session := walletSessionRecord{
SessionToken: "expired-session-token",
Wallet: ownerAddr,
DesignationCode: "1234567890123",
ChainID: a.cfg.ChainID,
IssuedAt: now.Add(-2 * time.Hour),
ExpiresAt: now.Add(-1 * time.Minute),
}
if err := a.store.putWalletSession(context.Background(), session); err != nil {
t.Fatalf("seed wallet session: %v", err)
}
errResp := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if code := errResp["code"]; code != "wallet_session_expired" {
t.Fatalf("expected wallet_session_expired, got %+v", errResp)
}
}
func TestWalletSessionRevokedBlocked(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
now := time.Now().UTC()
session := walletSessionRecord{
SessionToken: "revoked-session-token",
Wallet: ownerAddr,
DesignationCode: "1234567890123",
ChainID: a.cfg.ChainID,
IssuedAt: now.Add(-1 * time.Hour),
ExpiresAt: now.Add(1 * time.Hour),
RevokedAt: &now,
}
if err := a.store.putWalletSession(context.Background(), session); err != nil {
t.Fatalf("seed wallet session: %v", err)
}
errResp := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if code := errResp["code"]; code != "wallet_session_revoked" {
t.Fatalf("expected wallet_session_revoked, got %+v", errResp)
}
}
func TestWalletSessionRefreshRotatesToken(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
session, err := a.issueWalletSession(context.Background(), ownerAddr, "1234567890123")
if err != nil {
t.Fatalf("issue wallet session: %v", err)
}
refresh := postJSONExpectWithHeaders[walletSessionRefreshResponse](t, a, "/secret/wallet/session/refresh", walletSessionRefreshRequest{
Wallet: ownerAddr,
}, http.StatusOK, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if refresh.Status != "session_refreshed" {
t.Fatalf("expected refreshed status, got %+v", refresh)
}
if strings.TrimSpace(refresh.SessionToken) == "" || refresh.SessionToken == session.SessionToken {
t.Fatalf("expected rotated token, got %+v", refresh)
}
oldBlocked := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if code := oldBlocked["code"]; code != "wallet_session_revoked" {
t.Fatalf("expected old token revoked, got %+v", oldBlocked)
}
newAllowed := postJSONExpectWithHeaders[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusOK, map[string]string{
sessionHeaderToken: refresh.SessionToken,
})
if strings.TrimSpace(newAllowed.QuoteID) == "" {
t.Fatalf("expected quote with refreshed token")
}
}
func TestWalletSessionRevokeEndpointBlocksFurtherUse(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
session, err := a.issueWalletSession(context.Background(), ownerAddr, "1234567890123")
if err != nil {
t.Fatalf("issue wallet session: %v", err)
}
revoked := postJSONExpectWithHeaders[walletSessionRevokeResponse](t, a, "/secret/wallet/session/revoke", walletSessionRevokeRequest{
Wallet: ownerAddr,
}, http.StatusOK, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if revoked.Status != "session_revoked" {
t.Fatalf("unexpected revoke response: %+v", revoked)
}
errResp := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if code := errResp["code"]; code != "wallet_session_revoked" {
t.Fatalf("expected revoked token to fail, got %+v", errResp)
}
}
func TestMemberChannelRegisterRequiresWalletSession(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/member/channel/device/register", memberChannelDeviceRegisterRequest{
Wallet: ownerAddr,
ChainID: a.cfg.ChainID,
DeviceID: "desktop-local-01",
Platform: "desktop",
OrgRootID: "org_test",
PrincipalID: "principal_test",
PrincipalRole: "org_root_owner",
AppVersion: "0.1.0",
PushProvider: "none",
}, http.StatusUnauthorized)
if code := errResp["code"]; code != "wallet_session_required" {
t.Fatalf("expected wallet_session_required, got %+v", errResp)
}
}
func TestGovernanceInstallTokenRequiresWalletSession(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
now := time.Now().UTC()
if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{
Wallet: ownerAddr,
OrgRootID: "org_test",
PrincipalID: "principal_test",
PrincipalRole: "org_root_owner",
EntitlementID: "ent_test",
EntitlementStatus: "active",
AccessClass: "connected",
AvailabilityState: "active",
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed governance principal: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/governance/install/token", governanceInstallTokenRequest{
Wallet: ownerAddr,
OrgRootID: "org_test",
PrincipalID: "principal_test",
PrincipalRole: "org_root_owner",
DeviceID: "device-test",
LauncherVersion: "0.1.0",
Platform: "desktop",
}, http.StatusUnauthorized)
if code := errResp["code"]; code != "wallet_session_required" {
t.Fatalf("expected wallet_session_required, got %+v", errResp)
}
}
func TestIssueWalletSessionPrunesExpired(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
now := time.Now().UTC()
expired := walletSessionRecord{
SessionToken: "prune-me",
Wallet: ownerAddr,
DesignationCode: "1111111111111",
ChainID: a.cfg.ChainID,
IssuedAt: now.Add(-3 * time.Hour),
ExpiresAt: now.Add(-2 * time.Hour),
}
if err := a.store.putWalletSession(context.Background(), expired); err != nil {
t.Fatalf("seed expired session: %v", err)
}
if _, err := a.issueWalletSession(context.Background(), ownerAddr, "2222222222222"); err != nil {
t.Fatalf("issue wallet session: %v", err)
}
_, err := a.store.getWalletSession(context.Background(), expired.SessionToken)
if err != errNotFound {
t.Fatalf("expected expired session to be pruned, got err=%v", err)
}
}
type tWalletIntentResponse struct {
IntentID string `json:"intent_id"`
DesignationCode string `json:"designation_code"`
@ -1222,6 +1530,7 @@ type tWalletVerifyResponse struct {
Status string `json:"status"`
DesignationCode string `json:"designation_code"`
SessionToken string `json:"session_token"`
SessionExpiresAt string `json:"session_expires_at"`
}
func newTestApp(t *testing.T) (*app, Config, func()) {

View File

@ -2,6 +2,7 @@ package main
import (
"fmt"
"math/big"
"os"
"strconv"
"strings"
@ -76,6 +77,17 @@ func (c Config) Validate() error {
if c.ChainID <= 0 {
return fmt.Errorf("SECRET_API_CHAIN_ID must be positive")
}
if strings.ToUpper(strings.TrimSpace(c.MintCurrency)) != "USDC" {
return fmt.Errorf("SECRET_API_MINT_CURRENCY must be USDC")
}
if c.MintDecimals != 6 {
return fmt.Errorf("SECRET_API_MINT_DECIMALS must be 6")
}
amountRaw := strings.TrimSpace(c.MintAmountAtomic)
amount, ok := new(big.Int).SetString(amountRaw, 10)
if !ok || amount.Sign() <= 0 {
return fmt.Errorf("SECRET_API_MINT_AMOUNT_ATOMIC must be a positive base-10 integer")
}
if c.WalletSessionTTL <= 0 {
return fmt.Errorf("SECRET_API_WALLET_SESSION_TTL_SECONDS must be positive")
}

View File

@ -31,3 +31,30 @@ func TestConfigValidateRejectsNonPositiveChainID(t *testing.T) {
t.Fatalf("expected chain id validation failure")
}
}
func TestConfigValidateRejectsNonUSDCCurrency(t *testing.T) {
t.Parallel()
cfg := loadConfig()
cfg.MintCurrency = "ETH"
if err := cfg.Validate(); err == nil {
t.Fatalf("expected mint currency validation failure")
}
}
func TestConfigValidateRejectsNonSixMintDecimals(t *testing.T) {
t.Parallel()
cfg := loadConfig()
cfg.MintDecimals = 18
if err := cfg.Validate(); err == nil {
t.Fatalf("expected mint decimals validation failure")
}
}
func TestConfigValidateRejectsInvalidMintAmount(t *testing.T) {
t.Parallel()
cfg := loadConfig()
cfg.MintAmountAtomic = "not-a-number"
if err := cfg.Validate(); err == nil {
t.Fatalf("expected mint amount validation failure")
}
}

View File

@ -46,6 +46,11 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
MultiTenant: false,
EntitlementClass: "solo_core",
},
ExecutionProfile: marketplaceExecutionProfile{
ConnectorSurface: "hybrid",
PacingTier: "governed_human_pace",
HumanPaceFloorMS: 1200,
},
SortOrder: 10,
},
{
@ -68,6 +73,11 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
MultiTenant: false,
EntitlementClass: "workspace_core",
},
ExecutionProfile: marketplaceExecutionProfile{
ConnectorSurface: "hybrid",
PacingTier: "governed_human_pace",
HumanPaceFloorMS: 1200,
},
SortOrder: 20,
},
{
@ -91,6 +101,11 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
EntitlementClass: "workspace_ai",
RequiresOffers: []string{offerIDWorkspaceCore},
},
ExecutionProfile: marketplaceExecutionProfile{
ConnectorSurface: "hybrid",
PacingTier: "governed_human_pace",
HumanPaceFloorMS: 1200,
},
SortOrder: 30,
},
{
@ -114,6 +129,10 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
EntitlementClass: "workspace_lane24",
RequiresOffers: []string{offerIDWorkspaceCore},
},
ExecutionProfile: marketplaceExecutionProfile{
ConnectorSurface: "edut_native",
PacingTier: "local_hardware_speed",
},
SortOrder: 40,
},
{
@ -137,6 +156,10 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
EntitlementClass: "workspace_sovereign",
RequiresOffers: []string{offerIDWorkspaceCore},
},
ExecutionProfile: marketplaceExecutionProfile{
ConnectorSurface: "edut_native",
PacingTier: "local_hardware_speed",
},
SortOrder: 50,
},
}

View File

@ -10,6 +10,7 @@ type marketplaceOffer struct {
Status string `json:"status"`
Pricing marketplaceOfferPrice `json:"pricing"`
Policies marketplaceOfferPolicy `json:"policies"`
ExecutionProfile marketplaceExecutionProfile `json:"execution_profile,omitempty"`
SortOrder int `json:"-"`
}
@ -30,6 +31,12 @@ type marketplaceOfferPolicy struct {
RequiresOffers []string `json:"requires_offers,omitempty"`
}
type marketplaceExecutionProfile struct {
ConnectorSurface string `json:"connector_surface"`
PacingTier string `json:"pacing_tier"`
HumanPaceFloorMS int `json:"human_pace_floor_ms,omitempty"`
}
type marketplaceOffersResponse struct {
Offers []marketplaceOffer `json:"offers"`
}

View File

@ -37,6 +37,27 @@ type walletVerifyResponse struct {
SessionExpires string `json:"session_expires_at,omitempty"`
}
type walletSessionRefreshRequest struct {
Wallet string `json:"wallet"`
}
type walletSessionRefreshResponse struct {
Status string `json:"status"`
Wallet string `json:"wallet"`
SessionToken string `json:"session_token"`
SessionExpire string `json:"session_expires_at"`
}
type walletSessionRevokeRequest struct {
Wallet string `json:"wallet"`
}
type walletSessionRevokeResponse struct {
Status string `json:"status"`
Wallet string `json:"wallet"`
RevokedAt string `json:"revoked_at"`
}
type membershipQuoteRequest struct {
DesignationCode string `json:"designation_code"`
Address string `json:"address"`

View File

@ -13,6 +13,7 @@ const (
)
func (a *app) issueWalletSession(ctx context.Context, wallet, designationCode string) (walletSessionRecord, error) {
_, _ = a.store.deleteExpiredWalletSessions(ctx, time.Now().UTC())
token, err := randomHex(24)
if err != nil {
return walletSessionRecord{}, err

View File

@ -453,6 +453,40 @@ func (s *store) touchWalletSession(ctx context.Context, token string, touchedAt
return nil
}
func (s *store) revokeWalletSession(ctx context.Context, token string, revokedAt time.Time) error {
res, err := s.db.ExecContext(ctx, `
UPDATE wallet_sessions
SET revoked_at = ?
WHERE session_token = ?
`, revokedAt.UTC().Format(time.RFC3339Nano), strings.TrimSpace(token))
if err != nil {
return err
}
affected, err := res.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return errNotFound
}
return nil
}
func (s *store) deleteExpiredWalletSessions(ctx context.Context, now time.Time) (int64, error) {
res, err := s.db.ExecContext(ctx, `
DELETE FROM wallet_sessions
WHERE expires_at <= ?
`, now.UTC().Format(time.RFC3339Nano))
if err != nil {
return 0, err
}
affected, err := res.RowsAffected()
if err != nil {
return 0, err
}
return affected, nil
}
func (s *store) putQuote(ctx context.Context, quote quoteRecord) error {
_, err := s.db.ExecContext(ctx, `
INSERT INTO quotes (

View File

@ -19,6 +19,11 @@ Success (`200`):
"decimals": 6,
"chain_id": 8453
},
"execution_profile": {
"connector_surface": "hybrid",
"pacing_tier": "governed_human_pace",
"human_pace_floor_ms": 1200
},
"policies": {
"member_only": true,
"workspace_bound": true,
@ -101,6 +106,7 @@ Success (`200`):
1. `amount`/`amount_atomic` represent the license component.
2. `total_amount`/`total_amount_atomic` represent the actual payable quote total.
3. First checkout can include membership activation as a separate line item.
4. `execution_profile.pacing_tier` distinguishes external human-governed cadence from native local-speed execution.
Error (`403`):

View File

@ -80,7 +80,65 @@ Error (`400` intent expired):
}
```
## 3) `POST /secret/membership/quote`
## 3) `POST /secret/wallet/session/refresh`
Request:
Headers:
1. `Authorization: Bearer <session_token>` (or `X-Edut-Session`)
```json
{
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5"
}
```
Success (`200`):
```json
{
"status": "session_refreshed",
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"session_token": "f9bc20f15ecf7fd53f1f4ba8ca774564a1098e6ed9db6f0f",
"session_expires_at": "2026-03-18T07:42:10Z"
}
```
Error (`401` missing/expired token):
```json
{
"error": "wallet session required",
"code": "wallet_session_required"
}
```
## 4) `POST /secret/wallet/session/revoke`
Request:
Headers:
1. `Authorization: Bearer <session_token>` (or `X-Edut-Session`)
```json
{
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5"
}
```
Success (`200`):
```json
{
"status": "session_revoked",
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
"revoked_at": "2026-02-17T07:34:02Z"
}
```
## 5) `POST /secret/membership/quote`
Request:
@ -142,7 +200,7 @@ Error (`403` distinct payer without proof):
}
```
## 4) `POST /secret/membership/confirm`
## 6) `POST /secret/membership/confirm`
Request:
@ -184,7 +242,7 @@ Error (`400` tx mismatch):
}
```
## 5) `GET /secret/membership/status`
## 7) `GET /secret/membership/status`
Request by wallet:

View File

@ -137,6 +137,19 @@ components:
type: integer
chain_id:
type: integer
execution_profile:
type: object
required: [connector_surface, pacing_tier]
properties:
connector_surface:
type: string
enum: [edut_native, external_connector, hybrid]
pacing_tier:
type: string
enum: [governed_human_pace, local_hardware_speed]
human_pace_floor_ms:
type: integer
minimum: 0
policies:
type: object
required: [member_only, workspace_bound, transferable, internal_use_only, multi_tenant]

View File

@ -34,10 +34,70 @@ paths:
responses:
'200':
description: Signature verified
headers:
X-Edut-Session:
schema:
type: string
description: Wallet session token for follow-on wallet-scoped APIs.
X-Edut-Session-Expires-At:
schema:
type: string
format: date-time
description: Session expiry timestamp.
content:
application/json:
schema:
$ref: '#/components/schemas/WalletVerifyResponse'
/secret/wallet/session/refresh:
post:
summary: Rotate wallet session token
description: Requires a valid wallet session token (`Authorization: Bearer` or `X-Edut-Session`).
parameters:
- $ref: '#/components/parameters/WalletSessionHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WalletSessionRefreshRequest'
responses:
'200':
description: Session rotated
headers:
X-Edut-Session:
schema:
type: string
X-Edut-Session-Expires-At:
schema:
type: string
format: date-time
content:
application/json:
schema:
$ref: '#/components/schemas/WalletSessionRefreshResponse'
'401':
description: Session missing, invalid, revoked, or expired
/secret/wallet/session/revoke:
post:
summary: Revoke wallet session token
description: Requires a valid wallet session token (`Authorization: Bearer` or `X-Edut-Session`).
parameters:
- $ref: '#/components/parameters/WalletSessionHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WalletSessionRevokeRequest'
responses:
'200':
description: Session revoked
content:
application/json:
schema:
$ref: '#/components/schemas/WalletSessionRevokeResponse'
'401':
description: Session missing, invalid, revoked, or expired
/secret/membership/quote:
post:
summary: Get current membership mint quote
@ -93,6 +153,14 @@ paths:
schema:
$ref: '#/components/schemas/MembershipStatusResponse'
components:
parameters:
WalletSessionHeader:
in: header
name: X-Edut-Session
required: false
schema:
type: string
description: Wallet session token. `Authorization: Bearer <token>` is also accepted.
schemas:
WalletIntentRequest:
type: object
@ -166,6 +234,46 @@ components:
type: string
format: date-time
description: Session token expiry timestamp.
WalletSessionRefreshRequest:
type: object
required: [wallet]
properties:
wallet:
type: string
pattern: '^0x[a-fA-F0-9]{40}$'
WalletSessionRefreshResponse:
type: object
required: [status, wallet, session_token, session_expires_at]
properties:
status:
type: string
enum: [session_refreshed]
wallet:
type: string
session_token:
type: string
session_expires_at:
type: string
format: date-time
WalletSessionRevokeRequest:
type: object
required: [wallet]
properties:
wallet:
type: string
pattern: '^0x[a-fA-F0-9]{40}$'
WalletSessionRevokeResponse:
type: object
required: [status, wallet, revoked_at]
properties:
status:
type: string
enum: [session_revoked]
wallet:
type: string
revoked_at:
type: string
format: date-time
MembershipQuoteRequest:
type: object
required: [designation_code, address, chain_id]
@ -195,7 +303,7 @@ components:
type: integer
currency:
type: string
enum: [USDC, ETH]
enum: [USDC]
amount:
type: string
amount_atomic:

View File

@ -88,9 +88,11 @@ PrincipalRole:
1. `POST /secret/wallet/intent`
2. `POST /secret/wallet/verify`
3. `POST /secret/membership/quote`
4. `POST /secret/membership/confirm`
5. `GET /secret/membership/status?designation_code=...`
3. `POST /secret/wallet/session/refresh`
4. `POST /secret/wallet/session/revoke`
5. `POST /secret/membership/quote`
6. `POST /secret/membership/confirm`
7. `GET /secret/membership/status?designation_code=...`
## Marketplace

View File

@ -9,7 +9,7 @@
},
"quote_policy": {
"default_currency": "USDC",
"minimum_floor_usd": "5.00",
"minimum_floor_usd": "100.00",
"safety_multiplier": "1.5",
"quote_ttl_seconds": 300
},

View File

@ -66,12 +66,14 @@ Expected:
## Post-Deploy Verification
1. `POST /secret/wallet/intent` returns `intent_id` and `designation_code`.
2. `POST /secret/wallet/verify` accepts valid EIP-712 signature.
3. `POST /secret/membership/quote` returns tx payload.
4. `POST /secret/membership/confirm` marks membership active.
5. `POST /governance/install/token` enforces owner role and active membership.
6. `POST /governance/install/confirm` enforces package/runtime/policy match.
7. `GET /governance/install/status` resolves deterministic activation state.
8. `POST /member/channel/device/register` returns active channel binding.
9. `GET /member/channel/events` returns deterministic inbox page.
10. `POST /member/channel/events/{event_id}/ack` is idempotent per event+device.
2. `POST /secret/wallet/verify` accepts valid EIP-712 signature and returns `session_token`.
3. `POST /secret/wallet/session/refresh` rotates wallet session token.
4. `POST /secret/wallet/session/revoke` revokes wallet session token.
5. `POST /secret/membership/quote` returns tx payload.
6. `POST /secret/membership/confirm` marks membership active.
7. `POST /governance/install/token` enforces owner role and active membership.
8. `POST /governance/install/confirm` enforces package/runtime/policy match.
9. `GET /governance/install/status` resolves deterministic activation state.
10. `POST /member/channel/device/register` returns active channel binding.
11. `GET /member/channel/events` returns deterministic inbox page.
12. `POST /member/channel/events/{event_id}/ack` is idempotent per event+device.

View File

@ -10,9 +10,11 @@ Current implementation target in this repo:
1. `POST /secret/wallet/intent`
2. `POST /secret/wallet/verify`
3. `POST /secret/membership/quote`
4. `POST /secret/membership/confirm`
5. `GET /secret/membership/status`
3. `POST /secret/wallet/session/refresh`
4. `POST /secret/wallet/session/revoke`
5. `POST /secret/membership/quote`
6. `POST /secret/membership/confirm`
7. `GET /secret/membership/status`
## Web Behavior Dependency
@ -50,6 +52,25 @@ Must return:
1. `status = signature_verified`
2. `designation_code`
3. `display_token`
4. `session_token`
5. `session_expires_at`
## Wallet Session Refresh
Must return:
1. `status = session_refreshed`
2. `wallet`
3. `session_token`
4. `session_expires_at`
## Wallet Session Revoke
Must return:
1. `status = session_revoked`
2. `wallet`
3. `revoked_at`
## Membership Quote
@ -102,6 +123,9 @@ Must return:
10. Optional strict chain verification mode:
- when `SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION=true`,
- membership confirm must fail closed if chain RPC verification is unavailable.
11. Wallet-session fail-closed mode:
- when `SECRET_API_REQUIRE_WALLET_SESSION=true`,
- wallet-scoped APIs must reject missing/invalid/revoked/expired sessions.
## Data Persistence Requirements

View File

@ -29,6 +29,8 @@ This roadmap is intentionally step-based and dependency-ordered. No timeline com
2. EIP-712 signature proves wallet possession.
3. Server verify endpoint enforces replay protection and origin checks.
4. Intent payload includes price/currency/deadline for explicit consent.
5. Verify response issues wallet session token with deterministic expiry.
6. Session lifecycle includes rotate (`/secret/wallet/session/refresh`) and revoke (`/secret/wallet/session/revoke`) controls.
## Step 5: Add Membership Mint Transaction Stage

View File

@ -33,7 +33,7 @@ Implemented now:
4. Step-based roadmap without timelines.
5. Frozen v1 schemas and examples.
6. Interface target document for contracts/APIs.
7. Pricing policy with USD 5 floor rule.
7. Pricing policy with 100 USDC 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.
@ -58,6 +58,9 @@ Implemented now:
29. `secretapi` now validates critical config at startup and fails fast on invalid deploy combinations.
30. `secretapi` now ships an explicit `.env.example` deployment template aligned to current endpoint/runtime requirements.
31. Marketplace checkout confirm now validates on-chain tx sender/receipt and supports strict fail-closed verification mode.
32. Wallet session issuance and validation are implemented (`session_token` from `/secret/wallet/verify`) with optional fail-closed enforcement via `SECRET_API_REQUIRE_WALLET_SESSION`.
33. Marketplace/member/governance OpenAPI contracts now declare wallet-session usage for launcher/app-channel calls.
34. Offer catalogs and marketplace responses now carry execution pacing profiles (`governed_human_pace` vs `local_hardware_speed`) for connector/runtime policy alignment.
Remaining in this repo:
@ -67,7 +70,7 @@ Remaining in this repo:
Cross-repo dependencies (kernel/backend/contracts):
1. Implement `/secret/membership/quote` and `/secret/membership/confirm`: `IN_PROGRESS` (live deployment active; strict chain verification enabled; full confirm proof pending additional Sepolia wallet funding).
1. Implement `/secret/membership/quote` and `/secret/membership/confirm`: `IN_PROGRESS` (live deployment active; strict chain verification enabled; full confirm proof pending seeded Sepolia USDC test balance).
2. Implement membership contract and membership status reads: `IN_PROGRESS` (contract deployed on Base Sepolia; mainnet deployment pending).
3. Implement checkout APIs and entitlement mint pipeline.
4. Implement runtime entitlement gate and evidence receipts.

View File

@ -12,7 +12,12 @@
"workspace_bound": false,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
"multi_tenant": false,
"execution_profile": {
"connector_surface": "hybrid",
"pacing_tier": "governed_human_pace",
"human_pace_floor_ms": 1200
}
},
{
"offer_id": "edut.workspace.core",
@ -24,7 +29,12 @@
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
"multi_tenant": false,
"execution_profile": {
"connector_surface": "hybrid",
"pacing_tier": "governed_human_pace",
"human_pace_floor_ms": 1200
}
},
{
"offer_id": "edut.workspace.ai",
@ -36,7 +46,12 @@
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
"multi_tenant": false,
"execution_profile": {
"connector_surface": "hybrid",
"pacing_tier": "governed_human_pace",
"human_pace_floor_ms": 1200
}
},
{
"offer_id": "edut.workspace.lane24",
@ -48,7 +63,11 @@
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
"multi_tenant": false,
"execution_profile": {
"connector_surface": "edut_native",
"pacing_tier": "local_hardware_speed"
}
},
{
"offer_id": "edut.workspace.sovereign",
@ -60,7 +79,11 @@
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
"multi_tenant": false,
"execution_profile": {
"connector_surface": "edut_native",
"pacing_tier": "local_hardware_speed"
}
}
],
"published_at": "2026-02-17T00:00:00Z"

View File

@ -21,10 +21,16 @@
"requires_admin_approval": false
},
"entitlement": {
"type": "runtime_license",
"scope": "org_root",
"type": "module_license",
"scope": "workspace",
"runtime_policy_ref": "policy.workspace.core.v1"
},
"execution_profile": {
"connector_surface": "hybrid",
"pacing_tier": "governed_human_pace",
"human_pace_floor_ms": 1200,
"notes": "External egress paths are paced at governed human cadence."
},
"created_at": "2026-02-17T00:00:00Z",
"updated_at": "2026-02-17T00:00:00Z",
"metadata": {

View File

@ -35,7 +35,26 @@
"workspace_bound": { "type": "boolean" },
"transferable": { "type": "boolean" },
"internal_use_only": { "type": "boolean" },
"multi_tenant": { "type": "boolean" }
"multi_tenant": { "type": "boolean" },
"execution_profile": {
"type": "object",
"additionalProperties": false,
"required": ["connector_surface", "pacing_tier"],
"properties": {
"connector_surface": {
"type": "string",
"enum": ["edut_native", "external_connector", "hybrid"]
},
"pacing_tier": {
"type": "string",
"enum": ["governed_human_pace", "local_hardware_speed"]
},
"human_pace_floor_ms": {
"type": "integer",
"minimum": 0
}
}
}
}
}
},

View File

@ -143,6 +143,39 @@
}
}
},
"execution_profile": {
"type": "object",
"additionalProperties": false,
"required": [
"connector_surface",
"pacing_tier"
],
"properties": {
"connector_surface": {
"type": "string",
"enum": [
"edut_native",
"external_connector",
"hybrid"
]
},
"pacing_tier": {
"type": "string",
"enum": [
"governed_human_pace",
"local_hardware_speed"
]
},
"human_pace_floor_ms": {
"type": "integer",
"minimum": 0
},
"notes": {
"type": "string",
"maxLength": 300
}
}
},
"created_at": {
"type": "string",
"format": "date-time"

View File

@ -63,6 +63,8 @@ Post-mint success -> app download links (Desktop/iOS/Android)
| Landing UI | `edut.ai`, `edut.dev` | Continue flow + wallet intent/signature + membership mint UX |
| API | `api.edut.ai/secret/wallet/intent` | Create one-time designation intent |
| API | `api.edut.ai/secret/wallet/verify` | Verify signature and bind wallet identity |
| API | `api.edut.ai/secret/wallet/session/refresh` | Rotate wallet session token |
| API | `api.edut.ai/secret/wallet/session/revoke` | Revoke wallet session token |
| API | `api.edut.ai/secret/membership/quote` | Return current payable membership quote |
| API | `api.edut.ai/secret/membership/confirm` | Confirm membership tx and activation state |
| Chain | Base | Membership mint settlement and evidence |
@ -160,11 +162,77 @@ Response:
"status": "signature_verified",
"designation_code": "0217073045482",
"display_token": "0217-0730-4548-2",
"verified_at": "2026-02-17T07:31:12Z"
"verified_at": "2026-02-17T07:31:12Z",
"session_token": "9f2c50f8a0f5d8d0b0efc4fa665e4032f31bb0c4c4f31b8c",
"session_expires_at": "2026-03-18T07:31:12Z"
}
```
### 3) Membership Quote
### 3) Wallet Session Lifecycle
#### `POST /secret/wallet/session/refresh`
Request JSON:
```json
{
"wallet": "0xabc123..."
}
```
Requirements:
1. Valid wallet session token via `Authorization: Bearer <token>` or `X-Edut-Session`.
2. Session wallet must match request wallet.
Behavior:
1. Validate current token state (not expired/revoked, chain matches).
2. Revoke the presented token.
3. Issue a replacement token with fresh expiry.
Response:
```json
{
"status": "session_refreshed",
"wallet": "0xabc123...",
"session_token": "f9bc20f15ecf7fd53f1f4ba8ca774564a1098e6ed9db6f0f",
"session_expires_at": "2026-03-18T07:42:10Z"
}
```
#### `POST /secret/wallet/session/revoke`
Request JSON:
```json
{
"wallet": "0xabc123..."
}
```
Requirements:
1. Valid wallet session token via `Authorization: Bearer <token>` or `X-Edut-Session`.
2. Session wallet must match request wallet.
Behavior:
1. Validate current token state.
2. Mark session as revoked immediately.
Response:
```json
{
"status": "session_revoked",
"wallet": "0xabc123...",
"revoked_at": "2026-02-17T07:34:02Z"
}
```
### 4) Membership Quote
#### `POST /secret/membership/quote`
@ -200,7 +268,7 @@ Response:
}
```
### 4) Membership Confirm
### 5) Membership Confirm
#### `POST /secret/membership/confirm`
@ -299,6 +367,14 @@ location /secret/wallet/verify {
proxy_pass http://127.0.0.1:9091;
}
location /secret/wallet/session/refresh {
proxy_pass http://127.0.0.1:9091;
}
location /secret/wallet/session/revoke {
proxy_pass http://127.0.0.1:9091;
}
location /secret/membership/quote {
proxy_pass http://127.0.0.1:9091;
}
@ -314,5 +390,6 @@ location /secret/membership/confirm {
The wallet-first designation plus paid membership flow creates a deterministic two-factor identity and commitment chain:
1. signature proves wallet control,
2. paid mint proves intent,
3. membership gates all future marketplace purchases.
2. verify issues wallet session for fail-closed control-plane access,
3. paid mint proves intent,
4. membership gates all future marketplace purchases.

View File

@ -0,0 +1,65 @@
# EDUT Vocabulary Registry (v1)
This registry defines canonical naming across user-facing copy, support language, and technical implementation.
Rule: one concept -> one preferred user phrase.
## Identity and Access
1. User-facing: `EDUT ID`
Technical: `membership` (current code key and route family)
Notes: Use `EDUT ID` in all UI/legal/public copy. Keep technical names stable until intentional internal refactor.
2. User-facing: `EDUT ID activation`
Technical: `membership activation`, `membership mint`
Notes: One-time purchase event, non-recurring.
3. User-facing: `EDUT ID active`
Technical: `membership_active`
Notes: Binary status text for user surfaces.
4. User-facing: `designation`
Technical: `designation_code`, `designation_token`
Notes: Keep visible only when needed for evidence/diagnostics.
## Commerce and Runtime
1. User-facing: `license`
Technical: `entitlement`
Notes: Keep license language in customer copy; entitlement remains implementation object.
2. User-facing: `workspace`
Technical: `org_root_id`, `workspace_id`
Notes: Avoid exposing raw boundary identifiers in default UI.
3. User-facing: `Auto capacity` (or approved SKU title)
Technical: `lane`, `lane24`
Notes: Avoid exposing `lane` as a default UI term outside diagnostics/trust surfaces.
4. User-facing: `offline continuity`
Technical: `sovereign`, `capsule`
Notes: Reserve `sovereign/capsule` for technical docs unless explicitly required.
## Terms To Keep Out of Default User Surfaces
1. `member_only`
2. `workspace_member`
3. `org_root_owner`
4. `connector_surface`
5. `pacing_tier`
6. `membership_*` internals
These remain valid in API contracts, logs, conformance vectors, and implementation docs.
## Change Discipline
1. Copy-only rename pass: user-facing surfaces first.
2. Internal rename pass: only when routes/schemas/contracts are versioned for a clean break.
3. Never mix names in one surface (`Membership` and `EDUT ID` together is prohibited).
## Inline Glossary Pattern
1. Keep technically accurate terms in UI when possible.
2. Add a small help icon next to the term.
3. Help text must be one sentence, plain language, no jargon.
4. Use glossary help instead of inventing alternate names that diverge from implementation language.

View File

@ -71,13 +71,13 @@
<div class="container">
<p><a href="/">back</a></p>
<h1>EDUT Platform Android</h1>
<p class="muted">membership channel verification</p>
<p class="muted">EDUT ID channel verification</p>
<div class="card">
<p>Android delivery is tied to wallet-authenticated membership state.</p>
<p>Android delivery is tied to wallet-authenticated EDUT ID state.</p>
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
<p id="status" class="status" aria-live="polite"></p>
<div id="download-instructions" class="download-instructions">
<p>Membership verified. Android distribution remains staged by designation era.</p>
<p>EDUT ID verified. Android distribution remains staged by designation era.</p>
<p>When your channel opens, this endpoint delivers current install instructions for Android onboarding.</p>
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
</div>
@ -127,18 +127,18 @@ async function handleConnectWallet() {
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
const chainId = Number.parseInt(chainHex, 16);
setStatus('Checking membership status...', false);
setStatus('Checking EDUT ID status...', false);
const status = await fetchMembershipStatus(wallet, chainId);
if (status.status === 'active') {
setStatus('Membership active. Android channel is authorized.', false);
setStatus('EDUT ID active. Android channel is authorized.', false);
instructionsNode.classList.add('visible');
return;
}
setStatus('Membership is not active for this wallet (' + status.status + ').', true);
setStatus('EDUT ID is not active for this wallet (' + status.status + ').', true);
} catch (err) {
const message = err && err.message ? err.message : 'Membership check failed.';
const message = err && err.message ? err.message : 'EDUT ID check failed.';
setStatus(message, true);
}
}

View File

@ -71,13 +71,13 @@
<div class="container">
<p><a href="/">back</a></p>
<h1>EDUT Platform Desktop</h1>
<p class="muted">membership channel verification</p>
<p class="muted">EDUT ID channel verification</p>
<div class="card">
<p>Desktop delivery is tied to wallet-authenticated membership state.</p>
<p>Desktop delivery is tied to wallet-authenticated EDUT ID state.</p>
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
<p id="status" class="status" aria-live="polite"></p>
<div id="download-instructions" class="download-instructions">
<p>Membership verified. Desktop distribution remains staged by designation era.</p>
<p>EDUT ID verified. Desktop distribution remains staged by designation era.</p>
<p>When your channel opens, this endpoint delivers the current installer package and checksum manifest.</p>
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
</div>
@ -127,18 +127,18 @@ async function handleConnectWallet() {
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
const chainId = Number.parseInt(chainHex, 16);
setStatus('Checking membership status...', false);
setStatus('Checking EDUT ID status...', false);
const status = await fetchMembershipStatus(wallet, chainId);
if (status.status === 'active') {
setStatus('Membership active. Desktop channel is authorized.', false);
setStatus('EDUT ID active. Desktop channel is authorized.', false);
instructionsNode.classList.add('visible');
return;
}
setStatus('Membership is not active for this wallet (' + status.status + ').', true);
setStatus('EDUT ID is not active for this wallet (' + status.status + ').', true);
} catch (err) {
const message = err && err.message ? err.message : 'Membership check failed.';
const message = err && err.message ? err.message : 'EDUT ID check failed.';
setStatus(message, true);
}
}

View File

@ -71,13 +71,13 @@
<div class="container">
<p><a href="/">back</a></p>
<h1>EDUT Platform iOS</h1>
<p class="muted">membership channel verification</p>
<p class="muted">EDUT ID channel verification</p>
<div class="card">
<p>iOS delivery is tied to wallet-authenticated membership state.</p>
<p>iOS delivery is tied to wallet-authenticated EDUT ID state.</p>
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
<p id="status" class="status" aria-live="polite"></p>
<div id="download-instructions" class="download-instructions">
<p>Membership verified. iOS distribution remains staged by designation era.</p>
<p>EDUT ID verified. iOS distribution remains staged by designation era.</p>
<p>When your channel opens, this endpoint delivers current install instructions for iOS onboarding.</p>
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
</div>
@ -127,18 +127,18 @@ async function handleConnectWallet() {
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
const chainId = Number.parseInt(chainHex, 16);
setStatus('Checking membership status...', false);
setStatus('Checking EDUT ID status...', false);
const status = await fetchMembershipStatus(wallet, chainId);
if (status.status === 'active') {
setStatus('Membership active. iOS channel is authorized.', false);
setStatus('EDUT ID active. iOS channel is authorized.', false);
instructionsNode.classList.add('visible');
return;
}
setStatus('Membership is not active for this wallet (' + status.status + ').', true);
setStatus('EDUT ID is not active for this wallet (' + status.status + ').', true);
} catch (err) {
const message = err && err.message ? err.message : 'Membership check failed.';
const message = err && err.message ? err.message : 'EDUT ID check failed.';
setStatus(message, true);
}
}

View File

@ -402,7 +402,7 @@
<div class="flow-ui" id="flow-ui">
<button id="continue-action" class="ghost-action localizable flow-hidden" data-i18n="continue_label" type="button">continue</button>
<div id="wallet-panel" class="flow-panel flow-hidden">
<p class="flow-line localizable" data-i18n="wallet_intro">mint your membership</p>
<p class="flow-line localizable" data-i18n="wallet_intro">activate your EDUT ID</p>
<p class="flow-line subtle localizable" data-i18n="wallet_fact_no_tx">No transaction. Signature only.</p>
<p class="flow-line subtle localizable" data-i18n="wallet_fact_seed">Never share your seed phrase.</p>
<div class="flow-actions">
@ -422,7 +422,7 @@
<a id="download-ios" class="flow-link localizable" data-i18n="download_ios" href="/downloads/ios">iOS</a>
<a id="download-android" class="flow-link localizable" data-i18n="download_android" href="/downloads/android">android</a>
</div>
<p class="flow-line subtle localizable" data-i18n="app_notifications_note">member updates are delivered inside the app after wallet sign-in.</p>
<p class="flow-line subtle localizable" data-i18n="app_notifications_note">EDUT ID updates are delivered inside the app after wallet sign-in.</p>
</div>
</div>
<span class="sr-only localizable" id="interaction-hint" data-i18n="interaction_hint">Click anywhere on the page to begin your access request.</span>
@ -472,6 +472,8 @@ const flowState = {
currentIntent: null,
sessionToken: '',
sessionExpiresAt: '',
sessionWallet: '',
sessionRefreshInFlight: null,
};
const continueAction = document.getElementById('continue-action');
@ -519,6 +521,7 @@ function getStoredAcknowledgement() {
if (parsed && (parsed.code || parsed.token)) {
flowState.sessionToken = typeof parsed.session_token === 'string' ? parsed.session_token : '';
flowState.sessionExpiresAt = typeof parsed.session_expires_at === 'string' ? parsed.session_expires_at : '';
flowState.sessionWallet = typeof parsed.wallet === 'string' ? parsed.wallet : '';
return parsed;
}
} catch (err) {
@ -609,7 +612,92 @@ function formatQuoteDisplay(quote) {
return null;
}
async function postJSON(url, payload) {
function clearFlowSession(reason) {
flowState.sessionToken = '';
flowState.sessionExpiresAt = '';
flowState.sessionWallet = '';
if (reason) {
console.warn('wallet session cleared:', reason);
}
}
function captureFlowSessionHeaders(res) {
const token = String(res.headers.get('x-edut-session') || '').trim();
if (token) {
flowState.sessionToken = token;
}
const expiresAt = String(res.headers.get('x-edut-session-expires-at') || '').trim();
if (expiresAt) {
flowState.sessionExpiresAt = expiresAt;
}
}
function parseApiError(text, status) {
if (!text) {
return { message: 'HTTP ' + status, code: '' };
}
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object') {
return {
message: String(parsed.error || ('HTTP ' + status)),
code: String(parsed.code || ''),
};
}
} catch (err) {
// fall back to raw text
}
return { message: text, code: '' };
}
function isTerminalSessionCode(code) {
const normalized = String(code || '').toLowerCase();
return normalized === 'wallet_session_invalid' ||
normalized === 'wallet_session_expired' ||
normalized === 'wallet_session_revoked' ||
normalized === 'wallet_session_mismatch';
}
async function maybeRefreshFlowSession(wallet, requestUrl) {
if (!flowState.sessionToken || !wallet || !flowState.sessionExpiresAt) {
return;
}
if (String(requestUrl || '').indexOf('/secret/wallet/session/') === 0) {
return;
}
const expiresAt = Date.parse(flowState.sessionExpiresAt);
if (!Number.isFinite(expiresAt)) {
return;
}
if ((expiresAt - Date.now()) > (5 * 60 * 1000)) {
return;
}
if (flowState.sessionRefreshInFlight) {
await flowState.sessionRefreshInFlight;
return;
}
flowState.sessionRefreshInFlight = postJSON(
'/secret/wallet/session/refresh',
{ wallet },
{ wallet, skipSessionRefresh: true },
).then(function (out) {
if (out && typeof out.session_token === 'string') {
flowState.sessionToken = out.session_token;
}
if (out && typeof out.session_expires_at === 'string') {
flowState.sessionExpiresAt = out.session_expires_at;
}
}).finally(function () {
flowState.sessionRefreshInFlight = null;
});
await flowState.sessionRefreshInFlight;
}
async function postJSON(url, payload, options) {
const opts = options || {};
if (!opts.skipSessionRefresh) {
await maybeRefreshFlowSession(opts.wallet || flowState.sessionWallet || '', url);
}
const headers = {
'Content-Type': 'application/json',
};
@ -622,11 +710,17 @@ async function postJSON(url, payload) {
headers,
body: JSON.stringify(payload),
});
captureFlowSessionHeaders(res);
const text = await res.text();
if (!res.ok) {
const detail = await res.text();
throw new Error(detail || ('HTTP ' + res.status));
const detail = parseApiError(text, res.status);
if (isTerminalSessionCode(detail.code)) {
clearFlowSession(detail.code);
}
return res.json();
throw new Error(detail.message);
}
if (!text) return {};
return JSON.parse(text);
}
function resolveLanguage(input) {
@ -725,6 +819,10 @@ async function startWalletFlow() {
throw new Error('Wallet connection was not approved.');
}
const address = accounts[0];
if (flowState.sessionWallet && flowState.sessionWallet.toLowerCase() !== address.toLowerCase()) {
clearFlowSession('wallet_changed');
}
flowState.sessionWallet = address;
const chainIdHex = await window.ethereum.request({ method: 'eth_chainId' });
const chainId = Number.parseInt(chainIdHex, 16);
@ -734,7 +832,7 @@ async function startWalletFlow() {
origin: window.location.origin,
locale: localStorage.getItem('edut_lang') || 'en',
chain_id: chainId,
});
}, { wallet: address });
flowState.currentIntent = intent;
const typedData = {
@ -781,16 +879,17 @@ async function startWalletFlow() {
address,
chain_id: chainId,
signature,
});
}, { wallet: address });
flowState.sessionToken = verification.session_token || '';
flowState.sessionExpiresAt = verification.session_expires_at || '';
flowState.sessionWallet = address;
setFlowStatus('membership_quoting', 'Preparing membership mint...', false);
setFlowStatus('membership_quoting', 'Preparing EDUT ID activation...', false);
const quote = await postJSON('/secret/membership/quote', {
designation_code: verification.designation_code || intent.designation_code || null,
address,
chain_id: chainId,
});
}, { wallet: address });
const txParams = quote.tx || {
from: address,
@ -801,32 +900,32 @@ async function startWalletFlow() {
if (!txParams.from) txParams.from = address;
if (!txParams.to) {
throw new Error('Membership quote is missing destination contract.');
throw new Error('EDUT ID quote is missing destination contract.');
}
const quoteDisplay = formatQuoteDisplay(quote);
const mintPrompt = t('membership_minting', 'Confirm membership mint in your wallet...');
const mintPrompt = t('membership_minting', 'Confirm EDUT ID activation in your wallet...');
if (quoteDisplay) {
setFlowStatusMessage(mintPrompt + ' (' + quoteDisplay + ')', false);
} else {
setFlowStatus('membership_minting', 'Confirm membership mint in your wallet...', false);
setFlowStatus('membership_minting', 'Confirm EDUT ID activation in your wallet...', false);
}
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [txParams],
});
setFlowStatus('membership_confirming', 'Confirming membership on-chain...', false);
setFlowStatus('membership_confirming', 'Confirming EDUT ID on-chain...', false);
const confirmation = await postJSON('/secret/membership/confirm', {
designation_code: verification.designation_code || intent.designation_code || null,
quote_id: quote.quote_id || null,
tx_hash: txHash,
address,
chain_id: chainId,
});
}, { wallet: address });
if (confirmation.status !== 'membership_active') {
throw new Error('Membership transaction did not activate.');
throw new Error('EDUT ID transaction did not activate.');
}
const ackState = {
@ -841,7 +940,7 @@ async function startWalletFlow() {
saveAcknowledgement(ackState);
renderAcknowledged(ackState);
showPostMintPanel();
setFlowStatus('membership_active', 'Membership active. Designation acknowledged.', false);
setFlowStatus('membership_active', 'EDUT ID active. Designation acknowledged.', false);
} catch (err) {
const message = err && err.message ? err.message : 'Wallet flow failed.';
setFlowStatus('wallet_failed', message, true);

View File

@ -169,7 +169,7 @@
<p>We may use information we collect to operate, maintain, and improve our websites, products, and services; process transactions and send related confirmations, invoices, and receipts; communicate with you regarding security, operations, product updates, and support; analyze reliability and usage trends; detect and prevent fraud, abuse, and unauthorized access; comply with legal obligations; and enforce our agreements.</p>
<p><strong>Verified-channel purpose.</strong> Where designation workflows are used, we process cryptographic signature data to confirm wallet control, prevent automated abuse, bind protocol records to a stable identity anchor, and maintain auditable activation continuity. Member operational notices are delivered through platform software after wallet sign-in.</p>
<p><strong>Verified-channel purpose.</strong> Where designation workflows are used, we process cryptographic signature data to confirm wallet control, prevent automated abuse, bind protocol records to a stable identity anchor, and maintain auditable activation continuity. EDUT ID operational notices are delivered through platform software after wallet sign-in.</p>
<h2>Blockchain and Wallet Data</h2>
<p>Wallet addresses and related signature metadata may be processed to verify cryptographic intent and establish designation records. Public blockchain networks are independently operated systems; if designation or licensing records are written on-chain, related transaction data may be publicly visible and immutable by design. We do not control third-party blockchain explorers or wallet software.</p>

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EDUT Store Preview</title>
<meta name="description" content="EDUT membership-gated marketplace preview states.">
<meta name="description" content="EDUT ID-gated marketplace preview states.">
<meta name="theme-color" content="#f0f4f8">
<meta name="robots" content="noindex,nofollow,noarchive,nosnippet">
<style>
@ -69,6 +69,43 @@
line-height: 1.7;
color: #454b54;
}
.term-inline {
display: inline-flex;
align-items: center;
gap: 6px;
}
.inline-help {
position: relative;
display: inline-block;
}
.inline-help > summary {
list-style: none;
width: 16px;
height: 16px;
border: 1px solid #c2c8d0;
border-radius: 50%;
text-align: center;
line-height: 14px;
font-size: 10px;
color: #5a616a;
cursor: pointer;
user-select: none;
background: #fff;
}
.inline-help > summary::-webkit-details-marker { display: none; }
.inline-help > p {
position: absolute;
top: 20px;
left: 0;
width: min(280px, 70vw);
border: 1px solid #d0d5db;
background: #fff;
padding: 8px;
font-size: 11px;
line-height: 1.5;
color: #4d545d;
z-index: 20;
}
.state {
display: inline-block;
margin-top: 8px;
@ -147,18 +184,18 @@
<div class="container">
<a href="/" class="back">← Back</a>
<h1>EDUT Store</h1>
<p class="sub" id="preview-mode-note">Membership-gated checkout behavior (internal preview scaffold)</p>
<p class="sub" id="preview-mode-note">EDUT ID-gated checkout behavior (internal preview scaffold)</p>
<div class="grid">
<section class="card">
<p class="label">Wallet + Membership</p>
<p class="label"><span class="term-inline">Wallet + EDUT ID<details class="inline-help"><summary aria-label="What is EDUT ID?">?</summary><p>EDUT ID is your one-time identity credential required to buy and activate EDUT products.</p></details></span></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"><span class="term-inline">EDUT ID status<details class="inline-help"><summary aria-label="What does EDUT ID status mean?">?</summary><p>Active means this wallet can check out and receive entitlements. Any other status blocks purchase.</p></details></span>: <span class="mono" id="membership-label">unknown</span></p>
<p class="line">Gate decision: <span class="mono" id="gate-label">blocked</span></p>
<p class="line">Payer wallet override (optional):</p>
<input id="payer-wallet-input" type="text" placeholder="0x... (leave blank to use ownership wallet)">
<p class="line">Ownership proof: <span class="mono" id="proof-label">not required</span></p>
<span class="state block" id="gate-pill">membership required</span>
<p class="line"><span class="term-inline">Ownership proof<details class="inline-help"><summary aria-label="What is ownership proof?">?</summary><p>If payer wallet differs from ownership wallet, a signature proves the owner approved that payer.</p></details></span>: <span class="mono" id="proof-label">not required</span></p>
<span class="state block" id="gate-pill">EDUT ID required</span>
<div class="actions">
<button id="connect-btn" type="button">connect wallet</button>
<button id="refresh-btn" type="button">refresh state</button>
@ -183,23 +220,23 @@
<p class="line" id="offer-summary">Catalog data pending.</p>
<p class="line" id="offer-price">Price: --</p>
<p class="line" id="offer-policy">Policy: --</p>
<p class="line">Action chain: membership check -> ownership proof (if needed) -> quote -> wallet confirm -> entitlement receipt</p>
<p class="line">Action chain: EDUT ID check -> ownership proof (if needed) -> 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>
<div class="status-log" id="checkout-log">Checkout is blocked until EDUT ID 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">No EDUT ID: 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 intentionally deterministic: if membership cannot be confirmed, purchase remains blocked. <a href="/trust">Trust page</a>.</p>
<p class="foot">This page is intentionally deterministic: if EDUT ID cannot be confirmed, purchase remains blocked. <a href="/trust">Trust page</a>.</p>
</div>
<script>
@ -217,7 +254,9 @@
offers: [],
selectedOfferId: null,
sessionToken: '',
sessionExpiresAt: '',
sessionWallet: null,
sessionRefreshInFlight: null,
};
const walletLabel = document.getElementById('wallet-label');
@ -322,9 +361,16 @@
offerTitle.textContent = selected.title || selected.offer_id;
offerSummary.textContent = selected.summary || 'No summary provided.';
offerPrice.textContent = 'Price: ' + (selected.price || '--') + ' ' + (selected.currency || '');
offerPolicy.textContent = 'Policy: member-only=' + Boolean(selected.member_only) +
let profile = '';
if (selected.execution_profile && typeof selected.execution_profile === 'object') {
const pace = selected.execution_profile.pacing_tier || 'unknown';
const surface = selected.execution_profile.connector_surface || 'unknown';
profile = ', pacing=' + pace + ', surface=' + surface;
}
offerPolicy.textContent = 'Policy: EDUT-ID-required=' + Boolean(selected.member_only) +
', workspace-bound=' + Boolean(selected.workspace_bound) +
', transferable=' + Boolean(selected.transferable);
', transferable=' + Boolean(selected.transferable) +
profile;
}
function populateOfferSelect() {
@ -365,7 +411,7 @@
gatePill.textContent = 'status unknown';
} else {
gatePill.className = 'state block';
gatePill.textContent = 'membership required';
gatePill.textContent = 'EDUT ID required';
}
checkoutBtn.disabled = !state.gate || !state.selectedOfferId;
@ -397,6 +443,9 @@
if (typeof parsed.session_token === 'string') {
state.sessionToken = parsed.session_token.trim();
}
if (typeof parsed.session_expires_at === 'string') {
state.sessionExpiresAt = parsed.session_expires_at.trim();
}
if (typeof parsed.wallet === 'string') {
state.sessionWallet = parsed.wallet.trim();
}
@ -405,6 +454,107 @@
}
}
function clearSession(reason) {
state.sessionToken = '';
state.sessionExpiresAt = '';
state.sessionWallet = null;
if (reason) {
setLog('Wallet session cleared: ' + reason + '.');
}
}
function captureSessionHeaders(response) {
const token = String(response.headers.get('x-edut-session') || '').trim();
if (!token) return;
state.sessionToken = token;
const expiresAt = String(response.headers.get('x-edut-session-expires-at') || '').trim();
if (expiresAt) {
state.sessionExpiresAt = expiresAt;
}
if (state.wallet) {
state.sessionWallet = state.wallet;
}
}
function parseApiErrorPayload(text, fallbackStatus) {
if (!text) {
return { error: 'HTTP ' + fallbackStatus, code: '' };
}
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object') {
return {
error: String(parsed.error || ('HTTP ' + fallbackStatus)),
code: String(parsed.code || ''),
};
}
} catch (err) {
// non-json response
}
return { error: text, code: '' };
}
function isTerminalSessionCode(code) {
const normalized = String(code || '').toLowerCase();
return normalized === 'wallet_session_invalid' ||
normalized === 'wallet_session_expired' ||
normalized === 'wallet_session_revoked' ||
normalized === 'wallet_session_mismatch';
}
async function maybeRefreshSession(pathHint) {
if (!state.sessionToken || !state.wallet || !state.sessionExpiresAt) {
return;
}
if (String(pathHint || '').indexOf('/secret/wallet/session/') === 0) {
return;
}
const expiresAt = Date.parse(state.sessionExpiresAt);
if (!Number.isFinite(expiresAt)) {
return;
}
if ((expiresAt - Date.now()) > (5 * 60 * 1000)) {
return;
}
if (state.sessionRefreshInFlight) {
await state.sessionRefreshInFlight;
return;
}
state.sessionRefreshInFlight = (async function () {
const response = await fetch('/secret/wallet/session/refresh', {
method: 'POST',
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ wallet: state.wallet }),
});
captureSessionHeaders(response);
const text = await response.text();
if (!response.ok) {
const detail = parseApiErrorPayload(text, response.status);
if (isTerminalSessionCode(detail.code)) {
clearSession(detail.code);
}
throw new Error(detail.error);
}
let payload = {};
try {
payload = text ? JSON.parse(text) : {};
} catch (err) {
// ignore parse error for non-essential refresh payload
}
if (payload && typeof payload.session_token === 'string') {
state.sessionToken = payload.session_token.trim();
}
if (payload && typeof payload.session_expires_at === 'string') {
state.sessionExpiresAt = payload.session_expires_at.trim();
}
state.sessionWallet = state.wallet;
setLog('Wallet session refreshed.');
})().finally(function () {
state.sessionRefreshInFlight = null;
});
await state.sessionRefreshInFlight;
}
function authHeaders(extra) {
const headers = Object.assign({}, extra || {});
if (state.sessionToken) {
@ -415,16 +565,42 @@
}
async function fetchJson(url) {
await maybeRefreshSession(url);
const response = await fetch(url, {
method: 'GET',
headers: authHeaders(),
});
captureSessionHeaders(response);
if (!response.ok) {
throw new Error('HTTP ' + response.status);
const text = await response.text();
const detail = parseApiErrorPayload(text, response.status);
if (isTerminalSessionCode(detail.code)) {
clearSession(detail.code);
}
throw new Error(detail.error);
}
return response.json();
}
async function postJson(url, payload) {
await maybeRefreshSession(url);
const response = await fetch(url, {
method: 'POST',
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
});
captureSessionHeaders(response);
const text = await response.text();
if (!response.ok) {
const detail = parseApiErrorPayload(text, response.status);
if (isTerminalSessionCode(detail.code)) {
clearSession(detail.code);
}
throw new Error(detail.error);
}
return text ? JSON.parse(text) : {};
}
async function loadOffers() {
try {
const payload = await fetchJson('/store/offers.json');
@ -484,13 +660,13 @@
}
state.source = 'live';
setLog('Checking live membership status...');
setLog('Checking live EDUT ID status...');
try {
const membership = await fetchLiveMembershipStatus();
state.membership = membership;
applyGateState();
setLog('Live status resolved: ' + membership + '.');
setLog('Live EDUT ID status resolved: ' + membership + '.');
} catch (err) {
state.membership = 'unknown';
applyGateState();
@ -511,8 +687,9 @@
state.wallet = accounts[0];
state.ownershipProof = null;
if (state.sessionWallet && state.wallet && state.sessionWallet.toLowerCase() !== state.wallet.toLowerCase()) {
state.sessionToken = '';
clearSession('wallet changed');
}
state.sessionWallet = state.wallet;
setLog('Wallet connected: ' + abbreviateWallet(state.wallet) + '.');
await refreshMembershipState();
} catch (err) {
@ -578,7 +755,7 @@
async function requestCheckoutQuote() {
if (!state.gate) {
setCheckoutLog('Checkout blocked: membership is not active.');
setCheckoutLog('Checkout blocked: EDUT ID is not active.');
return;
}
@ -616,17 +793,7 @@
payload.ownership_proof = state.ownershipProof.signature;
}
const response = await fetch('/marketplace/checkout/quote', {
method: 'POST',
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
const quotePayload = await response.json();
const quotePayload = await postJson('/marketplace/checkout/quote', payload);
const quoteId = quotePayload.quote_id || 'unknown';
const amount = quotePayload.amount || quotePayload.amount_atomic || 'unknown';
const total = quotePayload.total_amount || quotePayload.total_amount_atomic || amount;

View File

@ -11,7 +11,12 @@
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
"multi_tenant": false,
"execution_profile": {
"connector_surface": "hybrid",
"pacing_tier": "governed_human_pace",
"human_pace_floor_ms": 1200
}
},
{
"offer_id": "edut.workspace.core",
@ -23,7 +28,12 @@
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
"multi_tenant": false,
"execution_profile": {
"connector_surface": "hybrid",
"pacing_tier": "governed_human_pace",
"human_pace_floor_ms": 1200
}
},
{
"offer_id": "edut.workspace.ai",
@ -35,7 +45,12 @@
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
"multi_tenant": false,
"execution_profile": {
"connector_surface": "hybrid",
"pacing_tier": "governed_human_pace",
"human_pace_floor_ms": 1200
}
},
{
"offer_id": "edut.workspace.lane24",
@ -47,7 +62,11 @@
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
"multi_tenant": false,
"execution_profile": {
"connector_surface": "edut_native",
"pacing_tier": "local_hardware_speed"
}
},
{
"offer_id": "edut.workspace.sovereign",
@ -59,7 +78,11 @@
"workspace_bound": true,
"transferable": false,
"internal_use_only": true,
"multi_tenant": false
"multi_tenant": false,
"execution_profile": {
"connector_surface": "edut_native",
"pacing_tier": "local_hardware_speed"
}
}
]
}

View File

@ -158,11 +158,11 @@
<h2>Eligibility</h2>
<p>You must be at least 16 years old and able to enter into a binding agreement to use the Services. If you use the Services on behalf of an organization, you represent that you have authority to bind that organization to these Terms.</p>
<h2>Designations and Membership</h2>
<p>Edut may issue designations through site interactions and wallet-based verification workflows. Designation identifiers are non-transferable evidence identifiers. Edut may also issue paid membership credentials that gate access to marketplace purchasing. Unless explicitly stated in writing, membership grants access eligibility only and does not itself grant product runtime rights.</p>
<h2>Designations and EDUT ID</h2>
<p>Edut may issue designations through site interactions and wallet-based verification workflows. Designation identifiers are non-transferable evidence identifiers. Edut may also issue paid EDUT ID credentials that gate access to marketplace purchasing. Unless explicitly stated in writing, EDUT ID grants access eligibility only and does not itself grant product runtime rights.</p>
<h2>Marketplace Access and Licenses</h2>
<p>Edut may require an active membership credential for purchase of offers, modules, or services through EDUT marketplace surfaces. Product-specific rights are granted by separate offer entitlements or licenses. Membership, designation, and product licenses are distinct instruments with different rights and limitations.</p>
<p>Edut may require an active EDUT ID credential for purchase of offers, modules, or services through EDUT marketplace surfaces. Product-specific rights are granted by separate offer entitlements or licenses. EDUT ID, designation, and product licenses are distinct instruments with different rights and limitations.</p>
<h2>Products, Licensing, and Availability</h2>
<p>Edut may offer software licenses, modules, deployment services, and related tools. Descriptions, availability, and requirements may change without notice. Additional product-specific terms may apply at purchase or activation time and, in case of conflict, those specific terms control for that product.</p>
@ -178,10 +178,10 @@
<h2>Payments</h2>
<p>Where paid offerings are available, you agree to pay applicable charges at checkout. Accepted payment methods may vary by offering. Unless required by law or stated otherwise in writing, fees are non-refundable.</p>
<p>For first-time purchases, EDUT may bundle membership activation with a license purchase in a single checkout total. When bundled, checkout displays the line-item composition (for example, membership activation plus license component) before transaction confirmation.</p>
<p>For first-time purchases, EDUT may bundle EDUT ID activation with a license purchase in a single checkout total. When bundled, checkout displays the line-item composition (for example, EDUT ID activation plus license component) before transaction confirmation.</p>
<h2>No Investment Expectation</h2>
<p>Designations, memberships, and related access credentials are utility access instruments for EDUT services. They are not investment contracts, securities, profit-sharing instruments, or claims on company equity, assets, or revenue. EDUT does not promise appreciation, resale value, financial return, or secondary-market liquidity for any access credential.</p>
<p>Designations, EDUT IDs, and related access credentials are utility access instruments for EDUT services. They are not investment contracts, securities, profit-sharing instruments, or claims on company equity, assets, or revenue. EDUT does not promise appreciation, resale value, financial return, or secondary-market liquidity for any access credential.</p>
<h2>Intellectual Property</h2>
<p>The Services, including software, text, visual assets, trademarks, logos, and documentation ("Edut IP"), are owned by Edut LLC or its licensors and are protected by applicable intellectual property laws. Except where explicitly licensed in writing, no rights are granted to copy, modify, distribute, reverse engineer, or create derivative works from Edut IP.</p>

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EDUT Trust</title>
<meta name="description" content="Operational trust facts for EDUT membership and marketplace infrastructure.">
<meta name="description" content="Operational trust facts for EDUT ID and marketplace infrastructure.">
<meta name="theme-color" content="#f0f4f8">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&display=swap');
@ -102,7 +102,7 @@
<section class="card">
<p class="label">Contracts</p>
<p class="line">Membership: <span class="mono">pending deployment</span></p>
<p class="line">EDUT ID: <span class="mono">pending deployment</span></p>
<p class="line">Offer Registry: <span class="mono">pending deployment</span></p>
<p class="line">Entitlement: <span class="mono">pending deployment</span></p>
<span class="status warn">addresses pending</span>
@ -110,7 +110,8 @@
<section class="card">
<p class="label">Policy Snapshot</p>
<p class="line">Membership floor: <span class="mono">USD 5.00 equivalent</span></p>
<p class="line">EDUT ID activation floor: <span class="mono">100.00 USDC</span></p>
<p class="line">Settlement rail: <span class="mono">USDC on Base</span></p>
<p class="line">Policy hash: <span class="mono">pending publication</span></p>
<p class="line">Updated: <span class="mono">pending deployment</span></p>
</section>
@ -119,6 +120,8 @@
<p class="label">API Health Targets</p>
<p class="line">/secret/wallet/intent</p>
<p class="line">/secret/wallet/verify</p>
<p class="line">/secret/wallet/session/refresh</p>
<p class="line">/secret/wallet/session/revoke</p>
<p class="line">/secret/membership/quote</p>
<p class="line">/secret/membership/confirm</p>
</section>

View File

@ -28,7 +28,7 @@
"note": "ملاحظة: هذه الصفحة بسيطة عن قصد. وهي تعكس فلسفة منتج ترى أن الحساب قد يكون معقدًا بينما تبقى الواجهات هادئة."
},
"continue_label": "متابعة",
"wallet_intro": "قم بسك عضويتك",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "لا توجد معاملة. فقط توقيع.",
"wallet_fact_seed": "لا تشارك عبارة الاسترداد الخاصة بك أبدًا.",
"wallet_have": "لدي محفظة",
@ -41,15 +41,15 @@
"wallet_signing": "بانتظار التوقيع...",
"wallet_verifying": "جارٍ التحقق من التوقيع...",
"wallet_success": "تم تأكيد التعيين.",
"membership_quoting": "جارٍ إعداد سك العضوية...",
"membership_minting": "أكد سك العضوية في محفظتك...",
"membership_confirming": "جارٍ تأكيد العضوية على السلسلة...",
"membership_active": "العضوية نشطة. تم تأكيد التعيين.",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "فشل مسار المحفظة.",
"wallet_missing": "لم يتم العثور على محفظة على هذا الجهاز.",
"download_heading": "نزّل منصتك",
"download_desktop": "سطح المكتب",
"download_ios": "iOS",
"download_android": "أندرويد",
"app_notifications_note": "تصل تحديثات الأعضاء داخل التطبيق بعد تسجيل الدخول بالمحفظة."
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "Hinweis: Diese Seite ist bewusst minimal. Sie spiegelt eine Produktphilosophie wider, bei der die Berechnung komplex und die Oberfläche ruhig bleibt."
},
"continue_label": "weiter",
"wallet_intro": "praege deine Mitgliedschaft",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "Keine Transaktion. Nur Signatur.",
"wallet_fact_seed": "Teile niemals deine Seed-Phrase.",
"wallet_have": "Ich habe ein Wallet",
@ -41,15 +41,15 @@
"wallet_signing": "Warte auf Signatur...",
"wallet_verifying": "Signatur wird verifiziert...",
"wallet_success": "Designation bestätigt.",
"membership_quoting": "Membership-Praegung wird vorbereitet...",
"membership_minting": "Bestaetige die Membership-Praegung in deiner Wallet...",
"membership_confirming": "Membership wird on-chain bestaetigt...",
"membership_active": "Membership aktiv. Designation bestaetigt.",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "Wallet-Ablauf fehlgeschlagen.",
"wallet_missing": "Kein Wallet auf diesem Gerät erkannt.",
"download_heading": "lade deine plattform herunter",
"download_desktop": "desktop",
"download_ios": "iOS",
"download_android": "android",
"app_notifications_note": "mitglieder-updates werden in der app nach wallet-anmeldung zugestellt."
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "Note: This page is intentionally minimal. It reflects a product philosophy where computation is complex and interfaces remain quiet."
},
"continue_label": "continue",
"wallet_intro": "mint your membership",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "No transaction. Signature only.",
"wallet_fact_seed": "Never share your seed phrase.",
"wallet_have": "I have a wallet",
@ -41,15 +41,15 @@
"wallet_signing": "Awaiting signature...",
"wallet_verifying": "Verifying signature...",
"wallet_success": "Designation acknowledged.",
"membership_quoting": "Preparing membership mint...",
"membership_minting": "Confirm membership mint in your wallet...",
"membership_confirming": "Confirming membership on-chain...",
"membership_active": "Membership active. Designation acknowledged.",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "Wallet flow failed.",
"wallet_missing": "No wallet detected on this device.",
"download_heading": "download your platform",
"download_desktop": "desktop",
"download_ios": "iOS",
"download_android": "android",
"app_notifications_note": "member updates are delivered inside the app after wallet sign-in."
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "Nota: Esta página es intencionalmente mínima. Refleja una filosofía de producto donde el cómputo es complejo y las interfaces permanecen silenciosas."
},
"continue_label": "continuar",
"wallet_intro": "acuña tu membresía",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "Sin transacción. Solo firma.",
"wallet_fact_seed": "Nunca compartas tu frase semilla.",
"wallet_have": "Tengo una wallet",
@ -41,15 +41,15 @@
"wallet_signing": "Esperando firma...",
"wallet_verifying": "Verificando firma...",
"wallet_success": "Designación reconocida.",
"membership_quoting": "Preparando acuñación de membresía...",
"membership_minting": "Confirma la acuñación de membresía en tu wallet...",
"membership_confirming": "Confirmando membresía en la cadena...",
"membership_active": "Membresía activa. Designación reconocida.",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "Falló el flujo de wallet.",
"wallet_missing": "No se detectó wallet en este dispositivo.",
"download_heading": "descarga tu plataforma",
"download_desktop": "escritorio",
"download_ios": "iOS",
"download_android": "android",
"app_notifications_note": "las actualizaciones para miembros se entregan dentro de la app tras iniciar sesion con wallet."
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "Note : Cette page est volontairement minimale. Elle reflète une philosophie produit où le calcul est complexe et les interfaces restent discrètes."
},
"continue_label": "continuer",
"wallet_intro": "frappez votre adhesion",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "Aucune transaction. Signature uniquement.",
"wallet_fact_seed": "Ne partagez jamais votre phrase secrete.",
"wallet_have": "Je possede un wallet",
@ -41,15 +41,15 @@
"wallet_signing": "Signature en attente...",
"wallet_verifying": "Verification de la signature...",
"wallet_success": "Designation confirmee.",
"membership_quoting": "Preparation du mint dadhesion...",
"membership_minting": "Confirmez le mint dadhesion dans votre wallet...",
"membership_confirming": "Confirmation de ladhesion on-chain...",
"membership_active": "Adhesion active. Designation confirmee.",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "Le flux wallet a echoue.",
"wallet_missing": "Aucun wallet detecte sur cet appareil.",
"download_heading": "telechargez votre plateforme",
"download_desktop": "bureau",
"download_ios": "iOS",
"download_android": "android",
"app_notifications_note": "les mises a jour membres sont envoyees dans lapp apres connexion du wallet."
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "הערה: דף זה מינימלי בכוונה. הוא משקף פילוסופיית מוצר שבה החישוב מורכב, אך הממשקים נשארים שקטים."
},
"continue_label": "המשך",
"wallet_intro": "הנפק את החברות שלך",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "אין עסקה. רק חתימה.",
"wallet_fact_seed": "לעולם אל תשתף את ביטוי השחזור שלך.",
"wallet_have": "יש לי ארנק",
@ -41,15 +41,15 @@
"wallet_signing": "ממתין לחתימה...",
"wallet_verifying": "מאמת חתימה...",
"wallet_success": "הייעוד אושר.",
"membership_quoting": "מכין הטבעת חברות...",
"membership_minting": "אשר הטבעת חברות בארנק שלך...",
"membership_confirming": "מאשר חברות על השרשרת...",
"membership_active": "החברות פעילה. הייעוד אושר.",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "תהליך הארנק נכשל.",
"wallet_missing": "לא זוהה ארנק במכשיר זה.",
"download_heading": "הורד את הפלטפורמה שלך",
"download_desktop": "דסקטופ",
"download_ios": "iOS",
"download_android": "אנדרואיד",
"app_notifications_note": "עדכוני חברים נמסרים בתוך האפליקציה לאחר התחברות בארנק."
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "नोट: यह पृष्ठ जानबूझकर न्यूनतम रखा गया है। यह उस उत्पाद दर्शन को दर्शाता है जिसमें गणना जटिल हो सकती है, लेकिन इंटरफेस शांत रहते हैं।"
},
"continue_label": "जारी रखें",
"wallet_intro": "अपनी सदस्यता मिंट करें",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "कोई ट्रांज़ैक्शन नहीं। केवल सिग्नेचर।",
"wallet_fact_seed": "अपना सीड फ़्रेज़ कभी साझा न करें।",
"wallet_have": "मेरे पास वॉलेट है",
@ -41,15 +41,15 @@
"wallet_signing": "सिग्नेचर की प्रतीक्षा...",
"wallet_verifying": "सिग्नेचर सत्यापित किया जा रहा है...",
"wallet_success": "नामांकन स्वीकार किया गया।",
"membership_quoting": "सदस्यता मिंट तैयार की जा रही है...",
"membership_minting": "अपने वॉलेट में सदस्यता मिंट की पुष्टि करें...",
"membership_confirming": "ऑन-चेन सदस्यता की पुष्टि की जा रही है...",
"membership_active": "सदस्यता सक्रिय है। नामांकन स्वीकार किया गया।",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "वॉलेट फ़्लो विफल रहा।",
"wallet_missing": "इस डिवाइस पर कोई वॉलेट नहीं मिला।",
"download_heading": "अपना प्लेटफ़ॉर्म डाउनलोड करें",
"download_desktop": "डेस्कटॉप",
"download_ios": "iOS",
"download_android": "एंड्रॉइड",
"app_notifications_note": "सदस्य अपडेट वॉलेट साइन-इन के बाद ऐप के अंदर दिए जाते हैं।"
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "注記: このページは意図的にミニマルです。計算は複雑でも、インターフェースは静かであるべきという製品哲学を表しています。"
},
"continue_label": "続行",
"wallet_intro": "メンバーシップをミントする",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "取引は発生しません。署名のみです。",
"wallet_fact_seed": "シードフレーズは絶対に共有しないでください。",
"wallet_have": "ウォレットを持っています",
@ -41,15 +41,15 @@
"wallet_signing": "署名を待機中...",
"wallet_verifying": "署名を検証しています...",
"wallet_success": "指定が確認されました。",
"membership_quoting": "メンバーシップのミントを準備中...",
"membership_minting": "ウォレットでメンバーシップのミントを確認してください...",
"membership_confirming": "オンチェーンでメンバーシップを確認中...",
"membership_active": "メンバーシップが有効です。指定が確認されました。",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "ウォレットフローに失敗しました。",
"wallet_missing": "この端末でウォレットが見つかりません。",
"download_heading": "プラットフォームをダウンロード",
"download_desktop": "デスクトップ",
"download_ios": "iOS",
"download_android": "android",
"app_notifications_note": "メンバー向け更新はウォレットサインイン後にアプリ内で配信されます。"
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "참고: 이 페이지는 의도적으로 미니멀합니다. 연산은 복잡하더라도 인터페이스는 조용해야 한다는 제품 철학을 반영합니다."
},
"continue_label": "계속",
"wallet_intro": "멤버십 민팅하기",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "거래는 없습니다. 서명만 진행됩니다.",
"wallet_fact_seed": "시드 문구를 절대 공유하지 마세요.",
"wallet_have": "지갑이 있습니다",
@ -41,15 +41,15 @@
"wallet_signing": "서명 대기 중...",
"wallet_verifying": "서명 검증 중...",
"wallet_success": "지정이 확인되었습니다.",
"membership_quoting": "멤버십 민팅을 준비하는 중...",
"membership_minting": "지갑에서 멤버십 민팅을 확인하세요...",
"membership_confirming": "온체인 멤버십을 확인하는 중...",
"membership_active": "멤버십이 활성화되었습니다. 지정이 확인되었습니다.",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "지갑 흐름에 실패했습니다.",
"wallet_missing": "이 기기에서 지갑을 찾을 수 없습니다.",
"download_heading": "플랫폼 다운로드",
"download_desktop": "데스크톱",
"download_ios": "iOS",
"download_android": "android",
"app_notifications_note": "회원 업데이트는 지갑 로그인 후 앱 안에서 전달됩니다."
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "Nota: Esta página é intencionalmente minimalista. Ela reflete uma filosofia de produto em que a computação é complexa e as interfaces permanecem discretas."
},
"continue_label": "continuar",
"wallet_intro": "cunhe sua associação",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "Sem transação. Apenas assinatura.",
"wallet_fact_seed": "Nunca compartilhe sua frase-semente.",
"wallet_have": "Tenho uma wallet",
@ -41,15 +41,15 @@
"wallet_signing": "Aguardando assinatura...",
"wallet_verifying": "Verificando assinatura...",
"wallet_success": "Designação confirmada.",
"membership_quoting": "Preparando mint de associação...",
"membership_minting": "Confirme o mint de associação na sua wallet...",
"membership_confirming": "Confirmando associação on-chain...",
"membership_active": "Associação ativa. Designação confirmada.",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "Falha no fluxo de wallet.",
"wallet_missing": "Nenhuma wallet detectada neste dispositivo.",
"download_heading": "baixe sua plataforma",
"download_desktop": "desktop",
"download_ios": "iOS",
"download_android": "android",
"app_notifications_note": "as atualizacoes para membros sao entregues no app apos login da wallet."
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "Примечание: эта страница намеренно минималистична. Она отражает философию продукта, где вычисления сложны, а интерфейсы остаются тихими."
},
"continue_label": "продолжить",
"wallet_intro": "минтите свое членство",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "Без транзакции. Только подпись.",
"wallet_fact_seed": "Никогда не делитесь seed-фразой.",
"wallet_have": "У меня есть кошелек",
@ -41,15 +41,15 @@
"wallet_signing": "Ожидание подписи...",
"wallet_verifying": "Проверка подписи...",
"wallet_success": "Назначение подтверждено.",
"membership_quoting": "Подготовка минта членства...",
"membership_minting": "Подтвердите минт членства в вашем кошельке...",
"membership_confirming": "Подтверждение членства в сети...",
"membership_active": "Членство активно. Назначение подтверждено.",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "Сбой процесса кошелька.",
"wallet_missing": "Кошелек на этом устройстве не обнаружен.",
"download_heading": "скачайте свою платформу",
"download_desktop": "десктоп",
"download_ios": "iOS",
"download_android": "android",
"app_notifications_note": "обновления для участников доставляются в приложении после входа через кошелек."
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}

View File

@ -28,7 +28,7 @@
"note": "说明:本页面刻意保持极简,体现了“计算可以复杂,界面应保持安静”的产品哲学。"
},
"continue_label": "继续",
"wallet_intro": "铸造你的会员资格",
"wallet_intro": "activate your EDUT ID",
"wallet_fact_no_tx": "无交易,仅签名。",
"wallet_fact_seed": "请勿分享你的助记词。",
"wallet_have": "我有钱包",
@ -41,15 +41,15 @@
"wallet_signing": "等待签名...",
"wallet_verifying": "正在验证签名...",
"wallet_success": "指定已确认。",
"membership_quoting": "正在准备会员铸造...",
"membership_minting": "请在钱包中确认会员铸造...",
"membership_confirming": "正在链上确认会员资格...",
"membership_active": "会员已激活。指定已确认。",
"membership_quoting": "Preparing EDUT ID activation...",
"membership_minting": "Confirm EDUT ID activation in your wallet...",
"membership_confirming": "Confirming EDUT ID on-chain...",
"membership_active": "EDUT ID active. Designation acknowledged.",
"wallet_failed": "钱包流程失败。",
"wallet_missing": "此设备未检测到钱包。",
"download_heading": "下载你的平台",
"download_desktop": "桌面版",
"download_ios": "iOS",
"download_android": "安卓",
"app_notifications_note": "会员更新会在钱包登录后通过应用内发送。"
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
}