From b9ca98e13ff3b14162a536133ed026708f0cb955 Mon Sep 17 00:00:00 2001 From: Joshua Date: Thu, 19 Feb 2026 14:42:40 -0800 Subject: [PATCH] Add EDUT ID alias routes for membership endpoints --- backend/secretapi/README.md | 5 ++- backend/secretapi/app.go | 3 ++ backend/secretapi/app_test.go | 61 +++++++++++++++++++++++++++++ docs/api/secret-system.openapi.yaml | 60 ++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) diff --git a/backend/secretapi/README.md b/backend/secretapi/README.md index 65bca20..aa3aaca 100644 --- a/backend/secretapi/README.md +++ b/backend/secretapi/README.md @@ -1,6 +1,6 @@ # Secret API Backend (`secretapi`) -Deterministic backend for wallet-first designation, membership activation, and governance install authorization. +Deterministic backend for wallet-first designation, EDUT ID activation, and governance install authorization. ## Run @@ -34,6 +34,9 @@ Copy `.env.example` in this folder and set contract/runtime values before deploy - `POST /secret/membership/quote` - `POST /secret/membership/confirm` - `GET /secret/membership/status` +- `POST /secret/id/quote` (alias to membership quote) +- `POST /secret/id/confirm` (alias to membership confirm) +- `GET /secret/id/status` (alias to membership status) ### Marketplace diff --git a/backend/secretapi/app.go b/backend/secretapi/app.go index 9b4411d..bf0225a 100644 --- a/backend/secretapi/app.go +++ b/backend/secretapi/app.go @@ -39,6 +39,9 @@ func (a *app) routes() http.Handler { mux.HandleFunc("/secret/membership/quote", a.withCORS(a.handleMembershipQuote)) mux.HandleFunc("/secret/membership/confirm", a.withCORS(a.handleMembershipConfirm)) mux.HandleFunc("/secret/membership/status", a.withCORS(a.handleMembershipStatus)) + mux.HandleFunc("/secret/id/quote", a.withCORS(a.handleMembershipQuote)) + mux.HandleFunc("/secret/id/confirm", a.withCORS(a.handleMembershipConfirm)) + mux.HandleFunc("/secret/id/status", a.withCORS(a.handleMembershipStatus)) mux.HandleFunc("/marketplace/offers", a.withCORS(a.handleMarketplaceOffers)) mux.HandleFunc("/marketplace/offers/", a.withCORS(a.handleMarketplaceOfferByID)) mux.HandleFunc("/marketplace/checkout/quote", a.withCORS(a.handleMarketplaceCheckoutQuote)) diff --git a/backend/secretapi/app_test.go b/backend/secretapi/app_test.go index 657b459..ce343c6 100644 --- a/backend/secretapi/app_test.go +++ b/backend/secretapi/app_test.go @@ -168,6 +168,67 @@ func TestMembershipCompanySponsorWithoutOwnerProof(t *testing.T) { } } +func TestEDUTIDAliasRoutesActivateMembership(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) + + quote := postJSONExpect[membershipQuoteResponse](t, a, "/secret/id/quote", membershipQuoteRequest{ + DesignationCode: verifyRes.DesignationCode, + Address: ownerAddr, + ChainID: cfg.ChainID, + }, http.StatusOK) + if strings.TrimSpace(quote.QuoteID) == "" { + t.Fatalf("expected quote id from /secret/id/quote, got %+v", quote) + } + + confirm := postJSONExpect[membershipConfirmResponse](t, a, "/secret/id/confirm", membershipConfirmRequest{ + DesignationCode: verifyRes.DesignationCode, + QuoteID: quote.QuoteID, + TxHash: "0x" + strings.Repeat("b", 64), + Address: ownerAddr, + ChainID: cfg.ChainID, + }, http.StatusOK) + if confirm.Status != "membership_active" { + t.Fatalf("expected membership_active status from /secret/id/confirm, got %+v", confirm) + } + + status := getJSONExpect[membershipStatusResponse](t, a, "/secret/id/status?wallet="+ownerAddr, http.StatusOK) + if status.Status != "active" { + t.Fatalf("expected active status from /secret/id/status, got %+v", status) + } + if status.DesignationCode != verifyRes.DesignationCode { + t.Fatalf("designation mismatch: got=%s want=%s", status.DesignationCode, verifyRes.DesignationCode) + } +} + func TestMembershipConfirmRequiresChainRPCWhenStrictVerificationEnabled(t *testing.T) { a, cfg, cleanup := newTestApp(t) defer cleanup() diff --git a/docs/api/secret-system.openapi.yaml b/docs/api/secret-system.openapi.yaml index bc40f1b..2ef2eac 100644 --- a/docs/api/secret-system.openapi.yaml +++ b/docs/api/secret-system.openapi.yaml @@ -101,6 +101,7 @@ paths: /secret/membership/quote: post: summary: Get current membership mint quote + description: Canonical technical route. Public EDUT ID alias is `/secret/id/quote`. requestBody: required: true content: @@ -117,6 +118,7 @@ paths: /secret/membership/confirm: post: summary: Confirm membership mint transaction + description: Canonical technical route. Public EDUT ID alias is `/secret/id/confirm`. requestBody: required: true content: @@ -133,6 +135,7 @@ paths: /secret/membership/status: get: summary: Resolve membership status by wallet or designation code + description: Canonical technical route. Public EDUT ID alias is `/secret/id/status`. parameters: - in: query name: wallet @@ -152,6 +155,63 @@ paths: application/json: schema: $ref: '#/components/schemas/MembershipStatusResponse' + /secret/id/quote: + post: + summary: Get current EDUT ID activation quote + description: Alias of `/secret/membership/quote`. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipQuoteRequest' + responses: + '200': + description: Quote created + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipQuoteResponse' + /secret/id/confirm: + post: + summary: Confirm EDUT ID activation transaction + description: Alias of `/secret/membership/confirm`. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipConfirmRequest' + responses: + '200': + description: EDUT ID active + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipConfirmResponse' + /secret/id/status: + get: + summary: Resolve EDUT ID status by wallet or designation code + description: Alias of `/secret/membership/status`. + parameters: + - in: query + name: wallet + required: false + schema: + type: string + pattern: '^0x[a-fA-F0-9]{40}$' + - in: query + name: designation_code + required: false + schema: + type: string + responses: + '200': + description: EDUT ID status resolved + content: + application/json: + schema: + $ref: '#/components/schemas/MembershipStatusResponse' components: parameters: WalletSessionHeader: