Compare commits
No commits in common. "2e0372858301d3bc6887b97d9f24857964ba7274" and "d1c60fe44ed0901972e46c2542d7ba934be63e72" have entirely different histories.
2e03728583
...
d1c60fe44e
@ -29,8 +29,6 @@ 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`
|
||||
@ -73,11 +71,6 @@ 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:
|
||||
@ -139,7 +132,7 @@ Policy gates:
|
||||
- `SECRET_API_DOMAIN_NAME`
|
||||
- `SECRET_API_VERIFYING_CONTRACT`
|
||||
- `SECRET_API_MEMBERSHIP_CONTRACT`
|
||||
- `SECRET_API_MINT_CURRENCY` (must be `USDC` in v1)
|
||||
- `SECRET_API_MINT_CURRENCY` (default `USDC`)
|
||||
- `SECRET_API_MINT_AMOUNT_ATOMIC` (default `100000000`)
|
||||
- `SECRET_API_MINT_DECIMALS` (default `6`)
|
||||
|
||||
|
||||
@ -34,8 +34,6 @@ 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))
|
||||
@ -232,97 +230,6 @@ 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")
|
||||
@ -1569,7 +1476,4 @@ 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)")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1210,314 +1210,6 @@ 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"`
|
||||
@ -1527,10 +1219,9 @@ type tWalletIntentResponse struct {
|
||||
}
|
||||
|
||||
type tWalletVerifyResponse struct {
|
||||
Status string `json:"status"`
|
||||
DesignationCode string `json:"designation_code"`
|
||||
SessionToken string `json:"session_token"`
|
||||
SessionExpiresAt string `json:"session_expires_at"`
|
||||
Status string `json:"status"`
|
||||
DesignationCode string `json:"designation_code"`
|
||||
SessionToken string `json:"session_token"`
|
||||
}
|
||||
|
||||
func newTestApp(t *testing.T) (*app, Config, func()) {
|
||||
|
||||
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -77,17 +76,6 @@ 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")
|
||||
}
|
||||
|
||||
@ -31,30 +31,3 @@ 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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,11 +46,6 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
||||
MultiTenant: false,
|
||||
EntitlementClass: "solo_core",
|
||||
},
|
||||
ExecutionProfile: marketplaceExecutionProfile{
|
||||
ConnectorSurface: "hybrid",
|
||||
PacingTier: "governed_human_pace",
|
||||
HumanPaceFloorMS: 1200,
|
||||
},
|
||||
SortOrder: 10,
|
||||
},
|
||||
{
|
||||
@ -73,11 +68,6 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
||||
MultiTenant: false,
|
||||
EntitlementClass: "workspace_core",
|
||||
},
|
||||
ExecutionProfile: marketplaceExecutionProfile{
|
||||
ConnectorSurface: "hybrid",
|
||||
PacingTier: "governed_human_pace",
|
||||
HumanPaceFloorMS: 1200,
|
||||
},
|
||||
SortOrder: 20,
|
||||
},
|
||||
{
|
||||
@ -101,11 +91,6 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
||||
EntitlementClass: "workspace_ai",
|
||||
RequiresOffers: []string{offerIDWorkspaceCore},
|
||||
},
|
||||
ExecutionProfile: marketplaceExecutionProfile{
|
||||
ConnectorSurface: "hybrid",
|
||||
PacingTier: "governed_human_pace",
|
||||
HumanPaceFloorMS: 1200,
|
||||
},
|
||||
SortOrder: 30,
|
||||
},
|
||||
{
|
||||
@ -129,10 +114,6 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
||||
EntitlementClass: "workspace_lane24",
|
||||
RequiresOffers: []string{offerIDWorkspaceCore},
|
||||
},
|
||||
ExecutionProfile: marketplaceExecutionProfile{
|
||||
ConnectorSurface: "edut_native",
|
||||
PacingTier: "local_hardware_speed",
|
||||
},
|
||||
SortOrder: 40,
|
||||
},
|
||||
{
|
||||
@ -156,10 +137,6 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
||||
EntitlementClass: "workspace_sovereign",
|
||||
RequiresOffers: []string{offerIDWorkspaceCore},
|
||||
},
|
||||
ExecutionProfile: marketplaceExecutionProfile{
|
||||
ConnectorSurface: "edut_native",
|
||||
PacingTier: "local_hardware_speed",
|
||||
},
|
||||
SortOrder: 50,
|
||||
},
|
||||
}
|
||||
|
||||
@ -3,15 +3,14 @@ package main
|
||||
import "time"
|
||||
|
||||
type marketplaceOffer struct {
|
||||
OfferID string `json:"offer_id"`
|
||||
IssuerID string `json:"issuer_id"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Pricing marketplaceOfferPrice `json:"pricing"`
|
||||
Policies marketplaceOfferPolicy `json:"policies"`
|
||||
ExecutionProfile marketplaceExecutionProfile `json:"execution_profile,omitempty"`
|
||||
SortOrder int `json:"-"`
|
||||
OfferID string `json:"offer_id"`
|
||||
IssuerID string `json:"issuer_id"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Pricing marketplaceOfferPrice `json:"pricing"`
|
||||
Policies marketplaceOfferPolicy `json:"policies"`
|
||||
SortOrder int `json:"-"`
|
||||
}
|
||||
|
||||
type marketplaceOfferPrice struct {
|
||||
@ -31,12 +30,6 @@ 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"`
|
||||
}
|
||||
|
||||
@ -37,27 +37,6 @@ 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"`
|
||||
|
||||
@ -13,7 +13,6 @@ 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
|
||||
|
||||
@ -453,40 +453,6 @@ 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 (
|
||||
|
||||
@ -19,11 +19,6 @@ 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,
|
||||
@ -106,7 +101,6 @@ 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`):
|
||||
|
||||
|
||||
@ -80,65 +80,7 @@ Error (`400` intent expired):
|
||||
}
|
||||
```
|
||||
|
||||
## 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`
|
||||
## 3) `POST /secret/membership/quote`
|
||||
|
||||
Request:
|
||||
|
||||
@ -200,7 +142,7 @@ Error (`403` distinct payer without proof):
|
||||
}
|
||||
```
|
||||
|
||||
## 6) `POST /secret/membership/confirm`
|
||||
## 4) `POST /secret/membership/confirm`
|
||||
|
||||
Request:
|
||||
|
||||
@ -242,7 +184,7 @@ Error (`400` tx mismatch):
|
||||
}
|
||||
```
|
||||
|
||||
## 7) `GET /secret/membership/status`
|
||||
## 5) `GET /secret/membership/status`
|
||||
|
||||
Request by wallet:
|
||||
|
||||
|
||||
@ -137,19 +137,6 @@ 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]
|
||||
|
||||
@ -34,70 +34,10 @@ 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
|
||||
@ -153,14 +93,6 @@ 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
|
||||
@ -234,46 +166,6 @@ 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]
|
||||
@ -303,7 +195,7 @@ components:
|
||||
type: integer
|
||||
currency:
|
||||
type: string
|
||||
enum: [USDC]
|
||||
enum: [USDC, ETH]
|
||||
amount:
|
||||
type: string
|
||||
amount_atomic:
|
||||
|
||||
@ -88,11 +88,9 @@ PrincipalRole:
|
||||
|
||||
1. `POST /secret/wallet/intent`
|
||||
2. `POST /secret/wallet/verify`
|
||||
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=...`
|
||||
3. `POST /secret/membership/quote`
|
||||
4. `POST /secret/membership/confirm`
|
||||
5. `GET /secret/membership/status?designation_code=...`
|
||||
|
||||
## Marketplace
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
},
|
||||
"quote_policy": {
|
||||
"default_currency": "USDC",
|
||||
"minimum_floor_usd": "100.00",
|
||||
"minimum_floor_usd": "5.00",
|
||||
"safety_multiplier": "1.5",
|
||||
"quote_ttl_seconds": 300
|
||||
},
|
||||
|
||||
@ -66,14 +66,12 @@ 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 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.
|
||||
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.
|
||||
|
||||
@ -10,11 +10,9 @@ Current implementation target in this repo:
|
||||
|
||||
1. `POST /secret/wallet/intent`
|
||||
2. `POST /secret/wallet/verify`
|
||||
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`
|
||||
3. `POST /secret/membership/quote`
|
||||
4. `POST /secret/membership/confirm`
|
||||
5. `GET /secret/membership/status`
|
||||
|
||||
## Web Behavior Dependency
|
||||
|
||||
@ -52,25 +50,6 @@ 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
|
||||
|
||||
@ -123,9 +102,6 @@ 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
|
||||
|
||||
|
||||
@ -29,8 +29,6 @@ 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
|
||||
|
||||
|
||||
@ -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 100 USDC floor rule.
|
||||
7. Pricing policy with USD 5 floor rule.
|
||||
8. Terms utility-only non-investment clause.
|
||||
9. Store page upgraded from static to live-state scaffold with membership gate behavior.
|
||||
10. OpenAPI contract + request/response examples for secret-system endpoints.
|
||||
@ -58,9 +58,6 @@ 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:
|
||||
|
||||
@ -70,7 +67,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 seeded Sepolia USDC test balance).
|
||||
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).
|
||||
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.
|
||||
|
||||
@ -12,12 +12,7 @@
|
||||
"workspace_bound": false,
|
||||
"transferable": false,
|
||||
"internal_use_only": true,
|
||||
"multi_tenant": false,
|
||||
"execution_profile": {
|
||||
"connector_surface": "hybrid",
|
||||
"pacing_tier": "governed_human_pace",
|
||||
"human_pace_floor_ms": 1200
|
||||
}
|
||||
"multi_tenant": false
|
||||
},
|
||||
{
|
||||
"offer_id": "edut.workspace.core",
|
||||
@ -29,12 +24,7 @@
|
||||
"workspace_bound": true,
|
||||
"transferable": false,
|
||||
"internal_use_only": true,
|
||||
"multi_tenant": false,
|
||||
"execution_profile": {
|
||||
"connector_surface": "hybrid",
|
||||
"pacing_tier": "governed_human_pace",
|
||||
"human_pace_floor_ms": 1200
|
||||
}
|
||||
"multi_tenant": false
|
||||
},
|
||||
{
|
||||
"offer_id": "edut.workspace.ai",
|
||||
@ -46,12 +36,7 @@
|
||||
"workspace_bound": true,
|
||||
"transferable": false,
|
||||
"internal_use_only": true,
|
||||
"multi_tenant": false,
|
||||
"execution_profile": {
|
||||
"connector_surface": "hybrid",
|
||||
"pacing_tier": "governed_human_pace",
|
||||
"human_pace_floor_ms": 1200
|
||||
}
|
||||
"multi_tenant": false
|
||||
},
|
||||
{
|
||||
"offer_id": "edut.workspace.lane24",
|
||||
@ -63,11 +48,7 @@
|
||||
"workspace_bound": true,
|
||||
"transferable": false,
|
||||
"internal_use_only": true,
|
||||
"multi_tenant": false,
|
||||
"execution_profile": {
|
||||
"connector_surface": "edut_native",
|
||||
"pacing_tier": "local_hardware_speed"
|
||||
}
|
||||
"multi_tenant": false
|
||||
},
|
||||
{
|
||||
"offer_id": "edut.workspace.sovereign",
|
||||
@ -79,11 +60,7 @@
|
||||
"workspace_bound": true,
|
||||
"transferable": false,
|
||||
"internal_use_only": true,
|
||||
"multi_tenant": false,
|
||||
"execution_profile": {
|
||||
"connector_surface": "edut_native",
|
||||
"pacing_tier": "local_hardware_speed"
|
||||
}
|
||||
"multi_tenant": false
|
||||
}
|
||||
],
|
||||
"published_at": "2026-02-17T00:00:00Z"
|
||||
|
||||
@ -21,16 +21,10 @@
|
||||
"requires_admin_approval": false
|
||||
},
|
||||
"entitlement": {
|
||||
"type": "module_license",
|
||||
"scope": "workspace",
|
||||
"type": "runtime_license",
|
||||
"scope": "org_root",
|
||||
"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": {
|
||||
|
||||
@ -35,26 +35,7 @@
|
||||
"workspace_bound": { "type": "boolean" },
|
||||
"transferable": { "type": "boolean" },
|
||||
"internal_use_only": { "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
|
||||
}
|
||||
}
|
||||
}
|
||||
"multi_tenant": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -143,39 +143,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
|
||||
@ -63,8 +63,6 @@ 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 |
|
||||
@ -162,77 +160,11 @@ Response:
|
||||
"status": "signature_verified",
|
||||
"designation_code": "0217073045482",
|
||||
"display_token": "0217-0730-4548-2",
|
||||
"verified_at": "2026-02-17T07:31:12Z",
|
||||
"session_token": "9f2c50f8a0f5d8d0b0efc4fa665e4032f31bb0c4c4f31b8c",
|
||||
"session_expires_at": "2026-03-18T07:31:12Z"
|
||||
"verified_at": "2026-02-17T07:31:12Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
### 3) Membership Quote
|
||||
|
||||
#### `POST /secret/membership/quote`
|
||||
|
||||
@ -268,7 +200,7 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
### 5) Membership Confirm
|
||||
### 4) Membership Confirm
|
||||
|
||||
#### `POST /secret/membership/confirm`
|
||||
|
||||
@ -367,14 +299,6 @@ 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;
|
||||
}
|
||||
@ -390,6 +314,5 @@ 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. verify issues wallet session for fail-closed control-plane access,
|
||||
3. paid mint proves intent,
|
||||
4. membership gates all future marketplace purchases.
|
||||
2. paid mint proves intent,
|
||||
3. membership gates all future marketplace purchases.
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
# 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.
|
||||
@ -71,13 +71,13 @@
|
||||
<div class="container">
|
||||
<p><a href="/">back</a></p>
|
||||
<h1>EDUT Platform Android</h1>
|
||||
<p class="muted">EDUT ID channel verification</p>
|
||||
<p class="muted">membership channel verification</p>
|
||||
<div class="card">
|
||||
<p>Android delivery is tied to wallet-authenticated EDUT ID state.</p>
|
||||
<p>Android delivery is tied to wallet-authenticated membership 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>EDUT ID verified. Android distribution remains staged by designation era.</p>
|
||||
<p>Membership 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 EDUT ID status...', false);
|
||||
setStatus('Checking membership status...', false);
|
||||
const status = await fetchMembershipStatus(wallet, chainId);
|
||||
|
||||
if (status.status === 'active') {
|
||||
setStatus('EDUT ID active. Android channel is authorized.', false);
|
||||
setStatus('Membership active. Android channel is authorized.', false);
|
||||
instructionsNode.classList.add('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('EDUT ID is not active for this wallet (' + status.status + ').', true);
|
||||
setStatus('Membership is not active for this wallet (' + status.status + ').', true);
|
||||
} catch (err) {
|
||||
const message = err && err.message ? err.message : 'EDUT ID check failed.';
|
||||
const message = err && err.message ? err.message : 'Membership check failed.';
|
||||
setStatus(message, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,13 +71,13 @@
|
||||
<div class="container">
|
||||
<p><a href="/">back</a></p>
|
||||
<h1>EDUT Platform Desktop</h1>
|
||||
<p class="muted">EDUT ID channel verification</p>
|
||||
<p class="muted">membership channel verification</p>
|
||||
<div class="card">
|
||||
<p>Desktop delivery is tied to wallet-authenticated EDUT ID state.</p>
|
||||
<p>Desktop delivery is tied to wallet-authenticated membership 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>EDUT ID verified. Desktop distribution remains staged by designation era.</p>
|
||||
<p>Membership 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 EDUT ID status...', false);
|
||||
setStatus('Checking membership status...', false);
|
||||
const status = await fetchMembershipStatus(wallet, chainId);
|
||||
|
||||
if (status.status === 'active') {
|
||||
setStatus('EDUT ID active. Desktop channel is authorized.', false);
|
||||
setStatus('Membership active. Desktop channel is authorized.', false);
|
||||
instructionsNode.classList.add('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('EDUT ID is not active for this wallet (' + status.status + ').', true);
|
||||
setStatus('Membership is not active for this wallet (' + status.status + ').', true);
|
||||
} catch (err) {
|
||||
const message = err && err.message ? err.message : 'EDUT ID check failed.';
|
||||
const message = err && err.message ? err.message : 'Membership check failed.';
|
||||
setStatus(message, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,13 +71,13 @@
|
||||
<div class="container">
|
||||
<p><a href="/">back</a></p>
|
||||
<h1>EDUT Platform iOS</h1>
|
||||
<p class="muted">EDUT ID channel verification</p>
|
||||
<p class="muted">membership channel verification</p>
|
||||
<div class="card">
|
||||
<p>iOS delivery is tied to wallet-authenticated EDUT ID state.</p>
|
||||
<p>iOS delivery is tied to wallet-authenticated membership 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>EDUT ID verified. iOS distribution remains staged by designation era.</p>
|
||||
<p>Membership 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 EDUT ID status...', false);
|
||||
setStatus('Checking membership status...', false);
|
||||
const status = await fetchMembershipStatus(wallet, chainId);
|
||||
|
||||
if (status.status === 'active') {
|
||||
setStatus('EDUT ID active. iOS channel is authorized.', false);
|
||||
setStatus('Membership active. iOS channel is authorized.', false);
|
||||
instructionsNode.classList.add('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('EDUT ID is not active for this wallet (' + status.status + ').', true);
|
||||
setStatus('Membership is not active for this wallet (' + status.status + ').', true);
|
||||
} catch (err) {
|
||||
const message = err && err.message ? err.message : 'EDUT ID check failed.';
|
||||
const message = err && err.message ? err.message : 'Membership check failed.';
|
||||
setStatus(message, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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">activate your EDUT ID</p>
|
||||
<p class="flow-line localizable" data-i18n="wallet_intro">mint your membership</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">EDUT ID updates are delivered inside the app after wallet sign-in.</p>
|
||||
<p class="flow-line subtle localizable" data-i18n="app_notifications_note">member 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,8 +472,6 @@ const flowState = {
|
||||
currentIntent: null,
|
||||
sessionToken: '',
|
||||
sessionExpiresAt: '',
|
||||
sessionWallet: '',
|
||||
sessionRefreshInFlight: null,
|
||||
};
|
||||
|
||||
const continueAction = document.getElementById('continue-action');
|
||||
@ -521,7 +519,6 @@ 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) {
|
||||
@ -612,92 +609,7 @@ function formatQuoteDisplay(quote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
async function postJSON(url, payload) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
@ -710,17 +622,11 @@ async function postJSON(url, payload, options) {
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
captureFlowSessionHeaders(res);
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
const detail = parseApiError(text, res.status);
|
||||
if (isTerminalSessionCode(detail.code)) {
|
||||
clearFlowSession(detail.code);
|
||||
}
|
||||
throw new Error(detail.message);
|
||||
const detail = await res.text();
|
||||
throw new Error(detail || ('HTTP ' + res.status));
|
||||
}
|
||||
if (!text) return {};
|
||||
return JSON.parse(text);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function resolveLanguage(input) {
|
||||
@ -819,10 +725,6 @@ 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);
|
||||
|
||||
@ -832,7 +734,7 @@ async function startWalletFlow() {
|
||||
origin: window.location.origin,
|
||||
locale: localStorage.getItem('edut_lang') || 'en',
|
||||
chain_id: chainId,
|
||||
}, { wallet: address });
|
||||
});
|
||||
flowState.currentIntent = intent;
|
||||
|
||||
const typedData = {
|
||||
@ -879,17 +781,16 @@ 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 EDUT ID activation...', false);
|
||||
setFlowStatus('membership_quoting', 'Preparing membership mint...', 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,
|
||||
@ -900,32 +801,32 @@ async function startWalletFlow() {
|
||||
|
||||
if (!txParams.from) txParams.from = address;
|
||||
if (!txParams.to) {
|
||||
throw new Error('EDUT ID quote is missing destination contract.');
|
||||
throw new Error('Membership quote is missing destination contract.');
|
||||
}
|
||||
|
||||
const quoteDisplay = formatQuoteDisplay(quote);
|
||||
const mintPrompt = t('membership_minting', 'Confirm EDUT ID activation in your wallet...');
|
||||
const mintPrompt = t('membership_minting', 'Confirm membership mint in your wallet...');
|
||||
if (quoteDisplay) {
|
||||
setFlowStatusMessage(mintPrompt + ' (' + quoteDisplay + ')', false);
|
||||
} else {
|
||||
setFlowStatus('membership_minting', 'Confirm EDUT ID activation in your wallet...', false);
|
||||
setFlowStatus('membership_minting', 'Confirm membership mint in your wallet...', false);
|
||||
}
|
||||
const txHash = await window.ethereum.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [txParams],
|
||||
});
|
||||
|
||||
setFlowStatus('membership_confirming', 'Confirming EDUT ID on-chain...', false);
|
||||
setFlowStatus('membership_confirming', 'Confirming membership 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('EDUT ID transaction did not activate.');
|
||||
throw new Error('Membership transaction did not activate.');
|
||||
}
|
||||
|
||||
const ackState = {
|
||||
@ -940,7 +841,7 @@ async function startWalletFlow() {
|
||||
saveAcknowledgement(ackState);
|
||||
renderAcknowledged(ackState);
|
||||
showPostMintPanel();
|
||||
setFlowStatus('membership_active', 'EDUT ID active. Designation acknowledged.', false);
|
||||
setFlowStatus('membership_active', 'Membership active. Designation acknowledged.', false);
|
||||
} catch (err) {
|
||||
const message = err && err.message ? err.message : 'Wallet flow failed.';
|
||||
setFlowStatus('wallet_failed', message, true);
|
||||
|
||||
@ -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. EDUT ID 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. Member 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>
|
||||
|
||||
@ -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 ID-gated marketplace preview states.">
|
||||
<meta name="description" content="EDUT membership-gated marketplace preview states.">
|
||||
<meta name="theme-color" content="#f0f4f8">
|
||||
<meta name="robots" content="noindex,nofollow,noarchive,nosnippet">
|
||||
<style>
|
||||
@ -69,43 +69,6 @@
|
||||
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;
|
||||
@ -184,18 +147,18 @@
|
||||
<div class="container">
|
||||
<a href="/" class="back">← Back</a>
|
||||
<h1>EDUT Store</h1>
|
||||
<p class="sub" id="preview-mode-note">EDUT ID-gated checkout behavior (internal preview scaffold)</p>
|
||||
<p class="sub" id="preview-mode-note">Membership-gated checkout behavior (internal preview scaffold)</p>
|
||||
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<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="label">Wallet + Membership</p>
|
||||
<p class="line">Wallet: <span class="mono" id="wallet-label">not connected</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">Membership status: <span class="mono" id="membership-label">unknown</span></p>
|
||||
<p class="line">Gate decision: <span class="mono" id="gate-label">blocked</span></p>
|
||||
<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"><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>
|
||||
<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>
|
||||
<div class="actions">
|
||||
<button id="connect-btn" type="button">connect wallet</button>
|
||||
<button id="refresh-btn" type="button">refresh state</button>
|
||||
@ -220,23 +183,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: EDUT ID check -> ownership proof (if needed) -> quote -> wallet confirm -> entitlement receipt</p>
|
||||
<p class="line">Action chain: membership 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 EDUT ID is active.</div>
|
||||
<div class="status-log" id="checkout-log">Checkout is blocked until membership is active.</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<p class="label">Fail-Closed States</p>
|
||||
<p class="line">No EDUT ID: checkout blocked.</p>
|
||||
<p class="line">No membership: checkout blocked.</p>
|
||||
<p class="line">Suspended/revoked: checkout and activation blocked.</p>
|
||||
<p class="line">Unknown state or API error: blocked by default.</p>
|
||||
<span class="state warn">default deny</span>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<p class="foot">This page is intentionally deterministic: if EDUT ID cannot be confirmed, purchase remains blocked. <a href="/trust">Trust page</a>.</p>
|
||||
<p class="foot">This page is intentionally deterministic: if membership cannot be confirmed, purchase remains blocked. <a href="/trust">Trust page</a>.</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@ -254,9 +217,7 @@
|
||||
offers: [],
|
||||
selectedOfferId: null,
|
||||
sessionToken: '',
|
||||
sessionExpiresAt: '',
|
||||
sessionWallet: null,
|
||||
sessionRefreshInFlight: null,
|
||||
};
|
||||
|
||||
const walletLabel = document.getElementById('wallet-label');
|
||||
@ -361,16 +322,9 @@
|
||||
offerTitle.textContent = selected.title || selected.offer_id;
|
||||
offerSummary.textContent = selected.summary || 'No summary provided.';
|
||||
offerPrice.textContent = 'Price: ' + (selected.price || '--') + ' ' + (selected.currency || '');
|
||||
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) +
|
||||
offerPolicy.textContent = 'Policy: member-only=' + Boolean(selected.member_only) +
|
||||
', workspace-bound=' + Boolean(selected.workspace_bound) +
|
||||
', transferable=' + Boolean(selected.transferable) +
|
||||
profile;
|
||||
', transferable=' + Boolean(selected.transferable);
|
||||
}
|
||||
|
||||
function populateOfferSelect() {
|
||||
@ -411,7 +365,7 @@
|
||||
gatePill.textContent = 'status unknown';
|
||||
} else {
|
||||
gatePill.className = 'state block';
|
||||
gatePill.textContent = 'EDUT ID required';
|
||||
gatePill.textContent = 'membership required';
|
||||
}
|
||||
|
||||
checkoutBtn.disabled = !state.gate || !state.selectedOfferId;
|
||||
@ -443,9 +397,6 @@
|
||||
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();
|
||||
}
|
||||
@ -454,107 +405,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -565,42 +415,16 @@
|
||||
}
|
||||
|
||||
async function fetchJson(url) {
|
||||
await maybeRefreshSession(url);
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: authHeaders(),
|
||||
});
|
||||
captureSessionHeaders(response);
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
const detail = parseApiErrorPayload(text, response.status);
|
||||
if (isTerminalSessionCode(detail.code)) {
|
||||
clearSession(detail.code);
|
||||
}
|
||||
throw new Error(detail.error);
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
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');
|
||||
@ -660,13 +484,13 @@
|
||||
}
|
||||
|
||||
state.source = 'live';
|
||||
setLog('Checking live EDUT ID status...');
|
||||
setLog('Checking live membership status...');
|
||||
|
||||
try {
|
||||
const membership = await fetchLiveMembershipStatus();
|
||||
state.membership = membership;
|
||||
applyGateState();
|
||||
setLog('Live EDUT ID status resolved: ' + membership + '.');
|
||||
setLog('Live status resolved: ' + membership + '.');
|
||||
} catch (err) {
|
||||
state.membership = 'unknown';
|
||||
applyGateState();
|
||||
@ -687,9 +511,8 @@
|
||||
state.wallet = accounts[0];
|
||||
state.ownershipProof = null;
|
||||
if (state.sessionWallet && state.wallet && state.sessionWallet.toLowerCase() !== state.wallet.toLowerCase()) {
|
||||
clearSession('wallet changed');
|
||||
state.sessionToken = '';
|
||||
}
|
||||
state.sessionWallet = state.wallet;
|
||||
setLog('Wallet connected: ' + abbreviateWallet(state.wallet) + '.');
|
||||
await refreshMembershipState();
|
||||
} catch (err) {
|
||||
@ -755,7 +578,7 @@
|
||||
|
||||
async function requestCheckoutQuote() {
|
||||
if (!state.gate) {
|
||||
setCheckoutLog('Checkout blocked: EDUT ID is not active.');
|
||||
setCheckoutLog('Checkout blocked: membership is not active.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -793,7 +616,17 @@
|
||||
payload.ownership_proof = state.ownershipProof.signature;
|
||||
}
|
||||
|
||||
const quotePayload = await postJson('/marketplace/checkout/quote', payload);
|
||||
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 quoteId = quotePayload.quote_id || 'unknown';
|
||||
const amount = quotePayload.amount || quotePayload.amount_atomic || 'unknown';
|
||||
const total = quotePayload.total_amount || quotePayload.total_amount_atomic || amount;
|
||||
|
||||
@ -11,12 +11,7 @@
|
||||
"workspace_bound": true,
|
||||
"transferable": false,
|
||||
"internal_use_only": true,
|
||||
"multi_tenant": false,
|
||||
"execution_profile": {
|
||||
"connector_surface": "hybrid",
|
||||
"pacing_tier": "governed_human_pace",
|
||||
"human_pace_floor_ms": 1200
|
||||
}
|
||||
"multi_tenant": false
|
||||
},
|
||||
{
|
||||
"offer_id": "edut.workspace.core",
|
||||
@ -28,12 +23,7 @@
|
||||
"workspace_bound": true,
|
||||
"transferable": false,
|
||||
"internal_use_only": true,
|
||||
"multi_tenant": false,
|
||||
"execution_profile": {
|
||||
"connector_surface": "hybrid",
|
||||
"pacing_tier": "governed_human_pace",
|
||||
"human_pace_floor_ms": 1200
|
||||
}
|
||||
"multi_tenant": false
|
||||
},
|
||||
{
|
||||
"offer_id": "edut.workspace.ai",
|
||||
@ -45,12 +35,7 @@
|
||||
"workspace_bound": true,
|
||||
"transferable": false,
|
||||
"internal_use_only": true,
|
||||
"multi_tenant": false,
|
||||
"execution_profile": {
|
||||
"connector_surface": "hybrid",
|
||||
"pacing_tier": "governed_human_pace",
|
||||
"human_pace_floor_ms": 1200
|
||||
}
|
||||
"multi_tenant": false
|
||||
},
|
||||
{
|
||||
"offer_id": "edut.workspace.lane24",
|
||||
@ -62,11 +47,7 @@
|
||||
"workspace_bound": true,
|
||||
"transferable": false,
|
||||
"internal_use_only": true,
|
||||
"multi_tenant": false,
|
||||
"execution_profile": {
|
||||
"connector_surface": "edut_native",
|
||||
"pacing_tier": "local_hardware_speed"
|
||||
}
|
||||
"multi_tenant": false
|
||||
},
|
||||
{
|
||||
"offer_id": "edut.workspace.sovereign",
|
||||
@ -78,11 +59,7 @@
|
||||
"workspace_bound": true,
|
||||
"transferable": false,
|
||||
"internal_use_only": true,
|
||||
"multi_tenant": false,
|
||||
"execution_profile": {
|
||||
"connector_surface": "edut_native",
|
||||
"pacing_tier": "local_hardware_speed"
|
||||
}
|
||||
"multi_tenant": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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 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>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>Marketplace Access and Licenses</h2>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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 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>
|
||||
<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>
|
||||
|
||||
<h2>No Investment Expectation</h2>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
@ -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 ID and marketplace infrastructure.">
|
||||
<meta name="description" content="Operational trust facts for EDUT membership 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">EDUT ID: <span class="mono">pending deployment</span></p>
|
||||
<p class="line">Membership: <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,8 +110,7 @@
|
||||
|
||||
<section class="card">
|
||||
<p class="label">Policy Snapshot</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">Membership floor: <span class="mono">USD 5.00 equivalent</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>
|
||||
@ -120,8 +119,6 @@
|
||||
<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>
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"note": "ملاحظة: هذه الصفحة بسيطة عن قصد. وهي تعكس فلسفة منتج ترى أن الحساب قد يكون معقدًا بينما تبقى الواجهات هادئة."
|
||||
},
|
||||
"continue_label": "متابعة",
|
||||
"wallet_intro": "activate your EDUT ID",
|
||||
"wallet_intro": "قم بسك عضويتك",
|
||||
"wallet_fact_no_tx": "لا توجد معاملة. فقط توقيع.",
|
||||
"wallet_fact_seed": "لا تشارك عبارة الاسترداد الخاصة بك أبدًا.",
|
||||
"wallet_have": "لدي محفظة",
|
||||
@ -41,15 +41,15 @@
|
||||
"wallet_signing": "بانتظار التوقيع...",
|
||||
"wallet_verifying": "جارٍ التحقق من التوقيع...",
|
||||
"wallet_success": "تم تأكيد التعيين.",
|
||||
"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.",
|
||||
"membership_quoting": "جارٍ إعداد سك العضوية...",
|
||||
"membership_minting": "أكد سك العضوية في محفظتك...",
|
||||
"membership_confirming": "جارٍ تأكيد العضوية على السلسلة...",
|
||||
"membership_active": "العضوية نشطة. تم تأكيد التعيين.",
|
||||
"wallet_failed": "فشل مسار المحفظة.",
|
||||
"wallet_missing": "لم يتم العثور على محفظة على هذا الجهاز.",
|
||||
"download_heading": "نزّل منصتك",
|
||||
"download_desktop": "سطح المكتب",
|
||||
"download_ios": "iOS",
|
||||
"download_android": "أندرويد",
|
||||
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "تصل تحديثات الأعضاء داخل التطبيق بعد تسجيل الدخول بالمحفظة."
|
||||
}
|
||||
|
||||
@ -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": "activate your EDUT ID",
|
||||
"wallet_intro": "praege deine Mitgliedschaft",
|
||||
"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": "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.",
|
||||
"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.",
|
||||
"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": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "mitglieder-updates werden in der app nach wallet-anmeldung zugestellt."
|
||||
}
|
||||
|
||||
@ -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": "activate your EDUT ID",
|
||||
"wallet_intro": "mint your membership",
|
||||
"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 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.",
|
||||
"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.",
|
||||
"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": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "member updates are delivered inside the app after wallet sign-in."
|
||||
}
|
||||
|
||||
@ -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": "activate your EDUT ID",
|
||||
"wallet_intro": "acuña tu membresía",
|
||||
"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": "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.",
|
||||
"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.",
|
||||
"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": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "las actualizaciones para miembros se entregan dentro de la app tras iniciar sesion con wallet."
|
||||
}
|
||||
|
||||
@ -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": "activate your EDUT ID",
|
||||
"wallet_intro": "frappez votre adhesion",
|
||||
"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": "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.",
|
||||
"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.",
|
||||
"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": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "les mises a jour membres sont envoyees dans lapp apres connexion du wallet."
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"note": "הערה: דף זה מינימלי בכוונה. הוא משקף פילוסופיית מוצר שבה החישוב מורכב, אך הממשקים נשארים שקטים."
|
||||
},
|
||||
"continue_label": "המשך",
|
||||
"wallet_intro": "activate your EDUT ID",
|
||||
"wallet_intro": "הנפק את החברות שלך",
|
||||
"wallet_fact_no_tx": "אין עסקה. רק חתימה.",
|
||||
"wallet_fact_seed": "לעולם אל תשתף את ביטוי השחזור שלך.",
|
||||
"wallet_have": "יש לי ארנק",
|
||||
@ -41,15 +41,15 @@
|
||||
"wallet_signing": "ממתין לחתימה...",
|
||||
"wallet_verifying": "מאמת חתימה...",
|
||||
"wallet_success": "הייעוד אושר.",
|
||||
"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.",
|
||||
"membership_quoting": "מכין הטבעת חברות...",
|
||||
"membership_minting": "אשר הטבעת חברות בארנק שלך...",
|
||||
"membership_confirming": "מאשר חברות על השרשרת...",
|
||||
"membership_active": "החברות פעילה. הייעוד אושר.",
|
||||
"wallet_failed": "תהליך הארנק נכשל.",
|
||||
"wallet_missing": "לא זוהה ארנק במכשיר זה.",
|
||||
"download_heading": "הורד את הפלטפורמה שלך",
|
||||
"download_desktop": "דסקטופ",
|
||||
"download_ios": "iOS",
|
||||
"download_android": "אנדרואיד",
|
||||
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "עדכוני חברים נמסרים בתוך האפליקציה לאחר התחברות בארנק."
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"note": "नोट: यह पृष्ठ जानबूझकर न्यूनतम रखा गया है। यह उस उत्पाद दर्शन को दर्शाता है जिसमें गणना जटिल हो सकती है, लेकिन इंटरफेस शांत रहते हैं।"
|
||||
},
|
||||
"continue_label": "जारी रखें",
|
||||
"wallet_intro": "activate your EDUT ID",
|
||||
"wallet_intro": "अपनी सदस्यता मिंट करें",
|
||||
"wallet_fact_no_tx": "कोई ट्रांज़ैक्शन नहीं। केवल सिग्नेचर।",
|
||||
"wallet_fact_seed": "अपना सीड फ़्रेज़ कभी साझा न करें।",
|
||||
"wallet_have": "मेरे पास वॉलेट है",
|
||||
@ -41,15 +41,15 @@
|
||||
"wallet_signing": "सिग्नेचर की प्रतीक्षा...",
|
||||
"wallet_verifying": "सिग्नेचर सत्यापित किया जा रहा है...",
|
||||
"wallet_success": "नामांकन स्वीकार किया गया।",
|
||||
"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.",
|
||||
"membership_quoting": "सदस्यता मिंट तैयार की जा रही है...",
|
||||
"membership_minting": "अपने वॉलेट में सदस्यता मिंट की पुष्टि करें...",
|
||||
"membership_confirming": "ऑन-चेन सदस्यता की पुष्टि की जा रही है...",
|
||||
"membership_active": "सदस्यता सक्रिय है। नामांकन स्वीकार किया गया।",
|
||||
"wallet_failed": "वॉलेट फ़्लो विफल रहा।",
|
||||
"wallet_missing": "इस डिवाइस पर कोई वॉलेट नहीं मिला।",
|
||||
"download_heading": "अपना प्लेटफ़ॉर्म डाउनलोड करें",
|
||||
"download_desktop": "डेस्कटॉप",
|
||||
"download_ios": "iOS",
|
||||
"download_android": "एंड्रॉइड",
|
||||
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "सदस्य अपडेट वॉलेट साइन-इन के बाद ऐप के अंदर दिए जाते हैं।"
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"note": "注記: このページは意図的にミニマルです。計算は複雑でも、インターフェースは静かであるべきという製品哲学を表しています。"
|
||||
},
|
||||
"continue_label": "続行",
|
||||
"wallet_intro": "activate your EDUT ID",
|
||||
"wallet_intro": "メンバーシップをミントする",
|
||||
"wallet_fact_no_tx": "取引は発生しません。署名のみです。",
|
||||
"wallet_fact_seed": "シードフレーズは絶対に共有しないでください。",
|
||||
"wallet_have": "ウォレットを持っています",
|
||||
@ -41,15 +41,15 @@
|
||||
"wallet_signing": "署名を待機中...",
|
||||
"wallet_verifying": "署名を検証しています...",
|
||||
"wallet_success": "指定が確認されました。",
|
||||
"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.",
|
||||
"membership_quoting": "メンバーシップのミントを準備中...",
|
||||
"membership_minting": "ウォレットでメンバーシップのミントを確認してください...",
|
||||
"membership_confirming": "オンチェーンでメンバーシップを確認中...",
|
||||
"membership_active": "メンバーシップが有効です。指定が確認されました。",
|
||||
"wallet_failed": "ウォレットフローに失敗しました。",
|
||||
"wallet_missing": "この端末でウォレットが見つかりません。",
|
||||
"download_heading": "プラットフォームをダウンロード",
|
||||
"download_desktop": "デスクトップ",
|
||||
"download_ios": "iOS",
|
||||
"download_android": "android",
|
||||
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "メンバー向け更新はウォレットサインイン後にアプリ内で配信されます。"
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"note": "참고: 이 페이지는 의도적으로 미니멀합니다. 연산은 복잡하더라도 인터페이스는 조용해야 한다는 제품 철학을 반영합니다."
|
||||
},
|
||||
"continue_label": "계속",
|
||||
"wallet_intro": "activate your EDUT ID",
|
||||
"wallet_intro": "멤버십 민팅하기",
|
||||
"wallet_fact_no_tx": "거래는 없습니다. 서명만 진행됩니다.",
|
||||
"wallet_fact_seed": "시드 문구를 절대 공유하지 마세요.",
|
||||
"wallet_have": "지갑이 있습니다",
|
||||
@ -41,15 +41,15 @@
|
||||
"wallet_signing": "서명 대기 중...",
|
||||
"wallet_verifying": "서명 검증 중...",
|
||||
"wallet_success": "지정이 확인되었습니다.",
|
||||
"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.",
|
||||
"membership_quoting": "멤버십 민팅을 준비하는 중...",
|
||||
"membership_minting": "지갑에서 멤버십 민팅을 확인하세요...",
|
||||
"membership_confirming": "온체인 멤버십을 확인하는 중...",
|
||||
"membership_active": "멤버십이 활성화되었습니다. 지정이 확인되었습니다.",
|
||||
"wallet_failed": "지갑 흐름에 실패했습니다.",
|
||||
"wallet_missing": "이 기기에서 지갑을 찾을 수 없습니다.",
|
||||
"download_heading": "플랫폼 다운로드",
|
||||
"download_desktop": "데스크톱",
|
||||
"download_ios": "iOS",
|
||||
"download_android": "android",
|
||||
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "회원 업데이트는 지갑 로그인 후 앱 안에서 전달됩니다."
|
||||
}
|
||||
|
||||
@ -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": "activate your EDUT ID",
|
||||
"wallet_intro": "cunhe sua associação",
|
||||
"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": "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.",
|
||||
"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.",
|
||||
"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": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "as atualizacoes para membros sao entregues no app apos login da wallet."
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"note": "Примечание: эта страница намеренно минималистична. Она отражает философию продукта, где вычисления сложны, а интерфейсы остаются тихими."
|
||||
},
|
||||
"continue_label": "продолжить",
|
||||
"wallet_intro": "activate your EDUT ID",
|
||||
"wallet_intro": "минтите свое членство",
|
||||
"wallet_fact_no_tx": "Без транзакции. Только подпись.",
|
||||
"wallet_fact_seed": "Никогда не делитесь seed-фразой.",
|
||||
"wallet_have": "У меня есть кошелек",
|
||||
@ -41,15 +41,15 @@
|
||||
"wallet_signing": "Ожидание подписи...",
|
||||
"wallet_verifying": "Проверка подписи...",
|
||||
"wallet_success": "Назначение подтверждено.",
|
||||
"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.",
|
||||
"membership_quoting": "Подготовка минта членства...",
|
||||
"membership_minting": "Подтвердите минт членства в вашем кошельке...",
|
||||
"membership_confirming": "Подтверждение членства в сети...",
|
||||
"membership_active": "Членство активно. Назначение подтверждено.",
|
||||
"wallet_failed": "Сбой процесса кошелька.",
|
||||
"wallet_missing": "Кошелек на этом устройстве не обнаружен.",
|
||||
"download_heading": "скачайте свою платформу",
|
||||
"download_desktop": "десктоп",
|
||||
"download_ios": "iOS",
|
||||
"download_android": "android",
|
||||
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "обновления для участников доставляются в приложении после входа через кошелек."
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"note": "说明:本页面刻意保持极简,体现了“计算可以复杂,界面应保持安静”的产品哲学。"
|
||||
},
|
||||
"continue_label": "继续",
|
||||
"wallet_intro": "activate your EDUT ID",
|
||||
"wallet_intro": "铸造你的会员资格",
|
||||
"wallet_fact_no_tx": "无交易,仅签名。",
|
||||
"wallet_fact_seed": "请勿分享你的助记词。",
|
||||
"wallet_have": "我有钱包",
|
||||
@ -41,15 +41,15 @@
|
||||
"wallet_signing": "等待签名...",
|
||||
"wallet_verifying": "正在验证签名...",
|
||||
"wallet_success": "指定已确认。",
|
||||
"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.",
|
||||
"membership_quoting": "正在准备会员铸造...",
|
||||
"membership_minting": "请在钱包中确认会员铸造...",
|
||||
"membership_confirming": "正在链上确认会员资格...",
|
||||
"membership_active": "会员已激活。指定已确认。",
|
||||
"wallet_failed": "钱包流程失败。",
|
||||
"wallet_missing": "此设备未检测到钱包。",
|
||||
"download_heading": "下载你的平台",
|
||||
"download_desktop": "桌面版",
|
||||
"download_ios": "iOS",
|
||||
"download_android": "安卓",
|
||||
"app_notifications_note": "EDUT ID updates are delivered inside the app after wallet sign-in."
|
||||
"app_notifications_note": "会员更新会在钱包登录后通过应用内发送。"
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user