8 Commits
Author SHA1 Message Date
dsql d11cefe3df build: use git+https for inter-lib deps (docker ssh limitation)
docker builds can't use git+ssh (no ssh key / agent in the build), so the inter-lib
dependency references move to git+https (repos are public, anonymous clone). pins are
unchanged in target; bump to 1.0.2 so the https dependency spec ships under a new tag.
README install lines intentionally keep the ssh form for local/dev use.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-20 22:25:06 -04:00
dsql 1e6f3bc44f fix: pin inter-lib dependencies to their v1.0.0 tags
the v1.0.0 release still pinned pre-1.0.0 sibling tags, so a fresh install dragged in
stale transitive deps. update the pin(s) to the current v1.0.x release and bump this lib
to 1.0.1 so the corrected dependency chain ships under a new tag (v1.0.0 left intact).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-17 17:46:10 -04:00
dsql 9cfbbb80ee release: 1.0.0
first stable release. pre-1.0.0 verification complete: all surviving MED regressions and
gaps resolved and independently re-fired, tree audited clean across the suite.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-09 18:53:15 -04:00
dsql bf7e50f91f fix: list tolerates a null _id; declare the cryptography dependency
A stored doc with a present-but-null _id crashed the whole list table: doc.get('_id',
'')[:16] slices None (the '' default only applies when _id is absent), so one bad doc
aborted the render - now uses (doc.get('_id') or '')[:16], mirroring revoke's finder, so
it renders a blank placeholder row. cli.py imports cryptography.exceptions.InvalidTag but
pyproject only pulled cryptography transitively via envelope_crypto - declare it directly.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-06 19:31:38 -04:00
dsql e5c3547c51 fix: a stored key doc with meta:null no longer crashes authorize/revoke
find_by_friendly, authorize.py's existing-target check, and revoke.py's
success-print all did doc.get("meta", {}).get(...) directly - the {} default
only applies when the key is ABSENT, not when its value is null (plausible in
shared mongo or a hand-edited JSON store), so a null meta raised AttributeError.
revoke's crash landed AFTER storage.delete() already succeeded, so a completed
revoke was reported as a raw traceback instead of a clean exit. list_keys._meta
already coerced correctly; that fix is now hoisted into commands/__init__.py as
doc_meta, the single source every meta-reading call site goes through. cli.py's
catch tuple also gains AttributeError as defense in depth for any future
unguarded meta access.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-06 00:16:52 -04:00
dsql 764bcb6146 refactor: derive __version__ from package metadata (single source)
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-03 17:00:00 -04:00
dsql 387830084a fix: coerce null _id to empty string in revoke fingerprint match (v0.1.7)
a key doc with an explicit `_id: null` made doc.get("_id", "") return None
instead of the default, so .startswith(prefix) raised an uncaught
AttributeError instead of the contracted clean [✘] + exit 1.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-03 16:15:53 -04:00
dsql e8ea45c15e docs: compress prose/module docstrings, em-dash->hyphen (de-bloat wave 1)
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-03 00:16:10 -04:00
16 changed files with 84 additions and 96 deletions
+4 -4
View File
@@ -13,26 +13,26 @@ authorization system and the key-document schema; the crypto primitives live in
## Install ## Install
``` ```
envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.5 envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v1.0.2
``` ```
Direct: Direct:
```bash ```bash
pip install "envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.5" pip install "envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v1.0.2"
``` ```
The base install uses a local JSON file for storage (stdlib only). For shared The base install uses a local JSON file for storage (stdlib only). For shared
dev→server storage, install the mongo extra: dev→server storage, install the mongo extra:
```bash ```bash
pip install "envelope_authorizer[mongo] @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.5" pip install "envelope_authorizer[mongo] @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v1.0.2"
``` ```
Installing pulls `envelope_crypto` (and `mongo` with the extra). After install, Installing pulls `envelope_crypto` (and `mongo` with the extra). After install,
the `authorizer` command is on your PATH; `python -m envelope_authorizer` also works. the `authorizer` command is on your PATH; `python -m envelope_authorizer` also works.
Drop the `@v0.1.5` suffix from the line above to install the latest unpinned. Drop the `@v1.0.2` suffix from the line above to install the latest unpinned.
## Trust model (read this) ## Trust model (read this)
+4 -3
View File
@@ -4,17 +4,18 @@ build-backend = "hatchling.build"
[project] [project]
name = "envelope_authorizer" name = "envelope_authorizer"
version = "0.1.5" version = "1.0.2"
description = "CLI key-authorization manager for envelope_crypto" description = "CLI key-authorization manager for envelope_crypto"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"envelope_crypto @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_crypto.git", "envelope_crypto @ git+https://git.rethinkstudios.io/rethink-public/envelope_crypto.git@v1.0.0",
"cryptography>=42.0",
"tomli>=2.0; python_version<'3.11'", "tomli>=2.0; python_version<'3.11'",
] ]
[project.optional-dependencies] [project.optional-dependencies]
mongo = [ mongo = [
"mongo @ git+ssh://git@git.rethinkstudios.io/rethink-public/mongo.git@v0.1.0", "mongo @ git+https://git.rethinkstudios.io/rethink-public/mongo.git@v1.0.0",
] ]
[project.scripts] [project.scripts]
+6 -1
View File
@@ -1 +1,6 @@
__version__ = "0.1.5" from importlib.metadata import version, PackageNotFoundError
try:
__version__ = version("envelope_authorizer")
except PackageNotFoundError:
__version__ = "0.0.0+unknown"
+8 -9
View File
@@ -2,8 +2,7 @@
`config init` runs without an existing config (it writes one); every other `config init` runs without an existing config (it writes one); every other
command loads config and resolves a storage backend first. expected failures command loads config and resolves a storage backend first. expected failures
(ConfigError, CommandError, RuntimeError from a guarded backend) print a clean print a clean `[✘] ...` line and exit non-zero - no traceback.
`[✘] ...` line and exit non-zero — no traceback.
""" """
import argparse import argparse
@@ -79,8 +78,6 @@ def main() -> int:
config_init.run(None, None, args) config_init.run(None, None, args)
return 0 return 0
except (CommandError, OSError) as error: except (CommandError, OSError) as error:
# config_init writes a file (cwd may be read-only, full, or gone) — an
# OSError must print a clean [✘] line, not a raw traceback
return _fail(str(error)) return _fail(str(error))
parser.parse_args(["config", "--help"]) parser.parse_args(["config", "--help"])
return 0 return 0
@@ -99,13 +96,15 @@ def main() -> int:
handlers[args.cmd](config, storage, args) handlers[args.cmd](config, storage, args)
return 0 return 0
except InvalidTag: except InvalidTag:
return _fail("capability flag failed authentication tampered or wrong DEK") return _fail("capability flag failed authentication - tampered or wrong DEK")
except PyMongoError as error: except PyMongoError as error:
return _fail(f"storage backend error: {error}") return _fail(f"storage backend error: {error}")
except (ConfigError, CommandError, RuntimeError, ValueError, OSError, KeyError, TypeError) as error: except (
# OSError covers the FileNotFoundError/PermissionError/IsADirectoryError family; ConfigError, CommandError, RuntimeError, ValueError, OSError, KeyError, TypeError, AttributeError,
# KeyError/TypeError cover a structurally-malformed flag/doc (unguarded indexing ) as error:
# of ['iv']/['meta']['authorizer']/['key']) — all print a clean [✘] line, not a traceback # covers a malformed config/doc/flag (incl. KeyError/TypeError from unguarded
# indexing, AttributeError from a null field a caller assumed was a dict) -
# all print a clean [✘] line, not a raw traceback
return _fail(str(error)) return _fail(str(error))
+17 -7
View File
@@ -1,9 +1,7 @@
"""command implementations + shared crypto helpers for the authorizer CLI """command implementations + shared crypto helpers for the authorizer CLI
each command module exposes `run(config, storage, args)`. helpers here own the each command module exposes `run(config, storage, args)`. helpers here own the
recurring crypto plumbing: minting/reading the encrypted capability flag and the capability-flag and DEK-boot plumbing; none of them print key material.
boot sequence (unwrap the local DEK and arm a crypto instance). these never
print key material.
""" """
import time import time
@@ -25,7 +23,7 @@ def read_flag(crypto: EnvelopeCrypto, blob: dict) -> bool:
"""decrypt a capability flag; return the `allowed` bool """decrypt a capability flag; return the `allowed` bool
fails closed: a non-dict / unexpected plaintext reads as not-allowed rather than fails closed: a non-dict / unexpected plaintext reads as not-allowed rather than
raising a privilege gate must default to deny on a malformed flag. raising - a privilege gate must default to deny on a malformed flag.
""" """
data = crypto.decrypt_data(blob) data = crypto.decrypt_data(blob)
return bool(data.get("allowed", False)) if isinstance(data, dict) else False return bool(data.get("allowed", False)) if isinstance(data, dict) else False
@@ -53,8 +51,8 @@ def local_fingerprint(crypto: EnvelopeCrypto, config) -> str:
def boot_local(config, storage) -> Tuple[EnvelopeCrypto, dict]: def boot_local(config, storage) -> Tuple[EnvelopeCrypto, dict]:
"""unwrap the local DEK and return an armed crypto plus the local key doc """unwrap the local DEK and return an armed crypto plus the local key doc
raises CommandError if this machine has no key doc (not initialized / raises CommandError if this machine has no key doc here. never logs or
authorized here). never logs or prints the unwrapped key. prints the unwrapped key.
""" """
crypto = EnvelopeCrypto() crypto = EnvelopeCrypto()
fingerprint = local_fingerprint(crypto, config) fingerprint = local_fingerprint(crypto, config)
@@ -70,9 +68,21 @@ def boot_local(config, storage) -> Tuple[EnvelopeCrypto, dict]:
return crypto, doc return crypto, doc
def doc_meta(doc: dict) -> dict:
"""the doc's meta block as a dict, coercing a missing/null/malformed one to {}
a stored doc's `meta` can be null (shared mongo, a hand-edited JSON file) - the
dict.get(key, {}) default only applies when the key is ABSENT, not when its
value is null, so every meta-reading call site must go through this rather than
`doc.get("meta", {})` directly or it raises AttributeError on a null meta.
"""
meta = doc.get("meta")
return meta if isinstance(meta, dict) else {}
def find_by_friendly(storage, friendly: str) -> Optional[dict]: def find_by_friendly(storage, friendly: str) -> Optional[dict]:
"""return the doc whose meta.friendly matches, or None""" """return the doc whose meta.friendly matches, or None"""
for doc in storage.get_all(): for doc in storage.get_all():
if doc.get("meta", {}).get("friendly") == friendly: if doc_meta(doc).get("friendly") == friendly:
return doc return doc
return None return None
+8 -10
View File
@@ -1,17 +1,15 @@
"""`authorizer authorize` grant another machine access to the DEK """`authorizer authorize` - grant another machine access to the DEK
boots the local DEK, verifies the local key is itself an authorizer, then wraps refuses a target key that already has a record - `save` upserts by `_id`, so
the same DEK to the target public key and stores a new key doc. `--can-authorize` re-authorizing a known key would silently overwrite its flag/friendly. see
decides whether the new key may authorize others (omit it for servers). Refuses `run()` below for the full refusal contract.
a target key that already has a record — `save` upserts by `_id`, so re-authorizing
a known key (most dangerously the local key itself) would silently overwrite its
flag/friendly under a success banner instead of adding a new key.
""" """
from . import ( from . import (
CommandError, CommandError,
boot_local, boot_local,
build_doc, build_doc,
doc_meta,
find_by_friendly, find_by_friendly,
local_fingerprint, local_fingerprint,
make_flag, make_flag,
@@ -33,15 +31,15 @@ def run(config, storage, args) -> None:
if new_fp == local_fingerprint(crypto, config): if new_fp == local_fingerprint(crypto, config):
raise CommandError( raise CommandError(
"target key is the local key; authorize would silently replace the " "target key is the local key; authorize would silently replace the "
"local authorizer record use a different keypair, or `authorizer " "local authorizer record - use a different keypair, or `authorizer "
"list` if you meant to check its status" "list` if you meant to check its status"
) )
existing = storage.get(new_fp) existing = storage.get(new_fp)
if existing: if existing:
existing_friendly = existing.get("meta", {}).get("friendly", "?") existing_friendly = doc_meta(existing).get("friendly", "?")
raise CommandError( raise CommandError(
f"target key is already authorized as '{existing_friendly}'; " f"target key is already authorized as '{existing_friendly}'; "
f"authorize would silently replace that record revoke it first " f"authorize would silently replace that record - revoke it first "
f"if you intend to re-authorize it" f"if you intend to re-authorize it"
) )
@@ -1,4 +1,4 @@
"""`authorizer config init` scaffold a starter .authorizer.toml in cwd""" """`authorizer config init` - scaffold a starter .authorizer.toml in cwd"""
from pathlib import Path from pathlib import Path
@@ -29,7 +29,7 @@ def run(config, storage, args) -> None:
"""write a commented starter config; refuse to overwrite an existing one""" """write a commented starter config; refuse to overwrite an existing one"""
target = Path.cwd() / CONFIG_NAME target = Path.cwd() / CONFIG_NAME
if target.exists(): if target.exists():
raise CommandError(f"{target} already exists refusing to overwrite") raise CommandError(f"{target} already exists - refusing to overwrite")
with open(target, "w", encoding="utf-8") as handle: with open(target, "w", encoding="utf-8") as handle:
handle.write(_TEMPLATE) handle.write(_TEMPLATE)
print(f"[✔] Wrote starter config: {target}") print(f"[✔] Wrote starter config: {target}")
+4 -5
View File
@@ -1,8 +1,7 @@
"""`authorizer init` create a fresh DEK and authorize the local key """`authorizer init` - create a fresh DEK and authorize the local key
generates a new AES data key, wraps it to the local public key, and stores the generates a new AES data key, wraps it to the local public key, and stores the
first key doc marked as an authorizer (allowed: True). refuses if the system is first key doc marked as an authorizer (allowed: True).
already initialized or the friendly name is taken.
""" """
from envelope_crypto import EnvelopeCrypto from envelope_crypto import EnvelopeCrypto
@@ -14,7 +13,7 @@ def run(config, storage, args) -> None:
"""initialize the key system on this machine as the first authorizer """initialize the key system on this machine as the first authorizer
the already-initialized / duplicate-friendly checks are non-atomic (TOCTOU under two the already-initialized / duplicate-friendly checks are non-atomic (TOCTOU under two
concurrent CLIs) by design one-shot admin tool, `save` upserts by `_id`, so the concurrent CLIs) by design - one-shot admin tool, `save` upserts by `_id`, so the
worst case is a cosmetic double-init with no security consequence. worst case is a cosmetic double-init with no security consequence.
""" """
if storage.get_all(): if storage.get_all():
@@ -33,4 +32,4 @@ def run(config, storage, args) -> None:
doc = build_doc(fingerprint, wrapped, flag, config.identity, args.friendly) doc = build_doc(fingerprint, wrapped, flag, config.identity, args.friendly)
storage.save(doc) storage.save(doc)
print(f"[✔] Initialized fingerprint: {fingerprint} | friendly: {args.friendly} [authorizer=True]") print(f"[✔] Initialized - fingerprint: {fingerprint} | friendly: {args.friendly} [authorizer=True]")
+7 -19
View File
@@ -1,33 +1,23 @@
"""`authorizer list` show every authorized key in a padded table """`authorizer list` - show every authorized key in a padded table
boots the local DEK once, then for each doc decrypts its capability flag to show boots the local DEK once, then for each doc decrypts its capability flag to show
CAN_AUTHORIZE. a doc the local key cannot unwrap shows `?` rather than crashing. CAN_AUTHORIZE (`?` if unreadable here). prints only fingerprint/metadata - never
prints only fingerprint/metadata — never the wrapped key or DEK. the wrapped key or DEK.
""" """
from datetime import datetime, timezone from datetime import datetime, timezone
from . import boot_local, read_flag from . import boot_local, doc_meta, read_flag
def _can_authorize(crypto, doc) -> str: def _can_authorize(crypto, doc) -> str:
"""decrypted authority of a doc as Yes/No, or `?` if unreadable here """decrypted authority of a doc as Yes/No, or `?` if unreadable here"""
`?` covers any failure (unwrap mismatch, missing/malformed doc) so the table
always renders instead of crashing on one bad row.
"""
try: try:
return "Yes" if read_flag(crypto, doc["meta"]["authorizer"]) else "No" return "Yes" if read_flag(crypto, doc["meta"]["authorizer"]) else "No"
except Exception: except Exception:
return "?" return "?"
def _meta(doc) -> dict:
"""the doc's meta block as a dict, coercing a missing/null/malformed one to {}"""
meta = doc.get("meta")
return meta if isinstance(meta, dict) else {}
def _created(meta: dict) -> str: def _created(meta: dict) -> str:
"""format created_at as a UTC timestamp string, or '-' if absent/unparseable""" """format created_at as a UTC timestamp string, or '-' if absent/unparseable"""
raw = meta.get("created_at") raw = meta.get("created_at")
@@ -36,8 +26,6 @@ def _created(meta: dict) -> str:
try: try:
return datetime.fromtimestamp(int(raw), tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") return datetime.fromtimestamp(int(raw), tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
except (TypeError, ValueError, OverflowError, OSError): except (TypeError, ValueError, OverflowError, OSError):
# one hand-edited/legacy doc with a bad created_at shouldn't abort the
# whole table render
return "-" return "-"
@@ -50,9 +38,9 @@ def run(config, storage, args) -> None:
print(header) print(header)
print("-" * len(header)) print("-" * len(header))
for doc in docs: for doc in docs:
meta = _meta(doc) meta = doc_meta(doc)
print( print(
f"{doc.get('_id', '')[:16]:<18} " f"{(doc.get('_id') or '')[:16]:<18} "
f"{str(meta.get('friendly', '-')):<16} " f"{str(meta.get('friendly', '-')):<16} "
f"{str(meta.get('created_by', '-')):<18} " f"{str(meta.get('created_by', '-')):<18} "
f"{_created(meta):<21} " f"{_created(meta):<21} "
+8 -10
View File
@@ -1,14 +1,13 @@
"""`authorizer revoke` remove a key's authorization record """`authorizer revoke` - remove a key's authorization record
finds the target by friendly name or fingerprint prefix, refuses to revoke the finds the target by friendly name or fingerprint prefix, refuses to revoke the
local key, deletes the record, and prints an honesty warning that revoke is local key, and deletes the record. revoke is bookkeeping only - it does not
bookkeeping only — it does not rotate the DEK or scrub it from a machine that rotate the DEK or scrub it from a machine that already unwrapped it.
already unwrapped it.
""" """
from envelope_crypto import EnvelopeCrypto from envelope_crypto import EnvelopeCrypto
from . import CommandError, find_by_friendly, local_fingerprint from . import CommandError, doc_meta, find_by_friendly, local_fingerprint
_WARNING = ( _WARNING = (
"[!] Revoke removes the record only. It does NOT rotate the DEK or scrub it\n" "[!] Revoke removes the record only. It does NOT rotate the DEK or scrub it\n"
@@ -18,14 +17,13 @@ _WARNING = (
def _find_by_fingerprint(storage, prefix: str): def _find_by_fingerprint(storage, prefix: str):
"""return the single doc whose `_id` starts with the given prefix, or None """return the single doc whose `_id` starts with the prefix, or None
rejects an empty prefix (matches everything) and an ambiguous one (matches rejects an empty or ambiguous prefix instead of silently revoking the first hit.
more than one key) instead of silently revoking the first hit.
""" """
if not prefix: if not prefix:
raise CommandError("fingerprint prefix must not be empty") raise CommandError("fingerprint prefix must not be empty")
matches = [doc for doc in storage.get_all() if doc.get("_id", "").startswith(prefix)] matches = [doc for doc in storage.get_all() if (doc.get("_id") or "").startswith(prefix)]
if len(matches) > 1: if len(matches) > 1:
ids = ", ".join(d["_id"][:16] for d in matches) ids = ", ".join(d["_id"][:16] for d in matches)
raise CommandError(f"fingerprint prefix '{prefix}' is ambiguous; matches: {ids}") raise CommandError(f"fingerprint prefix '{prefix}' is ambiguous; matches: {ids}")
@@ -49,6 +47,6 @@ def run(config, storage, args) -> None:
raise CommandError("refusing to revoke the local key") raise CommandError("refusing to revoke the local key")
storage.delete(doc["_id"]) storage.delete(doc["_id"])
friendly = doc.get("meta", {}).get("friendly", "?") friendly = doc_meta(doc).get("friendly", "?")
print(f"[✔] Revoked: {friendly} ({doc['_id'][:16]})") print(f"[✔] Revoked: {friendly} ({doc['_id'][:16]})")
print(_WARNING) print(_WARNING)
+3 -3
View File
@@ -1,8 +1,8 @@
"""`authorizer verify` health-check the local crypto setup """`authorizer verify` - health-check the local crypto setup
boots the local DEK, then runs envelope_crypto's self_test against the local boots the local DEK, then runs envelope_crypto's self_test against the local
keypair. if the installed envelope_crypto lacks self_test, falls back to a keypair (falls back to a minimal in-CLI round-trip if self_test is absent).
minimal in-CLI round-trip. never prints key material. never prints key material.
""" """
from . import CommandError, boot_local from . import CommandError, boot_local
+2 -5
View File
@@ -1,11 +1,8 @@
"""TOML config loader for the authorizer CLI """TOML config loader for the authorizer CLI
resolves a `.authorizer.toml` (cwd first, then ~), expands `~` and `$ENV_VARS` in resolves a `.authorizer.toml` (cwd first, then ~), expands `~` and `$ENV_VARS` in
every path field (key paths and the JSON storage path alike), and exposes typed every path field, and exposes typed accessors. no defaults are baked in: a
accessors. no defaults are baked in: a missing required field raises a clear missing required field raises a clear error naming the field and config path.
error naming the field and the config path that was searched. the loaded config
is the only place key paths, identity, and storage live — this lib never imports
a host `config` module.
""" """
import os import os
+1 -1
View File
@@ -1,7 +1,7 @@
"""storage backend resolution for the authorizer CLI """storage backend resolution for the authorizer CLI
`resolve(config)` reads `[storage].backend` and returns the matching sync `resolve(config)` reads `[storage].backend` and returns the matching sync
StorageBackend (json or mongo). the mongo backend is guarded importing this StorageBackend (json or mongo). the mongo backend is guarded - importing this
package never requires the mongo extra; only selecting the mongo backend does. package never requires the mongo extra; only selecting the mongo backend does.
""" """
+1 -1
View File
@@ -1,7 +1,7 @@
"""abstract storage interface for authorizer key documents """abstract storage interface for authorizer key documents
a backend persists key docs keyed by `_id` (the RSA fingerprint). the interface a backend persists key docs keyed by `_id` (the RSA fingerprint). the interface
is intentionally sync the CLI is a one-shot tool; an async backend (mongo) is intentionally sync - the CLI is a one-shot tool; an async backend (mongo)
bridges to sync internally. `save` is an upsert by `_id`, never a duplicate- bridges to sync internally. `save` is an upsert by `_id`, never a duplicate-
inserting or duplicate-raising operation. inserting or duplicate-raising operation.
""" """
@@ -35,12 +35,7 @@ class JsonStore(StorageBackend):
return data return data
def _write(self, docs: List[dict]) -> None: def _write(self, docs: List[dict]) -> None:
"""atomically write the docs list (unique temp file then os.replace) """atomically write the docs list via a unique temp file then os.replace"""
a unique temp per write (tempfile.mkstemp) keeps concurrent writers from
clobbering a shared `.tmp`; the temp is cleaned up if the write fails
before replace.
"""
self.path.parent.mkdir(parents=True, exist_ok=True) self.path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp( fd, tmp = tempfile.mkstemp(
dir=self.path.parent, prefix=self.path.name + ".", suffix=".tmp" dir=self.path.parent, prefix=self.path.name + ".", suffix=".tmp"
@@ -48,13 +43,12 @@ class JsonStore(StorageBackend):
wrapped = False wrapped = False
try: try:
with os.fdopen(fd, "w", encoding="utf-8") as handle: with os.fdopen(fd, "w", encoding="utf-8") as handle:
wrapped = True # fdopen owns fd now; its close() handles it wrapped = True
json.dump(docs, handle, indent=2) json.dump(docs, handle, indent=2)
handle.write("\n") handle.write("\n")
os.replace(tmp, self.path) os.replace(tmp, self.path)
except BaseException: except BaseException:
if not wrapped: if not wrapped:
# fdopen raised before taking ownership — close the raw fd ourselves
try: try:
os.close(fd) os.close(fd)
except OSError: except OSError:
@@ -2,15 +2,14 @@
bridges the async rethink-public `mongo` lib to the sync StorageBackend: each op bridges the async rethink-public `mongo` lib to the sync StorageBackend: each op
gets its own asyncio.run (connect -> op -> close), no module-level client or gets its own asyncio.run (connect -> op -> close), no module-level client or
shared loop, so it never collides with a running loop. per-call connect is an shared loop. requires envelope_authorizer[mongo]; without it every method raises
accepted tradeoff for a one-shot admin CLI. requires envelope_authorizer[mongo]; a clear RuntimeError.
without it every method raises a clear RuntimeError.
fail-loud: ops go through the mongo lib's raw collection escape hatch (the motor fail-loud: ops go through the mongo lib's raw collection escape hatch, not the
collection, which raises), not the swallow-and-default wrapped methods this is swallow-and-default wrapped methods - this is an auth backend, so a driver error
an auth backend, so a driver error must never read as "not initialized," and a must never read as "not initialized," and a no-op upsert raises instead of
no-op upsert raises instead of silently reporting success. `close()` is silently reporting success. `close()` is synchronous on the mongo lib - not
synchronous on the mongo lib — not awaited. awaited.
""" """
import asyncio import asyncio