Harden secretapi sessions and entitlement quote gating
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
70c54b1283
commit
b15e13fda5
@ -10,7 +10,7 @@ SECRET_API_REQUIRE_ONCHAIN_TX_VERIFICATION=false
|
|||||||
SECRET_API_INTENT_TTL_SECONDS=900
|
SECRET_API_INTENT_TTL_SECONDS=900
|
||||||
SECRET_API_QUOTE_TTL_SECONDS=900
|
SECRET_API_QUOTE_TTL_SECONDS=900
|
||||||
SECRET_API_WALLET_SESSION_TTL_SECONDS=2592000
|
SECRET_API_WALLET_SESSION_TTL_SECONDS=2592000
|
||||||
SECRET_API_REQUIRE_WALLET_SESSION=false
|
SECRET_API_REQUIRE_WALLET_SESSION=true
|
||||||
SECRET_API_DOMAIN_NAME=EDUT Designation
|
SECRET_API_DOMAIN_NAME=EDUT Designation
|
||||||
SECRET_API_VERIFYING_CONTRACT=0x0000000000000000000000000000000000000000
|
SECRET_API_VERIFYING_CONTRACT=0x0000000000000000000000000000000000000000
|
||||||
SECRET_API_MEMBERSHIP_CONTRACT=0x0000000000000000000000000000000000000000
|
SECRET_API_MEMBERSHIP_CONTRACT=0x0000000000000000000000000000000000000000
|
||||||
|
|||||||
@ -135,13 +135,18 @@ Policy gates:
|
|||||||
- `SECRET_API_INTENT_TTL_SECONDS` (default `900`)
|
- `SECRET_API_INTENT_TTL_SECONDS` (default `900`)
|
||||||
- `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 `false`; set `true` for launch hardening)
|
- `SECRET_API_REQUIRE_WALLET_SESSION` (default `true`; set `false` only for controlled local harness/debug usage)
|
||||||
- `SECRET_API_DOMAIN_NAME`
|
- `SECRET_API_DOMAIN_NAME`
|
||||||
- `SECRET_API_VERIFYING_CONTRACT`
|
- `SECRET_API_VERIFYING_CONTRACT`
|
||||||
- `SECRET_API_MEMBERSHIP_CONTRACT`
|
- `SECRET_API_MEMBERSHIP_CONTRACT`
|
||||||
- `SECRET_API_MINT_CURRENCY` (must be `USDC` in v1)
|
- `SECRET_API_MINT_CURRENCY` (`USDC` for launch; `ETH` allowed for Sepolia/test harness)
|
||||||
- `SECRET_API_MINT_AMOUNT_ATOMIC` (default `100000000`)
|
- `SECRET_API_MINT_AMOUNT_ATOMIC` (default `100000000`)
|
||||||
- `SECRET_API_MINT_DECIMALS` (default `6`)
|
- `SECRET_API_MINT_DECIMALS` (must be `6` for `USDC`, `18` for `ETH`)
|
||||||
|
|
||||||
|
### Marketplace
|
||||||
|
|
||||||
|
- `SECRET_API_ENTITLEMENT_CONTRACT` must be configured to issue checkout quotes.
|
||||||
|
- Marketplace quote fails closed with `entitlement_contract_unconfigured` when unset/zero.
|
||||||
|
|
||||||
### Governance install
|
### Governance install
|
||||||
|
|
||||||
|
|||||||
@ -329,6 +329,68 @@ func TestMembershipConfirmAcceptsOnrampAttestationAssurance(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMembershipStatusPrefersActiveDesignationWhenNewerRecordIsUnactivated(t *testing.T) {
|
||||||
|
a, _, cleanup := newTestApp(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
ownerKey := mustKey(t)
|
||||||
|
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
activeIssuedAt := now.Add(-2 * time.Hour)
|
||||||
|
activeCode := "1111111111111"
|
||||||
|
if err := a.store.putDesignation(context.Background(), designationRecord{
|
||||||
|
Code: activeCode,
|
||||||
|
DisplayToken: "1111-1111-1111-1",
|
||||||
|
IntentID: "intent-active",
|
||||||
|
Nonce: "nonce-active",
|
||||||
|
Origin: "https://edut.ai",
|
||||||
|
Locale: "en",
|
||||||
|
Address: ownerAddr,
|
||||||
|
ChainID: 84532,
|
||||||
|
IssuedAt: activeIssuedAt,
|
||||||
|
ExpiresAt: activeIssuedAt.Add(time.Hour),
|
||||||
|
VerifiedAt: &activeIssuedAt,
|
||||||
|
MembershipStatus: "active",
|
||||||
|
MembershipTxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
ActivatedAt: &activeIssuedAt,
|
||||||
|
IdentityAssurance: assuranceCryptoDirect,
|
||||||
|
IdentityAttestedBy: "",
|
||||||
|
IdentityAttestationID: "",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("seed active designation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newerIssuedAt := now
|
||||||
|
if err := a.store.putDesignation(context.Background(), designationRecord{
|
||||||
|
Code: "2222222222222",
|
||||||
|
DisplayToken: "2222-2222-2222-2",
|
||||||
|
IntentID: "intent-newer",
|
||||||
|
Nonce: "nonce-newer",
|
||||||
|
Origin: "https://edut.ai",
|
||||||
|
Locale: "en",
|
||||||
|
Address: ownerAddr,
|
||||||
|
ChainID: 84532,
|
||||||
|
IssuedAt: newerIssuedAt,
|
||||||
|
ExpiresAt: newerIssuedAt.Add(time.Hour),
|
||||||
|
VerifiedAt: &newerIssuedAt,
|
||||||
|
MembershipStatus: "none",
|
||||||
|
IdentityAssurance: assuranceNone,
|
||||||
|
IdentityAttestedBy: "",
|
||||||
|
IdentityAttestationID: "",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("seed newer unactivated designation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := getJSONExpect[membershipStatusResponse](t, a, "/secret/membership/status?wallet="+ownerAddr, http.StatusOK)
|
||||||
|
if status.Status != "active" {
|
||||||
|
t.Fatalf("expected active status, got %+v", status)
|
||||||
|
}
|
||||||
|
if status.DesignationCode != activeCode {
|
||||||
|
t.Fatalf("expected active designation code %s, got %+v", activeCode, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMembershipConfirmRejectsAlreadyConfirmedQuote(t *testing.T) {
|
func TestMembershipConfirmRejectsAlreadyConfirmedQuote(t *testing.T) {
|
||||||
a, cfg, cleanup := newTestApp(t)
|
a, cfg, cleanup := newTestApp(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@ -1062,6 +1124,29 @@ func TestMarketplaceQuoteUsesEntitlementContractTransactionWhenConfigured(t *tes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarketplaceQuoteFailsWhenEntitlementContractUnconfigured(t *testing.T) {
|
||||||
|
a, _, cleanup := newTestApp(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
a.cfg.EntitlementContract = "0x0000000000000000000000000000000000000000"
|
||||||
|
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: offerIDWorkspaceCore,
|
||||||
|
OrgRootID: "org.marketplace.unconfigured",
|
||||||
|
PrincipalID: "human.owner",
|
||||||
|
PrincipalRole: "org_root_owner",
|
||||||
|
}, http.StatusServiceUnavailable)
|
||||||
|
if code := errResp["code"]; code != "entitlement_contract_unconfigured" {
|
||||||
|
t.Fatalf("expected entitlement_contract_unconfigured, got %+v", errResp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarketplaceWorkspaceAddOnRequiresCoreEntitlement(t *testing.T) {
|
func TestMarketplaceWorkspaceAddOnRequiresCoreEntitlement(t *testing.T) {
|
||||||
a, _, cleanup := newTestApp(t)
|
a, _, cleanup := newTestApp(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@ -1545,6 +1630,8 @@ func newTestApp(t *testing.T) (*app, Config, func()) {
|
|||||||
cfg.GovernancePackageURL = "https://cdn.test/edutd.tar.gz"
|
cfg.GovernancePackageURL = "https://cdn.test/edutd.tar.gz"
|
||||||
cfg.GovernancePackageSig = "sig-test"
|
cfg.GovernancePackageSig = "sig-test"
|
||||||
cfg.GovernanceRuntimeVersion = "0.2.0"
|
cfg.GovernanceRuntimeVersion = "0.2.0"
|
||||||
|
cfg.RequireWalletSession = false
|
||||||
|
cfg.EntitlementContract = "0x1111111111111111111111111111111111111111"
|
||||||
st, err := openStore(cfg.DBPath)
|
st, err := openStore(cfg.DBPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("open store: %v", err)
|
t.Fatalf("open store: %v", err)
|
||||||
|
|||||||
@ -49,7 +49,7 @@ func loadConfig() Config {
|
|||||||
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,
|
||||||
WalletSessionTTL: time.Duration(envInt("SECRET_API_WALLET_SESSION_TTL_SECONDS", 2592000)) * time.Second,
|
WalletSessionTTL: time.Duration(envInt("SECRET_API_WALLET_SESSION_TTL_SECONDS", 2592000)) * time.Second,
|
||||||
RequireWalletSession: envBool("SECRET_API_REQUIRE_WALLET_SESSION", false),
|
RequireWalletSession: envBool("SECRET_API_REQUIRE_WALLET_SESSION", true),
|
||||||
InstallTokenTTL: time.Duration(envInt("SECRET_API_INSTALL_TOKEN_TTL_SECONDS", 900)) * time.Second,
|
InstallTokenTTL: time.Duration(envInt("SECRET_API_INSTALL_TOKEN_TTL_SECONDS", 900)) * time.Second,
|
||||||
LeaseTTL: time.Duration(envInt("SECRET_API_LEASE_TTL_SECONDS", 3600)) * time.Second,
|
LeaseTTL: time.Duration(envInt("SECRET_API_LEASE_TTL_SECONDS", 3600)) * time.Second,
|
||||||
OfflineRenewTTL: time.Duration(envInt("SECRET_API_OFFLINE_RENEW_TTL_SECONDS", 2592000)) * time.Second,
|
OfflineRenewTTL: time.Duration(envInt("SECRET_API_OFFLINE_RENEW_TTL_SECONDS", 2592000)) * time.Second,
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package main
|
|||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestConfigValidateAllowsDefaultLocalConfig(t *testing.T) {
|
func TestConfigValidateAllowsDefaultLocalConfig(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
cfg.ChainID = 84532
|
cfg.ChainID = 84532
|
||||||
cfg.RequireOnchainTxVerify = false
|
cfg.RequireOnchainTxVerify = false
|
||||||
@ -14,7 +13,6 @@ func TestConfigValidateAllowsDefaultLocalConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateRejectsStrictVerificationWithoutRPC(t *testing.T) {
|
func TestConfigValidateRejectsStrictVerificationWithoutRPC(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
cfg.RequireOnchainTxVerify = true
|
cfg.RequireOnchainTxVerify = true
|
||||||
cfg.ChainRPCURL = ""
|
cfg.ChainRPCURL = ""
|
||||||
@ -24,7 +22,6 @@ func TestConfigValidateRejectsStrictVerificationWithoutRPC(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateRejectsNonPositiveChainID(t *testing.T) {
|
func TestConfigValidateRejectsNonPositiveChainID(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
cfg.ChainID = 0
|
cfg.ChainID = 0
|
||||||
if err := cfg.Validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
@ -32,29 +29,54 @@ func TestConfigValidateRejectsNonPositiveChainID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateRejectsNonUSDCCurrency(t *testing.T) {
|
func TestConfigValidateAllowsETHWhenDecimalsMatch(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
cfg.MintCurrency = "ETH"
|
cfg.MintCurrency = "ETH"
|
||||||
if err := cfg.Validate(); err == nil {
|
cfg.MintDecimals = 18
|
||||||
t.Fatalf("expected mint currency validation failure")
|
cfg.MintAmountAtomic = "1"
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
t.Fatalf("expected ETH config to validate, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateRejectsNonSixMintDecimals(t *testing.T) {
|
func TestConfigValidateRejectsETHWhenDecimalsMismatch(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
|
cfg.MintCurrency = "ETH"
|
||||||
|
cfg.MintDecimals = 6
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Fatalf("expected ETH decimal validation failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidateRejectsUSDCWhenDecimalsMismatch(t *testing.T) {
|
||||||
|
cfg := loadConfig()
|
||||||
|
cfg.MintCurrency = "USDC"
|
||||||
cfg.MintDecimals = 18
|
cfg.MintDecimals = 18
|
||||||
if err := cfg.Validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
t.Fatalf("expected mint decimals validation failure")
|
t.Fatalf("expected USDC decimal validation failure")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigValidateRejectsInvalidMintAmount(t *testing.T) {
|
func TestConfigValidateRejectsInvalidMintAmount(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
cfg.MintAmountAtomic = "not-a-number"
|
cfg.MintAmountAtomic = "not-a-number"
|
||||||
if err := cfg.Validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
t.Fatalf("expected mint amount validation failure")
|
t.Fatalf("expected mint amount validation failure")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigDefaultsWalletSessionRequired(t *testing.T) {
|
||||||
|
t.Setenv("SECRET_API_REQUIRE_WALLET_SESSION", "")
|
||||||
|
cfg := loadConfig()
|
||||||
|
if !cfg.RequireWalletSession {
|
||||||
|
t.Fatalf("expected wallet session to be required by default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigWalletSessionCanBeDisabled(t *testing.T) {
|
||||||
|
t.Setenv("SECRET_API_REQUIRE_WALLET_SESSION", "false")
|
||||||
|
cfg := loadConfig()
|
||||||
|
if cfg.RequireWalletSession {
|
||||||
|
t.Fatalf("expected wallet session requirement to be disabled by env override")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -402,18 +402,18 @@ func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Requ
|
|||||||
writeErrorCode(w, http.StatusInternalServerError, "quote_generation_failed", "failed to generate quote id")
|
writeErrorCode(w, http.StatusInternalServerError, "quote_generation_failed", "failed to generate quote id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
txTo := strings.ToLower(strings.TrimSpace(a.cfg.MembershipContract))
|
entitlementContract := strings.ToLower(strings.TrimSpace(a.cfg.EntitlementContract))
|
||||||
txData := "0x"
|
if entitlementContract == "" || strings.EqualFold(entitlementContract, "0x0000000000000000000000000000000000000000") {
|
||||||
|
writeErrorCode(w, http.StatusServiceUnavailable, "entitlement_contract_unconfigured", "entitlement contract is not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
txTo := entitlementContract
|
||||||
txValueHex := "0x0"
|
txValueHex := "0x0"
|
||||||
if entitlementContract := strings.ToLower(strings.TrimSpace(a.cfg.EntitlementContract)); entitlementContract != "" &&
|
txData, calldataErr := encodePurchaseEntitlementCalldata(offer.OfferID, wallet, orgRootID, workspaceID)
|
||||||
!strings.EqualFold(entitlementContract, "0x0000000000000000000000000000000000000000") {
|
if calldataErr != nil {
|
||||||
entitlementCalldata, calldataErr := encodePurchaseEntitlementCalldata(offer.OfferID, wallet, orgRootID, workspaceID)
|
writeErrorCode(w, http.StatusInternalServerError, "quote_generation_failed", "failed to encode checkout transaction")
|
||||||
if calldataErr != nil {
|
return
|
||||||
writeErrorCode(w, http.StatusInternalServerError, "quote_generation_failed", "failed to encode checkout transaction")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
txTo = entitlementContract
|
|
||||||
txData = entitlementCalldata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|||||||
@ -363,7 +363,13 @@ func (s *store) getDesignationByAddress(ctx context.Context, address string) (de
|
|||||||
SELECT code, display_token, intent_id, nonce, origin, locale, address, chain_id, issued_at, expires_at, verified_at, membership_status, membership_tx_hash, activated_at, identity_assurance_level, identity_attested_by, identity_attestation_id, identity_attested_at
|
SELECT code, display_token, intent_id, nonce, origin, locale, address, chain_id, issued_at, expires_at, verified_at, membership_status, membership_tx_hash, activated_at, identity_assurance_level, identity_attested_by, identity_attestation_id, identity_attested_at
|
||||||
FROM designations
|
FROM designations
|
||||||
WHERE address = ?
|
WHERE address = ?
|
||||||
ORDER BY issued_at DESC
|
ORDER BY CASE LOWER(COALESCE(membership_status, 'none'))
|
||||||
|
WHEN 'active' THEN 0
|
||||||
|
WHEN 'suspended' THEN 1
|
||||||
|
WHEN 'revoked' THEN 2
|
||||||
|
ELSE 3
|
||||||
|
END,
|
||||||
|
issued_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, strings.ToLower(strings.TrimSpace(address)))
|
`, strings.ToLower(strings.TrimSpace(address)))
|
||||||
return scanDesignation(row)
|
return scanDesignation(row)
|
||||||
|
|||||||
@ -8,9 +8,9 @@
|
|||||||
"mint_currency_mode": "ETH_TEST",
|
"mint_currency_mode": "ETH_TEST",
|
||||||
"mint_amount_atomic": "1",
|
"mint_amount_atomic": "1",
|
||||||
"usdc_contract": "0x0000000000000000000000000000000000000000",
|
"usdc_contract": "0x0000000000000000000000000000000000000000",
|
||||||
|
"source": "api.edut.dev runtime environment",
|
||||||
"version": "v1",
|
"version": "v1",
|
||||||
"notes": [
|
"notes": [
|
||||||
"Entitlement contract deploy tx succeeded on Base Sepolia.",
|
"Entitlement contract deployment requires explicit offer-seeding verification before checkout e2e."
|
||||||
"Offer seeding call reverted during first pass; retry required before checkout e2e."
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ Current test-mode settings:
|
|||||||
1. Base Sepolia (`SECRET_API_CHAIN_ID=84532`)
|
1. Base Sepolia (`SECRET_API_CHAIN_ID=84532`)
|
||||||
2. ETH quote mode (`SECRET_API_MINT_CURRENCY=ETH`) for low-friction Sepolia smoke validation
|
2. ETH quote mode (`SECRET_API_MINT_CURRENCY=ETH`) for low-friction Sepolia smoke validation
|
||||||
3. Membership contract wired to `0x3EEb3342751D1Cfc0F90C9393e0B1cd5AcE6FfD8`
|
3. Membership contract wired to `0x3EEb3342751D1Cfc0F90C9393e0B1cd5AcE6FfD8`
|
||||||
|
4. Wallet session enforcement enabled by default (`SECRET_API_REQUIRE_WALLET_SESSION=true`)
|
||||||
|
|
||||||
## Build Targets
|
## Build Targets
|
||||||
|
|
||||||
@ -57,6 +58,8 @@ Critical values before launch:
|
|||||||
- `SECRET_API_GOV_POLICY_HASH`
|
- `SECRET_API_GOV_POLICY_HASH`
|
||||||
6. Member channel polling:
|
6. Member channel polling:
|
||||||
- `SECRET_API_MEMBER_POLL_INTERVAL_SECONDS`
|
- `SECRET_API_MEMBER_POLL_INTERVAL_SECONDS`
|
||||||
|
7. Marketplace contract wiring:
|
||||||
|
- `SECRET_API_ENTITLEMENT_CONTRACT` must be non-zero for checkout quote issuance
|
||||||
|
|
||||||
## Systemd Deployment (Hetzner/VPS)
|
## Systemd Deployment (Hetzner/VPS)
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ All marketplace endpoints require authenticated app/session context.
|
|||||||
3. Entitlement state must default fail-closed for unknown values.
|
3. Entitlement state must default fail-closed for unknown values.
|
||||||
4. Quote/confirm must deny cross-boundary paid execution when `org_root_id` does not match active suite entitlement.
|
4. Quote/confirm must deny cross-boundary paid execution when `org_root_id` does not match active suite entitlement.
|
||||||
5. `availability_state=parked` must block paid execution paths.
|
5. `availability_state=parked` must block paid execution paths.
|
||||||
|
6. Quote generation must fail closed when entitlement contract is unconfigured (`entitlement_contract_unconfigured`).
|
||||||
|
|
||||||
## Store Dependency Mapping
|
## Store Dependency Mapping
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,8 @@ Remaining in this repo:
|
|||||||
|
|
||||||
1. Wire live store checkout flow to production marketplace APIs when available.
|
1. Wire live store checkout flow to production marketplace APIs when available.
|
||||||
2. Replace deployment templates with real contract addresses after chain deployment: `IN_PROGRESS` (Base Sepolia addresses captured in `docs/deployment/contract-addresses.base-sepolia.json`; mainnet pending).
|
2. Replace deployment templates with real contract addresses after chain deployment: `IN_PROGRESS` (Base Sepolia addresses captured in `docs/deployment/contract-addresses.base-sepolia.json`; mainnet pending).
|
||||||
3. Add launcher/governance install UI that consumes governance installer APIs.
|
3. Keep cross-repo address parity with `/Users/vsg/Documents/VSG Codex/contracts/deploy/runtime-addresses.base-sepolia.json`: `IN_PROGRESS`.
|
||||||
|
4. Add launcher/governance install UI that consumes governance installer APIs.
|
||||||
|
|
||||||
Cross-repo dependencies (kernel/backend/contracts):
|
Cross-repo dependencies (kernel/backend/contracts):
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user