W0: add regulatory profile baseline to checkout and ID flows
Some checks are pending
check / secretapi (push) Waiting to run
Some checks are pending
check / secretapi (push) Waiting to run
This commit is contained in:
parent
c80b1db18b
commit
a3a53992bd
@ -153,6 +153,7 @@ The envelope is pre-execution pricing metadata and is authoritative for checkout
|
|||||||
- `SECRET_API_QUOTE_TTL_SECONDS` (default `900`)
|
- `SECRET_API_QUOTE_TTL_SECONDS` (default `900`)
|
||||||
- `SECRET_API_WALLET_SESSION_TTL_SECONDS` (default `2592000`)
|
- `SECRET_API_WALLET_SESSION_TTL_SECONDS` (default `2592000`)
|
||||||
- `SECRET_API_REQUIRE_WALLET_SESSION` (default `true`; set `false` only for controlled local harness/debug usage)
|
- `SECRET_API_REQUIRE_WALLET_SESSION` (default `true`; set `false` only for controlled local harness/debug usage)
|
||||||
|
- `SECRET_API_REGULATORY_PROFILE_ID` (`us_general_2026` default, `eu_ai_act_2026_baseline` supported)
|
||||||
- `SECRET_API_DOMAIN_NAME`
|
- `SECRET_API_DOMAIN_NAME`
|
||||||
- `SECRET_API_VERIFYING_CONTRACT`
|
- `SECRET_API_VERIFYING_CONTRACT`
|
||||||
- `SECRET_API_MEMBERSHIP_CONTRACT`
|
- `SECRET_API_MEMBERSHIP_CONTRACT`
|
||||||
|
|||||||
@ -465,22 +465,23 @@ func (a *app) handleMembershipQuote(w http.ResponseWriter, r *http.Request) {
|
|||||||
"value": quote.ValueHex,
|
"value": quote.ValueHex,
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, membershipQuoteResponse{
|
writeJSON(w, http.StatusOK, membershipQuoteResponse{
|
||||||
QuoteID: quote.QuoteID,
|
QuoteID: quote.QuoteID,
|
||||||
ChainID: quote.ChainID,
|
ChainID: quote.ChainID,
|
||||||
Currency: quote.Currency,
|
RegulatoryProfileID: a.cfg.RegulatoryProfileID,
|
||||||
AmountAtomic: quote.AmountAtomic,
|
Currency: quote.Currency,
|
||||||
Decimals: quote.Decimals,
|
AmountAtomic: quote.AmountAtomic,
|
||||||
CostEnvelope: newQuoteCostEnvelope(quote.Currency, quote.Decimals, quote.AmountAtomic),
|
Decimals: quote.Decimals,
|
||||||
Deadline: quote.ExpiresAt.Format(time.RFC3339Nano),
|
CostEnvelope: newQuoteCostEnvelope(quote.Currency, quote.Decimals, quote.AmountAtomic),
|
||||||
ContractAddress: quote.ContractAddress,
|
Deadline: quote.ExpiresAt.Format(time.RFC3339Nano),
|
||||||
Method: quote.Method,
|
ContractAddress: quote.ContractAddress,
|
||||||
Calldata: quote.Calldata,
|
Method: quote.Method,
|
||||||
Value: quote.ValueHex,
|
Calldata: quote.Calldata,
|
||||||
OwnerWallet: address,
|
Value: quote.ValueHex,
|
||||||
PayerWallet: payerAddress,
|
OwnerWallet: address,
|
||||||
SponsorshipMode: sponsorshipMode,
|
PayerWallet: payerAddress,
|
||||||
SponsorOrgRoot: strings.TrimSpace(req.SponsorOrgRoot),
|
SponsorshipMode: sponsorshipMode,
|
||||||
Tx: tx,
|
SponsorOrgRoot: strings.TrimSpace(req.SponsorOrgRoot),
|
||||||
|
Tx: tx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -602,6 +603,7 @@ func (a *app) handleMembershipConfirm(w http.ResponseWriter, r *http.Request) {
|
|||||||
Status: "membership_active",
|
Status: "membership_active",
|
||||||
DesignationCode: rec.Code,
|
DesignationCode: rec.Code,
|
||||||
DisplayToken: rec.DisplayToken,
|
DisplayToken: rec.DisplayToken,
|
||||||
|
RegulatoryProfileID: a.cfg.RegulatoryProfileID,
|
||||||
TxHash: strings.ToLower(req.TxHash),
|
TxHash: strings.ToLower(req.TxHash),
|
||||||
ActivatedAt: now.Format(time.RFC3339Nano),
|
ActivatedAt: now.Format(time.RFC3339Nano),
|
||||||
IdentityAssurance: rec.IdentityAssurance,
|
IdentityAssurance: rec.IdentityAssurance,
|
||||||
@ -639,7 +641,7 @@ func (a *app) handleMembershipStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errNotFound) {
|
if errors.Is(err, errNotFound) {
|
||||||
writeJSON(w, http.StatusOK, membershipStatusResponse{Status: "none"})
|
writeJSON(w, http.StatusOK, membershipStatusResponse{Status: "none", RegulatoryProfileID: a.cfg.RegulatoryProfileID})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeError(w, http.StatusInternalServerError, "failed to resolve membership status")
|
writeError(w, http.StatusInternalServerError, "failed to resolve membership status")
|
||||||
@ -654,6 +656,7 @@ func (a *app) handleMembershipStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
Status: status,
|
Status: status,
|
||||||
Wallet: rec.Address,
|
Wallet: rec.Address,
|
||||||
DesignationCode: rec.Code,
|
DesignationCode: rec.Code,
|
||||||
|
RegulatoryProfileID: a.cfg.RegulatoryProfileID,
|
||||||
IdentityAssurance: normalizeAssuranceLevel(rec.IdentityAssurance),
|
IdentityAssurance: normalizeAssuranceLevel(rec.IdentityAssurance),
|
||||||
IdentityAttestedBy: strings.TrimSpace(rec.IdentityAttestedBy),
|
IdentityAttestedBy: strings.TrimSpace(rec.IdentityAttestedBy),
|
||||||
IdentityAttestationID: strings.TrimSpace(rec.IdentityAttestationID),
|
IdentityAttestationID: strings.TrimSpace(rec.IdentityAttestationID),
|
||||||
|
|||||||
@ -89,6 +89,7 @@ func TestMembershipDistinctPayerProof(t *testing.T) {
|
|||||||
t.Fatalf("tx.from mismatch: %+v", quote.Tx)
|
t.Fatalf("tx.from mismatch: %+v", quote.Tx)
|
||||||
}
|
}
|
||||||
assertQuoteCostEnvelope(t, quote.CostEnvelope, quote.Currency, quote.Decimals, quote.AmountAtomic)
|
assertQuoteCostEnvelope(t, quote.CostEnvelope, quote.Currency, quote.Decimals, quote.AmountAtomic)
|
||||||
|
assertRegulatoryProfileID(t, quote.RegulatoryProfileID, cfg.RegulatoryProfileID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMembershipCompanySponsorWithoutOwnerProof(t *testing.T) {
|
func TestMembershipCompanySponsorWithoutOwnerProof(t *testing.T) {
|
||||||
@ -1095,6 +1096,7 @@ func TestMarketplaceCheckoutBundlesMembershipAndMintsEntitlement(t *testing.T) {
|
|||||||
t.Fatalf("expected default merchant id, got %+v", quote)
|
t.Fatalf("expected default merchant id, got %+v", quote)
|
||||||
}
|
}
|
||||||
assertQuoteCostEnvelope(t, quote.CostEnvelope, quote.Currency, quote.Decimals, quote.TotalAmountAtomic)
|
assertQuoteCostEnvelope(t, quote.CostEnvelope, quote.Currency, quote.Decimals, quote.TotalAmountAtomic)
|
||||||
|
assertRegulatoryProfileID(t, quote.RegulatoryProfileID, cfg.RegulatoryProfileID)
|
||||||
|
|
||||||
confirm := postJSONExpect[marketplaceCheckoutConfirmResponse](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
|
confirm := postJSONExpect[marketplaceCheckoutConfirmResponse](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
|
||||||
QuoteID: quote.QuoteID,
|
QuoteID: quote.QuoteID,
|
||||||
@ -1109,11 +1111,13 @@ func TestMarketplaceCheckoutBundlesMembershipAndMintsEntitlement(t *testing.T) {
|
|||||||
if confirm.Status != "entitlement_active" || confirm.EntitlementID == "" {
|
if confirm.Status != "entitlement_active" || confirm.EntitlementID == "" {
|
||||||
t.Fatalf("unexpected confirm response: %+v", confirm)
|
t.Fatalf("unexpected confirm response: %+v", confirm)
|
||||||
}
|
}
|
||||||
|
assertRegulatoryProfileID(t, confirm.RegulatoryProfileID, cfg.RegulatoryProfileID)
|
||||||
|
|
||||||
status := getJSONExpect[membershipStatusResponse](t, a, "/secret/membership/status?wallet="+ownerAddr, http.StatusOK)
|
status := getJSONExpect[membershipStatusResponse](t, a, "/secret/membership/status?wallet="+ownerAddr, http.StatusOK)
|
||||||
if status.Status != "active" {
|
if status.Status != "active" {
|
||||||
t.Fatalf("expected active membership after bundled checkout, got %+v", status)
|
t.Fatalf("expected active membership after bundled checkout, got %+v", status)
|
||||||
}
|
}
|
||||||
|
assertRegulatoryProfileID(t, status.RegulatoryProfileID, cfg.RegulatoryProfileID)
|
||||||
|
|
||||||
entitlements := getJSONExpect[marketplaceEntitlementsResponse](t, a, "/marketplace/entitlements?wallet="+ownerAddr, http.StatusOK)
|
entitlements := getJSONExpect[marketplaceEntitlementsResponse](t, a, "/marketplace/entitlements?wallet="+ownerAddr, http.StatusOK)
|
||||||
if len(entitlements.Entitlements) == 0 {
|
if len(entitlements.Entitlements) == 0 {
|
||||||
@ -2070,6 +2074,13 @@ func assertQuoteCostEnvelope(t *testing.T, got quoteCostEnvelope, wantCurrency s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertRegulatoryProfileID(t *testing.T, got string, want string) {
|
||||||
|
t.Helper()
|
||||||
|
if normalizeRegulatoryProfileID(got) != normalizeRegulatoryProfileID(want) {
|
||||||
|
t.Fatalf("regulatory profile mismatch: got=%s want=%s", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func signTypedData(t *testing.T, key *ecdsa.PrivateKey, typedData apitypes.TypedData) string {
|
func signTypedData(t *testing.T, key *ecdsa.PrivateKey, typedData apitypes.TypedData) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
hash, _, err := apitypes.TypedDataAndHash(typedData)
|
hash, _, err := apitypes.TypedDataAndHash(typedData)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ type Config struct {
|
|||||||
DBPath string
|
DBPath string
|
||||||
AllowedOrigin string
|
AllowedOrigin string
|
||||||
DeploymentClass string
|
DeploymentClass string
|
||||||
|
RegulatoryProfileID string
|
||||||
MemberPollIntervalSec int
|
MemberPollIntervalSec int
|
||||||
IntentTTL time.Duration
|
IntentTTL time.Duration
|
||||||
QuoteTTL time.Duration
|
QuoteTTL time.Duration
|
||||||
@ -47,6 +48,7 @@ func loadConfig() Config {
|
|||||||
DBPath: env("SECRET_API_DB_PATH", "./secret.db"),
|
DBPath: env("SECRET_API_DB_PATH", "./secret.db"),
|
||||||
AllowedOrigin: env("SECRET_API_ALLOWED_ORIGIN", "https://edut.ai"),
|
AllowedOrigin: env("SECRET_API_ALLOWED_ORIGIN", "https://edut.ai"),
|
||||||
DeploymentClass: normalizeDeploymentClass(env("SECRET_API_DEPLOYMENT_CLASS", "development")),
|
DeploymentClass: normalizeDeploymentClass(env("SECRET_API_DEPLOYMENT_CLASS", "development")),
|
||||||
|
RegulatoryProfileID: normalizeRegulatoryProfileID(env("SECRET_API_REGULATORY_PROFILE_ID", defaultRegulatoryProfileID)),
|
||||||
MemberPollIntervalSec: envInt("SECRET_API_MEMBER_POLL_INTERVAL_SECONDS", 30),
|
MemberPollIntervalSec: envInt("SECRET_API_MEMBER_POLL_INTERVAL_SECONDS", 30),
|
||||||
IntentTTL: time.Duration(envInt("SECRET_API_INTENT_TTL_SECONDS", 900)) * time.Second,
|
IntentTTL: time.Duration(envInt("SECRET_API_INTENT_TTL_SECONDS", 900)) * time.Second,
|
||||||
QuoteTTL: time.Duration(envInt("SECRET_API_QUOTE_TTL_SECONDS", 900)) * time.Second,
|
QuoteTTL: time.Duration(envInt("SECRET_API_QUOTE_TTL_SECONDS", 900)) * time.Second,
|
||||||
@ -79,6 +81,9 @@ 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 !isKnownRegulatoryProfileID(c.RegulatoryProfileID) {
|
||||||
|
return fmt.Errorf("SECRET_API_REGULATORY_PROFILE_ID must be %s or %s", regulatoryProfileUSGeneral2026, regulatoryProfileEUAIBaseline2026)
|
||||||
|
}
|
||||||
currency := strings.ToUpper(strings.TrimSpace(c.MintCurrency))
|
currency := strings.ToUpper(strings.TrimSpace(c.MintCurrency))
|
||||||
switch currency {
|
switch currency {
|
||||||
case "USDC":
|
case "USDC":
|
||||||
|
|||||||
@ -115,3 +115,21 @@ func TestConfigValidateRejectsProductionWhenStrictVerificationDisabled(t *testin
|
|||||||
t.Fatalf("expected production config validation failure when strict onchain verification disabled")
|
t.Fatalf("expected production config validation failure when strict onchain verification disabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigValidateAllowsKnownRegulatoryProfiles(t *testing.T) {
|
||||||
|
cfg := loadConfigIsolated(t)
|
||||||
|
for _, profileID := range []string{regulatoryProfileUSGeneral2026, regulatoryProfileEUAIBaseline2026} {
|
||||||
|
cfg.RegulatoryProfileID = profileID
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
t.Fatalf("expected regulatory profile %s to validate, got %v", profileID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidateRejectsUnknownRegulatoryProfile(t *testing.T) {
|
||||||
|
cfg := loadConfigIsolated(t)
|
||||||
|
cfg.RegulatoryProfileID = "unknown_profile"
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Fatalf("expected regulatory profile validation failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -489,6 +489,7 @@ func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Requ
|
|||||||
writeJSON(w, http.StatusOK, marketplaceCheckoutQuoteResponse{
|
writeJSON(w, http.StatusOK, marketplaceCheckoutQuoteResponse{
|
||||||
QuoteID: quote.QuoteID,
|
QuoteID: quote.QuoteID,
|
||||||
MerchantID: quote.MerchantID,
|
MerchantID: quote.MerchantID,
|
||||||
|
RegulatoryProfileID: a.cfg.RegulatoryProfileID,
|
||||||
Wallet: quote.Wallet,
|
Wallet: quote.Wallet,
|
||||||
PayerWallet: quote.PayerWallet,
|
PayerWallet: quote.PayerWallet,
|
||||||
OfferID: quote.OfferID,
|
OfferID: quote.OfferID,
|
||||||
@ -606,19 +607,20 @@ func (a *app) handleMarketplaceCheckoutConfirm(w http.ResponseWriter, r *http.Re
|
|||||||
if quote.ConfirmedAt != nil {
|
if quote.ConfirmedAt != nil {
|
||||||
if existing, existingErr := a.store.getMarketplaceEntitlementByQuote(r.Context(), quote.QuoteID); existingErr == nil {
|
if existing, existingErr := a.store.getMarketplaceEntitlementByQuote(r.Context(), quote.QuoteID); existingErr == nil {
|
||||||
writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{
|
writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{
|
||||||
Status: "entitlement_active",
|
Status: "entitlement_active",
|
||||||
EntitlementID: existing.EntitlementID,
|
EntitlementID: existing.EntitlementID,
|
||||||
MerchantID: existing.MerchantID,
|
MerchantID: existing.MerchantID,
|
||||||
OfferID: existing.OfferID,
|
RegulatoryProfileID: a.cfg.RegulatoryProfileID,
|
||||||
OrgRootID: existing.OrgRootID,
|
OfferID: existing.OfferID,
|
||||||
PrincipalID: existing.PrincipalID,
|
OrgRootID: existing.OrgRootID,
|
||||||
PrincipalRole: existing.PrincipalRole,
|
PrincipalID: existing.PrincipalID,
|
||||||
Wallet: existing.Wallet,
|
PrincipalRole: existing.PrincipalRole,
|
||||||
TxHash: existing.TxHash,
|
Wallet: existing.Wallet,
|
||||||
PolicyHash: existing.PolicyHash,
|
TxHash: existing.TxHash,
|
||||||
ActivatedAt: existing.IssuedAt.Format(time.RFC3339Nano),
|
PolicyHash: existing.PolicyHash,
|
||||||
AccessClass: existing.AccessClass,
|
ActivatedAt: existing.IssuedAt.Format(time.RFC3339Nano),
|
||||||
AvailabilityState: existing.AvailabilityState,
|
AccessClass: existing.AccessClass,
|
||||||
|
AvailabilityState: existing.AvailabilityState,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -709,19 +711,20 @@ func (a *app) handleMarketplaceCheckoutConfirm(w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{
|
writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{
|
||||||
Status: "entitlement_active",
|
Status: "entitlement_active",
|
||||||
EntitlementID: ent.EntitlementID,
|
EntitlementID: ent.EntitlementID,
|
||||||
MerchantID: ent.MerchantID,
|
MerchantID: ent.MerchantID,
|
||||||
OfferID: ent.OfferID,
|
RegulatoryProfileID: a.cfg.RegulatoryProfileID,
|
||||||
OrgRootID: ent.OrgRootID,
|
OfferID: ent.OfferID,
|
||||||
PrincipalID: ent.PrincipalID,
|
OrgRootID: ent.OrgRootID,
|
||||||
PrincipalRole: ent.PrincipalRole,
|
PrincipalID: ent.PrincipalID,
|
||||||
Wallet: ent.Wallet,
|
PrincipalRole: ent.PrincipalRole,
|
||||||
TxHash: ent.TxHash,
|
Wallet: ent.Wallet,
|
||||||
PolicyHash: ent.PolicyHash,
|
TxHash: ent.TxHash,
|
||||||
ActivatedAt: ent.IssuedAt.Format(time.RFC3339Nano),
|
PolicyHash: ent.PolicyHash,
|
||||||
AccessClass: ent.AccessClass,
|
ActivatedAt: ent.IssuedAt.Format(time.RFC3339Nano),
|
||||||
AvailabilityState: ent.AvailabilityState,
|
AccessClass: ent.AccessClass,
|
||||||
|
AvailabilityState: ent.AvailabilityState,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,7 @@ type marketplaceQuoteLineItem struct {
|
|||||||
type marketplaceCheckoutQuoteResponse struct {
|
type marketplaceCheckoutQuoteResponse struct {
|
||||||
QuoteID string `json:"quote_id"`
|
QuoteID string `json:"quote_id"`
|
||||||
MerchantID string `json:"merchant_id,omitempty"`
|
MerchantID string `json:"merchant_id,omitempty"`
|
||||||
|
RegulatoryProfileID string `json:"regulatory_profile_id"`
|
||||||
Wallet string `json:"wallet"`
|
Wallet string `json:"wallet"`
|
||||||
PayerWallet string `json:"payer_wallet,omitempty"`
|
PayerWallet string `json:"payer_wallet,omitempty"`
|
||||||
OfferID string `json:"offer_id"`
|
OfferID string `json:"offer_id"`
|
||||||
@ -104,19 +105,20 @@ type marketplaceCheckoutConfirmRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type marketplaceCheckoutConfirmResponse struct {
|
type marketplaceCheckoutConfirmResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
EntitlementID string `json:"entitlement_id"`
|
EntitlementID string `json:"entitlement_id"`
|
||||||
MerchantID string `json:"merchant_id,omitempty"`
|
MerchantID string `json:"merchant_id,omitempty"`
|
||||||
OfferID string `json:"offer_id"`
|
RegulatoryProfileID string `json:"regulatory_profile_id"`
|
||||||
OrgRootID string `json:"org_root_id,omitempty"`
|
OfferID string `json:"offer_id"`
|
||||||
PrincipalID string `json:"principal_id,omitempty"`
|
OrgRootID string `json:"org_root_id,omitempty"`
|
||||||
PrincipalRole string `json:"principal_role,omitempty"`
|
PrincipalID string `json:"principal_id,omitempty"`
|
||||||
Wallet string `json:"wallet"`
|
PrincipalRole string `json:"principal_role,omitempty"`
|
||||||
TxHash string `json:"tx_hash"`
|
Wallet string `json:"wallet"`
|
||||||
PolicyHash string `json:"policy_hash"`
|
TxHash string `json:"tx_hash"`
|
||||||
ActivatedAt string `json:"activated_at"`
|
PolicyHash string `json:"policy_hash"`
|
||||||
AccessClass string `json:"access_class"`
|
ActivatedAt string `json:"activated_at"`
|
||||||
AvailabilityState string `json:"availability_state"`
|
AccessClass string `json:"access_class"`
|
||||||
|
AvailabilityState string `json:"availability_state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type marketplaceEntitlement struct {
|
type marketplaceEntitlement struct {
|
||||||
|
|||||||
@ -68,22 +68,23 @@ type membershipQuoteRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type membershipQuoteResponse struct {
|
type membershipQuoteResponse struct {
|
||||||
QuoteID string `json:"quote_id"`
|
QuoteID string `json:"quote_id"`
|
||||||
ChainID int64 `json:"chain_id"`
|
ChainID int64 `json:"chain_id"`
|
||||||
Currency string `json:"currency"`
|
RegulatoryProfileID string `json:"regulatory_profile_id"`
|
||||||
AmountAtomic string `json:"amount_atomic"`
|
Currency string `json:"currency"`
|
||||||
Decimals int `json:"decimals"`
|
AmountAtomic string `json:"amount_atomic"`
|
||||||
CostEnvelope quoteCostEnvelope `json:"cost_envelope"`
|
Decimals int `json:"decimals"`
|
||||||
Deadline string `json:"deadline"`
|
CostEnvelope quoteCostEnvelope `json:"cost_envelope"`
|
||||||
ContractAddress string `json:"contract_address"`
|
Deadline string `json:"deadline"`
|
||||||
Method string `json:"method"`
|
ContractAddress string `json:"contract_address"`
|
||||||
Calldata string `json:"calldata"`
|
Method string `json:"method"`
|
||||||
Value string `json:"value"`
|
Calldata string `json:"calldata"`
|
||||||
OwnerWallet string `json:"owner_wallet,omitempty"`
|
Value string `json:"value"`
|
||||||
PayerWallet string `json:"payer_wallet,omitempty"`
|
OwnerWallet string `json:"owner_wallet,omitempty"`
|
||||||
SponsorshipMode string `json:"sponsorship_mode,omitempty"`
|
PayerWallet string `json:"payer_wallet,omitempty"`
|
||||||
SponsorOrgRoot string `json:"sponsor_org_root_id,omitempty"`
|
SponsorshipMode string `json:"sponsorship_mode,omitempty"`
|
||||||
Tx map[string]any `json:"tx"`
|
SponsorOrgRoot string `json:"sponsor_org_root_id,omitempty"`
|
||||||
|
Tx map[string]any `json:"tx"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type membershipConfirmRequest struct {
|
type membershipConfirmRequest struct {
|
||||||
@ -101,6 +102,7 @@ type membershipConfirmResponse struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
DesignationCode string `json:"designation_code"`
|
DesignationCode string `json:"designation_code"`
|
||||||
DisplayToken string `json:"display_token"`
|
DisplayToken string `json:"display_token"`
|
||||||
|
RegulatoryProfileID string `json:"regulatory_profile_id"`
|
||||||
TxHash string `json:"tx_hash"`
|
TxHash string `json:"tx_hash"`
|
||||||
ActivatedAt string `json:"activated_at"`
|
ActivatedAt string `json:"activated_at"`
|
||||||
IdentityAssurance string `json:"identity_assurance_level"`
|
IdentityAssurance string `json:"identity_assurance_level"`
|
||||||
@ -112,6 +114,7 @@ type membershipStatusResponse struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Wallet string `json:"wallet,omitempty"`
|
Wallet string `json:"wallet,omitempty"`
|
||||||
DesignationCode string `json:"designation_code,omitempty"`
|
DesignationCode string `json:"designation_code,omitempty"`
|
||||||
|
RegulatoryProfileID string `json:"regulatory_profile_id"`
|
||||||
IdentityAssurance string `json:"identity_assurance_level,omitempty"`
|
IdentityAssurance string `json:"identity_assurance_level,omitempty"`
|
||||||
IdentityAttestedBy string `json:"identity_attested_by,omitempty"`
|
IdentityAttestedBy string `json:"identity_attested_by,omitempty"`
|
||||||
IdentityAttestationID string `json:"identity_attestation_id,omitempty"`
|
IdentityAttestationID string `json:"identity_attestation_id,omitempty"`
|
||||||
|
|||||||
30
backend/secretapi/regulatory_profile.go
Normal file
30
backend/secretapi/regulatory_profile.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
regulatoryProfileUSGeneral2026 = "us_general_2026"
|
||||||
|
regulatoryProfileEUAIBaseline2026 = "eu_ai_act_2026_baseline"
|
||||||
|
defaultRegulatoryProfileID = regulatoryProfileUSGeneral2026
|
||||||
|
)
|
||||||
|
|
||||||
|
func normalizeRegulatoryProfileID(raw string) string {
|
||||||
|
value := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
switch value {
|
||||||
|
case "", "us", "us_general", regulatoryProfileUSGeneral2026:
|
||||||
|
return regulatoryProfileUSGeneral2026
|
||||||
|
case "eu", "eu_ai_act", regulatoryProfileEUAIBaseline2026:
|
||||||
|
return regulatoryProfileEUAIBaseline2026
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKnownRegulatoryProfileID(value string) bool {
|
||||||
|
switch normalizeRegulatoryProfileID(value) {
|
||||||
|
case regulatoryProfileUSGeneral2026, regulatoryProfileEUAIBaseline2026:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -58,6 +58,7 @@ Success (`200`):
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"quote_id": "cq_01HZZXFQ27ZP6MP0V2R9M6V3KX",
|
"quote_id": "cq_01HZZXFQ27ZP6MP0V2R9M6V3KX",
|
||||||
|
"regulatory_profile_id": "us_general_2026",
|
||||||
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
|
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
|
||||||
"payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11",
|
"payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11",
|
||||||
"offer_id": "edut.workspace.core",
|
"offer_id": "edut.workspace.core",
|
||||||
@ -157,6 +158,7 @@ Success (`200`):
|
|||||||
{
|
{
|
||||||
"status": "entitlement_active",
|
"status": "entitlement_active",
|
||||||
"entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001",
|
"entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001",
|
||||||
|
"regulatory_profile_id": "us_general_2026",
|
||||||
"offer_id": "edut.workspace.core",
|
"offer_id": "edut.workspace.core",
|
||||||
"org_root_id": "org.acme.root",
|
"org_root_id": "org.acme.root",
|
||||||
"principal_id": "human.joshua",
|
"principal_id": "human.joshua",
|
||||||
|
|||||||
@ -159,6 +159,7 @@ Success (`200`):
|
|||||||
{
|
{
|
||||||
"quote_id": "mq_01HZZX4F8VQXJ6A57R8P3SCB2W",
|
"quote_id": "mq_01HZZX4F8VQXJ6A57R8P3SCB2W",
|
||||||
"chain_id": 8453,
|
"chain_id": 8453,
|
||||||
|
"regulatory_profile_id": "us_general_2026",
|
||||||
"currency": "USDC",
|
"currency": "USDC",
|
||||||
"amount_atomic": "100000000",
|
"amount_atomic": "100000000",
|
||||||
"decimals": 6,
|
"decimals": 6,
|
||||||
@ -238,6 +239,7 @@ Success (`200`):
|
|||||||
"status": "membership_active",
|
"status": "membership_active",
|
||||||
"designation_code": "0217073045482",
|
"designation_code": "0217073045482",
|
||||||
"display_token": "0217-0730-4548-2",
|
"display_token": "0217-0730-4548-2",
|
||||||
|
"regulatory_profile_id": "us_general_2026",
|
||||||
"tx_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
"tx_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
"activated_at": "2026-02-17T07:33:09Z",
|
"activated_at": "2026-02-17T07:33:09Z",
|
||||||
"identity_assurance_level": "onramp_attested",
|
"identity_assurance_level": "onramp_attested",
|
||||||
@ -269,6 +271,7 @@ Success (`200`):
|
|||||||
"status": "active",
|
"status": "active",
|
||||||
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
|
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
|
||||||
"designation_code": "0217073045482",
|
"designation_code": "0217073045482",
|
||||||
|
"regulatory_profile_id": "us_general_2026",
|
||||||
"identity_assurance_level": "onramp_attested",
|
"identity_assurance_level": "onramp_attested",
|
||||||
"identity_attested_by": "moonpay",
|
"identity_attested_by": "moonpay",
|
||||||
"identity_attestation_id": "mp_session_01JAA..."
|
"identity_attestation_id": "mp_session_01JAA..."
|
||||||
|
|||||||
@ -222,12 +222,15 @@ components:
|
|||||||
description: If true, quote may bundle first-time membership fee into total.
|
description: If true, quote may bundle first-time membership fee into total.
|
||||||
CheckoutQuoteResponse:
|
CheckoutQuoteResponse:
|
||||||
type: object
|
type: object
|
||||||
required: [quote_id, wallet, offer_id, currency, amount_atomic, total_amount_atomic, decimals, cost_envelope, policy_hash, expires_at]
|
required: [quote_id, regulatory_profile_id, wallet, offer_id, currency, amount_atomic, total_amount_atomic, decimals, cost_envelope, policy_hash, expires_at]
|
||||||
properties:
|
properties:
|
||||||
quote_id:
|
quote_id:
|
||||||
type: string
|
type: string
|
||||||
merchant_id:
|
merchant_id:
|
||||||
type: string
|
type: string
|
||||||
|
regulatory_profile_id:
|
||||||
|
type: string
|
||||||
|
enum: [us_general_2026, eu_ai_act_2026_baseline]
|
||||||
wallet:
|
wallet:
|
||||||
type: string
|
type: string
|
||||||
payer_wallet:
|
payer_wallet:
|
||||||
@ -378,7 +381,7 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
CheckoutConfirmResponse:
|
CheckoutConfirmResponse:
|
||||||
type: object
|
type: object
|
||||||
required: [status, entitlement_id, offer_id, wallet, tx_hash]
|
required: [status, entitlement_id, regulatory_profile_id, offer_id, wallet, tx_hash]
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
@ -387,6 +390,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
merchant_id:
|
merchant_id:
|
||||||
type: string
|
type: string
|
||||||
|
regulatory_profile_id:
|
||||||
|
type: string
|
||||||
|
enum: [us_general_2026, eu_ai_act_2026_baseline]
|
||||||
offer_id:
|
offer_id:
|
||||||
type: string
|
type: string
|
||||||
org_root_id:
|
org_root_id:
|
||||||
|
|||||||
@ -355,12 +355,15 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
MembershipQuoteResponse:
|
MembershipQuoteResponse:
|
||||||
type: object
|
type: object
|
||||||
required: [quote_id, chain_id, currency, amount_atomic, decimals, cost_envelope, deadline, contract_address]
|
required: [quote_id, chain_id, regulatory_profile_id, currency, amount_atomic, decimals, cost_envelope, deadline, contract_address]
|
||||||
properties:
|
properties:
|
||||||
quote_id:
|
quote_id:
|
||||||
type: string
|
type: string
|
||||||
chain_id:
|
chain_id:
|
||||||
type: integer
|
type: integer
|
||||||
|
regulatory_profile_id:
|
||||||
|
type: string
|
||||||
|
enum: [us_general_2026, eu_ai_act_2026_baseline]
|
||||||
currency:
|
currency:
|
||||||
type: string
|
type: string
|
||||||
enum: [USDC]
|
enum: [USDC]
|
||||||
@ -469,7 +472,7 @@ components:
|
|||||||
description: Optional provider attestation reference id.
|
description: Optional provider attestation reference id.
|
||||||
MembershipConfirmResponse:
|
MembershipConfirmResponse:
|
||||||
type: object
|
type: object
|
||||||
required: [status, designation_code, display_token, tx_hash, activated_at, identity_assurance_level]
|
required: [status, designation_code, display_token, regulatory_profile_id, tx_hash, activated_at, identity_assurance_level]
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
@ -478,6 +481,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
display_token:
|
display_token:
|
||||||
type: string
|
type: string
|
||||||
|
regulatory_profile_id:
|
||||||
|
type: string
|
||||||
|
enum: [us_general_2026, eu_ai_act_2026_baseline]
|
||||||
tx_hash:
|
tx_hash:
|
||||||
type: string
|
type: string
|
||||||
activated_at:
|
activated_at:
|
||||||
@ -492,7 +498,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
MembershipStatusResponse:
|
MembershipStatusResponse:
|
||||||
type: object
|
type: object
|
||||||
required: [status]
|
required: [status, regulatory_profile_id]
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
@ -501,6 +507,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
designation_code:
|
designation_code:
|
||||||
type: string
|
type: string
|
||||||
|
regulatory_profile_id:
|
||||||
|
type: string
|
||||||
|
enum: [us_general_2026, eu_ai_act_2026_baseline]
|
||||||
identity_assurance_level:
|
identity_assurance_level:
|
||||||
type: string
|
type: string
|
||||||
enum: [none, crypto_direct_unattested, sponsored_unattested, onramp_attested]
|
enum: [none, crypto_direct_unattested, sponsored_unattested, onramp_attested]
|
||||||
|
|||||||
@ -258,6 +258,7 @@ Response:
|
|||||||
{
|
{
|
||||||
"quote_id": "mq_...",
|
"quote_id": "mq_...",
|
||||||
"chain_id": 8453,
|
"chain_id": 8453,
|
||||||
|
"regulatory_profile_id": "us_general_2026",
|
||||||
"currency": "USDC",
|
"currency": "USDC",
|
||||||
"amount_atomic": "100000000",
|
"amount_atomic": "100000000",
|
||||||
"decimals": 6,
|
"decimals": 6,
|
||||||
@ -314,11 +315,14 @@ Response:
|
|||||||
"status": "membership_active",
|
"status": "membership_active",
|
||||||
"designation_code": "0217073045482",
|
"designation_code": "0217073045482",
|
||||||
"display_token": "0217-0730-4548-2",
|
"display_token": "0217-0730-4548-2",
|
||||||
|
"regulatory_profile_id": "us_general_2026",
|
||||||
"tx_hash": "0x...",
|
"tx_hash": "0x...",
|
||||||
"activated_at": "2026-02-17T07:33:09Z"
|
"activated_at": "2026-02-17T07:33:09Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`regulatory_profile_id` binds quote/activation responses to deployment policy posture (`us_general_2026` or `eu_ai_act_2026_baseline`).
|
||||||
|
|
||||||
## Security Controls
|
## Security Controls
|
||||||
|
|
||||||
1. Intent TTL and one-time nonce consumption.
|
1. Intent TTL and one-time nonce consumption.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user