Add EDUT ID alias routes for membership endpoints
Some checks are pending
check / secretapi (push) Waiting to run

This commit is contained in:
Joshua 2026-02-19 14:42:40 -08:00
parent a18f9dd19b
commit b9ca98e13f
4 changed files with 128 additions and 1 deletions

View File

@ -1,6 +1,6 @@
# Secret API Backend (`secretapi`) # 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 ## 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/quote`
- `POST /secret/membership/confirm` - `POST /secret/membership/confirm`
- `GET /secret/membership/status` - `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 ### Marketplace

View File

@ -39,6 +39,9 @@ func (a *app) routes() http.Handler {
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))
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.handleMarketplaceOffers))
mux.HandleFunc("/marketplace/offers/", a.withCORS(a.handleMarketplaceOfferByID)) mux.HandleFunc("/marketplace/offers/", a.withCORS(a.handleMarketplaceOfferByID))
mux.HandleFunc("/marketplace/checkout/quote", a.withCORS(a.handleMarketplaceCheckoutQuote)) mux.HandleFunc("/marketplace/checkout/quote", a.withCORS(a.handleMarketplaceCheckoutQuote))

View File

@ -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) { func TestMembershipConfirmRequiresChainRPCWhenStrictVerificationEnabled(t *testing.T) {
a, cfg, cleanup := newTestApp(t) a, cfg, cleanup := newTestApp(t)
defer cleanup() defer cleanup()

View File

@ -101,6 +101,7 @@ paths:
/secret/membership/quote: /secret/membership/quote:
post: post:
summary: Get current membership mint quote summary: Get current membership mint quote
description: Canonical technical route. Public EDUT ID alias is `/secret/id/quote`.
requestBody: requestBody:
required: true required: true
content: content:
@ -117,6 +118,7 @@ paths:
/secret/membership/confirm: /secret/membership/confirm:
post: post:
summary: Confirm membership mint transaction summary: Confirm membership mint transaction
description: Canonical technical route. Public EDUT ID alias is `/secret/id/confirm`.
requestBody: requestBody:
required: true required: true
content: content:
@ -133,6 +135,7 @@ paths:
/secret/membership/status: /secret/membership/status:
get: get:
summary: Resolve membership status by wallet or designation code summary: Resolve membership status by wallet or designation code
description: Canonical technical route. Public EDUT ID alias is `/secret/id/status`.
parameters: parameters:
- in: query - in: query
name: wallet name: wallet
@ -152,6 +155,63 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/MembershipStatusResponse' $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: components:
parameters: parameters:
WalletSessionHeader: WalletSessionHeader: