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/intent`
|
||||||
- `POST /secret/wallet/verify`
|
- `POST /secret/wallet/verify`
|
||||||
- `POST /secret/wallet/session/refresh`
|
|
||||||
- `POST /secret/wallet/session/revoke`
|
|
||||||
- `POST /secret/membership/quote`
|
- `POST /secret/membership/quote`
|
||||||
- `POST /secret/membership/confirm`
|
- `POST /secret/membership/confirm`
|
||||||
- `GET /secret/membership/status`
|
- `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.
|
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
|
## Sponsorship Behavior
|
||||||
|
|
||||||
Membership quote supports ownership wallet and distinct payer wallet:
|
Membership quote supports ownership wallet and distinct payer wallet:
|
||||||
@ -139,7 +132,7 @@ Policy gates:
|
|||||||
- `SECRET_API_DOMAIN_NAME`
|
- `SECRET_API_DOMAIN_NAME`
|
||||||
- `SECRET_API_VERIFYING_CONTRACT`
|
- `SECRET_API_VERIFYING_CONTRACT`
|
||||||
- `SECRET_API_MEMBERSHIP_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_AMOUNT_ATOMIC` (default `100000000`)
|
||||||
- `SECRET_API_MINT_DECIMALS` (default `6`)
|
- `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("/healthz", a.withCORS(a.handleHealth))
|
||||||
mux.HandleFunc("/secret/wallet/intent", a.withCORS(a.handleWalletIntent))
|
mux.HandleFunc("/secret/wallet/intent", a.withCORS(a.handleWalletIntent))
|
||||||
mux.HandleFunc("/secret/wallet/verify", a.withCORS(a.handleWalletVerify))
|
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/quote", a.withCORS(a.handleMembershipQuote))
|
||||||
mux.HandleFunc("/secret/membership/confirm", a.withCORS(a.handleMembershipConfirm))
|
mux.HandleFunc("/secret/membership/confirm", a.withCORS(a.handleMembershipConfirm))
|
||||||
mux.HandleFunc("/secret/membership/status", a.withCORS(a.handleMembershipStatus))
|
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) {
|
func (a *app) handleMembershipQuote(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
@ -1569,7 +1476,4 @@ func isTxHash(value string) bool {
|
|||||||
|
|
||||||
func logConfig(cfg Config) {
|
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)
|
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 {
|
type tWalletIntentResponse struct {
|
||||||
IntentID string `json:"intent_id"`
|
IntentID string `json:"intent_id"`
|
||||||
DesignationCode string `json:"designation_code"`
|
DesignationCode string `json:"designation_code"`
|
||||||
@ -1527,10 +1219,9 @@ type tWalletIntentResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type tWalletVerifyResponse struct {
|
type tWalletVerifyResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
DesignationCode string `json:"designation_code"`
|
DesignationCode string `json:"designation_code"`
|
||||||
SessionToken string `json:"session_token"`
|
SessionToken string `json:"session_token"`
|
||||||
SessionExpiresAt string `json:"session_expires_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestApp(t *testing.T) (*app, Config, func()) {
|
func newTestApp(t *testing.T) (*app, Config, func()) {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -77,17 +76,6 @@ func (c Config) Validate() error {
|
|||||||
if c.ChainID <= 0 {
|
if c.ChainID <= 0 {
|
||||||
return fmt.Errorf("SECRET_API_CHAIN_ID must be positive")
|
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 {
|
if c.WalletSessionTTL <= 0 {
|
||||||
return fmt.Errorf("SECRET_API_WALLET_SESSION_TTL_SECONDS must be positive")
|
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")
|
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,
|
MultiTenant: false,
|
||||||
EntitlementClass: "solo_core",
|
EntitlementClass: "solo_core",
|
||||||
},
|
},
|
||||||
ExecutionProfile: marketplaceExecutionProfile{
|
|
||||||
ConnectorSurface: "hybrid",
|
|
||||||
PacingTier: "governed_human_pace",
|
|
||||||
HumanPaceFloorMS: 1200,
|
|
||||||
},
|
|
||||||
SortOrder: 10,
|
SortOrder: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -73,11 +68,6 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
|||||||
MultiTenant: false,
|
MultiTenant: false,
|
||||||
EntitlementClass: "workspace_core",
|
EntitlementClass: "workspace_core",
|
||||||
},
|
},
|
||||||
ExecutionProfile: marketplaceExecutionProfile{
|
|
||||||
ConnectorSurface: "hybrid",
|
|
||||||
PacingTier: "governed_human_pace",
|
|
||||||
HumanPaceFloorMS: 1200,
|
|
||||||
},
|
|
||||||
SortOrder: 20,
|
SortOrder: 20,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -101,11 +91,6 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
|||||||
EntitlementClass: "workspace_ai",
|
EntitlementClass: "workspace_ai",
|
||||||
RequiresOffers: []string{offerIDWorkspaceCore},
|
RequiresOffers: []string{offerIDWorkspaceCore},
|
||||||
},
|
},
|
||||||
ExecutionProfile: marketplaceExecutionProfile{
|
|
||||||
ConnectorSurface: "hybrid",
|
|
||||||
PacingTier: "governed_human_pace",
|
|
||||||
HumanPaceFloorMS: 1200,
|
|
||||||
},
|
|
||||||
SortOrder: 30,
|
SortOrder: 30,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -129,10 +114,6 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
|||||||
EntitlementClass: "workspace_lane24",
|
EntitlementClass: "workspace_lane24",
|
||||||
RequiresOffers: []string{offerIDWorkspaceCore},
|
RequiresOffers: []string{offerIDWorkspaceCore},
|
||||||
},
|
},
|
||||||
ExecutionProfile: marketplaceExecutionProfile{
|
|
||||||
ConnectorSurface: "edut_native",
|
|
||||||
PacingTier: "local_hardware_speed",
|
|
||||||
},
|
|
||||||
SortOrder: 40,
|
SortOrder: 40,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -156,10 +137,6 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
|||||||
EntitlementClass: "workspace_sovereign",
|
EntitlementClass: "workspace_sovereign",
|
||||||
RequiresOffers: []string{offerIDWorkspaceCore},
|
RequiresOffers: []string{offerIDWorkspaceCore},
|
||||||
},
|
},
|
||||||
ExecutionProfile: marketplaceExecutionProfile{
|
|
||||||
ConnectorSurface: "edut_native",
|
|
||||||
PacingTier: "local_hardware_speed",
|
|
||||||
},
|
|
||||||
SortOrder: 50,
|
SortOrder: 50,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,15 +3,14 @@ package main
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type marketplaceOffer struct {
|
type marketplaceOffer struct {
|
||||||
OfferID string `json:"offer_id"`
|
OfferID string `json:"offer_id"`
|
||||||
IssuerID string `json:"issuer_id"`
|
IssuerID string `json:"issuer_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Summary string `json:"summary,omitempty"`
|
Summary string `json:"summary,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Pricing marketplaceOfferPrice `json:"pricing"`
|
Pricing marketplaceOfferPrice `json:"pricing"`
|
||||||
Policies marketplaceOfferPolicy `json:"policies"`
|
Policies marketplaceOfferPolicy `json:"policies"`
|
||||||
ExecutionProfile marketplaceExecutionProfile `json:"execution_profile,omitempty"`
|
SortOrder int `json:"-"`
|
||||||
SortOrder int `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type marketplaceOfferPrice struct {
|
type marketplaceOfferPrice struct {
|
||||||
@ -31,12 +30,6 @@ type marketplaceOfferPolicy struct {
|
|||||||
RequiresOffers []string `json:"requires_offers,omitempty"`
|
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 {
|
type marketplaceOffersResponse struct {
|
||||||
Offers []marketplaceOffer `json:"offers"`
|
Offers []marketplaceOffer `json:"offers"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,27 +37,6 @@ type walletVerifyResponse struct {
|
|||||||
SessionExpires string `json:"session_expires_at,omitempty"`
|
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 {
|
type membershipQuoteRequest struct {
|
||||||
DesignationCode string `json:"designation_code"`
|
DesignationCode string `json:"designation_code"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
|
|||||||
@ -13,7 +13,6 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (a *app) issueWalletSession(ctx context.Context, wallet, designationCode string) (walletSessionRecord, error) {
|
func (a *app) issueWalletSession(ctx context.Context, wallet, designationCode string) (walletSessionRecord, error) {
|
||||||
_, _ = a.store.deleteExpiredWalletSessions(ctx, time.Now().UTC())
|
|
||||||
token, err := randomHex(24)
|
token, err := randomHex(24)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return walletSessionRecord{}, err
|
return walletSessionRecord{}, err
|
||||||
|
|||||||
@ -453,40 +453,6 @@ func (s *store) touchWalletSession(ctx context.Context, token string, touchedAt
|
|||||||
return nil
|
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 {
|
func (s *store) putQuote(ctx context.Context, quote quoteRecord) error {
|
||||||
_, err := s.db.ExecContext(ctx, `
|
_, err := s.db.ExecContext(ctx, `
|
||||||
INSERT INTO quotes (
|
INSERT INTO quotes (
|
||||||
|
|||||||
@ -19,11 +19,6 @@ Success (`200`):
|
|||||||
"decimals": 6,
|
"decimals": 6,
|
||||||
"chain_id": 8453
|
"chain_id": 8453
|
||||||
},
|
},
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "hybrid",
|
|
||||||
"pacing_tier": "governed_human_pace",
|
|
||||||
"human_pace_floor_ms": 1200
|
|
||||||
},
|
|
||||||
"policies": {
|
"policies": {
|
||||||
"member_only": true,
|
"member_only": true,
|
||||||
"workspace_bound": true,
|
"workspace_bound": true,
|
||||||
@ -106,7 +101,6 @@ Success (`200`):
|
|||||||
1. `amount`/`amount_atomic` represent the license component.
|
1. `amount`/`amount_atomic` represent the license component.
|
||||||
2. `total_amount`/`total_amount_atomic` represent the actual payable quote total.
|
2. `total_amount`/`total_amount_atomic` represent the actual payable quote total.
|
||||||
3. First checkout can include membership activation as a separate line item.
|
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`):
|
Error (`403`):
|
||||||
|
|
||||||
|
|||||||
@ -80,65 +80,7 @@ Error (`400` intent expired):
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3) `POST /secret/wallet/session/refresh`
|
## 3) `POST /secret/membership/quote`
|
||||||
|
|
||||||
Request:
|
|
||||||
|
|
||||||
Headers:
|
|
||||||
|
|
||||||
1. `Authorization: Bearer <session_token>` (or `X-Edut-Session`)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Success (`200`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "session_refreshed",
|
|
||||||
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
|
|
||||||
"session_token": "f9bc20f15ecf7fd53f1f4ba8ca774564a1098e6ed9db6f0f",
|
|
||||||
"session_expires_at": "2026-03-18T07:42:10Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Error (`401` missing/expired token):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "wallet session required",
|
|
||||||
"code": "wallet_session_required"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4) `POST /secret/wallet/session/revoke`
|
|
||||||
|
|
||||||
Request:
|
|
||||||
|
|
||||||
Headers:
|
|
||||||
|
|
||||||
1. `Authorization: Bearer <session_token>` (or `X-Edut-Session`)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Success (`200`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "session_revoked",
|
|
||||||
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
|
|
||||||
"revoked_at": "2026-02-17T07:34:02Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5) `POST /secret/membership/quote`
|
|
||||||
|
|
||||||
Request:
|
Request:
|
||||||
|
|
||||||
@ -200,7 +142,7 @@ Error (`403` distinct payer without proof):
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6) `POST /secret/membership/confirm`
|
## 4) `POST /secret/membership/confirm`
|
||||||
|
|
||||||
Request:
|
Request:
|
||||||
|
|
||||||
@ -242,7 +184,7 @@ Error (`400` tx mismatch):
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7) `GET /secret/membership/status`
|
## 5) `GET /secret/membership/status`
|
||||||
|
|
||||||
Request by wallet:
|
Request by wallet:
|
||||||
|
|
||||||
|
|||||||
@ -137,19 +137,6 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
chain_id:
|
chain_id:
|
||||||
type: integer
|
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:
|
policies:
|
||||||
type: object
|
type: object
|
||||||
required: [member_only, workspace_bound, transferable, internal_use_only, multi_tenant]
|
required: [member_only, workspace_bound, transferable, internal_use_only, multi_tenant]
|
||||||
|
|||||||
@ -34,70 +34,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Signature verified
|
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:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/WalletVerifyResponse'
|
$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:
|
/secret/membership/quote:
|
||||||
post:
|
post:
|
||||||
summary: Get current membership mint quote
|
summary: Get current membership mint quote
|
||||||
@ -153,14 +93,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/MembershipStatusResponse'
|
$ref: '#/components/schemas/MembershipStatusResponse'
|
||||||
components:
|
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:
|
schemas:
|
||||||
WalletIntentRequest:
|
WalletIntentRequest:
|
||||||
type: object
|
type: object
|
||||||
@ -234,46 +166,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
description: Session token expiry timestamp.
|
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:
|
MembershipQuoteRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [designation_code, address, chain_id]
|
required: [designation_code, address, chain_id]
|
||||||
@ -303,7 +195,7 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
currency:
|
currency:
|
||||||
type: string
|
type: string
|
||||||
enum: [USDC]
|
enum: [USDC, ETH]
|
||||||
amount:
|
amount:
|
||||||
type: string
|
type: string
|
||||||
amount_atomic:
|
amount_atomic:
|
||||||
|
|||||||
@ -88,11 +88,9 @@ PrincipalRole:
|
|||||||
|
|
||||||
1. `POST /secret/wallet/intent`
|
1. `POST /secret/wallet/intent`
|
||||||
2. `POST /secret/wallet/verify`
|
2. `POST /secret/wallet/verify`
|
||||||
3. `POST /secret/wallet/session/refresh`
|
3. `POST /secret/membership/quote`
|
||||||
4. `POST /secret/wallet/session/revoke`
|
4. `POST /secret/membership/confirm`
|
||||||
5. `POST /secret/membership/quote`
|
5. `GET /secret/membership/status?designation_code=...`
|
||||||
6. `POST /secret/membership/confirm`
|
|
||||||
7. `GET /secret/membership/status?designation_code=...`
|
|
||||||
|
|
||||||
## Marketplace
|
## Marketplace
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"quote_policy": {
|
"quote_policy": {
|
||||||
"default_currency": "USDC",
|
"default_currency": "USDC",
|
||||||
"minimum_floor_usd": "100.00",
|
"minimum_floor_usd": "5.00",
|
||||||
"safety_multiplier": "1.5",
|
"safety_multiplier": "1.5",
|
||||||
"quote_ttl_seconds": 300
|
"quote_ttl_seconds": 300
|
||||||
},
|
},
|
||||||
|
|||||||
@ -66,14 +66,12 @@ Expected:
|
|||||||
## Post-Deploy Verification
|
## Post-Deploy Verification
|
||||||
|
|
||||||
1. `POST /secret/wallet/intent` returns `intent_id` and `designation_code`.
|
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`.
|
2. `POST /secret/wallet/verify` accepts valid EIP-712 signature.
|
||||||
3. `POST /secret/wallet/session/refresh` rotates wallet session token.
|
3. `POST /secret/membership/quote` returns tx payload.
|
||||||
4. `POST /secret/wallet/session/revoke` revokes wallet session token.
|
4. `POST /secret/membership/confirm` marks membership active.
|
||||||
5. `POST /secret/membership/quote` returns tx payload.
|
5. `POST /governance/install/token` enforces owner role and active membership.
|
||||||
6. `POST /secret/membership/confirm` marks membership active.
|
6. `POST /governance/install/confirm` enforces package/runtime/policy match.
|
||||||
7. `POST /governance/install/token` enforces owner role and active membership.
|
7. `GET /governance/install/status` resolves deterministic activation state.
|
||||||
8. `POST /governance/install/confirm` enforces package/runtime/policy match.
|
8. `POST /member/channel/device/register` returns active channel binding.
|
||||||
9. `GET /governance/install/status` resolves deterministic activation state.
|
9. `GET /member/channel/events` returns deterministic inbox page.
|
||||||
10. `POST /member/channel/device/register` returns active channel binding.
|
10. `POST /member/channel/events/{event_id}/ack` is idempotent per event+device.
|
||||||
11. `GET /member/channel/events` returns deterministic inbox page.
|
|
||||||
12. `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`
|
1. `POST /secret/wallet/intent`
|
||||||
2. `POST /secret/wallet/verify`
|
2. `POST /secret/wallet/verify`
|
||||||
3. `POST /secret/wallet/session/refresh`
|
3. `POST /secret/membership/quote`
|
||||||
4. `POST /secret/wallet/session/revoke`
|
4. `POST /secret/membership/confirm`
|
||||||
5. `POST /secret/membership/quote`
|
5. `GET /secret/membership/status`
|
||||||
6. `POST /secret/membership/confirm`
|
|
||||||
7. `GET /secret/membership/status`
|
|
||||||
|
|
||||||
## Web Behavior Dependency
|
## Web Behavior Dependency
|
||||||
|
|
||||||
@ -52,25 +50,6 @@ Must return:
|
|||||||
1. `status = signature_verified`
|
1. `status = signature_verified`
|
||||||
2. `designation_code`
|
2. `designation_code`
|
||||||
3. `display_token`
|
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
|
## Membership Quote
|
||||||
|
|
||||||
@ -123,9 +102,6 @@ Must return:
|
|||||||
10. Optional strict chain verification mode:
|
10. Optional strict chain verification mode:
|
||||||
- when `SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION=true`,
|
- when `SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION=true`,
|
||||||
- membership confirm must fail closed if chain RPC verification is unavailable.
|
- 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
|
## 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.
|
2. EIP-712 signature proves wallet possession.
|
||||||
3. Server verify endpoint enforces replay protection and origin checks.
|
3. Server verify endpoint enforces replay protection and origin checks.
|
||||||
4. Intent payload includes price/currency/deadline for explicit consent.
|
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
|
## Step 5: Add Membership Mint Transaction Stage
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ Implemented now:
|
|||||||
4. Step-based roadmap without timelines.
|
4. Step-based roadmap without timelines.
|
||||||
5. Frozen v1 schemas and examples.
|
5. Frozen v1 schemas and examples.
|
||||||
6. Interface target document for contracts/APIs.
|
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.
|
8. Terms utility-only non-investment clause.
|
||||||
9. Store page upgraded from static to live-state scaffold with membership gate behavior.
|
9. Store page upgraded from static to live-state scaffold with membership gate behavior.
|
||||||
10. OpenAPI contract + request/response examples for secret-system endpoints.
|
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.
|
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.
|
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.
|
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:
|
Remaining in this repo:
|
||||||
|
|
||||||
@ -70,7 +67,7 @@ Remaining in this repo:
|
|||||||
|
|
||||||
Cross-repo dependencies (kernel/backend/contracts):
|
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).
|
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.
|
3. Implement checkout APIs and entitlement mint pipeline.
|
||||||
4. Implement runtime entitlement gate and evidence receipts.
|
4. Implement runtime entitlement gate and evidence receipts.
|
||||||
|
|||||||
@ -12,12 +12,7 @@
|
|||||||
"workspace_bound": false,
|
"workspace_bound": false,
|
||||||
"transferable": false,
|
"transferable": false,
|
||||||
"internal_use_only": true,
|
"internal_use_only": true,
|
||||||
"multi_tenant": false,
|
"multi_tenant": false
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "hybrid",
|
|
||||||
"pacing_tier": "governed_human_pace",
|
|
||||||
"human_pace_floor_ms": 1200
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"offer_id": "edut.workspace.core",
|
"offer_id": "edut.workspace.core",
|
||||||
@ -29,12 +24,7 @@
|
|||||||
"workspace_bound": true,
|
"workspace_bound": true,
|
||||||
"transferable": false,
|
"transferable": false,
|
||||||
"internal_use_only": true,
|
"internal_use_only": true,
|
||||||
"multi_tenant": false,
|
"multi_tenant": false
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "hybrid",
|
|
||||||
"pacing_tier": "governed_human_pace",
|
|
||||||
"human_pace_floor_ms": 1200
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"offer_id": "edut.workspace.ai",
|
"offer_id": "edut.workspace.ai",
|
||||||
@ -46,12 +36,7 @@
|
|||||||
"workspace_bound": true,
|
"workspace_bound": true,
|
||||||
"transferable": false,
|
"transferable": false,
|
||||||
"internal_use_only": true,
|
"internal_use_only": true,
|
||||||
"multi_tenant": false,
|
"multi_tenant": false
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "hybrid",
|
|
||||||
"pacing_tier": "governed_human_pace",
|
|
||||||
"human_pace_floor_ms": 1200
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"offer_id": "edut.workspace.lane24",
|
"offer_id": "edut.workspace.lane24",
|
||||||
@ -63,11 +48,7 @@
|
|||||||
"workspace_bound": true,
|
"workspace_bound": true,
|
||||||
"transferable": false,
|
"transferable": false,
|
||||||
"internal_use_only": true,
|
"internal_use_only": true,
|
||||||
"multi_tenant": false,
|
"multi_tenant": false
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "edut_native",
|
|
||||||
"pacing_tier": "local_hardware_speed"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"offer_id": "edut.workspace.sovereign",
|
"offer_id": "edut.workspace.sovereign",
|
||||||
@ -79,11 +60,7 @@
|
|||||||
"workspace_bound": true,
|
"workspace_bound": true,
|
||||||
"transferable": false,
|
"transferable": false,
|
||||||
"internal_use_only": true,
|
"internal_use_only": true,
|
||||||
"multi_tenant": false,
|
"multi_tenant": false
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "edut_native",
|
|
||||||
"pacing_tier": "local_hardware_speed"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"published_at": "2026-02-17T00:00:00Z"
|
"published_at": "2026-02-17T00:00:00Z"
|
||||||
|
|||||||
@ -21,16 +21,10 @@
|
|||||||
"requires_admin_approval": false
|
"requires_admin_approval": false
|
||||||
},
|
},
|
||||||
"entitlement": {
|
"entitlement": {
|
||||||
"type": "module_license",
|
"type": "runtime_license",
|
||||||
"scope": "workspace",
|
"scope": "org_root",
|
||||||
"runtime_policy_ref": "policy.workspace.core.v1"
|
"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",
|
"created_at": "2026-02-17T00:00:00Z",
|
||||||
"updated_at": "2026-02-17T00:00:00Z",
|
"updated_at": "2026-02-17T00:00:00Z",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
|||||||
@ -35,26 +35,7 @@
|
|||||||
"workspace_bound": { "type": "boolean" },
|
"workspace_bound": { "type": "boolean" },
|
||||||
"transferable": { "type": "boolean" },
|
"transferable": { "type": "boolean" },
|
||||||
"internal_use_only": { "type": "boolean" },
|
"internal_use_only": { "type": "boolean" },
|
||||||
"multi_tenant": { "type": "boolean" },
|
"multi_tenant": { "type": "boolean" }
|
||||||
"execution_profile": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["connector_surface", "pacing_tier"],
|
|
||||||
"properties": {
|
|
||||||
"connector_surface": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["edut_native", "external_connector", "hybrid"]
|
|
||||||
},
|
|
||||||
"pacing_tier": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["governed_human_pace", "local_hardware_speed"]
|
|
||||||
},
|
|
||||||
"human_pace_floor_ms": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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": {
|
"created_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"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 |
|
| 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/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/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/quote` | Return current payable membership quote |
|
||||||
| API | `api.edut.ai/secret/membership/confirm` | Confirm membership tx and activation state |
|
| API | `api.edut.ai/secret/membership/confirm` | Confirm membership tx and activation state |
|
||||||
| Chain | Base | Membership mint settlement and evidence |
|
| Chain | Base | Membership mint settlement and evidence |
|
||||||
@ -162,77 +160,11 @@ Response:
|
|||||||
"status": "signature_verified",
|
"status": "signature_verified",
|
||||||
"designation_code": "0217073045482",
|
"designation_code": "0217073045482",
|
||||||
"display_token": "0217-0730-4548-2",
|
"display_token": "0217-0730-4548-2",
|
||||||
"verified_at": "2026-02-17T07:31:12Z",
|
"verified_at": "2026-02-17T07:31:12Z"
|
||||||
"session_token": "9f2c50f8a0f5d8d0b0efc4fa665e4032f31bb0c4c4f31b8c",
|
|
||||||
"session_expires_at": "2026-03-18T07:31:12Z"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3) Wallet Session Lifecycle
|
### 3) Membership Quote
|
||||||
|
|
||||||
#### `POST /secret/wallet/session/refresh`
|
|
||||||
|
|
||||||
Request JSON:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"wallet": "0xabc123..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
1. Valid wallet session token via `Authorization: Bearer <token>` or `X-Edut-Session`.
|
|
||||||
2. Session wallet must match request wallet.
|
|
||||||
|
|
||||||
Behavior:
|
|
||||||
|
|
||||||
1. Validate current token state (not expired/revoked, chain matches).
|
|
||||||
2. Revoke the presented token.
|
|
||||||
3. Issue a replacement token with fresh expiry.
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "session_refreshed",
|
|
||||||
"wallet": "0xabc123...",
|
|
||||||
"session_token": "f9bc20f15ecf7fd53f1f4ba8ca774564a1098e6ed9db6f0f",
|
|
||||||
"session_expires_at": "2026-03-18T07:42:10Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `POST /secret/wallet/session/revoke`
|
|
||||||
|
|
||||||
Request JSON:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"wallet": "0xabc123..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
1. Valid wallet session token via `Authorization: Bearer <token>` or `X-Edut-Session`.
|
|
||||||
2. Session wallet must match request wallet.
|
|
||||||
|
|
||||||
Behavior:
|
|
||||||
|
|
||||||
1. Validate current token state.
|
|
||||||
2. Mark session as revoked immediately.
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "session_revoked",
|
|
||||||
"wallet": "0xabc123...",
|
|
||||||
"revoked_at": "2026-02-17T07:34:02Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4) Membership Quote
|
|
||||||
|
|
||||||
#### `POST /secret/membership/quote`
|
#### `POST /secret/membership/quote`
|
||||||
|
|
||||||
@ -268,7 +200,7 @@ Response:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5) Membership Confirm
|
### 4) Membership Confirm
|
||||||
|
|
||||||
#### `POST /secret/membership/confirm`
|
#### `POST /secret/membership/confirm`
|
||||||
|
|
||||||
@ -367,14 +299,6 @@ location /secret/wallet/verify {
|
|||||||
proxy_pass http://127.0.0.1:9091;
|
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 {
|
location /secret/membership/quote {
|
||||||
proxy_pass http://127.0.0.1:9091;
|
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:
|
The wallet-first designation plus paid membership flow creates a deterministic two-factor identity and commitment chain:
|
||||||
|
|
||||||
1. signature proves wallet control,
|
1. signature proves wallet control,
|
||||||
2. verify issues wallet session for fail-closed control-plane access,
|
2. paid mint proves intent,
|
||||||
3. paid mint proves intent,
|
3. membership gates all future marketplace purchases.
|
||||||
4. 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">
|
<div class="container">
|
||||||
<p><a href="/">back</a></p>
|
<p><a href="/">back</a></p>
|
||||||
<h1>EDUT Platform Android</h1>
|
<h1>EDUT Platform Android</h1>
|
||||||
<p class="muted">EDUT ID channel verification</p>
|
<p class="muted">membership channel verification</p>
|
||||||
<div class="card">
|
<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>
|
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
|
||||||
<p id="status" class="status" aria-live="polite"></p>
|
<p id="status" class="status" aria-live="polite"></p>
|
||||||
<div id="download-instructions" class="download-instructions">
|
<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>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>
|
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -127,18 +127,18 @@ async function handleConnectWallet() {
|
|||||||
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
|
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
const chainId = Number.parseInt(chainHex, 16);
|
const chainId = Number.parseInt(chainHex, 16);
|
||||||
|
|
||||||
setStatus('Checking EDUT ID status...', false);
|
setStatus('Checking membership status...', false);
|
||||||
const status = await fetchMembershipStatus(wallet, chainId);
|
const status = await fetchMembershipStatus(wallet, chainId);
|
||||||
|
|
||||||
if (status.status === 'active') {
|
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');
|
instructionsNode.classList.add('visible');
|
||||||
return;
|
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) {
|
} 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);
|
setStatus(message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,13 +71,13 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<p><a href="/">back</a></p>
|
<p><a href="/">back</a></p>
|
||||||
<h1>EDUT Platform Desktop</h1>
|
<h1>EDUT Platform Desktop</h1>
|
||||||
<p class="muted">EDUT ID channel verification</p>
|
<p class="muted">membership channel verification</p>
|
||||||
<div class="card">
|
<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>
|
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
|
||||||
<p id="status" class="status" aria-live="polite"></p>
|
<p id="status" class="status" aria-live="polite"></p>
|
||||||
<div id="download-instructions" class="download-instructions">
|
<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>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>
|
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -127,18 +127,18 @@ async function handleConnectWallet() {
|
|||||||
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
|
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
const chainId = Number.parseInt(chainHex, 16);
|
const chainId = Number.parseInt(chainHex, 16);
|
||||||
|
|
||||||
setStatus('Checking EDUT ID status...', false);
|
setStatus('Checking membership status...', false);
|
||||||
const status = await fetchMembershipStatus(wallet, chainId);
|
const status = await fetchMembershipStatus(wallet, chainId);
|
||||||
|
|
||||||
if (status.status === 'active') {
|
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');
|
instructionsNode.classList.add('visible');
|
||||||
return;
|
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) {
|
} 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);
|
setStatus(message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,13 +71,13 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<p><a href="/">back</a></p>
|
<p><a href="/">back</a></p>
|
||||||
<h1>EDUT Platform iOS</h1>
|
<h1>EDUT Platform iOS</h1>
|
||||||
<p class="muted">EDUT ID channel verification</p>
|
<p class="muted">membership channel verification</p>
|
||||||
<div class="card">
|
<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>
|
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
|
||||||
<p id="status" class="status" aria-live="polite"></p>
|
<p id="status" class="status" aria-live="polite"></p>
|
||||||
<div id="download-instructions" class="download-instructions">
|
<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>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>
|
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -127,18 +127,18 @@ async function handleConnectWallet() {
|
|||||||
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
|
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
const chainId = Number.parseInt(chainHex, 16);
|
const chainId = Number.parseInt(chainHex, 16);
|
||||||
|
|
||||||
setStatus('Checking EDUT ID status...', false);
|
setStatus('Checking membership status...', false);
|
||||||
const status = await fetchMembershipStatus(wallet, chainId);
|
const status = await fetchMembershipStatus(wallet, chainId);
|
||||||
|
|
||||||
if (status.status === 'active') {
|
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');
|
instructionsNode.classList.add('visible');
|
||||||
return;
|
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) {
|
} 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);
|
setStatus(message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -402,7 +402,7 @@
|
|||||||
<div class="flow-ui" id="flow-ui">
|
<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>
|
<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">
|
<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_no_tx">No transaction. Signature only.</p>
|
||||||
<p class="flow-line subtle localizable" data-i18n="wallet_fact_seed">Never share your seed phrase.</p>
|
<p class="flow-line subtle localizable" data-i18n="wallet_fact_seed">Never share your seed phrase.</p>
|
||||||
<div class="flow-actions">
|
<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-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>
|
<a id="download-android" class="flow-link localizable" data-i18n="download_android" href="/downloads/android">android</a>
|
||||||
</div>
|
</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>
|
||||||
</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>
|
<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,
|
currentIntent: null,
|
||||||
sessionToken: '',
|
sessionToken: '',
|
||||||
sessionExpiresAt: '',
|
sessionExpiresAt: '',
|
||||||
sessionWallet: '',
|
|
||||||
sessionRefreshInFlight: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const continueAction = document.getElementById('continue-action');
|
const continueAction = document.getElementById('continue-action');
|
||||||
@ -521,7 +519,6 @@ function getStoredAcknowledgement() {
|
|||||||
if (parsed && (parsed.code || parsed.token)) {
|
if (parsed && (parsed.code || parsed.token)) {
|
||||||
flowState.sessionToken = typeof parsed.session_token === 'string' ? parsed.session_token : '';
|
flowState.sessionToken = typeof parsed.session_token === 'string' ? parsed.session_token : '';
|
||||||
flowState.sessionExpiresAt = typeof parsed.session_expires_at === 'string' ? parsed.session_expires_at : '';
|
flowState.sessionExpiresAt = typeof parsed.session_expires_at === 'string' ? parsed.session_expires_at : '';
|
||||||
flowState.sessionWallet = typeof parsed.wallet === 'string' ? parsed.wallet : '';
|
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -612,92 +609,7 @@ function formatQuoteDisplay(quote) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearFlowSession(reason) {
|
async function postJSON(url, payload) {
|
||||||
flowState.sessionToken = '';
|
|
||||||
flowState.sessionExpiresAt = '';
|
|
||||||
flowState.sessionWallet = '';
|
|
||||||
if (reason) {
|
|
||||||
console.warn('wallet session cleared:', reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function captureFlowSessionHeaders(res) {
|
|
||||||
const token = String(res.headers.get('x-edut-session') || '').trim();
|
|
||||||
if (token) {
|
|
||||||
flowState.sessionToken = token;
|
|
||||||
}
|
|
||||||
const expiresAt = String(res.headers.get('x-edut-session-expires-at') || '').trim();
|
|
||||||
if (expiresAt) {
|
|
||||||
flowState.sessionExpiresAt = expiresAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseApiError(text, status) {
|
|
||||||
if (!text) {
|
|
||||||
return { message: 'HTTP ' + status, code: '' };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(text);
|
|
||||||
if (parsed && typeof parsed === 'object') {
|
|
||||||
return {
|
|
||||||
message: String(parsed.error || ('HTTP ' + status)),
|
|
||||||
code: String(parsed.code || ''),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// fall back to raw text
|
|
||||||
}
|
|
||||||
return { message: text, code: '' };
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTerminalSessionCode(code) {
|
|
||||||
const normalized = String(code || '').toLowerCase();
|
|
||||||
return normalized === 'wallet_session_invalid' ||
|
|
||||||
normalized === 'wallet_session_expired' ||
|
|
||||||
normalized === 'wallet_session_revoked' ||
|
|
||||||
normalized === 'wallet_session_mismatch';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function maybeRefreshFlowSession(wallet, requestUrl) {
|
|
||||||
if (!flowState.sessionToken || !wallet || !flowState.sessionExpiresAt) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (String(requestUrl || '').indexOf('/secret/wallet/session/') === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const expiresAt = Date.parse(flowState.sessionExpiresAt);
|
|
||||||
if (!Number.isFinite(expiresAt)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((expiresAt - Date.now()) > (5 * 60 * 1000)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (flowState.sessionRefreshInFlight) {
|
|
||||||
await flowState.sessionRefreshInFlight;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
flowState.sessionRefreshInFlight = postJSON(
|
|
||||||
'/secret/wallet/session/refresh',
|
|
||||||
{ wallet },
|
|
||||||
{ wallet, skipSessionRefresh: true },
|
|
||||||
).then(function (out) {
|
|
||||||
if (out && typeof out.session_token === 'string') {
|
|
||||||
flowState.sessionToken = out.session_token;
|
|
||||||
}
|
|
||||||
if (out && typeof out.session_expires_at === 'string') {
|
|
||||||
flowState.sessionExpiresAt = out.session_expires_at;
|
|
||||||
}
|
|
||||||
}).finally(function () {
|
|
||||||
flowState.sessionRefreshInFlight = null;
|
|
||||||
});
|
|
||||||
await flowState.sessionRefreshInFlight;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postJSON(url, payload, options) {
|
|
||||||
const opts = options || {};
|
|
||||||
if (!opts.skipSessionRefresh) {
|
|
||||||
await maybeRefreshFlowSession(opts.wallet || flowState.sessionWallet || '', url);
|
|
||||||
}
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
@ -710,17 +622,11 @@ async function postJSON(url, payload, options) {
|
|||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
captureFlowSessionHeaders(res);
|
|
||||||
const text = await res.text();
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const detail = parseApiError(text, res.status);
|
const detail = await res.text();
|
||||||
if (isTerminalSessionCode(detail.code)) {
|
throw new Error(detail || ('HTTP ' + res.status));
|
||||||
clearFlowSession(detail.code);
|
|
||||||
}
|
|
||||||
throw new Error(detail.message);
|
|
||||||
}
|
}
|
||||||
if (!text) return {};
|
return res.json();
|
||||||
return JSON.parse(text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLanguage(input) {
|
function resolveLanguage(input) {
|
||||||
@ -819,10 +725,6 @@ async function startWalletFlow() {
|
|||||||
throw new Error('Wallet connection was not approved.');
|
throw new Error('Wallet connection was not approved.');
|
||||||
}
|
}
|
||||||
const address = accounts[0];
|
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 chainIdHex = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
const chainId = Number.parseInt(chainIdHex, 16);
|
const chainId = Number.parseInt(chainIdHex, 16);
|
||||||
|
|
||||||
@ -832,7 +734,7 @@ async function startWalletFlow() {
|
|||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
locale: localStorage.getItem('edut_lang') || 'en',
|
locale: localStorage.getItem('edut_lang') || 'en',
|
||||||
chain_id: chainId,
|
chain_id: chainId,
|
||||||
}, { wallet: address });
|
});
|
||||||
flowState.currentIntent = intent;
|
flowState.currentIntent = intent;
|
||||||
|
|
||||||
const typedData = {
|
const typedData = {
|
||||||
@ -879,17 +781,16 @@ async function startWalletFlow() {
|
|||||||
address,
|
address,
|
||||||
chain_id: chainId,
|
chain_id: chainId,
|
||||||
signature,
|
signature,
|
||||||
}, { wallet: address });
|
});
|
||||||
flowState.sessionToken = verification.session_token || '';
|
flowState.sessionToken = verification.session_token || '';
|
||||||
flowState.sessionExpiresAt = verification.session_expires_at || '';
|
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', {
|
const quote = await postJSON('/secret/membership/quote', {
|
||||||
designation_code: verification.designation_code || intent.designation_code || null,
|
designation_code: verification.designation_code || intent.designation_code || null,
|
||||||
address,
|
address,
|
||||||
chain_id: chainId,
|
chain_id: chainId,
|
||||||
}, { wallet: address });
|
});
|
||||||
|
|
||||||
const txParams = quote.tx || {
|
const txParams = quote.tx || {
|
||||||
from: address,
|
from: address,
|
||||||
@ -900,32 +801,32 @@ async function startWalletFlow() {
|
|||||||
|
|
||||||
if (!txParams.from) txParams.from = address;
|
if (!txParams.from) txParams.from = address;
|
||||||
if (!txParams.to) {
|
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 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) {
|
if (quoteDisplay) {
|
||||||
setFlowStatusMessage(mintPrompt + ' (' + quoteDisplay + ')', false);
|
setFlowStatusMessage(mintPrompt + ' (' + quoteDisplay + ')', false);
|
||||||
} else {
|
} 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({
|
const txHash = await window.ethereum.request({
|
||||||
method: 'eth_sendTransaction',
|
method: 'eth_sendTransaction',
|
||||||
params: [txParams],
|
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', {
|
const confirmation = await postJSON('/secret/membership/confirm', {
|
||||||
designation_code: verification.designation_code || intent.designation_code || null,
|
designation_code: verification.designation_code || intent.designation_code || null,
|
||||||
quote_id: quote.quote_id || null,
|
quote_id: quote.quote_id || null,
|
||||||
tx_hash: txHash,
|
tx_hash: txHash,
|
||||||
address,
|
address,
|
||||||
chain_id: chainId,
|
chain_id: chainId,
|
||||||
}, { wallet: address });
|
});
|
||||||
|
|
||||||
if (confirmation.status !== 'membership_active') {
|
if (confirmation.status !== 'membership_active') {
|
||||||
throw new Error('EDUT ID transaction did not activate.');
|
throw new Error('Membership transaction did not activate.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const ackState = {
|
const ackState = {
|
||||||
@ -940,7 +841,7 @@ async function startWalletFlow() {
|
|||||||
saveAcknowledgement(ackState);
|
saveAcknowledgement(ackState);
|
||||||
renderAcknowledged(ackState);
|
renderAcknowledged(ackState);
|
||||||
showPostMintPanel();
|
showPostMintPanel();
|
||||||
setFlowStatus('membership_active', 'EDUT ID active. Designation acknowledged.', false);
|
setFlowStatus('membership_active', 'Membership active. Designation acknowledged.', false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err && err.message ? err.message : 'Wallet flow failed.';
|
const message = err && err.message ? err.message : 'Wallet flow failed.';
|
||||||
setFlowStatus('wallet_failed', message, true);
|
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>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>
|
<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>
|
<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 charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>EDUT Store Preview</title>
|
<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="theme-color" content="#f0f4f8">
|
||||||
<meta name="robots" content="noindex,nofollow,noarchive,nosnippet">
|
<meta name="robots" content="noindex,nofollow,noarchive,nosnippet">
|
||||||
<style>
|
<style>
|
||||||
@ -69,43 +69,6 @@
|
|||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: #454b54;
|
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 {
|
.state {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@ -184,18 +147,18 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<a href="/" class="back">← Back</a>
|
<a href="/" class="back">← Back</a>
|
||||||
<h1>EDUT Store</h1>
|
<h1>EDUT Store</h1>
|
||||||
<p class="sub" 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">
|
<div class="grid">
|
||||||
<section class="card">
|
<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">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">Gate decision: <span class="mono" id="gate-label">blocked</span></p>
|
||||||
<p class="line">Payer wallet override (optional):</p>
|
<p class="line">Payer wallet override (optional):</p>
|
||||||
<input id="payer-wallet-input" type="text" placeholder="0x... (leave blank to use ownership wallet)">
|
<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>
|
<p class="line">Ownership proof: <span class="mono" id="proof-label">not required</span></p>
|
||||||
<span class="state block" id="gate-pill">EDUT ID required</span>
|
<span class="state block" id="gate-pill">membership required</span>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="connect-btn" type="button">connect wallet</button>
|
<button id="connect-btn" type="button">connect wallet</button>
|
||||||
<button id="refresh-btn" type="button">refresh state</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-summary">Catalog data pending.</p>
|
||||||
<p class="line" id="offer-price">Price: --</p>
|
<p class="line" id="offer-price">Price: --</p>
|
||||||
<p class="line" id="offer-policy">Policy: --</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">
|
<div class="actions">
|
||||||
<button id="checkout-btn" type="button" disabled>request checkout quote</button>
|
<button id="checkout-btn" type="button" disabled>request checkout quote</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<p class="label">Fail-Closed States</p>
|
<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">Suspended/revoked: checkout and activation blocked.</p>
|
||||||
<p class="line">Unknown state or API error: blocked by default.</p>
|
<p class="line">Unknown state or API error: blocked by default.</p>
|
||||||
<span class="state warn">default deny</span>
|
<span class="state warn">default deny</span>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -254,9 +217,7 @@
|
|||||||
offers: [],
|
offers: [],
|
||||||
selectedOfferId: null,
|
selectedOfferId: null,
|
||||||
sessionToken: '',
|
sessionToken: '',
|
||||||
sessionExpiresAt: '',
|
|
||||||
sessionWallet: null,
|
sessionWallet: null,
|
||||||
sessionRefreshInFlight: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const walletLabel = document.getElementById('wallet-label');
|
const walletLabel = document.getElementById('wallet-label');
|
||||||
@ -361,16 +322,9 @@
|
|||||||
offerTitle.textContent = selected.title || selected.offer_id;
|
offerTitle.textContent = selected.title || selected.offer_id;
|
||||||
offerSummary.textContent = selected.summary || 'No summary provided.';
|
offerSummary.textContent = selected.summary || 'No summary provided.';
|
||||||
offerPrice.textContent = 'Price: ' + (selected.price || '--') + ' ' + (selected.currency || '');
|
offerPrice.textContent = 'Price: ' + (selected.price || '--') + ' ' + (selected.currency || '');
|
||||||
let profile = '';
|
offerPolicy.textContent = 'Policy: member-only=' + Boolean(selected.member_only) +
|
||||||
if (selected.execution_profile && typeof selected.execution_profile === 'object') {
|
|
||||||
const pace = selected.execution_profile.pacing_tier || 'unknown';
|
|
||||||
const surface = selected.execution_profile.connector_surface || 'unknown';
|
|
||||||
profile = ', pacing=' + pace + ', surface=' + surface;
|
|
||||||
}
|
|
||||||
offerPolicy.textContent = 'Policy: EDUT-ID-required=' + Boolean(selected.member_only) +
|
|
||||||
', workspace-bound=' + Boolean(selected.workspace_bound) +
|
', workspace-bound=' + Boolean(selected.workspace_bound) +
|
||||||
', transferable=' + Boolean(selected.transferable) +
|
', transferable=' + Boolean(selected.transferable);
|
||||||
profile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateOfferSelect() {
|
function populateOfferSelect() {
|
||||||
@ -411,7 +365,7 @@
|
|||||||
gatePill.textContent = 'status unknown';
|
gatePill.textContent = 'status unknown';
|
||||||
} else {
|
} else {
|
||||||
gatePill.className = 'state block';
|
gatePill.className = 'state block';
|
||||||
gatePill.textContent = 'EDUT ID required';
|
gatePill.textContent = 'membership required';
|
||||||
}
|
}
|
||||||
|
|
||||||
checkoutBtn.disabled = !state.gate || !state.selectedOfferId;
|
checkoutBtn.disabled = !state.gate || !state.selectedOfferId;
|
||||||
@ -443,9 +397,6 @@
|
|||||||
if (typeof parsed.session_token === 'string') {
|
if (typeof parsed.session_token === 'string') {
|
||||||
state.sessionToken = parsed.session_token.trim();
|
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') {
|
if (typeof parsed.wallet === 'string') {
|
||||||
state.sessionWallet = parsed.wallet.trim();
|
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) {
|
function authHeaders(extra) {
|
||||||
const headers = Object.assign({}, extra || {});
|
const headers = Object.assign({}, extra || {});
|
||||||
if (state.sessionToken) {
|
if (state.sessionToken) {
|
||||||
@ -565,42 +415,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJson(url) {
|
async function fetchJson(url) {
|
||||||
await maybeRefreshSession(url);
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: authHeaders(),
|
headers: authHeaders(),
|
||||||
});
|
});
|
||||||
captureSessionHeaders(response);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text();
|
throw new Error('HTTP ' + response.status);
|
||||||
const detail = parseApiErrorPayload(text, response.status);
|
|
||||||
if (isTerminalSessionCode(detail.code)) {
|
|
||||||
clearSession(detail.code);
|
|
||||||
}
|
|
||||||
throw new Error(detail.error);
|
|
||||||
}
|
}
|
||||||
return response.json();
|
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() {
|
async function loadOffers() {
|
||||||
try {
|
try {
|
||||||
const payload = await fetchJson('/store/offers.json');
|
const payload = await fetchJson('/store/offers.json');
|
||||||
@ -660,13 +484,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.source = 'live';
|
state.source = 'live';
|
||||||
setLog('Checking live EDUT ID status...');
|
setLog('Checking live membership status...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const membership = await fetchLiveMembershipStatus();
|
const membership = await fetchLiveMembershipStatus();
|
||||||
state.membership = membership;
|
state.membership = membership;
|
||||||
applyGateState();
|
applyGateState();
|
||||||
setLog('Live EDUT ID status resolved: ' + membership + '.');
|
setLog('Live status resolved: ' + membership + '.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.membership = 'unknown';
|
state.membership = 'unknown';
|
||||||
applyGateState();
|
applyGateState();
|
||||||
@ -687,9 +511,8 @@
|
|||||||
state.wallet = accounts[0];
|
state.wallet = accounts[0];
|
||||||
state.ownershipProof = null;
|
state.ownershipProof = null;
|
||||||
if (state.sessionWallet && state.wallet && state.sessionWallet.toLowerCase() !== state.wallet.toLowerCase()) {
|
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) + '.');
|
setLog('Wallet connected: ' + abbreviateWallet(state.wallet) + '.');
|
||||||
await refreshMembershipState();
|
await refreshMembershipState();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -755,7 +578,7 @@
|
|||||||
|
|
||||||
async function requestCheckoutQuote() {
|
async function requestCheckoutQuote() {
|
||||||
if (!state.gate) {
|
if (!state.gate) {
|
||||||
setCheckoutLog('Checkout blocked: EDUT ID is not active.');
|
setCheckoutLog('Checkout blocked: membership is not active.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -793,7 +616,17 @@
|
|||||||
payload.ownership_proof = state.ownershipProof.signature;
|
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 quoteId = quotePayload.quote_id || 'unknown';
|
||||||
const amount = quotePayload.amount || quotePayload.amount_atomic || 'unknown';
|
const amount = quotePayload.amount || quotePayload.amount_atomic || 'unknown';
|
||||||
const total = quotePayload.total_amount || quotePayload.total_amount_atomic || amount;
|
const total = quotePayload.total_amount || quotePayload.total_amount_atomic || amount;
|
||||||
|
|||||||
@ -11,12 +11,7 @@
|
|||||||
"workspace_bound": true,
|
"workspace_bound": true,
|
||||||
"transferable": false,
|
"transferable": false,
|
||||||
"internal_use_only": true,
|
"internal_use_only": true,
|
||||||
"multi_tenant": false,
|
"multi_tenant": false
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "hybrid",
|
|
||||||
"pacing_tier": "governed_human_pace",
|
|
||||||
"human_pace_floor_ms": 1200
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"offer_id": "edut.workspace.core",
|
"offer_id": "edut.workspace.core",
|
||||||
@ -28,12 +23,7 @@
|
|||||||
"workspace_bound": true,
|
"workspace_bound": true,
|
||||||
"transferable": false,
|
"transferable": false,
|
||||||
"internal_use_only": true,
|
"internal_use_only": true,
|
||||||
"multi_tenant": false,
|
"multi_tenant": false
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "hybrid",
|
|
||||||
"pacing_tier": "governed_human_pace",
|
|
||||||
"human_pace_floor_ms": 1200
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"offer_id": "edut.workspace.ai",
|
"offer_id": "edut.workspace.ai",
|
||||||
@ -45,12 +35,7 @@
|
|||||||
"workspace_bound": true,
|
"workspace_bound": true,
|
||||||
"transferable": false,
|
"transferable": false,
|
||||||
"internal_use_only": true,
|
"internal_use_only": true,
|
||||||
"multi_tenant": false,
|
"multi_tenant": false
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "hybrid",
|
|
||||||
"pacing_tier": "governed_human_pace",
|
|
||||||
"human_pace_floor_ms": 1200
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"offer_id": "edut.workspace.lane24",
|
"offer_id": "edut.workspace.lane24",
|
||||||
@ -62,11 +47,7 @@
|
|||||||
"workspace_bound": true,
|
"workspace_bound": true,
|
||||||
"transferable": false,
|
"transferable": false,
|
||||||
"internal_use_only": true,
|
"internal_use_only": true,
|
||||||
"multi_tenant": false,
|
"multi_tenant": false
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "edut_native",
|
|
||||||
"pacing_tier": "local_hardware_speed"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"offer_id": "edut.workspace.sovereign",
|
"offer_id": "edut.workspace.sovereign",
|
||||||
@ -78,11 +59,7 @@
|
|||||||
"workspace_bound": true,
|
"workspace_bound": true,
|
||||||
"transferable": false,
|
"transferable": false,
|
||||||
"internal_use_only": true,
|
"internal_use_only": true,
|
||||||
"multi_tenant": false,
|
"multi_tenant": false
|
||||||
"execution_profile": {
|
|
||||||
"connector_surface": "edut_native",
|
|
||||||
"pacing_tier": "local_hardware_speed"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -158,11 +158,11 @@
|
|||||||
<h2>Eligibility</h2>
|
<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>
|
<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>
|
<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 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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>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>
|
<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>
|
<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>
|
<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 charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>EDUT Trust</title>
|
<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">
|
<meta name="theme-color" content="#f0f4f8">
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&display=swap');
|
||||||
@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<p class="label">Contracts</p>
|
<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">Offer Registry: <span class="mono">pending deployment</span></p>
|
||||||
<p class="line">Entitlement: <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>
|
<span class="status warn">addresses pending</span>
|
||||||
@ -110,8 +110,7 @@
|
|||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<p class="label">Policy Snapshot</p>
|
<p class="label">Policy Snapshot</p>
|
||||||
<p class="line">EDUT ID activation floor: <span class="mono">100.00 USDC</span></p>
|
<p class="line">Membership floor: <span class="mono">USD 5.00 equivalent</span></p>
|
||||||
<p class="line">Settlement rail: <span class="mono">USDC on Base</span></p>
|
|
||||||
<p class="line">Policy hash: <span class="mono">pending publication</span></p>
|
<p class="line">Policy hash: <span class="mono">pending publication</span></p>
|
||||||
<p class="line">Updated: <span class="mono">pending deployment</span></p>
|
<p class="line">Updated: <span class="mono">pending deployment</span></p>
|
||||||
</section>
|
</section>
|
||||||
@ -120,8 +119,6 @@
|
|||||||
<p class="label">API Health Targets</p>
|
<p class="label">API Health Targets</p>
|
||||||
<p class="line">/secret/wallet/intent</p>
|
<p class="line">/secret/wallet/intent</p>
|
||||||
<p class="line">/secret/wallet/verify</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/quote</p>
|
||||||
<p class="line">/secret/membership/confirm</p>
|
<p class="line">/secret/membership/confirm</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
"note": "ملاحظة: هذه الصفحة بسيطة عن قصد. وهي تعكس فلسفة منتج ترى أن الحساب قد يكون معقدًا بينما تبقى الواجهات هادئة."
|
"note": "ملاحظة: هذه الصفحة بسيطة عن قصد. وهي تعكس فلسفة منتج ترى أن الحساب قد يكون معقدًا بينما تبقى الواجهات هادئة."
|
||||||
},
|
},
|
||||||
"continue_label": "متابعة",
|
"continue_label": "متابعة",
|
||||||
"wallet_intro": "activate your EDUT ID",
|
"wallet_intro": "قم بسك عضويتك",
|
||||||
"wallet_fact_no_tx": "لا توجد معاملة. فقط توقيع.",
|
"wallet_fact_no_tx": "لا توجد معاملة. فقط توقيع.",
|
||||||
"wallet_fact_seed": "لا تشارك عبارة الاسترداد الخاصة بك أبدًا.",
|
"wallet_fact_seed": "لا تشارك عبارة الاسترداد الخاصة بك أبدًا.",
|
||||||
"wallet_have": "لدي محفظة",
|
"wallet_have": "لدي محفظة",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "بانتظار التوقيع...",
|
"wallet_signing": "بانتظار التوقيع...",
|
||||||
"wallet_verifying": "جارٍ التحقق من التوقيع...",
|
"wallet_verifying": "جارٍ التحقق من التوقيع...",
|
||||||
"wallet_success": "تم تأكيد التعيين.",
|
"wallet_success": "تم تأكيد التعيين.",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "جارٍ إعداد سك العضوية...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "أكد سك العضوية في محفظتك...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "جارٍ تأكيد العضوية على السلسلة...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "العضوية نشطة. تم تأكيد التعيين.",
|
||||||
"wallet_failed": "فشل مسار المحفظة.",
|
"wallet_failed": "فشل مسار المحفظة.",
|
||||||
"wallet_missing": "لم يتم العثور على محفظة على هذا الجهاز.",
|
"wallet_missing": "لم يتم العثور على محفظة على هذا الجهاز.",
|
||||||
"download_heading": "نزّل منصتك",
|
"download_heading": "نزّل منصتك",
|
||||||
"download_desktop": "سطح المكتب",
|
"download_desktop": "سطح المكتب",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "أندرويد",
|
"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."
|
"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",
|
"continue_label": "weiter",
|
||||||
"wallet_intro": "activate your EDUT ID",
|
"wallet_intro": "praege deine Mitgliedschaft",
|
||||||
"wallet_fact_no_tx": "Keine Transaktion. Nur Signatur.",
|
"wallet_fact_no_tx": "Keine Transaktion. Nur Signatur.",
|
||||||
"wallet_fact_seed": "Teile niemals deine Seed-Phrase.",
|
"wallet_fact_seed": "Teile niemals deine Seed-Phrase.",
|
||||||
"wallet_have": "Ich habe ein Wallet",
|
"wallet_have": "Ich habe ein Wallet",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "Warte auf Signatur...",
|
"wallet_signing": "Warte auf Signatur...",
|
||||||
"wallet_verifying": "Signatur wird verifiziert...",
|
"wallet_verifying": "Signatur wird verifiziert...",
|
||||||
"wallet_success": "Designation bestätigt.",
|
"wallet_success": "Designation bestätigt.",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "Membership-Praegung wird vorbereitet...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "Bestaetige die Membership-Praegung in deiner Wallet...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "Membership wird on-chain bestaetigt...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "Membership aktiv. Designation bestaetigt.",
|
||||||
"wallet_failed": "Wallet-Ablauf fehlgeschlagen.",
|
"wallet_failed": "Wallet-Ablauf fehlgeschlagen.",
|
||||||
"wallet_missing": "Kein Wallet auf diesem Gerät erkannt.",
|
"wallet_missing": "Kein Wallet auf diesem Gerät erkannt.",
|
||||||
"download_heading": "lade deine plattform herunter",
|
"download_heading": "lade deine plattform herunter",
|
||||||
"download_desktop": "desktop",
|
"download_desktop": "desktop",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "android",
|
"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."
|
"note": "Note: This page is intentionally minimal. It reflects a product philosophy where computation is complex and interfaces remain quiet."
|
||||||
},
|
},
|
||||||
"continue_label": "continue",
|
"continue_label": "continue",
|
||||||
"wallet_intro": "activate your EDUT ID",
|
"wallet_intro": "mint your membership",
|
||||||
"wallet_fact_no_tx": "No transaction. Signature only.",
|
"wallet_fact_no_tx": "No transaction. Signature only.",
|
||||||
"wallet_fact_seed": "Never share your seed phrase.",
|
"wallet_fact_seed": "Never share your seed phrase.",
|
||||||
"wallet_have": "I have a wallet",
|
"wallet_have": "I have a wallet",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "Awaiting signature...",
|
"wallet_signing": "Awaiting signature...",
|
||||||
"wallet_verifying": "Verifying signature...",
|
"wallet_verifying": "Verifying signature...",
|
||||||
"wallet_success": "Designation acknowledged.",
|
"wallet_success": "Designation acknowledged.",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "Preparing membership mint...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "Confirm membership mint in your wallet...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "Confirming membership on-chain...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "Membership active. Designation acknowledged.",
|
||||||
"wallet_failed": "Wallet flow failed.",
|
"wallet_failed": "Wallet flow failed.",
|
||||||
"wallet_missing": "No wallet detected on this device.",
|
"wallet_missing": "No wallet detected on this device.",
|
||||||
"download_heading": "download your platform",
|
"download_heading": "download your platform",
|
||||||
"download_desktop": "desktop",
|
"download_desktop": "desktop",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "android",
|
"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."
|
"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",
|
"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_no_tx": "Sin transacción. Solo firma.",
|
||||||
"wallet_fact_seed": "Nunca compartas tu frase semilla.",
|
"wallet_fact_seed": "Nunca compartas tu frase semilla.",
|
||||||
"wallet_have": "Tengo una wallet",
|
"wallet_have": "Tengo una wallet",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "Esperando firma...",
|
"wallet_signing": "Esperando firma...",
|
||||||
"wallet_verifying": "Verificando firma...",
|
"wallet_verifying": "Verificando firma...",
|
||||||
"wallet_success": "Designación reconocida.",
|
"wallet_success": "Designación reconocida.",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "Preparando acuñación de membresía...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "Confirma la acuñación de membresía en tu wallet...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "Confirmando membresía en la cadena...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "Membresía activa. Designación reconocida.",
|
||||||
"wallet_failed": "Falló el flujo de wallet.",
|
"wallet_failed": "Falló el flujo de wallet.",
|
||||||
"wallet_missing": "No se detectó wallet en este dispositivo.",
|
"wallet_missing": "No se detectó wallet en este dispositivo.",
|
||||||
"download_heading": "descarga tu plataforma",
|
"download_heading": "descarga tu plataforma",
|
||||||
"download_desktop": "escritorio",
|
"download_desktop": "escritorio",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "android",
|
"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."
|
"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",
|
"continue_label": "continuer",
|
||||||
"wallet_intro": "activate your EDUT ID",
|
"wallet_intro": "frappez votre adhesion",
|
||||||
"wallet_fact_no_tx": "Aucune transaction. Signature uniquement.",
|
"wallet_fact_no_tx": "Aucune transaction. Signature uniquement.",
|
||||||
"wallet_fact_seed": "Ne partagez jamais votre phrase secrete.",
|
"wallet_fact_seed": "Ne partagez jamais votre phrase secrete.",
|
||||||
"wallet_have": "Je possede un wallet",
|
"wallet_have": "Je possede un wallet",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "Signature en attente...",
|
"wallet_signing": "Signature en attente...",
|
||||||
"wallet_verifying": "Verification de la signature...",
|
"wallet_verifying": "Verification de la signature...",
|
||||||
"wallet_success": "Designation confirmee.",
|
"wallet_success": "Designation confirmee.",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "Preparation du mint dadhesion...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "Confirmez le mint dadhesion dans votre wallet...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "Confirmation de ladhesion on-chain...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "Adhesion active. Designation confirmee.",
|
||||||
"wallet_failed": "Le flux wallet a echoue.",
|
"wallet_failed": "Le flux wallet a echoue.",
|
||||||
"wallet_missing": "Aucun wallet detecte sur cet appareil.",
|
"wallet_missing": "Aucun wallet detecte sur cet appareil.",
|
||||||
"download_heading": "telechargez votre plateforme",
|
"download_heading": "telechargez votre plateforme",
|
||||||
"download_desktop": "bureau",
|
"download_desktop": "bureau",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "android",
|
"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": "הערה: דף זה מינימלי בכוונה. הוא משקף פילוסופיית מוצר שבה החישוב מורכב, אך הממשקים נשארים שקטים."
|
"note": "הערה: דף זה מינימלי בכוונה. הוא משקף פילוסופיית מוצר שבה החישוב מורכב, אך הממשקים נשארים שקטים."
|
||||||
},
|
},
|
||||||
"continue_label": "המשך",
|
"continue_label": "המשך",
|
||||||
"wallet_intro": "activate your EDUT ID",
|
"wallet_intro": "הנפק את החברות שלך",
|
||||||
"wallet_fact_no_tx": "אין עסקה. רק חתימה.",
|
"wallet_fact_no_tx": "אין עסקה. רק חתימה.",
|
||||||
"wallet_fact_seed": "לעולם אל תשתף את ביטוי השחזור שלך.",
|
"wallet_fact_seed": "לעולם אל תשתף את ביטוי השחזור שלך.",
|
||||||
"wallet_have": "יש לי ארנק",
|
"wallet_have": "יש לי ארנק",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "ממתין לחתימה...",
|
"wallet_signing": "ממתין לחתימה...",
|
||||||
"wallet_verifying": "מאמת חתימה...",
|
"wallet_verifying": "מאמת חתימה...",
|
||||||
"wallet_success": "הייעוד אושר.",
|
"wallet_success": "הייעוד אושר.",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "מכין הטבעת חברות...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "אשר הטבעת חברות בארנק שלך...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "מאשר חברות על השרשרת...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "החברות פעילה. הייעוד אושר.",
|
||||||
"wallet_failed": "תהליך הארנק נכשל.",
|
"wallet_failed": "תהליך הארנק נכשל.",
|
||||||
"wallet_missing": "לא זוהה ארנק במכשיר זה.",
|
"wallet_missing": "לא זוהה ארנק במכשיר זה.",
|
||||||
"download_heading": "הורד את הפלטפורמה שלך",
|
"download_heading": "הורד את הפלטפורמה שלך",
|
||||||
"download_desktop": "דסקטופ",
|
"download_desktop": "דסקטופ",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "אנדרואיד",
|
"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": "नोट: यह पृष्ठ जानबूझकर न्यूनतम रखा गया है। यह उस उत्पाद दर्शन को दर्शाता है जिसमें गणना जटिल हो सकती है, लेकिन इंटरफेस शांत रहते हैं।"
|
"note": "नोट: यह पृष्ठ जानबूझकर न्यूनतम रखा गया है। यह उस उत्पाद दर्शन को दर्शाता है जिसमें गणना जटिल हो सकती है, लेकिन इंटरफेस शांत रहते हैं।"
|
||||||
},
|
},
|
||||||
"continue_label": "जारी रखें",
|
"continue_label": "जारी रखें",
|
||||||
"wallet_intro": "activate your EDUT ID",
|
"wallet_intro": "अपनी सदस्यता मिंट करें",
|
||||||
"wallet_fact_no_tx": "कोई ट्रांज़ैक्शन नहीं। केवल सिग्नेचर।",
|
"wallet_fact_no_tx": "कोई ट्रांज़ैक्शन नहीं। केवल सिग्नेचर।",
|
||||||
"wallet_fact_seed": "अपना सीड फ़्रेज़ कभी साझा न करें।",
|
"wallet_fact_seed": "अपना सीड फ़्रेज़ कभी साझा न करें।",
|
||||||
"wallet_have": "मेरे पास वॉलेट है",
|
"wallet_have": "मेरे पास वॉलेट है",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "सिग्नेचर की प्रतीक्षा...",
|
"wallet_signing": "सिग्नेचर की प्रतीक्षा...",
|
||||||
"wallet_verifying": "सिग्नेचर सत्यापित किया जा रहा है...",
|
"wallet_verifying": "सिग्नेचर सत्यापित किया जा रहा है...",
|
||||||
"wallet_success": "नामांकन स्वीकार किया गया।",
|
"wallet_success": "नामांकन स्वीकार किया गया।",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "सदस्यता मिंट तैयार की जा रही है...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "अपने वॉलेट में सदस्यता मिंट की पुष्टि करें...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "ऑन-चेन सदस्यता की पुष्टि की जा रही है...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "सदस्यता सक्रिय है। नामांकन स्वीकार किया गया।",
|
||||||
"wallet_failed": "वॉलेट फ़्लो विफल रहा।",
|
"wallet_failed": "वॉलेट फ़्लो विफल रहा।",
|
||||||
"wallet_missing": "इस डिवाइस पर कोई वॉलेट नहीं मिला।",
|
"wallet_missing": "इस डिवाइस पर कोई वॉलेट नहीं मिला।",
|
||||||
"download_heading": "अपना प्लेटफ़ॉर्म डाउनलोड करें",
|
"download_heading": "अपना प्लेटफ़ॉर्म डाउनलोड करें",
|
||||||
"download_desktop": "डेस्कटॉप",
|
"download_desktop": "डेस्कटॉप",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "एंड्रॉइड",
|
"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": "注記: このページは意図的にミニマルです。計算は複雑でも、インターフェースは静かであるべきという製品哲学を表しています。"
|
"note": "注記: このページは意図的にミニマルです。計算は複雑でも、インターフェースは静かであるべきという製品哲学を表しています。"
|
||||||
},
|
},
|
||||||
"continue_label": "続行",
|
"continue_label": "続行",
|
||||||
"wallet_intro": "activate your EDUT ID",
|
"wallet_intro": "メンバーシップをミントする",
|
||||||
"wallet_fact_no_tx": "取引は発生しません。署名のみです。",
|
"wallet_fact_no_tx": "取引は発生しません。署名のみです。",
|
||||||
"wallet_fact_seed": "シードフレーズは絶対に共有しないでください。",
|
"wallet_fact_seed": "シードフレーズは絶対に共有しないでください。",
|
||||||
"wallet_have": "ウォレットを持っています",
|
"wallet_have": "ウォレットを持っています",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "署名を待機中...",
|
"wallet_signing": "署名を待機中...",
|
||||||
"wallet_verifying": "署名を検証しています...",
|
"wallet_verifying": "署名を検証しています...",
|
||||||
"wallet_success": "指定が確認されました。",
|
"wallet_success": "指定が確認されました。",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "メンバーシップのミントを準備中...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "ウォレットでメンバーシップのミントを確認してください...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "オンチェーンでメンバーシップを確認中...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "メンバーシップが有効です。指定が確認されました。",
|
||||||
"wallet_failed": "ウォレットフローに失敗しました。",
|
"wallet_failed": "ウォレットフローに失敗しました。",
|
||||||
"wallet_missing": "この端末でウォレットが見つかりません。",
|
"wallet_missing": "この端末でウォレットが見つかりません。",
|
||||||
"download_heading": "プラットフォームをダウンロード",
|
"download_heading": "プラットフォームをダウンロード",
|
||||||
"download_desktop": "デスクトップ",
|
"download_desktop": "デスクトップ",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "android",
|
"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": "참고: 이 페이지는 의도적으로 미니멀합니다. 연산은 복잡하더라도 인터페이스는 조용해야 한다는 제품 철학을 반영합니다."
|
"note": "참고: 이 페이지는 의도적으로 미니멀합니다. 연산은 복잡하더라도 인터페이스는 조용해야 한다는 제품 철학을 반영합니다."
|
||||||
},
|
},
|
||||||
"continue_label": "계속",
|
"continue_label": "계속",
|
||||||
"wallet_intro": "activate your EDUT ID",
|
"wallet_intro": "멤버십 민팅하기",
|
||||||
"wallet_fact_no_tx": "거래는 없습니다. 서명만 진행됩니다.",
|
"wallet_fact_no_tx": "거래는 없습니다. 서명만 진행됩니다.",
|
||||||
"wallet_fact_seed": "시드 문구를 절대 공유하지 마세요.",
|
"wallet_fact_seed": "시드 문구를 절대 공유하지 마세요.",
|
||||||
"wallet_have": "지갑이 있습니다",
|
"wallet_have": "지갑이 있습니다",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "서명 대기 중...",
|
"wallet_signing": "서명 대기 중...",
|
||||||
"wallet_verifying": "서명 검증 중...",
|
"wallet_verifying": "서명 검증 중...",
|
||||||
"wallet_success": "지정이 확인되었습니다.",
|
"wallet_success": "지정이 확인되었습니다.",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "멤버십 민팅을 준비하는 중...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "지갑에서 멤버십 민팅을 확인하세요...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "온체인 멤버십을 확인하는 중...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "멤버십이 활성화되었습니다. 지정이 확인되었습니다.",
|
||||||
"wallet_failed": "지갑 흐름에 실패했습니다.",
|
"wallet_failed": "지갑 흐름에 실패했습니다.",
|
||||||
"wallet_missing": "이 기기에서 지갑을 찾을 수 없습니다.",
|
"wallet_missing": "이 기기에서 지갑을 찾을 수 없습니다.",
|
||||||
"download_heading": "플랫폼 다운로드",
|
"download_heading": "플랫폼 다운로드",
|
||||||
"download_desktop": "데스크톱",
|
"download_desktop": "데스크톱",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "android",
|
"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."
|
"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",
|
"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_no_tx": "Sem transação. Apenas assinatura.",
|
||||||
"wallet_fact_seed": "Nunca compartilhe sua frase-semente.",
|
"wallet_fact_seed": "Nunca compartilhe sua frase-semente.",
|
||||||
"wallet_have": "Tenho uma wallet",
|
"wallet_have": "Tenho uma wallet",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "Aguardando assinatura...",
|
"wallet_signing": "Aguardando assinatura...",
|
||||||
"wallet_verifying": "Verificando assinatura...",
|
"wallet_verifying": "Verificando assinatura...",
|
||||||
"wallet_success": "Designação confirmada.",
|
"wallet_success": "Designação confirmada.",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "Preparando mint de associação...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "Confirme o mint de associação na sua wallet...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "Confirmando associação on-chain...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "Associação ativa. Designação confirmada.",
|
||||||
"wallet_failed": "Falha no fluxo de wallet.",
|
"wallet_failed": "Falha no fluxo de wallet.",
|
||||||
"wallet_missing": "Nenhuma wallet detectada neste dispositivo.",
|
"wallet_missing": "Nenhuma wallet detectada neste dispositivo.",
|
||||||
"download_heading": "baixe sua plataforma",
|
"download_heading": "baixe sua plataforma",
|
||||||
"download_desktop": "desktop",
|
"download_desktop": "desktop",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "android",
|
"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": "Примечание: эта страница намеренно минималистична. Она отражает философию продукта, где вычисления сложны, а интерфейсы остаются тихими."
|
"note": "Примечание: эта страница намеренно минималистична. Она отражает философию продукта, где вычисления сложны, а интерфейсы остаются тихими."
|
||||||
},
|
},
|
||||||
"continue_label": "продолжить",
|
"continue_label": "продолжить",
|
||||||
"wallet_intro": "activate your EDUT ID",
|
"wallet_intro": "минтите свое членство",
|
||||||
"wallet_fact_no_tx": "Без транзакции. Только подпись.",
|
"wallet_fact_no_tx": "Без транзакции. Только подпись.",
|
||||||
"wallet_fact_seed": "Никогда не делитесь seed-фразой.",
|
"wallet_fact_seed": "Никогда не делитесь seed-фразой.",
|
||||||
"wallet_have": "У меня есть кошелек",
|
"wallet_have": "У меня есть кошелек",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "Ожидание подписи...",
|
"wallet_signing": "Ожидание подписи...",
|
||||||
"wallet_verifying": "Проверка подписи...",
|
"wallet_verifying": "Проверка подписи...",
|
||||||
"wallet_success": "Назначение подтверждено.",
|
"wallet_success": "Назначение подтверждено.",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "Подготовка минта членства...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "Подтвердите минт членства в вашем кошельке...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "Подтверждение членства в сети...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "Членство активно. Назначение подтверждено.",
|
||||||
"wallet_failed": "Сбой процесса кошелька.",
|
"wallet_failed": "Сбой процесса кошелька.",
|
||||||
"wallet_missing": "Кошелек на этом устройстве не обнаружен.",
|
"wallet_missing": "Кошелек на этом устройстве не обнаружен.",
|
||||||
"download_heading": "скачайте свою платформу",
|
"download_heading": "скачайте свою платформу",
|
||||||
"download_desktop": "десктоп",
|
"download_desktop": "десктоп",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "android",
|
"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": "说明:本页面刻意保持极简,体现了“计算可以复杂,界面应保持安静”的产品哲学。"
|
"note": "说明:本页面刻意保持极简,体现了“计算可以复杂,界面应保持安静”的产品哲学。"
|
||||||
},
|
},
|
||||||
"continue_label": "继续",
|
"continue_label": "继续",
|
||||||
"wallet_intro": "activate your EDUT ID",
|
"wallet_intro": "铸造你的会员资格",
|
||||||
"wallet_fact_no_tx": "无交易,仅签名。",
|
"wallet_fact_no_tx": "无交易,仅签名。",
|
||||||
"wallet_fact_seed": "请勿分享你的助记词。",
|
"wallet_fact_seed": "请勿分享你的助记词。",
|
||||||
"wallet_have": "我有钱包",
|
"wallet_have": "我有钱包",
|
||||||
@ -41,15 +41,15 @@
|
|||||||
"wallet_signing": "等待签名...",
|
"wallet_signing": "等待签名...",
|
||||||
"wallet_verifying": "正在验证签名...",
|
"wallet_verifying": "正在验证签名...",
|
||||||
"wallet_success": "指定已确认。",
|
"wallet_success": "指定已确认。",
|
||||||
"membership_quoting": "Preparing EDUT ID activation...",
|
"membership_quoting": "正在准备会员铸造...",
|
||||||
"membership_minting": "Confirm EDUT ID activation in your wallet...",
|
"membership_minting": "请在钱包中确认会员铸造...",
|
||||||
"membership_confirming": "Confirming EDUT ID on-chain...",
|
"membership_confirming": "正在链上确认会员资格...",
|
||||||
"membership_active": "EDUT ID active. Designation acknowledged.",
|
"membership_active": "会员已激活。指定已确认。",
|
||||||
"wallet_failed": "钱包流程失败。",
|
"wallet_failed": "钱包流程失败。",
|
||||||
"wallet_missing": "此设备未检测到钱包。",
|
"wallet_missing": "此设备未检测到钱包。",
|
||||||
"download_heading": "下载你的平台",
|
"download_heading": "下载你的平台",
|
||||||
"download_desktop": "桌面版",
|
"download_desktop": "桌面版",
|
||||||
"download_ios": "iOS",
|
"download_ios": "iOS",
|
||||||
"download_android": "安卓",
|
"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