WASI WASM project for testing encrypted secrets with different access conditions.
test-secrets-example is a WASI P2 application that:
- Reads JSON input from stdin (required by NEAR OutLayer)
- Reads the
SECRETenvironment variable from WASI env - Returns JSON output to stdout with secret status
This verifies:
- Secrets are correctly decrypted by the keystore
- Access control conditions are properly validated
- WASI environment variables injection works
- Proper WASI P2 stdin/stdout JSON format
A run of this project sees two kinds of secret:
- the author's β
AUTHOR_SECRET, from the profile the manifest names (manifest.json:author_secrets.profile = "author"), stored by the project owner underProject(<owner>/test-secrets)and decrypted into every run. Its access condition is who may run the project at all:AllowAllfor everyone, a whitelist or DAO role for a circle. A declared profile nobody stored refuses the run, with the message saying what to store. - the caller's β
SECRET,USER_SECRET, β¦, from the profile the call names insecrets_ref, under whichever account stored it, gated by that row's condition. An owner hands a secret to their agents by storing it once under their own account and whitelisting the agents' wallet accounts (update_access); each agent names{owner, profile}.
The answer reports both, plus who the run acts as and who paid:
{ "author": true, "user": true, "sender": "you.testnet", "payer": "you.testnet",
"secrets": [ { "key": "AUTHOR_SECRET", "found": true, "value": "β¦" }, β¦ ] }fetch_url in the input performs one GET and reports its status: the manifest
declares no network section, so a project with a manifest keeps unrestricted
egress. (A manifest with connector_id would not β that is a connector's.)
The manifest is a cargo feature. ./build.sh turns it on for the artefact you
publish as a project; the GitHub-source build the Repo-accessor tests below
compile has no project to hold an author secret under, so it carries none.
Publish: ./build.sh β outlayer upload target/wasm32-wasip2/release/test-secrets-example.wasm
β outlayer deploy test-secrets <FastFS URL> β store the author profile:
outlayer secrets set '{"AUTHOR_SECRET":"β¦"}' --project <you>/test-secrets --profile author --access allow-all.
Until that profile exists every run of the published project is refused.
The project-model matrix β author secret, delegation by whitelist, revocation,
lookalike accounts, identity under a binding β is tests/03_project_model.sh
(dry-run by default, --apply to spend). It is the one script here that needs
the parent repository checked out around this directory: it sources
tests/lib/hos_common.sh (testnet fixtures, the throttle, payment keys) and
tests/lib/secrets_common.sh (the stored row, one run on chain or over HTTPS),
which it shares with tests/secrets_security_e2e.sh there. Cloning this example
on its own gives you 01 and 02, which reach nothing outside it and stay the
regression for repository-bound secrets
(tests/01_store_secrets.sh, tests/02_request_execution.sh).
02 is the one that matters after changing this source: it builds the example
FROM GITHUB under the Repo accessor, so it catches a build that no longer
compiles or an answer whose shape moved. It needs nothing but the profiles
already stored on chain and a key for each account in tests/test_config.sh.
01, which stores those profiles, talks to the keystore directly and so needs
KEYSTORE_BASE_URL in .env to be current β a keystore redeploy changes its
app id and therefore its host, and a stale one fails with a connection error
before anything is stored.
# Ensure wasm32-wasip2 target is installed
rustup target add wasm32-wasip2
# Build
./build.sh
# Output: target/wasm32-wasip2/release/test-secrets-example.wasmAll test settings are centralized in tests/test_config.sh:
# Edit this file to customize:
export CONTRACT="outlayer.testnet"
export REPO="github.com/test-user/test-secrets-example"
export OWNER="owner.testnet"
# Test accounts for each scenario
export TEST2_WHITELIST=("allowed1.testnet" "allowed2.testnet")
export TEST6_ALLOWED_ACCOUNTS=("rich.testnet")
# ... etc-
Running services:
- Coordinator API (port 8080)
- Keystore worker (port 8081)
- Worker with event monitor
- PostgreSQL + Redis
-
Contract deployed:
- Edit
CONTRACTintests/test_config.sh
- Edit
-
Test accounts:
- Edit account names in
tests/test_config.sh - Fund accounts as specified in config
- Default accounts: owner.testnet, allowed1.testnet, alice.testnet, rich.testnet, etc.
- Edit account names in
-
GitHub repository:
- Edit
REPOintests/test_config.sh - Upload
test-secrets-example.wasmto the repo
- Edit
# Build first
./build.sh
# Test locally without secrets
echo '{"message":"test"}' | wasmtime target/wasm32-wasip2/release/test-secrets-example.wasm
# Expected output:
# {"status":"error","secret_value":null,"secret_found":false,"message":"SECRET environment variable not found"}
# Test with SECRET env var
echo '{"message":"test"}' | wasmtime --env SECRET=my-secret-value target/wasm32-wasip2/release/test-secrets-example.wasm
# Expected output:
# {"status":"success","secret_value":"my-secret-value","secret_found":true,"message":"SECRET found! Value: my-secret-value"}Edit tests/test_config.sh with your accounts and settings:
# Example: Run test 2 with custom accounts
export TEST2_WHITELIST=("myuser1.testnet" "myuser2.testnet")
export TEST2_ALLOWED_ACCOUNTS=("${TEST2_WHITELIST[@]}")
export TEST2_DENIED_ACCOUNTS=("other.testnet")cd ../../keystore-worker/
# Run all tests (1-13)
bash ../wasi-examples/test-secrets-example/tests/01_store_secrets.sh
# Run specific tests only
bash ../wasi-examples/test-secrets-example/tests/01_store_secrets.sh 1 2 3
# Run single test
bash ../wasi-examples/test-secrets-example/tests/01_store_secrets.sh 2The script generates near call commands for storing secrets. Available tests:
| Test | Profile | Access Condition | Description |
|---|---|---|---|
| 1 | test1_allow_all |
AllowAll |
Anyone can access |
| 2 | test2_whitelist |
Whitelist([allowed1, allowed2]) |
Only whitelisted accounts |
| 3 | test3_pattern_testnet |
AccountPattern("*.testnet") |
Only *.testnet accounts |
| 4 | test4_pattern_a |
AccountPattern("a*") |
Accounts starting with 'a' |
| 5 | test5_pattern_length |
AccountPattern("?????????*") |
Accounts with 10+ chars |
| 6 | test6_near_balance |
NearBalance { min: 5 NEAR } |
Accounts with >= 5 NEAR |
| 7 | test7_ft_balance |
FtBalance { contract, min: 1000 } |
Token holders |
| 8 | test8_nft_token |
NftOwned { contract, token_id: "123" } |
Specific NFT owner |
| 9 | test9_nft_any |
NftOwned { contract, token_id: null } |
Any NFT from collection |
| 10 | test10_logic_and |
AND(*.testnet, >= 5 NEAR) |
Both conditions |
| 11 | test11_logic_or |
OR(whitelist, *.testnet) |
Either condition |
| 12 | test12_logic_not |
NOT(>= 1 NEAR) |
Accounts with < 1 NEAR |
| 13 | test13_complex |
AND(OR(whitelist, *.testnet), >= 5 NEAR) |
Nested logic |
Copy and run each command manually - the script outputs ready-to-use near call commands.
# From keystore-worker/ directory
# Run all tests
bash ../wasi-examples/test-secrets-example/tests/02_request_execution.sh
# Run specific tests only
bash ../wasi-examples/test-secrets-example/tests/02_request_execution.sh 1 2 3
# Run single test
bash ../wasi-examples/test-secrets-example/tests/02_request_execution.sh 2The script generates near call commands using accounts from test_config.sh. For each test:
- SUCCESS cases - should decrypt secrets and return JSON:
{"status":"success","secret_value":"test1_allow_all","secret_found":true,"message":"SECRET found! Value: test1_allow_all"} - FAIL cases - should fail at keystore decryption or return JSON:
{"status":"error","secret_value":null,"secret_found":false,"message":"SECRET environment variable not found"}
Copy and run each command manually. The script shows which accounts should succeed (β ) or fail (β).
# Check execution result
near view outlayer.testnet get_request '{"request_id": 1}'
# Check worker logs
docker logs offchainvm-worker
# Check keystore logs
docker logs offchainvm-keystorecd tests/
# Store secret and send to chain immediately
./01_store_secrets.sh 1 --send
# Output:
# π Seed: github.com/user/repo:owner.testnet:main
# β
Got pubkey: a1b2c3d4...
# π Encrypting via keystore...
# β
Encrypted successfully
# π€ Sending transaction to chain...
# β
Transaction sent!
# Test execution and send immediately
./02_request_execution.sh 1 --send
# All transactions will be sent automatically!# Just generate commands for test 2
./01_store_secrets.sh 2
# Output shows command - copy and run manually
near contract call-function as-transaction \
outlayer.testnet \
store_secrets \
json-args '{"repo":"..."}' \
...# Generate commands for tests 1, 3, 6
./01_store_secrets.sh 1 3 6
# Or send all immediately
./01_store_secrets.sh 1 3 6 --send# Check what test 10 will do
source tests/test_config.sh
print_test_info 10
# Output:
# Test 10 Configuration:
# Type: Logic AND
# Pattern: *.testnet
# Min NEAR: 5000000000000000000000000 yoctoNEAR (5 NEAR)
# Should ALLOW: rich.testnet
# Should DENY: poor.testnet bob.near// Match *.testnet accounts
"AccountPattern": "*.testnet"
// Match accounts starting with 'a'
"AccountPattern": "a*"
// Match accounts with 10+ characters (9 '?' + '*')
"AccountPattern": "?????????*"
// Match accounts ending with '.near'
"AccountPattern": "*.near"// AND - both conditions must pass
"Logic": {
"And": [
{"AccountPattern": "*.testnet"},
{"NearBalance": {"min": "5000000000000000000000000"}}
]
}
// OR - either condition can pass
"Logic": {
"Or": [
{"Whitelist": ["allowed1.testnet"]},
{"AccountPattern": "*.testnet"}
]
}
// NOT - inverted condition
"Logic": {
"Not": {
"NearBalance": {"min": "1000000000000000000000000"}
}
}
// Complex nested logic
"Logic": {
"And": [
{
"Logic": {
"Or": [
{"Whitelist": ["allowed1.testnet"]},
{"AccountPattern": "*.testnet"}
]
}
},
{"NearBalance": {"min": "5000000000000000000000000"}}
]
}| Test | Account | Expected | Output Field | Reason |
|---|---|---|---|---|
| 1 | alice.testnet | β SUCCESS | secret_found: true |
AllowAll |
| 2 | allowed1.testnet | β SUCCESS | secret_found: true |
In whitelist |
| 2b | bob.testnet | β FAIL | secret_found: false |
Not in whitelist |
| 3 | alice.testnet | β SUCCESS | secret_found: true |
Matches *.testnet |
| 3b | bob.near | β FAIL | secret_found: false |
Doesn't match *.testnet |
| 4 | alice.testnet | β SUCCESS | secret_found: true |
Starts with 'a' |
| 4b | bob.testnet | β FAIL | secret_found: false |
Doesn't start with 'a' |
| 5 | longaccount.testnet | β SUCCESS | secret_found: true |
10+ chars |
| 5b | bob.near | β FAIL | secret_found: false |
< 10 chars |
| 6 | rich.testnet | β SUCCESS | secret_found: true |
>= 5 NEAR |
| 6b | poor.testnet | β FAIL | secret_found: false |
< 5 NEAR |
| 7 | holder.testnet | β SUCCESS | secret_found: true |
>= 1000 tokens |
| 8 | nftowner.testnet | β SUCCESS | secret_found: true |
Owns token #123 |
| 9 | nftowner.testnet | β SUCCESS | secret_found: true |
Owns any NFT |
| 10 | rich.testnet | β SUCCESS | secret_found: true |
*.testnet AND >= 5 NEAR |
| 10b | poor.testnet | β FAIL | secret_found: false |
*.testnet but < 5 NEAR |
| 11 | alice.testnet | β SUCCESS | secret_found: true |
Not whitelisted but *.testnet |
| 12 | poor.testnet | β SUCCESS | secret_found: true |
NOT(>= 1 NEAR) = < 1 NEAR |
| 13 | rich.testnet | β SUCCESS | secret_found: true |
Complex nested conditions |
Before running tests, you may need to:
-
Update test scripts:
- Change
REPOto your GitHub repository - Change
CONTRACTto your contract ID - Change
OWNERto your secrets owner account
- Change
-
Adjust balances:
# Transfer to rich.testnet near send <your-account> rich.testnet 10 # Check balance near view rich.testnet state
-
Setup FT/NFT contracts (optional):
- Deploy test FT contract
- Mint tokens to
holder.testnet - Deploy test NFT contract
- Mint NFT to
nftowner.testnet - Update contract IDs in
01_store_secrets.sh
-
Monitor worker:
# Watch worker logs in real-time docker logs -f offchainvm-worker # Watch keystore logs docker logs -f offchainvm-keystore
Secrets not decrypting:
- Check keystore worker is running on port 8081
- Verify worker can reach keystore (
KEYSTORE_BASE_URLSin worker.env) - Check worker logs for decryption errors
Access denied errors:
- Verify account meets access conditions
- Check keystore logs for validation details
- Ensure NEAR RPC URL is correct for balance checks
WASM execution fails:
- Verify WASM file uploaded to correct GitHub repo/branch
- Check worker can compile/download WASM
- Verify coordinator cache settings
"SECRET not found" in output (JSON with secret_found: false):
- Secrets not decrypted (access control denied) OR
- Secrets successfully passed but WASM didn't receive env var
- Check keystore logs for access control validation
- Check worker WASI environment injection code
- Verify executor passes env vars to WASM instance
Invalid JSON output:
- Check WASM is reading from stdin and writing to stdout
- Ensure
io::stdout().flush()is called - Verify input_data is valid JSON:
{"message":"test"}