AWS from Wyn
One import, boto3-style. The official aws package resolves your credentials the same way boto3 does, signs every request with SigV4 in pure Wyn, and keeps secrets off the command line. No SDK, no OpenSSL bindings.
Requires wyn > 1.19.1 (upcoming release)
Crypto.hmac_sha256_hex and the native SHA-256 the signer relies on are on main but not yet in v1.19.1. Build from source or wait for the next release.
Install
wyn add awsOnce the package is published to github.com/wynlang/aws, wyn add aws resolves the name to that repo, clones it into the global package cache, and records it in your project's wyn.toml [dependencies]. Until then (or to hack on it locally), a checkout works the same way: clone the repo and symlink or copy src/aws.wyn next to your program - the compiler resolves import aws from the source directory first.
Whoami
import aws
fn main() -> int {
id = aws.whoami()
print("Account: " + aws.account(id))
print("Arn: " + aws.arn(id))
return 0
}Put credentials in the environment and run it:
eval "$(aws configure export-credentials --profile myprofile --format env)"
wyn run whoami.wynAccount: 123456789012
Arn: arn:aws:iam::123456789012:user/meThat is a real, verified response from a live AWS account (identifiers sanitized). Every snippet on this page ran against real AWS before it landed here.
Credentials, the boto3 way
You never hand the package a key. It resolves credentials internally, in the same order boto3 does:
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKENenvironment variables~/.aws/credentials, using the profile fromAWS_PROFILE(defaultdefault)
The region comes from AWS_REGION, then AWS_DEFAULT_REGION, then the profile's region in ~/.aws/config, with us-east-1 as the fallback. aws.region() and aws.profile() tell you what was resolved. Secrets are never printed, logged, or placed in a command line.
For SSO or assumed-role profiles, export short-lived credentials first - that is the eval "$(aws configure export-credentials ...)" line above.
S3
List buckets, write an object, read it back, list keys, clean up:
import aws
fn main() -> int {
for line in aws.s3_list_buckets().split("\n") {
print(aws.name(line) + " " + aws.created(line))
}
return 0
}import aws
fn main() -> int {
r = aws.s3_put_object("my-bucket", "notes/hello.txt", "hello from wyn")
if aws.is_error(r) {
print(r)
return 1
}
body = aws.s3_get_object("my-bucket", "notes/hello.txt")
print(body)
for line in aws.s3_list_objects("my-bucket").split("\n") {
print(aws.key(line) + " (" + aws.size(line).to_string() + " bytes)")
}
aws.s3_delete_object("my-bucket", "notes/hello.txt")
return 0
}hello from wyn
notes/hello.txt (14 bytes)s3_create_bucket(bucket) and s3_delete_bucket(bucket) (empty buckets only) round out the surface. Two current limits: s3_get_object handles text objects (a body with a zero byte truncates - Wyn strings are C strings), and list calls return the first page (up to 1000 keys).
EC2
import aws
fn main() -> int {
for r in aws.ec2_regions().split("\n") {
print(r)
}
instances = aws.ec2_describe_instances()
if instances.len() > 0 and not aws.is_error(instances) {
for line in instances.split("\n") {
print(aws.instance_id(line) + " " + aws.state(line) + " " + aws.instance_type(line))
}
}
return 0
}ap-south-1
eu-north-1
...
us-west-2
i-0abc123def456789a running t3.microErrors
Every call returns a string. A failure is a string starting with error: that carries the AWS error code and message - check it with aws.is_error:
import aws
fn main() -> int {
body = aws.s3_get_object("my-bucket", "missing.txt")
if aws.is_error(body) {
print(body) // error: NoSuchKey - The specified key does not exist.
return 1
}
print(body)
return 0
}List results are newline-separated lines with tab-separated fields, read through accessors like aws.name(line) and aws.key(line). When cross-module struct support lands in the compiler, these grow into real Bucket/Object/Identity types without breaking your call sites.
How it works under the hood
Every AWS request carries an Authorization header computed with AWS Signature Version 4: a canonical rendering of the request is hashed, wrapped into a string-to-sign, and signed with a key derived from your secret by chaining four HMAC-SHA256 rounds (date, region, service, aws4_request). The package does all of this in pure Wyn with the native Crypto.hmac_sha256 / Crypto.hmac_sha256_hex builtins - no subprocess, no OpenSSL - and its signer is pinned by the AWS documentation's published signature example in the test suite. Read the source: src/aws.wyn - the signer is about 60 lines.
Transport is curl (the one universally installed tool that does TLS well), driven by a config file created with 0600 permissions before any content is written. The signed headers - session token included - go in the file; only the file path appears in argv, so credentials never show up in ps output or shell history.
The SigV4 pipeline in detail
1. Canonical request - a byte-exact rendering of the request: method, path, sorted URI-encoded query, lowercase sorted headers, and the SHA-256 of the payload. One byte of disagreement with what AWS reconstructs and you get SignatureDoesNotMatch.
2. String-to-sign - the canonical request is hashed and wrapped with the algorithm name, timestamp, and credential scope (date/region/service/aws4_request).
3. Four-round signing key - SigV4 never signs with your secret directly:
kDate = HMAC("AWS4" + secret, date)
kRegion = HMAC(kDate, region)
kService = HMAC(kRegion, service)
kSigning = HMAC(kService, "aws4_request")The intermediate digests are raw binary, and Wyn strings are C strings, so the digests travel between rounds as hex: Crypto.hmac_sha256 returns hex, and Crypto.hmac_sha256_hex takes its key as hex and decodes it internally.
kdate = Crypto.hmac_sha256("AWS4" + secret, datestamp) // hex out
kregion = Crypto.hmac_sha256_hex(kdate, region) // hex key in
kservice = Crypto.hmac_sha256_hex(kregion, service)
ksigning = Crypto.hmac_sha256_hex(kservice, "aws4_request")
sig = Crypto.hmac_sha256_hex(ksigning, string_to_sign)4. Authorization header - the signature plus your access key ID and scope, e.g. AWS4-HMAC-SHA256 Credential=AKIA.../20260722/us-east-1/sts/aws4_request, SignedHeaders=..., Signature=....
Both HMAC builtins are native in-process HMAC-SHA256 (RFC 2104) over a native SHA-256 (FIPS 180-4), validated against the RFC 4231 vectors in the compiler's test suite.
Beyond the wrapped services
The SigV4 primitives are public. Any AWS API the package does not wrap yet is a signed request away - aws.signature(...), aws.canonical_request(...), and the aws.xml_text(xml, tag) extractor are all exported, and the package source shows the exact call pattern for a new service: host, region, service name, and the service's query or body protocol.
See Also
- The aws package source - signer, transport, clients, and tests
- Crypto - SHA-256, HMAC, and friends
- Networking - the HTTP client and server builtins
- Build a CLI Tool - turn a call into a real command