diff --git a/backend/secretapi/.env.example b/backend/secretapi/.env.example index 32619c8..dc092cc 100644 --- a/backend/secretapi/.env.example +++ b/backend/secretapi/.env.example @@ -12,9 +12,9 @@ SECRET_API_QUOTE_TTL_SECONDS=900 SECRET_API_DOMAIN_NAME=EDUT Designation SECRET_API_VERIFYING_CONTRACT=0x0000000000000000000000000000000000000000 SECRET_API_MEMBERSHIP_CONTRACT=0x0000000000000000000000000000000000000000 -SECRET_API_MINT_CURRENCY=ETH -SECRET_API_MINT_AMOUNT_ATOMIC=5000000000000000 -SECRET_API_MINT_DECIMALS=18 +SECRET_API_MINT_CURRENCY=USDC +SECRET_API_MINT_AMOUNT_ATOMIC=100000000 +SECRET_API_MINT_DECIMALS=6 SECRET_API_INSTALL_TOKEN_TTL_SECONDS=900 SECRET_API_LEASE_TTL_SECONDS=3600 diff --git a/backend/secretapi/README.md b/backend/secretapi/README.md index 75b362c..1a9ae39 100644 --- a/backend/secretapi/README.md +++ b/backend/secretapi/README.md @@ -86,9 +86,9 @@ Company-first sponsor path is also supported: - `SECRET_API_DOMAIN_NAME` - `SECRET_API_VERIFYING_CONTRACT` - `SECRET_API_MEMBERSHIP_CONTRACT` -- `SECRET_API_MINT_CURRENCY` (default `ETH`) -- `SECRET_API_MINT_AMOUNT_ATOMIC` (default `5000000000000000`) -- `SECRET_API_MINT_DECIMALS` (default `18`) +- `SECRET_API_MINT_CURRENCY` (default `USDC`) +- `SECRET_API_MINT_AMOUNT_ATOMIC` (default `100000000`) +- `SECRET_API_MINT_DECIMALS` (default `6`) ### Governance install diff --git a/backend/secretapi/app.go b/backend/secretapi/app.go index f061551..0e7a5ee 100644 --- a/backend/secretapi/app.go +++ b/backend/secretapi/app.go @@ -37,6 +37,11 @@ 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("/marketplace/offers", a.withCORS(a.handleMarketplaceOffers)) + mux.HandleFunc("/marketplace/offers/", a.withCORS(a.handleMarketplaceOfferByID)) + mux.HandleFunc("/marketplace/checkout/quote", a.withCORS(a.handleMarketplaceCheckoutQuote)) + mux.HandleFunc("/marketplace/checkout/confirm", a.withCORS(a.handleMarketplaceCheckoutConfirm)) + mux.HandleFunc("/marketplace/entitlements", a.withCORS(a.handleMarketplaceEntitlements)) mux.HandleFunc("/governance/install/token", a.withCORS(a.handleGovernanceInstallToken)) mux.HandleFunc("/governance/install/confirm", a.withCORS(a.handleGovernanceInstallConfirm)) mux.HandleFunc("/governance/install/status", a.withCORS(a.handleGovernanceInstallStatus)) @@ -286,6 +291,15 @@ func (a *app) handleMembershipQuote(w http.ResponseWriter, r *http.Request) { writeErrorCode(w, http.StatusForbidden, "sponsor_not_authorized", "sponsor entitlement is not active") return } + hasWorkspaceCore, entErr := a.store.hasActiveEntitlement(r.Context(), payerAddress, offerIDWorkspaceCore, sponsorOrgRoot) + if entErr != nil { + writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to validate sponsor entitlement") + return + } + if !hasWorkspaceCore { + writeErrorCode(w, http.StatusForbidden, "sponsor_not_authorized", "workspace core entitlement required for sponsored member onboarding") + return + } sponsorshipMode = "sponsored_company" } else { writeError(w, http.StatusForbidden, "distinct payer requires ownership proof") @@ -842,6 +856,20 @@ func (a *app) handleGovernanceOfflineRenew(w http.ResponseWriter, r *http.Reques writeErrorCode(w, http.StatusForbidden, "entitlement_inactive", "entitlement inactive") return } + isSoloRoot := strings.EqualFold(strings.TrimSpace(principal.OrgRootID), defaultSoloOrgRootID(wallet)) + requiredOfferID := offerIDWorkspaceSovereign + if isSoloRoot { + requiredOfferID = offerIDSoloCore + } + hasRequired, entErr := a.store.hasActiveEntitlement(r.Context(), wallet, requiredOfferID, principal.OrgRootID) + if entErr != nil { + writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve sovereign entitlement") + return + } + if !hasRequired { + writeErrorCode(w, http.StatusForbidden, "sovereign_entitlement_required", "sovereign entitlement required for offline renew") + return + } renewedUntil := time.Now().UTC().Add(a.cfg.OfflineRenewTTL) principal.AccessClass = "sovereign" principal.AvailabilityState = "active" @@ -1267,8 +1295,8 @@ func (a *app) resolveOrCreatePrincipal(ctx context.Context, wallet, orgRootID, p OrgRootID: orgRootID, PrincipalID: principalID, PrincipalRole: principalRole, - EntitlementID: "gov_" + wallet[2:10], - EntitlementStatus: "active", + EntitlementID: "", + EntitlementStatus: "inactive", AccessClass: "connected", AvailabilityState: "active", LeaseExpiresAt: &leaseExpires, diff --git a/backend/secretapi/app_test.go b/backend/secretapi/app_test.go index ce8c928..6313290 100644 --- a/backend/secretapi/app_test.go +++ b/backend/secretapi/app_test.go @@ -112,6 +112,23 @@ func TestMembershipCompanySponsorWithoutOwnerProof(t *testing.T) { }); err != nil { t.Fatalf("seed governance principal: %v", err) } + if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{ + EntitlementID: "ent:sponsor:workspace-core", + QuoteID: "seed-q-sponsor-workspace-core", + OfferID: offerIDWorkspaceCore, + Wallet: companyPayerAddr, + OrgRootID: "org_company_b", + PrincipalID: "principal_company_owner", + PrincipalRole: "org_root_owner", + State: "active", + AccessClass: "connected", + AvailabilityState: "active", + PolicyHash: "sha256:testpolicy", + IssuedAt: now, + TxHash: "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + }); err != nil { + t.Fatalf("seed sponsor workspace core entitlement: %v", err) + } intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{ Address: ownerAddr, @@ -227,6 +244,37 @@ func TestGovernanceInstallOwnerOnlyAndConfirm(t *testing.T) { if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil { t.Fatalf("seed membership: %v", err) } + now := time.Now().UTC() + if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{ + EntitlementID: "ent:install:workspace-core", + QuoteID: "seed-q-install-workspace-core", + OfferID: offerIDWorkspaceCore, + Wallet: ownerAddr, + OrgRootID: "org_root_a", + PrincipalID: "principal_owner", + PrincipalRole: "org_root_owner", + State: "active", + AccessClass: "connected", + AvailabilityState: "active", + PolicyHash: cfg.GovernancePolicyHash, + IssuedAt: now, + TxHash: "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + }); err != nil { + t.Fatalf("seed workspace core entitlement: %v", err) + } + if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{ + Wallet: ownerAddr, + OrgRootID: "org_root_a", + PrincipalID: "principal_owner", + PrincipalRole: "org_root_owner", + EntitlementID: "ent:install:workspace-core", + EntitlementStatus: "active", + AccessClass: "connected", + AvailabilityState: "active", + UpdatedAt: now, + }); err != nil { + t.Fatalf("seed owner principal: %v", err) + } tokenRes := postJSONExpect[governanceInstallTokenResponse](t, a, "/governance/install/token", governanceInstallTokenRequest{ Wallet: ownerAddr, @@ -289,6 +337,54 @@ func TestGovernanceLeaseAndOfflineRenew(t *testing.T) { if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil { t.Fatalf("seed membership: %v", err) } + now := time.Now().UTC() + if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{ + EntitlementID: "ent:lease:workspace-core", + QuoteID: "seed-q-lease-workspace-core", + OfferID: offerIDWorkspaceCore, + Wallet: ownerAddr, + OrgRootID: "org_root_b", + PrincipalID: "principal_owner_b", + PrincipalRole: "org_root_owner", + State: "active", + AccessClass: "connected", + AvailabilityState: "active", + PolicyHash: "sha256:testpolicy", + IssuedAt: now, + TxHash: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + }); err != nil { + t.Fatalf("seed workspace core entitlement: %v", err) + } + if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{ + EntitlementID: "ent:lease:workspace-sovereign", + QuoteID: "seed-q-lease-workspace-sovereign", + OfferID: offerIDWorkspaceSovereign, + Wallet: ownerAddr, + OrgRootID: "org_root_b", + PrincipalID: "principal_owner_b", + PrincipalRole: "org_root_owner", + State: "active", + AccessClass: "sovereign", + AvailabilityState: "active", + PolicyHash: "sha256:testpolicy", + IssuedAt: now, + TxHash: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }); err != nil { + t.Fatalf("seed workspace sovereign entitlement: %v", err) + } + if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{ + Wallet: ownerAddr, + OrgRootID: "org_root_b", + PrincipalID: "principal_owner_b", + PrincipalRole: "org_root_owner", + EntitlementID: "ent:lease:workspace-core", + EntitlementStatus: "active", + AccessClass: "connected", + AvailabilityState: "active", + UpdatedAt: now, + }); err != nil { + t.Fatalf("seed owner principal: %v", err) + } _ = postJSONExpect[governanceInstallTokenResponse](t, a, "/governance/install/token", governanceInstallTokenRequest{ Wallet: ownerAddr, @@ -487,6 +583,176 @@ func TestMemberChannelSupportTicketOwnerOnly(t *testing.T) { } } +func TestMarketplaceCheckoutBundlesMembershipAndMintsEntitlement(t *testing.T) { + a, cfg, cleanup := newTestApp(t) + defer cleanup() + + ownerKey := mustKey(t) + ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex()) + + quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{ + Wallet: ownerAddr, + OfferID: offerIDWorkspaceCore, + OrgRootID: "org.marketplace.a", + PrincipalID: "human.owner", + PrincipalRole: "org_root_owner", + }, http.StatusOK) + if !quote.MembershipActivationIncluded { + t.Fatalf("expected bundled membership on first checkout: %+v", quote) + } + if len(quote.LineItems) < 2 { + t.Fatalf("expected license + membership line items: %+v", quote.LineItems) + } + + confirm := postJSONExpect[marketplaceCheckoutConfirmResponse](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{ + QuoteID: quote.QuoteID, + Wallet: ownerAddr, + OfferID: offerIDWorkspaceCore, + OrgRootID: "org.marketplace.a", + PrincipalID: "human.owner", + PrincipalRole: "org_root_owner", + TxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ChainID: cfg.ChainID, + }, http.StatusOK) + if confirm.Status != "entitlement_active" || confirm.EntitlementID == "" { + t.Fatalf("unexpected confirm response: %+v", confirm) + } + + status := getJSONExpect[membershipStatusResponse](t, a, "/secret/membership/status?wallet="+ownerAddr, http.StatusOK) + if status.Status != "active" { + t.Fatalf("expected active membership after bundled checkout, got %+v", status) + } + + entitlements := getJSONExpect[marketplaceEntitlementsResponse](t, a, "/marketplace/entitlements?wallet="+ownerAddr, http.StatusOK) + if len(entitlements.Entitlements) == 0 { + t.Fatalf("expected active entitlement after confirm") + } + if entitlements.Entitlements[0].OfferID != offerIDWorkspaceCore { + t.Fatalf("unexpected entitlement offer: %+v", entitlements.Entitlements[0]) + } +} + +func TestMarketplaceDistinctPayerRequiresOwnershipProof(t *testing.T) { + a, _, cleanup := newTestApp(t) + defer cleanup() + + ownerKey := mustKey(t) + ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex()) + payerKey := mustKey(t) + payerAddr := strings.ToLower(crypto.PubkeyToAddress(payerKey.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, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{ + Wallet: ownerAddr, + PayerWallet: payerAddr, + OfferID: offerIDWorkspaceCore, + }, http.StatusForbidden) + if code := errResp["code"]; code != "ownership_proof_required" { + t.Fatalf("expected ownership_proof_required, got %+v", errResp) + } + + quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{ + Wallet: ownerAddr, + PayerWallet: payerAddr, + OfferID: offerIDWorkspaceCore, + OwnershipProof: "0x" + strings.Repeat("a", 130), + OrgRootID: "org.marketplace.ownerproof", + PrincipalID: "human.owner", + PrincipalRole: "org_root_owner", + }, http.StatusOK) + if got := strings.ToLower(strings.TrimSpace(quote.PayerWallet)); got != payerAddr { + t.Fatalf("payer wallet mismatch: got=%s want=%s", got, payerAddr) + } + if quote.Decimals != 6 || quote.OfferID != offerIDWorkspaceCore || quote.AccessClass != "connected" { + t.Fatalf("unexpected quote response: %+v", quote) + } +} + +func TestMarketplaceWorkspaceAddOnRequiresCoreEntitlement(t *testing.T) { + a, _, cleanup := newTestApp(t) + defer cleanup() + + 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, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{ + Wallet: ownerAddr, + OfferID: offerIDWorkspaceAI, + OrgRootID: "org.marketplace.addon", + PrincipalID: "human.owner", + PrincipalRole: "org_root_owner", + }, http.StatusForbidden) + if code := errResp["code"]; code != "prerequisite_required" { + t.Fatalf("expected prerequisite_required, got %+v", errResp) + } + + if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{ + EntitlementID: "ent:seed:workspace-core", + QuoteID: "seed-q-workspace-core", + OfferID: offerIDWorkspaceCore, + Wallet: ownerAddr, + OrgRootID: "org.marketplace.addon", + PrincipalID: "human.owner", + PrincipalRole: "org_root_owner", + State: "active", + AccessClass: "connected", + AvailabilityState: "active", + PolicyHash: "sha256:testpolicy", + IssuedAt: time.Now().UTC(), + TxHash: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + }); err != nil { + t.Fatalf("seed workspace core entitlement: %v", err) + } + + quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{ + Wallet: ownerAddr, + OfferID: offerIDWorkspaceAI, + OrgRootID: "org.marketplace.addon", + PrincipalID: "human.owner", + PrincipalRole: "org_root_owner", + }, http.StatusOK) + if quote.OfferID != offerIDWorkspaceAI || quote.AmountAtomic != marketplaceStandardOfferAtomic { + t.Fatalf("unexpected add-on quote response: %+v", quote) + } +} + +func TestMarketplaceSoloCoreScopeValidation(t *testing.T) { + a, _, cleanup := newTestApp(t) + defer cleanup() + + 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) + } + + quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{ + Wallet: ownerAddr, + OfferID: offerIDSoloCore, + PrincipalRole: "org_root_owner", + }, http.StatusOK) + expectedRoot := defaultSoloOrgRootID(ownerAddr) + if !strings.EqualFold(strings.TrimSpace(quote.OrgRootID), expectedRoot) { + t.Fatalf("expected auto solo org root %s, got %+v", expectedRoot, quote) + } + + errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{ + Wallet: ownerAddr, + OfferID: offerIDSoloCore, + OrgRootID: "org.invalid.workspace", + PrincipalRole: "org_root_owner", + }, http.StatusBadRequest) + if code := errResp["code"]; code != "invalid_scope" { + t.Fatalf("expected invalid_scope, got %+v", errResp) + } +} + type tWalletIntentResponse struct { IntentID string `json:"intent_id"` DesignationCode string `json:"designation_code"` diff --git a/backend/secretapi/config.go b/backend/secretapi/config.go index c6c601c..0620cc7 100644 --- a/backend/secretapi/config.go +++ b/backend/secretapi/config.go @@ -51,9 +51,9 @@ func loadConfig() Config { DomainName: env("SECRET_API_DOMAIN_NAME", "EDUT Designation"), VerifyingContract: strings.ToLower(env("SECRET_API_VERIFYING_CONTRACT", "0x0000000000000000000000000000000000000000")), MembershipContract: strings.ToLower(env("SECRET_API_MEMBERSHIP_CONTRACT", "0x0000000000000000000000000000000000000000")), - MintCurrency: strings.ToUpper(env("SECRET_API_MINT_CURRENCY", "ETH")), - MintAmountAtomic: env("SECRET_API_MINT_AMOUNT_ATOMIC", "5000000000000000"), - MintDecimals: envInt("SECRET_API_MINT_DECIMALS", 18), + MintCurrency: strings.ToUpper(env("SECRET_API_MINT_CURRENCY", "USDC")), + MintAmountAtomic: env("SECRET_API_MINT_AMOUNT_ATOMIC", "100000000"), + MintDecimals: envInt("SECRET_API_MINT_DECIMALS", 6), ChainRPCURL: env("SECRET_API_CHAIN_RPC_URL", ""), RequireOnchainTxVerify: envBool("SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION", false), GovernanceRuntimeVersion: env("SECRET_API_GOV_RUNTIME_VERSION", "0.1.0"), diff --git a/backend/secretapi/marketplace.go b/backend/secretapi/marketplace.go new file mode 100644 index 0000000..f1a2f09 --- /dev/null +++ b/backend/secretapi/marketplace.go @@ -0,0 +1,714 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "net/http" + "sort" + "strconv" + "strings" + "time" +) + +const ( + marketplaceMembershipActivationAtomic = "100000000" // 100.00 USDC (6 decimals) + marketplaceStandardOfferAtomic = "1000000000" // 1000.00 USDC (6 decimals) + + offerIDSoloCore = "edut.solo.core" + offerIDWorkspaceCore = "edut.workspace.core" + offerIDWorkspaceAI = "edut.workspace.ai" + offerIDWorkspaceLane24 = "edut.workspace.lane24" + offerIDWorkspaceSovereign = "edut.workspace.sovereign" +) + +func (a *app) marketplaceOffers() []marketplaceOffer { + offers := []marketplaceOffer{ + { + OfferID: offerIDSoloCore, + IssuerID: "edut.firstparty", + Title: "EDUT Solo Core", + Summary: "Single-principal governance runtime for personal operations.", + Status: "active", + Pricing: marketplaceOfferPrice{ + Currency: "USDC", + AmountAtomic: marketplaceStandardOfferAtomic, + Decimals: 6, + ChainID: a.cfg.ChainID, + }, + Policies: marketplaceOfferPolicy{ + MemberOnly: true, + WorkspaceBound: true, + Transferable: false, + InternalUseOnly: true, + MultiTenant: false, + EntitlementClass: "solo_core", + }, + SortOrder: 10, + }, + { + OfferID: offerIDWorkspaceCore, + IssuerID: "edut.firstparty", + Title: "EDUT Workspace Core", + Summary: "Org-bound deterministic governance runtime for team operations.", + Status: "active", + Pricing: marketplaceOfferPrice{ + Currency: "USDC", + AmountAtomic: marketplaceStandardOfferAtomic, + Decimals: 6, + ChainID: a.cfg.ChainID, + }, + Policies: marketplaceOfferPolicy{ + MemberOnly: true, + WorkspaceBound: true, + Transferable: false, + InternalUseOnly: true, + MultiTenant: false, + EntitlementClass: "workspace_core", + }, + SortOrder: 20, + }, + { + OfferID: offerIDWorkspaceAI, + IssuerID: "edut.firstparty", + Title: "EDUT Workspace AI Layer", + Summary: "AI reasoning layer for governed workspace operations.", + Status: "active", + Pricing: marketplaceOfferPrice{ + Currency: "USDC", + AmountAtomic: marketplaceStandardOfferAtomic, + Decimals: 6, + ChainID: a.cfg.ChainID, + }, + Policies: marketplaceOfferPolicy{ + MemberOnly: true, + WorkspaceBound: true, + Transferable: false, + InternalUseOnly: true, + MultiTenant: false, + EntitlementClass: "workspace_ai", + RequiresOffers: []string{offerIDWorkspaceCore}, + }, + SortOrder: 30, + }, + { + OfferID: offerIDWorkspaceLane24, + IssuerID: "edut.firstparty", + Title: "EDUT Workspace 24h Lane", + Summary: "Autonomous execution lane capacity for workspace queue throughput.", + Status: "active", + Pricing: marketplaceOfferPrice{ + Currency: "USDC", + AmountAtomic: marketplaceStandardOfferAtomic, + Decimals: 6, + ChainID: a.cfg.ChainID, + }, + Policies: marketplaceOfferPolicy{ + MemberOnly: true, + WorkspaceBound: true, + Transferable: false, + InternalUseOnly: true, + MultiTenant: false, + EntitlementClass: "workspace_lane24", + RequiresOffers: []string{offerIDWorkspaceCore}, + }, + SortOrder: 40, + }, + { + OfferID: offerIDWorkspaceSovereign, + IssuerID: "edut.firstparty", + Title: "EDUT Workspace Sovereign Continuity", + Summary: "Workspace continuity profile for stronger local/offline operation.", + Status: "active", + Pricing: marketplaceOfferPrice{ + Currency: "USDC", + AmountAtomic: marketplaceStandardOfferAtomic, + Decimals: 6, + ChainID: a.cfg.ChainID, + }, + Policies: marketplaceOfferPolicy{ + MemberOnly: true, + WorkspaceBound: true, + Transferable: false, + InternalUseOnly: true, + MultiTenant: false, + EntitlementClass: "workspace_sovereign", + RequiresOffers: []string{offerIDWorkspaceCore}, + }, + SortOrder: 50, + }, + } + sort.SliceStable(offers, func(i, j int) bool { + return offers[i].SortOrder < offers[j].SortOrder + }) + return offers +} + +func (a *app) marketplaceOfferByID(offerID string) (marketplaceOffer, error) { + target := strings.TrimSpace(strings.ToLower(offerID)) + for _, offer := range a.marketplaceOffers() { + if strings.EqualFold(strings.TrimSpace(offer.OfferID), target) { + return offer, nil + } + } + return marketplaceOffer{}, errNotFound +} + +func isSoloOffer(offerID string) bool { + return strings.EqualFold(strings.TrimSpace(offerID), offerIDSoloCore) +} + +func isWorkspaceOffer(offerID string) bool { + switch strings.ToLower(strings.TrimSpace(offerID)) { + case offerIDWorkspaceCore, offerIDWorkspaceAI, offerIDWorkspaceLane24, offerIDWorkspaceSovereign: + return true + default: + return false + } +} + +func shouldBindPrincipalOnOffer(offerID string) bool { + switch strings.ToLower(strings.TrimSpace(offerID)) { + case offerIDSoloCore, offerIDWorkspaceCore: + return true + default: + return false + } +} + +func defaultSoloOrgRootID(wallet string) string { + return "solo:" + strings.ToLower(strings.TrimSpace(wallet)) +} + +func (a *app) handleMarketplaceOffers(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + writeJSON(w, http.StatusOK, marketplaceOffersResponse{Offers: a.marketplaceOffers()}) +} + +func (a *app) handleMarketplaceOfferByID(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + const prefix = "/marketplace/offers/" + offerID := strings.TrimSpace(strings.TrimPrefix(r.URL.Path, prefix)) + if offerID == "" || strings.Contains(offerID, "/") { + writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found") + return + } + offer, err := a.marketplaceOfferByID(offerID) + if err != nil { + writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found") + return + } + writeJSON(w, http.StatusOK, offer) +} + +func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + writeError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + var req marketplaceCheckoutQuoteRequest + 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 + } + offer, err := a.marketplaceOfferByID(req.OfferID) + if err != nil { + writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found") + return + } + + payerWallet := wallet + if strings.TrimSpace(req.PayerWallet) != "" { + payerWallet, err = normalizeAddress(req.PayerWallet) + if err != nil { + writeErrorCode(w, http.StatusBadRequest, "invalid_payer_wallet", err.Error()) + return + } + } + if !strings.EqualFold(wallet, payerWallet) { + if !isLikelyHexSignature(req.OwnershipProof) { + writeErrorCode(w, http.StatusForbidden, "ownership_proof_required", "distinct payer requires ownership proof") + return + } + } + + orgRootID := strings.TrimSpace(req.OrgRootID) + principalID := strings.TrimSpace(req.PrincipalID) + if principalID == "" { + principalID = wallet + } + principalRole := strings.ToLower(strings.TrimSpace(req.PrincipalRole)) + if principalRole == "" { + principalRole = "org_root_owner" + } + workspaceID := strings.TrimSpace(req.WorkspaceID) + + if isSoloOffer(offer.OfferID) { + expectedSoloRoot := defaultSoloOrgRootID(wallet) + if orgRootID == "" { + orgRootID = expectedSoloRoot + } + if !strings.EqualFold(orgRootID, expectedSoloRoot) { + writeErrorCode(w, http.StatusBadRequest, "invalid_scope", "solo core must use wallet-scoped solo org root") + return + } + if principalRole != "org_root_owner" { + writeErrorCode(w, http.StatusForbidden, "role_insufficient", "solo core checkout requires org_root_owner role") + return + } + if workspaceID != "" { + writeErrorCode(w, http.StatusBadRequest, "invalid_scope", "solo core does not bind workspace_id") + return + } + } + + if isWorkspaceOffer(offer.OfferID) { + if orgRootID == "" { + writeErrorCode(w, http.StatusBadRequest, "invalid_scope", "workspace offer requires org_root_id") + return + } + if principalRole != "org_root_owner" { + writeErrorCode(w, http.StatusForbidden, "role_insufficient", "workspace checkout requires org_root_owner role") + return + } + } + + for _, requiredOfferID := range offer.Policies.RequiresOffers { + hasRequired, err := a.store.hasActiveEntitlement(r.Context(), wallet, requiredOfferID, orgRootID) + if err != nil { + writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve prerequisite entitlements") + return + } + if !hasRequired { + writeErrorCode(w, http.StatusForbidden, "prerequisite_required", fmt.Sprintf("active entitlement required: %s", requiredOfferID)) + return + } + } + + includeMembership := true + if req.IncludeMembershipIfMissing != nil { + includeMembership = *req.IncludeMembershipIfMissing + } + + membershipStatus, statusErr := a.resolveMembershipStatusForWallet(r, wallet) + if statusErr != nil { + writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve membership status") + return + } + membershipIncluded := false + switch membershipStatus { + case "active": + // no-op + case "none", "unknown", "": + if includeMembership { + membershipIncluded = true + } else { + writeErrorCode(w, http.StatusForbidden, "membership_required", "active membership is required for checkout") + return + } + case "suspended", "revoked": + writeErrorCode(w, http.StatusForbidden, "membership_required", "active membership is required for checkout") + return + default: + if includeMembership { + membershipIncluded = true + } else { + writeErrorCode(w, http.StatusForbidden, "membership_required", "active membership is required for checkout") + return + } + } + + licenseAmount := offer.Pricing.AmountAtomic + totalAmount := new(big.Int) + if _, ok := totalAmount.SetString(licenseAmount, 10); !ok { + writeErrorCode(w, http.StatusInternalServerError, "pricing_invalid", "offer pricing is invalid") + return + } + + lineItems := []marketplaceQuoteLineItem{ + { + Kind: "license", + Label: offer.Title, + Amount: formatAtomicAmount(licenseAmount, offer.Pricing.Decimals), + AmountAtomic: licenseAmount, + Decimals: offer.Pricing.Decimals, + Currency: offer.Pricing.Currency, + }, + } + if membershipIncluded { + membershipAmount := new(big.Int) + if _, ok := membershipAmount.SetString(marketplaceMembershipActivationAtomic, 10); !ok { + writeErrorCode(w, http.StatusInternalServerError, "pricing_invalid", "membership pricing is invalid") + return + } + totalAmount = new(big.Int).Add(totalAmount, membershipAmount) + lineItems = append(lineItems, marketplaceQuoteLineItem{ + Kind: "membership", + Label: "EDUT Membership Activation", + Amount: formatAtomicAmount(marketplaceMembershipActivationAtomic, offer.Pricing.Decimals), + AmountAtomic: marketplaceMembershipActivationAtomic, + Decimals: offer.Pricing.Decimals, + Currency: offer.Pricing.Currency, + }) + } + + lineItemsJSON, err := json.Marshal(lineItems) + if err != nil { + writeErrorCode(w, http.StatusInternalServerError, "encoding_failed", "failed to encode quote line items") + return + } + + quoteIDRaw, err := randomHex(16) + if err != nil { + writeErrorCode(w, http.StatusInternalServerError, "quote_generation_failed", "failed to generate quote id") + return + } + now := time.Now().UTC() + expiresAt := now.Add(a.cfg.QuoteTTL) + accessClass := "connected" + if strings.EqualFold(strings.TrimSpace(offer.OfferID), offerIDWorkspaceSovereign) { + accessClass = "sovereign" + } + quote := marketplaceQuoteRecord{ + QuoteID: "cq_" + quoteIDRaw, + Wallet: wallet, + PayerWallet: payerWallet, + OfferID: offer.OfferID, + OrgRootID: orgRootID, + PrincipalID: principalID, + PrincipalRole: principalRole, + WorkspaceID: workspaceID, + Currency: offer.Pricing.Currency, + AmountAtomic: licenseAmount, + TotalAmountAtomic: totalAmount.String(), + Decimals: offer.Pricing.Decimals, + MembershipIncluded: membershipIncluded, + LineItemsJSON: string(lineItemsJSON), + PolicyHash: a.cfg.GovernancePolicyHash, + AccessClass: accessClass, + AvailabilityState: "active", + CreatedAt: now, + ExpiresAt: expiresAt, + } + if err := a.store.putMarketplaceQuote(r.Context(), quote); err != nil { + writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to persist quote") + return + } + + writeJSON(w, http.StatusOK, marketplaceCheckoutQuoteResponse{ + QuoteID: quote.QuoteID, + Wallet: quote.Wallet, + PayerWallet: quote.PayerWallet, + OfferID: quote.OfferID, + OrgRootID: quote.OrgRootID, + PrincipalID: quote.PrincipalID, + PrincipalRole: quote.PrincipalRole, + Currency: quote.Currency, + Amount: formatAtomicAmount(quote.AmountAtomic, quote.Decimals), + AmountAtomic: quote.AmountAtomic, + TotalAmount: formatAtomicAmount(quote.TotalAmountAtomic, quote.Decimals), + TotalAmountAtomic: quote.TotalAmountAtomic, + Decimals: quote.Decimals, + MembershipActivationIncluded: quote.MembershipIncluded, + LineItems: lineItems, + PolicyHash: quote.PolicyHash, + ExpiresAt: quote.ExpiresAt.Format(time.RFC3339Nano), + Tx: map[string]any{ + "to": strings.ToLower(a.cfg.MembershipContract), + "value": "0x0", + "data": "0x", + }, + AccessClass: quote.AccessClass, + AvailabilityState: quote.AvailabilityState, + }) +} + +func (a *app) handleMarketplaceCheckoutConfirm(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + writeError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + var req marketplaceCheckoutConfirmRequest + 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 + } + if req.ChainID != a.cfg.ChainID { + writeErrorCode(w, http.StatusBadRequest, "unsupported_chain_id", fmt.Sprintf("unsupported chain_id: %d", req.ChainID)) + return + } + if !isTxHash(req.TxHash) { + writeErrorCode(w, http.StatusBadRequest, "invalid_tx_hash", "invalid tx hash") + return + } + quote, err := a.store.getMarketplaceQuote(r.Context(), strings.TrimSpace(req.QuoteID)) + if err != nil { + if errors.Is(err, errNotFound) { + writeErrorCode(w, http.StatusNotFound, "quote_not_found", "quote not found") + return + } + writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to load quote") + return + } + if time.Now().UTC().After(quote.ExpiresAt) { + writeErrorCode(w, http.StatusConflict, "quote_expired", "quote expired") + return + } + if !strings.EqualFold(quote.Wallet, wallet) || + !strings.EqualFold(strings.TrimSpace(quote.OfferID), strings.TrimSpace(req.OfferID)) { + writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "quote context mismatch") + return + } + if strings.TrimSpace(req.OrgRootID) != "" && !strings.EqualFold(strings.TrimSpace(req.OrgRootID), quote.OrgRootID) { + writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "org_root_id mismatch") + return + } + if strings.TrimSpace(req.PrincipalID) != "" && !strings.EqualFold(strings.TrimSpace(req.PrincipalID), quote.PrincipalID) { + writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "principal_id mismatch") + return + } + if strings.TrimSpace(req.PrincipalRole) != "" && !strings.EqualFold(strings.TrimSpace(req.PrincipalRole), quote.PrincipalRole) { + writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "principal_role mismatch") + return + } + if strings.TrimSpace(req.WorkspaceID) != "" && !strings.EqualFold(strings.TrimSpace(req.WorkspaceID), quote.WorkspaceID) { + writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "workspace_id mismatch") + return + } + if quote.ConfirmedAt != nil { + if existing, existingErr := a.store.getMarketplaceEntitlementByQuote(r.Context(), quote.QuoteID); existingErr == nil { + writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{ + Status: "entitlement_active", + EntitlementID: existing.EntitlementID, + OfferID: existing.OfferID, + OrgRootID: existing.OrgRootID, + PrincipalID: existing.PrincipalID, + PrincipalRole: existing.PrincipalRole, + Wallet: existing.Wallet, + TxHash: existing.TxHash, + PolicyHash: existing.PolicyHash, + ActivatedAt: existing.IssuedAt.Format(time.RFC3339Nano), + AccessClass: existing.AccessClass, + AvailabilityState: existing.AvailabilityState, + }) + return + } + writeErrorCode(w, http.StatusConflict, "quote_already_confirmed", "quote already confirmed") + return + } + + now := time.Now().UTC() + quote.ConfirmedAt = &now + quote.ConfirmedTxHash = strings.ToLower(strings.TrimSpace(req.TxHash)) + if err := a.store.putMarketplaceQuote(r.Context(), quote); err != nil { + writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to persist quote confirmation") + return + } + + if quote.MembershipIncluded { + if err := a.ensureMembershipActiveForWallet(r.Context(), wallet, quote.ConfirmedTxHash); err != nil { + writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to activate membership") + return + } + } + + entitlementID := buildEntitlementID(a.cfg.ChainID, wallet) + ent := marketplaceEntitlementRecord{ + EntitlementID: entitlementID, + QuoteID: quote.QuoteID, + OfferID: quote.OfferID, + Wallet: wallet, + PayerWallet: quote.PayerWallet, + OrgRootID: quote.OrgRootID, + PrincipalID: quote.PrincipalID, + PrincipalRole: quote.PrincipalRole, + WorkspaceID: quote.WorkspaceID, + State: "active", + AccessClass: quote.AccessClass, + AvailabilityState: quote.AvailabilityState, + PolicyHash: quote.PolicyHash, + IssuedAt: now, + TxHash: quote.ConfirmedTxHash, + } + if err := a.store.putMarketplaceEntitlement(r.Context(), ent); err != nil { + writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to persist entitlement") + return + } + + if shouldBindPrincipalOnOffer(quote.OfferID) { + principal, principalErr := a.resolveOrCreatePrincipal(r.Context(), wallet, quote.OrgRootID, quote.PrincipalID, quote.PrincipalRole) + if principalErr == nil { + principal.EntitlementID = ent.EntitlementID + principal.EntitlementStatus = "active" + principal.AccessClass = quote.AccessClass + principal.AvailabilityState = quote.AvailabilityState + principal.UpdatedAt = now + _ = a.store.putGovernancePrincipal(r.Context(), principal) + } + } + + writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{ + Status: "entitlement_active", + EntitlementID: ent.EntitlementID, + OfferID: ent.OfferID, + OrgRootID: ent.OrgRootID, + PrincipalID: ent.PrincipalID, + PrincipalRole: ent.PrincipalRole, + Wallet: ent.Wallet, + TxHash: ent.TxHash, + PolicyHash: ent.PolicyHash, + ActivatedAt: ent.IssuedAt.Format(time.RFC3339Nano), + AccessClass: ent.AccessClass, + AvailabilityState: ent.AvailabilityState, + }) +} + +func (a *app) handleMarketplaceEntitlements(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + wallet, err := normalizeAddress(strings.TrimSpace(r.URL.Query().Get("wallet"))) + if err != nil { + writeErrorCode(w, http.StatusBadRequest, "invalid_wallet", err.Error()) + return + } + records, err := a.store.listMarketplaceEntitlementsByWallet(r.Context(), wallet) + if err != nil { + writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to list entitlements") + return + } + out := make([]marketplaceEntitlement, 0, len(records)) + for _, rec := range records { + out = append(out, marketplaceEntitlement{ + EntitlementID: rec.EntitlementID, + OfferID: rec.OfferID, + WalletAddress: rec.Wallet, + WorkspaceID: rec.WorkspaceID, + OrgRootID: rec.OrgRootID, + State: rec.State, + AccessClass: rec.AccessClass, + AvailabilityState: rec.AvailabilityState, + PolicyHash: rec.PolicyHash, + IssuedAt: rec.IssuedAt.Format(time.RFC3339Nano), + }) + } + writeJSON(w, http.StatusOK, marketplaceEntitlementsResponse{Entitlements: out}) +} + +func (a *app) resolveMembershipStatusForWallet(r *http.Request, wallet string) (string, error) { + rec, err := a.store.getDesignationByAddress(r.Context(), wallet) + if err != nil { + if errors.Is(err, errNotFound) { + return "none", nil + } + return "unknown", err + } + status := strings.ToLower(strings.TrimSpace(rec.MembershipStatus)) + if status == "" { + status = "none" + } + return status, nil +} + +func (a *app) ensureMembershipActiveForWallet(ctx context.Context, wallet, txHash string) error { + rec, err := a.store.getDesignationByAddress(ctx, wallet) + if err != nil { + if !errors.Is(err, errNotFound) { + return err + } + now := time.Now().UTC() + intentID, err := randomHex(16) + if err != nil { + return err + } + nonce, err := randomHex(16) + if err != nil { + return err + } + code, displayToken, err := newDesignationCode() + if err != nil { + return err + } + rec = designationRecord{ + Code: code, + DisplayToken: displayToken, + IntentID: intentID, + Nonce: nonce, + Origin: "edut-launcher", + Locale: "en", + Address: wallet, + ChainID: a.cfg.ChainID, + IssuedAt: now, + ExpiresAt: now.Add(a.cfg.IntentTTL), + VerifiedAt: &now, + MembershipStatus: "none", + } + } + now := time.Now().UTC() + rec.MembershipStatus = "active" + rec.MembershipTxHash = strings.ToLower(strings.TrimSpace(txHash)) + rec.ActivatedAt = &now + return a.store.putDesignation(ctx, rec) +} + +func buildEntitlementID(chainID int64, wallet string) string { + token, _ := randomHex(4) + return fmt.Sprintf("ent:%d:%s:%s", chainID, wallet, token) +} + +func formatAtomicAmount(amountAtomic string, decimals int) string { + amountAtomic = strings.TrimSpace(amountAtomic) + if amountAtomic == "" { + return "0" + } + n := new(big.Int) + if _, ok := n.SetString(amountAtomic, 10); !ok { + return "0" + } + sign := "" + if n.Sign() < 0 { + sign = "-" + n.Abs(n) + } + raw := n.String() + if decimals <= 0 { + return sign + raw + } + if len(raw) <= decimals { + raw = strings.Repeat("0", decimals-len(raw)+1) + raw + } + whole := raw[:len(raw)-decimals] + fraction := strings.TrimRight(raw[len(raw)-decimals:], "0") + if fraction == "" { + return sign + whole + } + return sign + whole + "." + fraction +} + +func isLikelyHexSignature(value string) bool { + value = strings.TrimSpace(strings.ToLower(value)) + if !strings.HasPrefix(value, "0x") || len(value) < 132 { + return false + } + _, err := strconv.ParseUint(value[2:18], 16, 64) + return err == nil +} diff --git a/backend/secretapi/marketplace_models.go b/backend/secretapi/marketplace_models.go new file mode 100644 index 0000000..cf96d58 --- /dev/null +++ b/backend/secretapi/marketplace_models.go @@ -0,0 +1,166 @@ +package main + +import "time" + +type marketplaceOffer struct { + OfferID string `json:"offer_id"` + IssuerID string `json:"issuer_id"` + Title string `json:"title"` + Summary string `json:"summary,omitempty"` + Status string `json:"status"` + Pricing marketplaceOfferPrice `json:"pricing"` + Policies marketplaceOfferPolicy `json:"policies"` + SortOrder int `json:"-"` +} + +type marketplaceOfferPrice struct { + Currency string `json:"currency"` + AmountAtomic string `json:"amount_atomic"` + Decimals int `json:"decimals"` + ChainID int64 `json:"chain_id"` +} + +type marketplaceOfferPolicy struct { + MemberOnly bool `json:"member_only"` + WorkspaceBound bool `json:"workspace_bound"` + Transferable bool `json:"transferable"` + InternalUseOnly bool `json:"internal_use_only"` + MultiTenant bool `json:"multi_tenant"` + EntitlementClass string `json:"entitlement_class,omitempty"` + RequiresOffers []string `json:"requires_offers,omitempty"` +} + +type marketplaceOffersResponse struct { + Offers []marketplaceOffer `json:"offers"` +} + +type marketplaceCheckoutQuoteRequest struct { + Wallet string `json:"wallet"` + PayerWallet string `json:"payer_wallet,omitempty"` + OfferID string `json:"offer_id"` + OrgRootID string `json:"org_root_id,omitempty"` + PrincipalID string `json:"principal_id,omitempty"` + PrincipalRole string `json:"principal_role,omitempty"` + WorkspaceID string `json:"workspace_id,omitempty"` + OwnershipProof string `json:"ownership_proof,omitempty"` + IncludeMembershipIfMissing *bool `json:"include_membership_if_missing,omitempty"` +} + +type marketplaceQuoteLineItem struct { + Kind string `json:"kind"` + Label string `json:"label"` + Amount string `json:"amount"` + AmountAtomic string `json:"amount_atomic"` + Decimals int `json:"decimals"` + Currency string `json:"currency"` +} + +type marketplaceCheckoutQuoteResponse struct { + QuoteID string `json:"quote_id"` + Wallet string `json:"wallet"` + PayerWallet string `json:"payer_wallet,omitempty"` + OfferID string `json:"offer_id"` + OrgRootID string `json:"org_root_id,omitempty"` + PrincipalID string `json:"principal_id,omitempty"` + PrincipalRole string `json:"principal_role,omitempty"` + Currency string `json:"currency"` + Amount string `json:"amount"` + AmountAtomic string `json:"amount_atomic"` + TotalAmount string `json:"total_amount"` + TotalAmountAtomic string `json:"total_amount_atomic"` + Decimals int `json:"decimals"` + MembershipActivationIncluded bool `json:"membership_activation_included"` + LineItems []marketplaceQuoteLineItem `json:"line_items"` + PolicyHash string `json:"policy_hash"` + ExpiresAt string `json:"expires_at"` + Tx map[string]any `json:"tx"` + AccessClass string `json:"access_class"` + AvailabilityState string `json:"availability_state"` +} + +type marketplaceCheckoutConfirmRequest struct { + QuoteID string `json:"quote_id"` + Wallet string `json:"wallet"` + PayerWallet string `json:"payer_wallet,omitempty"` + OfferID string `json:"offer_id"` + OrgRootID string `json:"org_root_id,omitempty"` + PrincipalID string `json:"principal_id,omitempty"` + PrincipalRole string `json:"principal_role,omitempty"` + WorkspaceID string `json:"workspace_id,omitempty"` + TxHash string `json:"tx_hash"` + ChainID int64 `json:"chain_id"` +} + +type marketplaceCheckoutConfirmResponse struct { + Status string `json:"status"` + EntitlementID string `json:"entitlement_id"` + OfferID string `json:"offer_id"` + OrgRootID string `json:"org_root_id,omitempty"` + PrincipalID string `json:"principal_id,omitempty"` + PrincipalRole string `json:"principal_role,omitempty"` + Wallet string `json:"wallet"` + TxHash string `json:"tx_hash"` + PolicyHash string `json:"policy_hash"` + ActivatedAt string `json:"activated_at"` + AccessClass string `json:"access_class"` + AvailabilityState string `json:"availability_state"` +} + +type marketplaceEntitlement struct { + EntitlementID string `json:"entitlement_id"` + OfferID string `json:"offer_id"` + WalletAddress string `json:"wallet_address"` + WorkspaceID string `json:"workspace_id,omitempty"` + OrgRootID string `json:"org_root_id,omitempty"` + State string `json:"state"` + AccessClass string `json:"access_class"` + AvailabilityState string `json:"availability_state"` + PolicyHash string `json:"policy_hash"` + IssuedAt string `json:"issued_at"` +} + +type marketplaceEntitlementsResponse struct { + Entitlements []marketplaceEntitlement `json:"entitlements"` +} + +type marketplaceQuoteRecord struct { + QuoteID string + Wallet string + PayerWallet string + OfferID string + OrgRootID string + PrincipalID string + PrincipalRole string + WorkspaceID string + Currency string + AmountAtomic string + TotalAmountAtomic string + Decimals int + MembershipIncluded bool + LineItemsJSON string + PolicyHash string + AccessClass string + AvailabilityState string + CreatedAt time.Time + ExpiresAt time.Time + ConfirmedAt *time.Time + ConfirmedTxHash string +} + +type marketplaceEntitlementRecord struct { + EntitlementID string + QuoteID string + OfferID string + Wallet string + PayerWallet string + OrgRootID string + PrincipalID string + PrincipalRole string + WorkspaceID string + State string + AccessClass string + AvailabilityState string + PolicyHash string + IssuedAt time.Time + TxHash string +} diff --git a/backend/secretapi/store.go b/backend/secretapi/store.go index b137ea7..6a93e37 100644 --- a/backend/secretapi/store.go +++ b/backend/secretapi/store.go @@ -79,6 +79,48 @@ func (s *store) migrate(ctx context.Context) error { FOREIGN KEY(designation_code) REFERENCES designations(code) );`, `CREATE INDEX IF NOT EXISTS idx_quotes_designation ON quotes(designation_code);`, + `CREATE TABLE IF NOT EXISTS marketplace_quotes ( + quote_id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + payer_wallet TEXT, + offer_id TEXT NOT NULL, + org_root_id TEXT, + principal_id TEXT, + principal_role TEXT, + workspace_id TEXT, + currency TEXT NOT NULL, + amount_atomic TEXT NOT NULL, + total_amount_atomic TEXT NOT NULL, + decimals INTEGER NOT NULL, + membership_included INTEGER NOT NULL DEFAULT 0, + line_items_json TEXT NOT NULL, + policy_hash TEXT NOT NULL, + access_class TEXT NOT NULL, + availability_state TEXT NOT NULL, + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL, + confirmed_at TEXT, + confirmed_tx_hash TEXT + );`, + `CREATE INDEX IF NOT EXISTS idx_marketplace_quotes_wallet ON marketplace_quotes(wallet);`, + `CREATE TABLE IF NOT EXISTS marketplace_entitlements ( + entitlement_id TEXT PRIMARY KEY, + quote_id TEXT NOT NULL UNIQUE, + offer_id TEXT NOT NULL, + wallet TEXT NOT NULL, + payer_wallet TEXT, + org_root_id TEXT, + principal_id TEXT, + principal_role TEXT, + workspace_id TEXT, + state TEXT NOT NULL, + access_class TEXT NOT NULL, + availability_state TEXT NOT NULL, + policy_hash TEXT NOT NULL, + issued_at TEXT NOT NULL, + tx_hash TEXT NOT NULL + );`, + `CREATE INDEX IF NOT EXISTS idx_marketplace_entitlements_wallet ON marketplace_entitlements(wallet);`, `CREATE TABLE IF NOT EXISTS governance_principals ( wallet TEXT PRIMARY KEY, org_root_id TEXT NOT NULL, @@ -373,6 +415,259 @@ func (s *store) getQuote(ctx context.Context, quoteID string) (quoteRecord, erro return rec, nil } +func (s *store) putMarketplaceQuote(ctx context.Context, quote marketplaceQuoteRecord) error { + _, err := s.db.ExecContext(ctx, ` + INSERT INTO marketplace_quotes ( + quote_id, wallet, payer_wallet, offer_id, org_root_id, principal_id, principal_role, workspace_id, + currency, amount_atomic, total_amount_atomic, decimals, membership_included, line_items_json, + policy_hash, access_class, availability_state, created_at, expires_at, confirmed_at, confirmed_tx_hash + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(quote_id) DO UPDATE SET + wallet=excluded.wallet, + payer_wallet=excluded.payer_wallet, + offer_id=excluded.offer_id, + org_root_id=excluded.org_root_id, + principal_id=excluded.principal_id, + principal_role=excluded.principal_role, + workspace_id=excluded.workspace_id, + currency=excluded.currency, + amount_atomic=excluded.amount_atomic, + total_amount_atomic=excluded.total_amount_atomic, + decimals=excluded.decimals, + membership_included=excluded.membership_included, + line_items_json=excluded.line_items_json, + policy_hash=excluded.policy_hash, + access_class=excluded.access_class, + availability_state=excluded.availability_state, + created_at=excluded.created_at, + expires_at=excluded.expires_at, + confirmed_at=excluded.confirmed_at, + confirmed_tx_hash=excluded.confirmed_tx_hash + `, quote.QuoteID, + strings.ToLower(strings.TrimSpace(quote.Wallet)), + nullableString(strings.ToLower(strings.TrimSpace(quote.PayerWallet))), + strings.TrimSpace(quote.OfferID), + nullableString(strings.TrimSpace(quote.OrgRootID)), + nullableString(strings.TrimSpace(quote.PrincipalID)), + nullableString(strings.ToLower(strings.TrimSpace(quote.PrincipalRole))), + nullableString(strings.TrimSpace(quote.WorkspaceID)), + strings.TrimSpace(quote.Currency), + strings.TrimSpace(quote.AmountAtomic), + strings.TrimSpace(quote.TotalAmountAtomic), + quote.Decimals, + boolToInt(quote.MembershipIncluded), + quote.LineItemsJSON, + strings.TrimSpace(quote.PolicyHash), + strings.ToLower(strings.TrimSpace(quote.AccessClass)), + strings.ToLower(strings.TrimSpace(quote.AvailabilityState)), + quote.CreatedAt.UTC().Format(time.RFC3339Nano), + quote.ExpiresAt.UTC().Format(time.RFC3339Nano), + formatNullableTime(quote.ConfirmedAt), + nullableString(strings.ToLower(strings.TrimSpace(quote.ConfirmedTxHash))), + ) + return err +} + +func (s *store) getMarketplaceQuote(ctx context.Context, quoteID string) (marketplaceQuoteRecord, error) { + row := s.db.QueryRowContext(ctx, ` + SELECT quote_id, wallet, payer_wallet, offer_id, org_root_id, principal_id, principal_role, workspace_id, + currency, amount_atomic, total_amount_atomic, decimals, membership_included, line_items_json, + policy_hash, access_class, availability_state, created_at, expires_at, confirmed_at, confirmed_tx_hash + FROM marketplace_quotes + WHERE quote_id = ? + `, strings.TrimSpace(quoteID)) + var rec marketplaceQuoteRecord + var payerWallet, orgRootID, principalID, principalRole, workspaceID sql.NullString + var createdAt, expiresAt, confirmedAt, confirmedTxHash sql.NullString + var membershipIncluded int + err := row.Scan( + &rec.QuoteID, + &rec.Wallet, + &payerWallet, + &rec.OfferID, + &orgRootID, + &principalID, + &principalRole, + &workspaceID, + &rec.Currency, + &rec.AmountAtomic, + &rec.TotalAmountAtomic, + &rec.Decimals, + &membershipIncluded, + &rec.LineItemsJSON, + &rec.PolicyHash, + &rec.AccessClass, + &rec.AvailabilityState, + &createdAt, + &expiresAt, + &confirmedAt, + &confirmedTxHash, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return marketplaceQuoteRecord{}, errNotFound + } + return marketplaceQuoteRecord{}, err + } + rec.PayerWallet = payerWallet.String + rec.OrgRootID = orgRootID.String + rec.PrincipalID = principalID.String + rec.PrincipalRole = principalRole.String + rec.WorkspaceID = workspaceID.String + rec.MembershipIncluded = membershipIncluded == 1 + rec.CreatedAt = parseRFC3339Nullable(createdAt) + rec.ExpiresAt = parseRFC3339Nullable(expiresAt) + rec.ConfirmedAt = parseRFC3339Ptr(confirmedAt) + rec.ConfirmedTxHash = confirmedTxHash.String + return rec, nil +} + +func (s *store) putMarketplaceEntitlement(ctx context.Context, ent marketplaceEntitlementRecord) error { + _, err := s.db.ExecContext(ctx, ` + INSERT INTO marketplace_entitlements ( + entitlement_id, quote_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role, + workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(entitlement_id) DO UPDATE SET + quote_id=excluded.quote_id, + offer_id=excluded.offer_id, + wallet=excluded.wallet, + payer_wallet=excluded.payer_wallet, + org_root_id=excluded.org_root_id, + principal_id=excluded.principal_id, + principal_role=excluded.principal_role, + workspace_id=excluded.workspace_id, + state=excluded.state, + access_class=excluded.access_class, + availability_state=excluded.availability_state, + policy_hash=excluded.policy_hash, + issued_at=excluded.issued_at, + tx_hash=excluded.tx_hash + `, strings.TrimSpace(ent.EntitlementID), + strings.TrimSpace(ent.QuoteID), + strings.TrimSpace(ent.OfferID), + strings.ToLower(strings.TrimSpace(ent.Wallet)), + nullableString(strings.ToLower(strings.TrimSpace(ent.PayerWallet))), + nullableString(strings.TrimSpace(ent.OrgRootID)), + nullableString(strings.TrimSpace(ent.PrincipalID)), + nullableString(strings.ToLower(strings.TrimSpace(ent.PrincipalRole))), + nullableString(strings.TrimSpace(ent.WorkspaceID)), + strings.ToLower(strings.TrimSpace(ent.State)), + strings.ToLower(strings.TrimSpace(ent.AccessClass)), + strings.ToLower(strings.TrimSpace(ent.AvailabilityState)), + strings.TrimSpace(ent.PolicyHash), + ent.IssuedAt.UTC().Format(time.RFC3339Nano), + strings.ToLower(strings.TrimSpace(ent.TxHash)), + ) + return err +} + +func (s *store) getMarketplaceEntitlementByQuote(ctx context.Context, quoteID string) (marketplaceEntitlementRecord, error) { + row := s.db.QueryRowContext(ctx, ` + SELECT entitlement_id, quote_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role, + workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash + FROM marketplace_entitlements + WHERE quote_id = ? + `, strings.TrimSpace(quoteID)) + return scanMarketplaceEntitlement(row) +} + +func (s *store) listMarketplaceEntitlementsByWallet(ctx context.Context, wallet string) ([]marketplaceEntitlementRecord, error) { + rows, err := s.db.QueryContext(ctx, ` + SELECT entitlement_id, quote_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role, + workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash + FROM marketplace_entitlements + WHERE wallet = ? + ORDER BY issued_at DESC + `, strings.ToLower(strings.TrimSpace(wallet))) + if err != nil { + return nil, err + } + defer rows.Close() + + records := make([]marketplaceEntitlementRecord, 0) + for rows.Next() { + rec, err := scanMarketplaceEntitlement(rows) + if err != nil { + return nil, err + } + records = append(records, rec) + } + if err := rows.Err(); err != nil { + return nil, err + } + return records, nil +} + +func (s *store) hasActiveEntitlement(ctx context.Context, wallet, offerID, orgRootID string) (bool, error) { + wallet = strings.ToLower(strings.TrimSpace(wallet)) + offerID = strings.TrimSpace(offerID) + orgRootID = strings.TrimSpace(orgRootID) + if wallet == "" || offerID == "" { + return false, nil + } + + query := ` + SELECT 1 + FROM marketplace_entitlements + WHERE wallet = ? + AND offer_id = ? + AND state = 'active' + ` + args := []any{wallet, offerID} + if orgRootID != "" { + query += " AND org_root_id = ?" + args = append(args, orgRootID) + } + query += " LIMIT 1" + + row := s.db.QueryRowContext(ctx, query, args...) + var marker int + if err := row.Scan(&marker); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, err + } + return marker == 1, nil +} + +func scanMarketplaceEntitlement(row interface{ Scan(dest ...any) error }) (marketplaceEntitlementRecord, error) { + var rec marketplaceEntitlementRecord + var payerWallet, orgRootID, principalID, principalRole, workspaceID sql.NullString + var issuedAt sql.NullString + err := row.Scan( + &rec.EntitlementID, + &rec.QuoteID, + &rec.OfferID, + &rec.Wallet, + &payerWallet, + &orgRootID, + &principalID, + &principalRole, + &workspaceID, + &rec.State, + &rec.AccessClass, + &rec.AvailabilityState, + &rec.PolicyHash, + &issuedAt, + &rec.TxHash, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return marketplaceEntitlementRecord{}, errNotFound + } + return marketplaceEntitlementRecord{}, err + } + rec.PayerWallet = payerWallet.String + rec.OrgRootID = orgRootID.String + rec.PrincipalID = principalID.String + rec.PrincipalRole = principalRole.String + rec.WorkspaceID = workspaceID.String + rec.IssuedAt = parseRFC3339Nullable(issuedAt) + return rec, nil +} + func (s *store) putGovernancePrincipal(ctx context.Context, rec governancePrincipalRecord) error { _, err := s.db.ExecContext(ctx, ` INSERT INTO governance_principals ( diff --git a/docs/api/examples/marketplace.examples.md b/docs/api/examples/marketplace.examples.md index ef3841a..0f48073 100644 --- a/docs/api/examples/marketplace.examples.md +++ b/docs/api/examples/marketplace.examples.md @@ -8,14 +8,14 @@ Success (`200`): { "offers": [ { - "offer_id": "edut.governance.core", + "offer_id": "edut.workspace.core", "issuer_id": "edut.firstparty", - "title": "EDUT Governance Core", - "summary": "First paid runtime license.", + "title": "EDUT Workspace Core", + "summary": "Org-bound deterministic governance runtime.", "status": "active", "pricing": { "currency": "USDC", - "amount_atomic": "499000000", + "amount_atomic": "1000000000", "decimals": 6, "chain_id": 8453 }, @@ -39,7 +39,7 @@ Request: { "wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5", "payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11", - "offer_id": "edut.governance.core", + "offer_id": "edut.workspace.core", "org_root_id": "org.acme.root", "principal_id": "human.joshua", "principal_role": "org_root_owner", @@ -55,31 +55,31 @@ Success (`200`): "quote_id": "cq_01HZZXFQ27ZP6MP0V2R9M6V3KX", "wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5", "payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11", - "offer_id": "edut.governance.core", + "offer_id": "edut.workspace.core", "org_root_id": "org.acme.root", "principal_id": "human.joshua", "principal_role": "org_root_owner", "currency": "USDC", - "amount": "500.00", - "amount_atomic": "500000000", - "total_amount": "505.00", - "total_amount_atomic": "505000000", + "amount": "1000.00", + "amount_atomic": "1000000000", + "total_amount": "1100.00", + "total_amount_atomic": "1100000000", "decimals": 6, "membership_activation_included": true, "line_items": [ { "kind": "license", - "label": "Governance Core License", - "amount": "500.00", - "amount_atomic": "500000000", + "label": "EDUT Workspace Core", + "amount": "1000.00", + "amount_atomic": "1000000000", "decimals": 6, "currency": "USDC" }, { "kind": "membership", "label": "EDUT Membership Activation", - "amount": "5.00", - "amount_atomic": "5000000", + "amount": "100.00", + "amount_atomic": "100000000", "decimals": 6, "currency": "USDC" } @@ -120,7 +120,7 @@ Request: "quote_id": "cq_01HZZXFQ27ZP6MP0V2R9M6V3KX", "wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5", "payer_wallet": "0x2299547f6fA9A8f9b6d9aEA9F9D8A4B53C8A0e11", - "offer_id": "edut.governance.core", + "offer_id": "edut.workspace.core", "org_root_id": "org.acme.root", "principal_id": "human.joshua", "principal_role": "org_root_owner", @@ -136,7 +136,7 @@ Success (`200`): { "status": "entitlement_active", "entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001", - "offer_id": "edut.governance.core", + "offer_id": "edut.workspace.core", "org_root_id": "org.acme.root", "principal_id": "human.joshua", "principal_role": "org_root_owner", @@ -158,7 +158,7 @@ Success (`200`): "entitlements": [ { "entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001", - "offer_id": "edut.crm.pro.annual", + "offer_id": "edut.workspace.core", "wallet_address": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5", "workspace_id": "workspace.work.acme", "org_root_id": "org.acme.root", diff --git a/docs/api/examples/secret-system.examples.md b/docs/api/examples/secret-system.examples.md index 0cadb58..f8fddc3 100644 --- a/docs/api/examples/secret-system.examples.md +++ b/docs/api/examples/secret-system.examples.md @@ -95,8 +95,8 @@ Success (`200`): "quote_id": "mq_01HZZX4F8VQXJ6A57R8P3SCB2W", "chain_id": 8453, "currency": "USDC", - "amount": "5.00", - "amount_atomic": "5000000", + "amount": "100.00", + "amount_atomic": "100000000", "decimals": 6, "deadline": "2026-02-17T07:36:12Z", "contract_address": "0x1111111111111111111111111111111111111111", diff --git a/docs/membership-pricing-policy.md b/docs/membership-pricing-policy.md index 28567b3..b7b2322 100644 --- a/docs/membership-pricing-policy.md +++ b/docs/membership-pricing-policy.md @@ -4,7 +4,7 @@ This policy defines deterministic rules for membership mint pricing. ## Policy Objectives -1. Keep onboarding friction low. +1. Keep onboarding deterministic and explicit. 2. Guarantee mint-cost coverage. 3. Support configurable growth tiers. 4. Keep pricing behavior transparent and auditable. @@ -13,35 +13,22 @@ This policy defines deterministic rules for membership mint pricing. 1. Membership is paid. 2. Membership is required for marketplace purchases. -3. Membership price is configurable but must not go below policy floor. +3. Membership price is fixed at `100 USDC` for launch. ## Floor Rule -1. Default floor target: USD 5.00 equivalent. -2. Effective configured price must satisfy: - -```text -configured_price >= max(minimum_floor, estimated_network_cost * safety_multiplier) -``` - -3. Recommended default `safety_multiplier`: `1.5`. +1. Launch membership price: `100.00 USDC` (`100000000` atomic with 6 decimals). +2. No discounts, no bundles, no alternate path pricing. +3. Any future change must be owner-governed and event-emitted. ## Supported Settlement Currencies (v1) -1. `USDC` on Base (preferred stable settlement). -2. `ETH` on Base (optional). +1. `USDC` on Base. -`BTC` display may be shown as reference only; settlement remains Base-native. +## Tier Policy -## Tier Policy (Optional) - -1. Tiering is supply-based (`total_membership_minted`). -2. Each tier has: - - `max_supply` - - `currency` - - `amount_atomic` -3. Price transitions occur automatically when supply crosses a tier boundary. -4. Tier changes emit on-chain events. +1. Disabled for launch. +2. No supply curve, no promotions, no coupons. ## Quote Lock Requirements @@ -65,9 +52,8 @@ A wallet confirmation is valid only when tx matches quote values exactly. ## UX Disclosure Rules 1. Show exact payable amount before wallet confirmation. -2. Show currency clearly (`USDC` or `ETH`). -3. If BTC equivalent is shown, label it `reference only`. -4. Never imply investment return or speculative upside. +2. Show currency clearly (`USDC`). +3. Never imply investment return or speculative upside. ## Evidence Requirements diff --git a/docs/schemas/examples/entitlement.v1.example.json b/docs/schemas/examples/entitlement.v1.example.json index 7665b95..03097f9 100644 --- a/docs/schemas/examples/entitlement.v1.example.json +++ b/docs/schemas/examples/entitlement.v1.example.json @@ -1,7 +1,7 @@ { "schema_version": "entitlement.v1", "entitlement_id": "ent:8453:0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5:000001", - "offer_id": "edut.crm.pro.annual", + "offer_id": "edut.workspace.core", "issuer_id": "edut.firstparty", "wallet_address": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5", "workspace_id": "workspace.work.acme", @@ -10,7 +10,7 @@ "tx_hash": "0x1111111111111111111111111111111111111111111111111111111111111111", "chain_id": 8453, "currency": "USDC", - "amount_atomic": "199000000", + "amount_atomic": "1000000000", "policy_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, "issued_at": "2026-02-17T00:02:12Z", diff --git a/docs/schemas/examples/launch-offers-catalog.v1.example.json b/docs/schemas/examples/launch-offers-catalog.v1.example.json index fe5c170..809a99f 100644 --- a/docs/schemas/examples/launch-offers-catalog.v1.example.json +++ b/docs/schemas/examples/launch-offers-catalog.v1.example.json @@ -3,10 +3,10 @@ "catalog_id": "launch-2026-operator", "offers": [ { - "offer_id": "edut.governance.core", - "title": "EDUT Governance Core", - "summary": "First paid runtime license. Activates deterministic governance runtime.", - "price": "499.00", + "offer_id": "edut.solo.core", + "title": "EDUT Solo Core", + "summary": "Single-principal governance runtime for personal operations.", + "price": "1000.00", "currency": "USDC", "member_only": true, "workspace_bound": false, @@ -15,10 +15,10 @@ "multi_tenant": false }, { - "offer_id": "edut.crm.pro.annual", - "title": "EDUT CRM Pro", - "summary": "Workspace-bound CRM module with governance and evidence integration.", - "price": "199.00", + "offer_id": "edut.workspace.core", + "title": "EDUT Workspace Core", + "summary": "Org-bound deterministic governance runtime for team operations.", + "price": "1000.00", "currency": "USDC", "member_only": true, "workspace_bound": true, @@ -27,10 +27,34 @@ "multi_tenant": false }, { - "offer_id": "edut.invoicing.core.annual", - "title": "EDUT Invoicing Core", - "summary": "Invoicing workflow module for member workspaces.", - "price": "99.00", + "offer_id": "edut.workspace.ai", + "title": "EDUT Workspace AI Layer", + "summary": "AI reasoning layer for governed workspace operations.", + "price": "1000.00", + "currency": "USDC", + "member_only": true, + "workspace_bound": true, + "transferable": false, + "internal_use_only": true, + "multi_tenant": false + }, + { + "offer_id": "edut.workspace.lane24", + "title": "EDUT Workspace 24h Lane", + "summary": "Autonomous execution lane capacity for workspace throughput.", + "price": "1000.00", + "currency": "USDC", + "member_only": true, + "workspace_bound": true, + "transferable": false, + "internal_use_only": true, + "multi_tenant": false + }, + { + "offer_id": "edut.workspace.sovereign", + "title": "EDUT Workspace Sovereign Continuity", + "summary": "Continuity profile for stronger local/offline workspace operation.", + "price": "1000.00", "currency": "USDC", "member_only": true, "workspace_bound": true, diff --git a/docs/schemas/examples/offer.v1.example.json b/docs/schemas/examples/offer.v1.example.json index dfb74c4..eace1d5 100644 --- a/docs/schemas/examples/offer.v1.example.json +++ b/docs/schemas/examples/offer.v1.example.json @@ -1,13 +1,13 @@ { "schema_version": "offer.v1", - "offer_id": "edut.crm.pro.annual", + "offer_id": "edut.workspace.core", "issuer_id": "edut.firstparty", - "title": "EDUT CRM Pro", - "summary": "Workspace-bound CRM module entitlement with annual billing.", + "title": "EDUT Workspace Core", + "summary": "Org-bound deterministic governance runtime entitlement.", "status": "active", "pricing": { "currency": "USDC", - "amount_atomic": "199000000", + "amount_atomic": "1000000000", "decimals": 6, "chain_id": 8453 }, @@ -17,18 +17,18 @@ "transferable": false, "internal_use_only": true, "multi_tenant": false, - "max_per_wallet": 5, + "max_per_wallet": 1, "requires_admin_approval": false }, "entitlement": { - "type": "module_license", - "scope": "workspace", - "runtime_policy_ref": "policy.crm.pro.v1" + "type": "runtime_license", + "scope": "org_root", + "runtime_policy_ref": "policy.workspace.core.v1" }, "created_at": "2026-02-17T00:00:00Z", "updated_at": "2026-02-17T00:00:00Z", "metadata": { - "category": "business", - "support_tier": "standard" + "category": "governance", + "support_tier": "zero_ops" } } diff --git a/docs/secret-system-spec.md b/docs/secret-system-spec.md index a45a244..fc3433b 100644 --- a/docs/secret-system-spec.md +++ b/docs/secret-system-spec.md @@ -191,8 +191,8 @@ Response: "quote_id": "mq_...", "chain_id": 8453, "currency": "USDC", - "amount": "5.00", - "amount_atomic": "5000000", + "amount": "100.00", + "amount_atomic": "100000000", "deadline": "2026-02-17T07:36:12Z", "contract_address": "0x...", "method": "mintMembership", diff --git a/public/store/index.html b/public/store/index.html index b6da34c..0079aeb 100644 --- a/public/store/index.html +++ b/public/store/index.html @@ -406,13 +406,13 @@ setCheckoutLog('Offer catalog loaded: ' + payload.offers.length + ' offers.'); } catch (err) { state.offers = [{ - offer_id: 'edut.governance.core', - title: 'EDUT Governance Core (fallback)', - summary: 'Fallback governance offer loaded because catalog fetch failed.', - price: '499.00', + offer_id: 'edut.workspace.core', + title: 'EDUT Workspace Core (fallback)', + summary: 'Fallback workspace core offer loaded because catalog fetch failed.', + price: '1000.00', currency: 'USDC', member_only: true, - workspace_bound: false, + workspace_bound: true, transferable: false, }]; state.selectedOfferId = state.offers[0].offer_id; diff --git a/public/store/offers.json b/public/store/offers.json index 2d28650..a1cd685 100644 --- a/public/store/offers.json +++ b/public/store/offers.json @@ -2,22 +2,10 @@ "catalog_id": "launch-2026-operator", "offers": [ { - "offer_id": "edut.governance.core", - "title": "EDUT Governance Core", - "summary": "First paid runtime license. Activates deterministic governance engine on entitled devices.", - "price": "499.00", - "currency": "USDC", - "member_only": true, - "workspace_bound": false, - "transferable": false, - "internal_use_only": true, - "multi_tenant": false - }, - { - "offer_id": "edut.crm.pro.annual", - "title": "EDUT CRM Pro", - "summary": "Workspace-bound CRM module with governance and evidence integration.", - "price": "199.00", + "offer_id": "edut.solo.core", + "title": "EDUT Solo Core", + "summary": "Single-principal governance runtime for personal operations.", + "price": "1000.00", "currency": "USDC", "member_only": true, "workspace_bound": true, @@ -26,10 +14,46 @@ "multi_tenant": false }, { - "offer_id": "edut.invoicing.core.annual", - "title": "EDUT Invoicing Core", - "summary": "Invoicing workflow module for member workspaces.", - "price": "99.00", + "offer_id": "edut.workspace.core", + "title": "EDUT Workspace Core", + "summary": "Org-bound deterministic governance runtime for team operations.", + "price": "1000.00", + "currency": "USDC", + "member_only": true, + "workspace_bound": true, + "transferable": false, + "internal_use_only": true, + "multi_tenant": false + }, + { + "offer_id": "edut.workspace.ai", + "title": "EDUT Workspace AI Layer", + "summary": "AI reasoning layer for governed workspace operations.", + "price": "1000.00", + "currency": "USDC", + "member_only": true, + "workspace_bound": true, + "transferable": false, + "internal_use_only": true, + "multi_tenant": false + }, + { + "offer_id": "edut.workspace.lane24", + "title": "EDUT Workspace 24h Lane", + "summary": "Autonomous execution lane capacity for workspace queue throughput.", + "price": "1000.00", + "currency": "USDC", + "member_only": true, + "workspace_bound": true, + "transferable": false, + "internal_use_only": true, + "multi_tenant": false + }, + { + "offer_id": "edut.workspace.sovereign", + "title": "EDUT Workspace Sovereign Continuity", + "summary": "Stronger local/offline continuity profile for governed workspaces.", + "price": "1000.00", "currency": "USDC", "member_only": true, "workspace_bound": true,