Repository navigation
Releases: afit21/rnfs
Release list
rnfs 0.2.1
Fixes, and additions to the local API for apps. No wire-format or CID changes: these nodes, 0.2.0, 0.1.6, 0.1.5,
0.1.4 and 0.1.3 nodes work together. Every API addition is additive: older clients see the answers they knew, with
new fields beside them, and GET /api/capabilities lists gc_dry_run, config_dry_run, add_origin,
sealed_tar, ping_details, streamed_answers and restart.
Added (the local API, rnfs.Client/rnfs.Node, and the CLI where it helps):
- A peer's real last sign of life:
GET /api/peers(Peer.last_seen) gives when each peer last showed it was
up (an answer, a request of its own, a link, a fresh announce), as wall time; it is whatonlineis decided
from.last_heardis no such sign: any node's path response refreshes it. - Why a ping failed:
GET /api/ping(ping_info(),Ping) addsreason(no_path,no_identity,
wrong_identity,not_member,link_refused,link_closed,link_timeout) and the path'shops,
interfaceandpath_since;stream=1answers in JSON lines, telling of each step (path, identity, link) as it
begins.rnfs pingsays why. - Providers as they are found:
GET /api/providers?stream=1sends each provider as the lookup finds it
(providers(on_provider=)); closing the connection ends the lookup.rnfs providersprints them as they come. - A stream add's origin:
POST /api/add/streamtakesorigin(add(..., origin=),add_tar(..., origin=)),
whatpin lssays it was added from, so an app's adds aren't all(stdin)and the user's label stays free. - Sealed folders from a tar upload:
POST /api/add/stream?format=tarwithto/to_selfseals the folder the
tar holds asadd --to <folder>seals one (nameis the folder's), re-packing its members as it streams: nothing
but the sealed bytes is stored. - The shared folder's figures:
GET /api/status(Status.shared_folder) gives its path, entries and their
bytes, whether a scan runs, when the last ended, why the folder is refused or the last scan failed, and the
entries that failed to import;rnfs statussums it up. - What
gcwould free:POST /api/gcwithdry_run(gc(dry_run=True),rnfs gc --dry-run) changes nothing
and says what it would remove, and the stored bytes split by what keeps them: pins, cached fetches, operations in
progress, nothing, with what the state database takes. A client checks thegc_dry_runcapability first: an
older daemon would ignoredry_runand collect. - Settings checked without writing them:
POST /api/config/setwithdry_run(config_set(..., dry_run=True)) makes every check and says what would apply now and what at the next start (capability
config_dry_run, for the same reason). - How the daemon runs:
daemon.jsongainsstarted_by(systemd,launchd,windows_task,terminal,
unknown, or what the program starting it setRNFS_STARTED_BYto),service,sandbox(flatpak) and
can_restart. - Restart through the API:
POST /api/restart(restart(wait=),rnfs restart) stops the daemon as SIGTERM
does and starts it again with the same command line, so settings that need a restart apply to a daemon started
in a terminal as much as one a service runs. On POSIX it starts again in place, keeping its process ID; on
Windows the task wrapper (contrib/install-windows-task.ps1, which now setsRNFS_STARTED_BY) restarts it. - The routing table:
GET /api/routing(routing(),Routing) gives the buckets and each node's times and
failed queries, the nodes avoided as unreliable, and the bans with when they end and why. - The daemon's log:
GET /api/log(log()) gives its recent lines, rnfs's and Reticulum's as its console shows
them (the last 2000, in memory), so an app shows the log of a daemon it didn't start. - Uploads are jobs: a stream add or import (
POST /api/add/stream,/api/import/stream) is listed in
GET /api/jobswhile it runs (stream,stream_name), with the bytes read, and cancelling it stops it (409,
codecancelled). Uploads are never saved or resumed, and take no job slot. - More events:
config(a setting changed through the API, with what applied, what needs a restart and every
setting pending one),name_sync(a sync started, stopped or changed),apps(added, removed, requested,
denied) andstatus(the status fields that changed, checked every 30 s), so a status page needn't poll. - Pairing: an app with no token asks for one (
POST /api/apps/request;rnfs.Client.pair()), shows its user
a 6-digit code, and the user approves it where that code shows too (rnfs app requests,rnfs app approve,
deny; an app told by therequestedevent). The app collects its token itself, once; the approver never sees
it. - Reticulum's interfaces, read-only:
GET /api/stats(Stats.reticulum_interfaces,rnfs stats) gives each
interface's state, mode, bitrate, bytes and speeds (the shared instance's when attached to one). rnfs still never
edits Reticulum's config. - The code checked at approval:
POST /api/apps/approvetakescode(ApiConnection.app_approve(id, code=),
rnfs app approve <id> --code), andrnfs app approveat a terminal asks for the code the app shows. Without it
the request is approved unchecked, as before.
Changed:
- Sizes are written in decimal units everywhere (KB, MB, as
rnfs jobs,rnfs peersand the log already
wrote them):rnfs status's record store line was in KiB and MiB. One function writes them all
(rnfs.units.human_size). - The API and the gateway also listen on
[::1](AUD-H2, below), on the same ports: a port another program
holds on[::1]stops the daemon, as one taken on127.0.0.1does. Only a host without IPv6 runs on
127.0.0.1alone.[::1]is still not an acceptedHost. pin add, remote pins, following,dir setanddir rmstop atmax_output_entriesentries (AUD-M6, below),
asgetandexportdid: pinning a folder of more entries (100,000 by default) is refused; raise the setting to
pin it.- Pairing: a name may have several requests waiting, each with its own code; approving one denies the others,
and a name already taken is refused when approved, not when asked. A full table of 32 requests drops the oldest
instead of refusing new ones (AUD-M5, AUD-L1, below). - The network size estimate is sampled by lookups the node chooses (AUD-M1, below): up to 8 lookups of random
keys after joining, then one while the estimate is short of samples or an hour old (paced on slow nodes),
besides publishing's.
Security (the 0.2.1 audit, AUD: each fix has a regression test in tests/security, listed in its README):
- Links waiting out upload pacing are bounded again (AUD-H1, a 0.2.1 regression; specification section 3): an
answer that fits one packet took no response slot even while it waited for upload pacing, holding its link's
/blocksslot, and a link with one in progress is never closed to make room. Somax_incoming_linkslinks
looping on tiny paced answers (about 8 KB/s) had every new incoming link refused. An answer that must wait now
takes a pool slot, as in 0.2.0, and one outside the pools ends when its link closes. - The gateway's sites and the API can't be taken over on
[::1](AUD-H2, section 11): the daemon listened on
127.0.0.1 only, but browsers and curl try*.localhostandlocalhoston ::1 first, so another local user
listening on[::1]:<port>while the daemon ran could serve their own pages in every site's origin and relay
what clients sent. The API and the gateway now also listen on[::1]:<port>, sharing their connection cap. addcan't be led into the rnfs dir by a path swapped while it runs (AUD-H3, section 8):addchecked the
path against the rnfs dir, then opened it again following a link, so a file or folder swapped for a link to
api_token, the identity or a name key between the two was added, pinned and provided. The check is made again
on what was opened (its own final path), for plain, sealed and only-hash adds.- A node asked first can't hold off the provider search by dripping its answer (AUD-H4, section 7): once a probe
runs pastdeadline_slack(longer over slow links) the search starts, and a provider it finds is asked for the
block as end-game duplication does; a probe waits for its link's/blocksslot no longer than that, so another
fetch's dripping response can't make later fetches fail without searching; and a remembered provider whose probe
was cancelled, or ran that long without sending anything, is forgotten. - Searches no longer feed the network size estimate (AUD-M1, section 6): anyone can make a node search keys of
their choosing, through the gateway for one, and 9 keys inside one tight cluster of identities inflated the
estimate up to a million times, which turned the density check off. Samples come from publish lookups and from
lookups for random keys; a key the density check flags loses its sample, and widened lookups take none. - A fetch's searches share one
/findcap again (AUD-M2, section 6): runs of a lookup together send at most
8 × K/find, and a run counts as having found providers only if the fetch could use one. Sybils listing
made-up providers in every answer had one fetch send 66,426/findin 15 s. - Partition mode's probe doesn't go to the server that named the failing identities (AUD-M3, section 6) while
another target exists, and prefers senior servers; a probe left unanswered doesn't enter partition mode if
another off-island server answered or announced itself while it was out. - Links posing as slow can't hold the slow pool for good (AUD-M4, section 3): two links on a fast path that kept
their MTU at 500 and asked again the moment each response ended held both slow-pool slots, and a LoRa requester
...
rnfs 0.2.0
The feature-complete release: the local API and the library for companion apps; providing at scale (sweep
providing, provider records on disk within a byte budget, storage tiers against floods); finding content when
identities are generated next to its key (the density check, disjoint lookup paths, the announce first-seen
log); LoRa islands (fast nodes sparing them, two-level records, partition mode); and new features (per-app API
tokens, encrypted folders, NomadNet add, gateway sites, name sync, name key backups, remote pin status, network
status, files inside folders provided by their own CIDs). LXMF messaging moved out of rnfs into an app of its own
(Removed, below). The wire format gained one optional request, /range (Added, below), which these nodes send
only to nodes that answer it; nothing else on the wire changed, and CIDs are unchanged: these nodes, 0.1.6, 0.1.5,
0.1.4 and 0.1.3 nodes work together (tests/interop.py against 0.1.6 passes). The local API stays compatible but
for LXMF: every route, field and status of 0.1.6 is unchanged but LXMF's, which are gone, and error bodies gain a
field. The upgrade of an <rnfs dir> is automatic and one-way.
Upgrade notes:
- State: the state database gains tables (
pin_times,pin_labels,name_exports,apps,block_sizes,
routing,peer_scores,announce_first_seen,provider_standing,remote_pins_asked,remote_pins_served,
name_syncs,name_sync_files,published_heal,name_announces,remote_pin_retries,range_support,
publish_items,publish_stores); an older rnfs can't open it afterwards. The publisher'spublishedand
published_earlierrows move intopublish_items(a row per provided item), leaving those tables empty. Pins
made before the upgrade have noaddedtime, name keys made before it count as never exported (rnfs status
lists them untilrnfs name exportis run), and remote pins asked before it have no status entry. - LXMF: rnfs no longer sends or receives LXMF (Removed, below). Pins of links auto-fetched over LXMF change
source fromlxmf:<address>toapp:lxmf:<address>(pin lsshows the new one), an app calledlxmf's, so
they stay pinned for an LXMF app to take over; nothing is released by the change, and an olderpins.json
imported at the upgrade gets the same rewrite.storage/lxmf_identity,lxmf/and the inbox and outbox in
rnfs.dbare kept, untouched.lxmf_*settings inrnfs.tomlare ignored with a warning. If you rely on
rnfs sendandrnfs inbox, don't upgrade before an LXMF app is there: the upgrade can't be undone. - Provider records: the upgrade gives each publisher of the records a node stores a standing
(provider_standing, and indexes onprovider_records): Earned if it was first seen at least 3 days before its
latest store and refreshed a record outside the newcomer pool a day or more after storing it, else Newcomer. The
newcomer pool's records are no longer pooled, and the remembered departed and evicted publishers are dropped. max_recordsis retired: amax_recordsleft inrnfs.toml(or given tornfs.Node) is ignored with a
warning instead of stopping the daemon;record_budget(bytes) replaces it.rnfs config set max_recordsis
refused as an unknown setting;rnfs config unset max_recordsremoves it.- Blocks: they move from
blocks/<cid>into 1,024 subdirectories,blocks/<xy>/<cid>(the CID's third- and
second-to-last characters), at the first start, which takes a while on a large store (a rename per block; the API
answers once it is done: 6 minutes for 883k blocks on a desktop SSD, and 6 for 857k on a 2-vCPU cloud VM, of which
110 s the move; the log says how far the listing and the move got about every 30 seconds, and, if they take over 10
seconds, the listing of every block that follows and the first working out of what the node provides). Each start
moves whatever is left, so an interrupted upgrade loses nothing, and a block Windows won't let rnfs move stays
readable until a later start moves it. An older rnfs doesn't find the moved blocks. A subdirectory ofblocks/that
is a link, not a directory or another user's stops the start. The state database gains ablock_sizestable, every
stored block's size, which the first start fills by listing the blocks. <rnfs dir>/daemon.json: the daemon writes it while it runs. Clients from 0.2.0 on read the API port from
it first, so one that can't read a newer daemon'srnfs.tomlstill finds it.- Settings:
rnfs.tomlmay now be rewritten by the daemon (rnfs config set,POST /api/config/set). Only the
assignments changed are replaced; comments and other lines are kept. Settings whoseapi_portequals a
non-zerogateway_portare now refused at start (such a daemon could never listen on both). - Routing state: the state database gains tables for the routing table (
routing), which is now kept across
restarts, for local scores (peer_scores) and for the announce first-seen log (announce_first_seen). - Files and folders inside pins are provided (
provide_entries, below, on by default): after the upgrade a node
that isn't slow also publishes the CID of every file and folder inside its own complete pins (not remote pins or
followed names), up toprovide_entries_max(100,000), so its first rounds publish more keys. Set
provide_entries = falseto keep 0.1.6's behaviour.
Security:
- A provider can't keep a fetch alive for ever by alternating unreachable and unestablished links (REL-1): a "no
path" result (retried once, a minute later) reset the count of lost requests, and a link that didn't establish
(lost) re-armed the retry, so a provider whose results alternated the two, as a node ignoring link requests and
answering every other path request gets, never failed, and its fetch, thread and searches lived as long as it
liked. Only an answer, or an outright failure, resets the lost count now. - A request for nothing served can't cancel a download (REL-2, specification section 3): a
/blocksor/range
request took a response slot, displacing a response in progress when the pool was full, before the node asked
whether it serves anything requested. One ~100-byte request for a made-up CID from a fresh link cancelled an honest
slow-link download, and repeating it broke every one. Such a request is now answered at once, with the same answer
as before, without a slot. - A request of the wrong shape is dropped before RNS indexes it (REL-3): RNS reads a request as
[time, path hash, data]unchecked, so one whose plaintext was an int, a short list or had a list as its path
raised on RNS's request thread, a traceback in the log for every such packet, before rnfs's per-link rate limit
saw it. The guards now drop it with a throttled verbose line; no handler could have served it. - A tar member of negative size is refused by rnfs itself (REL-4): Python's tarfile parses a negative size from
a base-256 field or a paxsize=record, and only interpreters with its own offset check (3.12.3 and later fixes)
refused such a stream; rnfs supports 3.11. Sealed folders and tar adds now refuse it on every interpreter. - A file in the rnfs dir can't be added through another name for it (REL-8, completing REV1): the check that
add
refuses the rnfs dir compared paths, and a file added as the add's root isn't walked (the walks skip the rnfs dir by
inode), so on a case-insensitive disk (macOS:~/.RNFS/api_token) or through a bind mount an app could add the
CLI's token, the node identity or a name key and read it back. The file's directories are now compared by device and
inode too, up to the rnfs dir'sfiles/. - Gateway documents are revalidated, not cached as immutable (REL-9, specification section 11): every
/rnfs/
response wasimmutablefor a year, though its Content-Security-Policy followsgateway_scripts, so a page cached
while scripts were allowed kept running them in the browser after they were turned off. HTML, SVG, other XML, PDF
and listings now carryCache-Control: public, no-cache(the browser keeps them and asks again, a304from the
node), and a304carries noCache-Control. Other files stay immutable. - Withdrawing and restoring the same records spends no write budget (REL-13, completing REV2): a withdrawal
spent a row of the provider write budget and a restore before the next save spent nothing but gave nothing back,
though neither row is then written. One identity looping over its own 27 records spent the budget other publishers'
new records need, while writing almost nothing, and every other Newcomer's new record was refused (an Earned one's
too, from an Earned attacker). The row a withdrawal spent is now given back when it is undone. - A keeper can't make this node keep and serve a name below the sequence it knows (REL-14): the keeper store's
rollback check, and the sequence a keeper query checks answers against, left out the name floor (the highest
sequence of a name resolved before, kept after its record left the table). A lying keeper's old, validly signed
record of such a name, once kept, was served onward by this node. The floor counts now. - A flood of fresh identities can't set the clock-offset estimate (REL-15, specification section 9): it was the
median of the 128 senders heard most recently, so 128 announces of fresh identities emitted 2 h ahead pushed every
honest sender out, and the node warned that its correct clock was 2 h behind. Once 5 senior servers were heard the
estimate is theirs alone, and the table forgets other senders first. - What fuzzing the gateway, the API, settings and tar streams found (REL-17): a web page could set any
Content-Type, control characters included, with the gateway's?filename=data:<type>,(the type is now taken from
the name's exte...
rnfs 0.1.6
Security fixes, maintainability work and performance work. No wire-format or CID changes: these nodes, 0.1.5,
0.1.4 and 0.1.3 nodes work together. The upgrade of an <rnfs dir> is automatic and one-way.
Upgrade notes (each is described in full below):
- Settings and the rnfs dir: a symlinked
rnfs.toml, or one or a subdirectory owned by another user, stops the
start; a loose one (such as a 0664rnfs.tomlor 0775blocks/) is made owner-only, with a warning. - The shared folder: one that leads into the rnfs dir (or a directory holding it) is refused, and its
pins with sourcefolderare released. - Following names: a followed name's target may take at most
follow_quota(new, default 1 GiB). - Deny lists: while the deny list has
sha256:entries, a file repeating its content more than 16 times
(beyond 64 MiB) can't be read or imported (451). - Local API:
POST /api/lxmf/inbox/getanswers 404 for a name link whose name can't be resolved, unless the
request setsuse_sent_cid. - Gateway: browsers that send no
Sec-Fetch-Site(Safari before 16.4, Firefox before 90) are treated as web
pages, so?format=downloads give them 409. rnfs inbox: its message line is reordered (address and trust first, the display name last, quoted).- install.sh: it stops when the GitHub CLI can check provenance but isn't logged in; log in with
gh auth login, or setRNFS_REQUIRE_ATTESTATION=0. - Flatpak: after updating, the Flatpak no longer sees the rest of your home folder or any device. Grant what
you used again:flatpak override --user --device=all io.github.afit21.rnfsfor an RNode or other serial
interface, andflatpak override --user --filesystem=<folder> io.github.afit21.rnfsfor other folders,
~/.config/reticulumincluded if your Reticulum config is there. - State: the state database gains a table (the shared folder's file cache); an older rnfs can't open it
afterwards.
Security:
- The release workflow's Flatpak build no longer uses the GitHub Actions cache. Any workflow run on the default
branch can write that cache, and a poisoned one would have gone into the attested.flatpakbundle. rnfs inbox getno longer quietly fetches the CID a sender put in a name link when the name can't be
resolved. The sender chose that CID and nothing ties it to the name, so any sender could pose as a
well-known name. It now fails before writing anything, unless you pass--use-sent-cid; then the result
carries a warning. Auto-fetch (trusted senders only) keeps the fallback, and records it.- A
/blocksrequester that rejects or cancels a response before a quarter of it was sent now cools down for
blocks_cooldown, per link and per identity, as one that rejected it unsent already did. Before, a requester
could take the first window of each response, cancel it and ask again at once, so the node built a whole
response per window. Closing the link still starts no cooldown, so an honest reader that gives up early isn't
held back. - While the deny list has file hashes (
sha256:entries), a file that declares more than 16 times the bytes of
its distinct chunks, plus 64 MiB, is refused (451) before it is hashed. Hashing reads every repeat of
repeated content, so six blocks (128 KB) declaring 2^56 bytes made a one-bytecat, a gateway range request,
?format=archiveor animporthash for about a year, holding a gateway slot.catand the gateway now fetch
a file's whole DAG before checking it (unless its hash is already known), somax_download_sizeand the disk
reserve apply as they do forget. - A gateway HEAD (or a range not starting at byte 0) of a stored file the deny list blocks answers 451, like
GET. Before, it took its Content-Type from a denied stored chunk, and a HEAD of a file with a denied hash
answered 200. - A name whose owner is offline can still be resolved through its keepers when Sybil nodes near its keeper key
stall the DHT lookup. They could answer/findslowly with ever closer IDs, so the search used all of
lookup_timeoutand the keepers then had one second to answer. Each batch of/namequeries now has its own
budget (87.5 s at the defaults); the search for keepers stops once some are known andlookup_timeout/ 5 has
passed, and resumes if their answers don't settle the name. - A neighbour replaying an older, validly signed name announce no longer makes its record look current: rnfs
reads the replayed announce's own emission time from RNS's announce cache, treats one older than the path's
(or carrying another record) as a suspected replay, gives its real age and asks keepers. - A provider can no longer hold a fetch for hours by dripping a
/blocksresponse: once nothing is left to
request and a provider is idle, a batch in flight longer thandeadline_slackis asked of it too, the first
verified copy wins, and the slow request's link is closed (left to finish if another fetch uses it) without
counting as a stall. Simulated: 20.6 → 3.3 s. getof a directory can no longer be redirected by someone who can rename inside the output swapping one of
its directories for a link or junction (or another directory) while it is written. On Linux and macOS the
output being written is private (0700, setgid kept) and must start empty, every entry is created relative to
its parent's descriptor without following links, and each directory is checked by device and inode before
anything is written into it. On Windows it gets a private access list while it is written, and each file's
final path must be inside it. A swap fails the get with "output was changed while being written".- On Linux and macOS,
rnfs.tomlis now checked before its settings are used, as on Windows: a symlink, a FIFO
or anything else that isn't a regular file is refused, and so is a file another user owns; one others may
access is made 0600, with a warning; at most 1 MiB of it is read. Its settings choose the Reticulum config, and
with it programs rnfs starts, so anyone who could write it could run code as you. Reticulum'sconfiggets the
same check whenreticulum_diris the rnfs dir. - The rnfs dir's subdirectories (
state,blocks,storage,names,lxmf) are checked at every start: a
symlink, a non-directory or one another user owns stops the start before Reticulum starts; a loose one is made
0700, with a notice; and if others could write to it, entries another user owns are reported. Before, a user
who could once write in a loose rnfs dir kept their access toblocks/andnames/after it was tightened. - The shared folder is no longer scanned when
files/leads into the rnfs dir. Before, a symlinkedfiles/that
led tostorage/ornames/(or that someone who could write to a directory above it retargeted there)
imported, pinned and provided the node's identity or the name keys, and one that led to the rnfs dir or a
directory above it shared everything in it. A folder that resolves, as opened, to the rnfs dir, a directory
holding it, or anything insidestorage/,names/,state/,blocks/orlxmf/is now refused with a
warning and scanned as empty: nothing is imported, and every pin with sourcefolderis released, so keys a
0.1.5 node already shared that way stop being provided at the first scan after the upgrade. On Linux and macOS,
a directory above the folder that other users can change gets a warning, once. - A node no longer reads request packets sent to it on
rnfs.dhtlinks it opened. That end of a link has no
request handlers, yet RNS decoded each request (up to the link's packet size) and started a thread for it
before finding nothing to run, for any node rnfs linked to. LXMF and host-program links are unchanged. - An explicit
max_upload_ratenow applies to every/blocksresponse, including one that fits in one packet.
Such responses never give the link a rate estimate, and the cap applied only once there was one, so on a
large-MTU path (RNS's TCP interfaces) a requester asking only for single-packet answers was never paced. Waits
of up to 5.6 s are held and longer ones refused as busy, as for larger responses. The default cap (half the
rate estimate, on slow links) is unchanged. - The wait for a previous
/blocksresponse's proof uses the round-trip time the requester claims only if it is
a finite, non-negative number, and still at most 5 s. Before, a negative claim gave an honest proof no grace,
and a value that wasn't a number made the request handler fail. install.shties a release install to the tag it asked for: whereghsupports them, the provenance check
passes--source-ref=refs/tags/<tag>and--deny-self-hosted-runners, and only anrnfs-<version>-*.whlof
that tag's version is installed. Before, an older release's genuinely attested sdist, renamed to the new
release's file name, passed both checks.install.shstops when the GitHub CLI hasgh attestationbut isn't logged in (root installs usually hit
this), instead of quietly checkingSHA256SUMSonly. Log in withgh auth login, or set
RNFS_REQUIRE_ATTESTATION=0to install with the checksum only. Withoutghit still warns, and the README now
says thatcurl | shwithoutghtrusts whoever can write the release's files.- The Flatpak's sandbox is narrowed to
~/.rnfs,~/.reticulumand the Downloads folder, with no devices
(before: your whole home folder and every device, so a compromised daemon could write~/.bashrc). See the
upgrade note below. install.shwarns whenRNFS_SOURCEis, or names, a plainhttp://URL, and the.deb's postinst
byte-compiles withpython3 -I.- The file-index-node NomadNet page renders at most once a minute, one visit at a time, and serves its cached
page meanwhile (before, every visit ranrnfs pin ls). examples/rnfs_share.pyandrnfs.Client's log escape names and...
rnfs 0.1.5
rnfs 0.1.4
rnfs 0.1.3
rnfs 0.1.2
rnfs 0.1.1
rnfs 0.1.0
Full Changelog: https://github.com/afit21/rnfs/commits/v0.1.0