aweshare 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.ai.md +1 -1
  2. package/README.md +39 -9
  3. package/apps/hub/dist/auth.js +0 -12
  4. package/apps/hub/dist/auth.js.map +1 -1
  5. package/apps/hub/dist/backups.d.ts +43 -0
  6. package/apps/hub/dist/backups.js +122 -0
  7. package/apps/hub/dist/backups.js.map +1 -0
  8. package/apps/hub/dist/cli.js +353 -121
  9. package/apps/hub/dist/cli.js.map +1 -1
  10. package/apps/hub/dist/config.d.ts +2 -0
  11. package/apps/hub/dist/config.js +1 -0
  12. package/apps/hub/dist/config.js.map +1 -1
  13. package/apps/hub/dist/consumer.d.ts +3 -0
  14. package/apps/hub/dist/consumer.js +164 -54
  15. package/apps/hub/dist/consumer.js.map +1 -1
  16. package/apps/hub/dist/db.d.ts +7 -10
  17. package/apps/hub/dist/db.js +20 -22
  18. package/apps/hub/dist/db.js.map +1 -1
  19. package/apps/hub/dist/http.d.ts +7 -1
  20. package/apps/hub/dist/http.js +6 -1
  21. package/apps/hub/dist/http.js.map +1 -1
  22. package/apps/hub/dist/invites.js +9 -2
  23. package/apps/hub/dist/invites.js.map +1 -1
  24. package/apps/hub/dist/server.d.ts +7 -3
  25. package/apps/hub/dist/server.js +23 -8
  26. package/apps/hub/dist/server.js.map +1 -1
  27. package/apps/hub/dist/table.d.ts +1 -3
  28. package/apps/hub/dist/table.js +2 -6
  29. package/apps/hub/dist/table.js.map +1 -1
  30. package/apps/hub/dist/tunnel.d.ts +0 -2
  31. package/apps/hub/dist/tunnel.js +2 -16
  32. package/apps/hub/dist/tunnel.js.map +1 -1
  33. package/package.json +1 -1
  34. package/packages/producer-core/dist/catalog.js +9 -1
  35. package/packages/producer-core/dist/catalog.js.map +1 -1
  36. package/packages/protocol/dist/models.js +6 -1
  37. package/packages/protocol/dist/models.js.map +1 -1
package/README.ai.md CHANGED
@@ -241,7 +241,7 @@ Remind the consumer: prompts and responses transit the hub in plaintext — only
241
241
  ```
242
242
  Consumers redeem codes themselves (`aweshare consumer join`); producers via `aweshare producer join`.
243
243
  3. Administering a remote hub: run admin commands on the server (`ssh` + CLI, or `docker exec aweshare-hub aweshare hub ...`), or locally with `AWESHARE_HUB_URL=https://<hub-host>` plus the admin-token file in a local data dir — otherwise they fail with "no admin token" / connection refused.
244
- 4. Guardrails you can tune on request: per-consumer `aweshare hub limits NAME [--rps N] [--tpm N] [--max-total-tokens N] ...`, suspension `aweshare hub admin invite revoke N` / `restore N` (reversible, invite-keyed).
244
+ 4. Guardrails you can tune on request: per-consumer `aweshare hub limits NAME [--rps N] [--tpm N] [--max-total-tokens N] ...`, per-alias failover queues `aweshare hub backups ALIAS --add OTHER/BACKUP` (file: `config.backups.toml`, hot-reloads), suspension `aweshare hub admin invite revoke N` / `restore N` (reversible, invite-keyed).
245
245
  5. Security notes for the operator: keep :8787 off the public internet behind TLS; a redeemed consumer key can call **every** offering on the hub; tokens/codes stored plaintext-recoverable (`hub list invites --reveal` / `--token`) means a data-dir leak exposes identities — guard it.
246
246
 
247
247
  ---
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  <a href="https://ko-fi.com/mugpeng"><img src="https://img.shields.io/badge/Ko--fi-Buy%20me%20a%20coffee-FF5E5B?style=flat-square&logo=ko-fi&logoColor=white" alt="Ko-fi"></a>
15
15
  </p>
16
16
  <p>
17
- <a href="https://github.com/wehuman01/aweshare-source/releases"><img src="https://img.shields.io/badge/version-0.7.3-7C3AED?style=flat-square" alt="Version"></a>
17
+ <a href="https://github.com/wehuman01/aweshare-source/releases"><img src="https://img.shields.io/badge/version-0.7.4-7C3AED?style=flat-square" alt="Version"></a>
18
18
  <a href="https://github.com/wehuman01/aweshare"><img src="https://img.shields.io/badge/node-%E2%89%A522-0EA5E9?style=flat-square" alt="Node"></a>
19
19
  <a href="https://github.com/wehuman01/aweshare/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-proprietary-E34F26?style=flat-square" alt="License"></a>
20
20
  <a href="https://www.npmjs.com/package/aweshare"><img src="https://img.shields.io/badge/npm-aweshare-7C3AED?style=flat-square" alt="npm package"></a>
@@ -344,6 +344,35 @@ curl -X PUT https://hub.example.com/admin/v1/consumers/alice/limits \
344
344
 
345
345
  Honest limits: token-based caps count what upstreams report — Ollama streams report no usage, so they contribute 0. Both TPM and the lifetime budget are observed-usage thresholds, not hard reservations: one request can cross the threshold, and concurrent requests that start before earlier usage is recorded can overshoot it further. Once recorded usage has reached the threshold, new requests are rejected.
346
346
 
347
+ ## Alias backups (hub-side failover)
348
+
349
+ An alias dies in ways no retry fixes: the producer drops offline, the alias burns its daily budget, its backend degrades, the operator blocks it. Without help, every consumer pointed at that alias breaks until someone hand-edits a model name. **Alias backups** fix that on the hub side: the operator gives each alias an ordered list of backup aliases, and when the alias cannot serve, the hub retries the queue in order before answering with the alias's own error. The consumer configures one model and keeps it — a backup that serves stamps the response with `x-aweshare-fallback: <alias actually served>`, so the swap is visible on the wire, in the usage log (each attempt is its own row, under the alias that served it) and in the model-honesty audit (keyed to the alias that answered).
350
+
351
+ Two ways to manage the queues — a file and a command over the same data:
352
+
353
+ ```toml
354
+ # ~/.aweshare-hub/config.backups.toml — key = alias, value = ordered backups
355
+ "peng/qwen3-coder" = ["alice/qwen3-coder", "hub/glm-4.6"]
356
+ ```
357
+
358
+ ```bash
359
+ aweshare hub backups # list every queue
360
+ aweshare hub backups peng/qwen3-coder # show one
361
+ aweshare hub backups peng/qwen3-coder --add alice/qwen3-coder,hub/glm-4.6
362
+ aweshare hub backups peng/qwen3-coder --add hub/glm-4.6 --first # prepend
363
+ aweshare hub backups peng/qwen3-coder --remove hub/glm-4.6
364
+ aweshare hub backups peng/qwen3-coder --clear
365
+ ```
366
+
367
+ Semantics, deliberately close to a queue of literal strings:
368
+
369
+ - **The queue is exactly what was written** — no implicit hops, a backup's own queue is never expanded, each entry is tried at most once per request. Entries must be distinct, may not include the alias itself, and may name aliases that don't exist yet (producers come and go); an entry with no offering on the request's wire protocol is simply skipped.
370
+ - **Failover triggers on alias-side faults only**: producer offline, degraded backend, blocked alias, closed share window, spent daily budget, per-alias concurrency caps — and pre-head relay failures (the producer accepted the dispatch but never delivered a response head: tunnel closed, backend timeout). Once the response head reached the consumer the stream is committed — no mid-stream swaps. Consumer-side denials (rate limits, probe budget, the consumer's own token budget) never fail over: swapping aliases cannot rescue the consumer's own limits, and `consumer list --ping` must tell the truth about the alias it names, so probes never fail over either.
371
+ - **Each attempt runs full admission again** — the backup's per-alias caps and daily budget apply as they would for a direct request, and the backup's usage lands on its own account, not the failed alias's.
372
+ - When every entry fails, the response is the **primary's original error** — the one the consumer asked about is the one worth explaining.
373
+
374
+ The file lives in the hub data dir next to `config.toml` (`~/.aweshare-hub/config.backups.toml`; Docker: `/data/config.backups.toml`) and hot-reloads like every other hub config — edits apply within ~2s, a broken file is logged while the previous queues keep serving, an invalid file fails `hub serve` at startup. `hub init` scaffolds it (commented, empty). Like `config.toml` itself it is a this-machine file, not part of the admin REST API: on a remote hub, run the command there (ssh / `docker exec`) or edit the file.
375
+
347
376
  ## Endpoints and errors
348
377
 
349
378
  | Endpoint | Purpose |
@@ -370,18 +399,19 @@ Both sides at a glance — details in the sections above.
370
399
  | `aweshare hub init` | create data dir + admin token (printed once) |
371
400
  | `aweshare hub serve [--host H] [--port N]` | run the hub — the only runner; a `config.produce.toml` in the data dir mounts automatically: its `[[backends]]`/`[[offerings]]` sections become `hub/…` offerings served in-process (keys in the data dir's secrets.json; edits hot-reload) |
372
401
  | `aweshare hub produce init` | scaffold `config.produce.toml` + empty `secrets.json` in the data dir (kept if they exist); also initializes the data dir, db, pepper and admin token |
373
- | `aweshare hub admin invite mint [--role producer\|consumer] [--name NAME] [--count N] [--expires-in D\|none]` | mint one-time invite codes (`asi_…`, printed once, expire after 7 d by default; re-view with `list invites --reveal`); `--expires-in` also bounds the lifetime of the token it mints an expired key fails auth with 401 `TOKEN_EXPIRED` (`none` = code and identity never expire; identities minted before this change never expire); producer codes: bound (`--name`) or unbound (name + email at redeem, `--count` batches); consumer codes: always bound to one name |
374
- | `aweshare hub admin invite revoke N` · `aweshare hub admin invite restore N` | kill an invite / undo — a redeemed code suspends the producer it minted, restore revives both |
375
- | `aweshare hub admin invite extend N --expires-in D\|none` | set an invite's expiry from now; for a redeemed invite, updates the producer or consumer token's expiry too, so an expired token works again. Does not restore a manually revoked identity. |
376
- | `aweshare hub admin offering revoke ALIAS` · `aweshare hub admin offering restore ALIAS` | the per-alias scalpel between doing nothing and revoking a whole producer: revoke one offering (every protocol row of the alias) — new requests get 503 `OFFERING_BLOCKED`, `list offerings` shows `blocked`, the producer's other offerings keep serving. Manual revokes survive re-registers; auto revokes (model mismatch, see `autoBlockModelMismatch`) clear once the producer re-declares a different `upstreamModel` |
402
+ | `aweshare hub admin invite mint [--role producer\|consumer] [--name NAME] [--count N] [--expires-in D\|none]` | mint one-time invite codes (`asi_…`, printed once; re-view with `list invites --reveal`); `--expires-in` (default 7d, `none` = no deadline) bounds only how long the code stays redeemable the token a redeem mints never expires, revoke/restore is its lifecycle handle; producer codes: bound (`--name`) or unbound (name + email at redeem, `--count` batches); consumer codes: always bound to one name |
403
+ | `aweshare hub admin invite revoke N…` · `aweshare hub admin invite restore N…` | kill invite(s) / undo — a redeemed code suspends the producer it minted, restore revives both; several ids per call (spaces or commas), one failure does not stop the rest |
404
+ | `aweshare hub admin invite extend N --expires-in D\|none` | reset a pending invite's redemption window from now (`none` = no deadline); a redeemed invite answers `redeemed: true` its token never expires, there is nothing to extend |
405
+ | `aweshare hub admin offering revoke ALIAS…` · `aweshare hub admin offering restore ALIAS…` | the per-alias scalpel between doing nothing and revoking a whole producer: revoke offerings (every protocol row of each alias) — new requests get 503 `OFFERING_BLOCKED`, `list offerings` shows `blocked`, the producer's other offerings keep serving; several aliases per call. Manual revokes survive re-registers; auto revokes (model mismatch, see `autoBlockModelMismatch`) clear once the producer re-declares a different `upstreamModel` |
377
406
  | `aweshare hub list [invites\|producers\|consumers\|offerings\|usage]` | read hub state, one table per noun (default: invites) |
378
- | `aweshare hub list invites [--reveal] [--token] [--json]` | the invite ledger: every code, the identity it minted and its lifecycle (pending/used/suspended/revoked/expired; a redeemed code shows `expired` once its identity's expiry passes); `--reveal` re-shows the codes, `--token` the minted tokens with last seen |
407
+ | `aweshare hub list invites [--reveal] [--token] [--json]` | the invite ledger: every code, the identity it minted and its lifecycle (pending/used/suspended/revoked/expired — `expired` marks a pending code whose redemption window closed; redeemed identities never expire); `--reveal` re-shows the codes, `--token` the minted tokens with last seen |
379
408
  | `aweshare hub list producers [--json]` · `aweshare hub list consumers [--json]` | the rosters: name, status (active/suspended/built-in), online state (producers), last seen, created |
380
409
  | `aweshare hub list offerings [--json]` | the catalog: offerings counted per deduplicated alias (several protocols → one verdict, the worst), one row per alias — the same columns as `consumer list` and `producer list`, worst status first — with observed model, caps, live occupancy (`IN USE n/max`) and today's remaining daily tokens |
381
410
  | `aweshare hub status` | the live dashboard: capacity (producer slots, consumers, offering counts), a last-5m requests/ok-rate/errors line from the usage summary (hub-admission 429s are not metered), admission-rejection pressure (top throttled alias/consumer) and the effective consumer defaults |
382
- | `aweshare hub limits NAME [--rps N] [--burst N] [--max-concurrent N] [--tpm N] [--max-total-tokens N] [--probe-budget N] [--clear] [--json]` | show, merge or clear one consumer's limit overrides (unset keys keep the hub-wide defaults) |
411
+ | `aweshare hub limits NAME [--rps N] [--burst N] [--max-concurrent N] [--tpm N] [--max-total-tokens N] [--probe-budget N] [--clear] [--json]` | show, merge or clear consumers' limit overrides, one consumer or several per call (unset keys keep the hub-wide defaults) |
412
+ | `aweshare hub backups [ALIAS] [--add A[,A…]] [--first] [--remove A[,A…]] [--clear] [--json]` | per-alias failover queues in the data dir's `config.backups.toml` (hot-reloads; this machine's file, like `config.toml` itself): bare `ALIAS` shows one queue, no `ALIAS` lists all; `--add` appends (comma-separate several), `--first` prepends, `--remove` deletes entries, `--clear` drops the queue — see Alias backups |
383
413
  | `aweshare hub list usage [--details] [--consumer NAME] [--producer NAME] [--alias ns/model] [--group-by consumer-alias\|consumer\|alias] [--since 7d\|all] [--sort time\|consumer\|producer\|model\|tokens\|requests] [--limit N] [--json]` | who used how much (default): aggregate per consumer × model, most recently used first — requests, errors, rate, best-effort token totals, unknown-token count, mean duration; window defaults to 7d and is printed with the table; `--sort` re-orders (consumer/producer/model alphabetical, tokens/requests busiest first) · `--details`: per-request log, newest first, zero content stored, each row naming its consumer |
384
- | `aweshare hub produce refresh NAME [--add N] [--clear] [--json]` · `aweshare hub produce refresh --all [--json]` | reopen a hub-hosted model's daily token budget mid-day (`hub/` prefix optional): bare call re-anchors today's window at this moment, `--add N` raises today's cap by N tokens until Beijing midnight, `--clear` drops both markers. Hub-hosted (`hub/…`) offerings only — a producer's models are its own to refresh. `--all` bare-refreshes every `hub/…` offering with a daily cap in one run (unlimited ones are reported and skipped; one failure does not stop the rest) |
414
+ | `aweshare hub produce refresh NAME [--add N] [--clear] [--json]` · `aweshare hub produce refresh --all [--json]` | reopen hub-hosted models' daily token budgets mid-day (`hub/` prefix optional; several names per call): bare call re-anchors today's window at this moment, `--add N` raises today's cap by N tokens until Beijing midnight, `--clear` drops both markers. Hub-hosted (`hub/…`) offerings only — a producer's models are its own to refresh. `--all` bare-refreshes every `hub/…` offering with a daily cap in one run (unlimited ones are reported and skipped; one failure does not stop the rest) |
385
415
 
386
416
  Token issuance runs through invites (both roles). `admin`, `limits` and `list usage` are thin wrappers over the admin REST API (`/admin/v1/*`, see Endpoints and errors) — curl works too.
387
417
 
@@ -420,7 +450,7 @@ No always-on box to share from, or looking for models others share? The project'
420
450
 
421
451
  The hub reads `config.toml` from its data dir (`~/.aweshare-hub/config.toml`; Docker: `/data/config.toml`). `aweshare hub init` writes the template with every key commented out — uncomment to override a default. Keys use the same names as below, camelCase (`consumerRps`, `headTimeoutMs`, …). Precedence: `serve` flags (`--host`/`--port`) > env vars > config.toml > defaults. A broken file (invalid TOML, unknown key, non-positive value) fails fast at startup with the key named; `AWESHARE_HUB_DATA_DIR` itself stays env-only (it locates the file).
422
452
 
423
- **Hot reload:** every tunable in the table except host/port applies live on `SIGHUP` (`kill -HUP <pid>`; Docker: `docker kill -s HUP aweshare-hub`) — the new file is validated first, and a broken edit is logged while the previous values keep serving. Env vars are fixed at process start, so keys pinned by `AWESHARE_*` ignore the reloaded file (same precedence as startup); host/port need a restart. Producer-side offerings and caps reload via `aweshare producer reload`.
453
+ **Hot reload:** every tunable in the table except host/port applies live on `SIGHUP` (`kill -HUP <pid>`; Docker: `docker kill -s HUP aweshare-hub`) — the new file is validated first, and a broken edit is logged while the previous values keep serving. Env vars are fixed at process start, so keys pinned by `AWESHARE_*` ignore the reloaded file (same precedence as startup); host/port need a restart. Producer-side offerings and caps reload via `aweshare producer reload`. `config.backups.toml` (per-alias failover queues, see Alias backups) rides the same watcher: edits hot-reload within ~2s, a broken edit keeps the previous queues, an invalid file fails `hub serve` at startup.
424
454
 
425
455
  **Hub-hosted models (`hub produce`):** `config.produce.toml` carries `[[backends]]` and `[[offerings]]` sections (producer format; alias namespace `hub/…` — bare names are auto-prefixed) with upstream keys in `secrets.json` next to it (chmod 600). `config.toml` remains exclusively for Hub runtime settings. Those offerings appear in the catalog under producer `hub` and are served by the hub process directly — no tunnel, and they never count against `AWESHARE_MAX_PRODUCERS`. Caps (`maxConcurrencyPerUser`, `maxConcurrentUsers`, `dailyTokens`), usage metering and consumer limits apply exactly as for remote producers. The built-in `hub` producer is not an identity (no token, no invite, cannot be revoked); it appears in the `hub list producers` roster — status `built-in` — only while it carries offerings. An `enabled = true|false` key at the top of the file is the catalog's master switch: `false` unloads every `hub/…` model within the hot-reload window (the definitions stay; flip back to bring them all back) — the produce-side counterpart of revoking a remote producer's invite. A non-boolean value fails loudly instead of silently serving or unloading. Catalog and key edits hot-reload like the tunables; a broken catalog keeps the previous one and is logged.
426
456
 
@@ -17,8 +17,6 @@ export function requireProducer(req, auth) {
17
17
  if (row.revoked_at) {
18
18
  throw new HttpError(401, 'TOKEN_REVOKED', 'producer token was suspended by the operator — ask them to restore it (aweshare hub admin invite restore N)');
19
19
  }
20
- if (isExpired(row.expires_at))
21
- throw expiredError('producer token');
22
20
  return { row, token };
23
21
  }
24
22
  export function requireConsumer(req, auth) {
@@ -31,16 +29,6 @@ export function requireConsumer(req, auth) {
31
29
  if (row.revoked_at) {
32
30
  throw new HttpError(401, 'TOKEN_REVOKED', 'consumer key was suspended by the operator — ask them to restore it (admin API)');
33
31
  }
34
- if (isExpired(row.expires_at))
35
- throw expiredError('consumer key');
36
32
  return { row, token };
37
33
  }
38
- /** Identity expiry is copied from the minting invite at redeem (v12+); NULL
39
- * never expires. Compared as ISO strings, like every other timestamp gate. */
40
- function isExpired(expiresAt) {
41
- return expiresAt !== null && expiresAt <= new Date().toISOString();
42
- }
43
- function expiredError(what) {
44
- return new HttpError(401, 'TOKEN_EXPIRED', `${what} has expired — the invite that minted it reached its expiry; ask the operator for a new one`);
45
- }
46
34
  //# sourceMappingURL=auth.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoB,kBAAkB,EAAE,kBAAkB,EAAoB,MAAM,SAAS,CAAA;AACpG,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAQlD,MAAM,UAAU,YAAY,CAAC,GAAoB,EAAE,IAAiB;IAClE,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAAA;IACjE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,GAAoB,EACpB,IAAiB;IAEjB,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,yBAAyB,CAAC,CAAA;IAC9E,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;IACtE,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,wBAAwB,CAAC,CAAA;IAC3E,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QACnB,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,eAAe,EACf,6GAA6G,CAC9G,CAAA;IACH,CAAC;IACD,IAAI,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC;QAAE,MAAM,YAAY,CAAC,gBAAgB,CAAC,CAAA;IACnE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;AACvB,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,GAAoB,EACpB,IAAiB;IAEjB,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,uBAAuB,CAAC,CAAA;IAC5E,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;IACtE,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAAA;IACzE,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QACnB,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,eAAe,EACf,iFAAiF,CAClF,CAAA;IACH,CAAC;IACD,IAAI,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC;QAAE,MAAM,YAAY,CAAC,cAAc,CAAC,CAAA;IACjE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;AACvB,CAAC;AAED;8EAC8E;AAC9E,SAAS,SAAS,CAAC,SAAwB;IACzC,OAAO,SAAS,KAAK,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;AACpE,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO,IAAI,SAAS,CAClB,GAAG,EACH,eAAe,EACf,GAAG,IAAI,6FAA6F,CACrG,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoB,kBAAkB,EAAE,kBAAkB,EAAoB,MAAM,SAAS,CAAA;AACpG,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAQlD,MAAM,UAAU,YAAY,CAAC,GAAoB,EAAE,IAAiB;IAClE,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAAA;IACjE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,GAAoB,EACpB,IAAiB;IAEjB,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,yBAAyB,CAAC,CAAA;IAC9E,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;IACtE,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,wBAAwB,CAAC,CAAA;IAC3E,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QACnB,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,eAAe,EACf,6GAA6G,CAC9G,CAAA;IACH,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;AACvB,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,GAAoB,EACpB,IAAiB;IAEjB,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,uBAAuB,CAAC,CAAA;IAC5E,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;IACtE,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAAA;IACzE,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QACnB,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,eAAe,EACf,iFAAiF,CAClF,CAAA;IACH,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;AACvB,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Per-alias failover queues (config.backups.toml): the operator's ordered
3
+ * "try next" list for each alias. When an alias cannot serve — offline
4
+ * producer, spent daily budget, degraded backend, blocked, closed share
5
+ * window, busy — the hub retries the queue in order before giving up.
6
+ *
7
+ * The queue is exactly what was written: no implicit hops, a backup's own
8
+ * queue is never expanded, each entry is tried at most once per request.
9
+ * Entries may name aliases that do not (yet) exist — producers register and
10
+ * leave dynamically — so unresolvable entries are skipped per request, not
11
+ * rejected at load time.
12
+ */
13
+ export type BackupMap = ReadonlyMap<string, readonly string[]>;
14
+ /** Parse and validate the file's text: alias -> non-empty ordered list of
15
+ * distinct alias strings, none of them the key itself. Throws naming every
16
+ * problem, so the reload path can keep the previous map and the CLI can fail
17
+ * loudly. */
18
+ export declare function parseBackups(raw: string): Map<string, string[]>;
19
+ /** Serialize queues back to the file's text: header first, then one
20
+ * `alias = [backups]` line per key in map order. */
21
+ export declare function serializeBackups(queues: BackupMap): string;
22
+ /** Read and parse the file; a missing file means no backups are configured.
23
+ * Throws on an invalid one (the caller decides: startup fails fast, reload
24
+ * keeps the previous values). */
25
+ export declare function readBackupsFile(backupsFile: string | undefined): Map<string, string[]>;
26
+ /** The live per-alias queues: loaded once at startup, reloaded on SIGHUP and
27
+ * on the config watcher's stat-poll — same cadence and keep-previous-on-error
28
+ * rules as config.toml. */
29
+ export declare class Backups {
30
+ private readonly backupsFile;
31
+ private queues;
32
+ constructor(backupsFile: string | undefined);
33
+ /** Re-read the file; throws on an invalid one so the caller (the config
34
+ * reloader) keeps the previous values. A file deleted at runtime empties
35
+ * the queues — the file is the single source of truth. */
36
+ reload(): void;
37
+ /** Ordered queue for one alias; empty when none is configured. */
38
+ queueFor(alias: string): readonly string[];
39
+ /** Every configured queue, file order. */
40
+ all(): BackupMap;
41
+ }
42
+ /** CLI write path: replace the file's contents wholesale with `queues`. */
43
+ export declare function writeBackupsFile(backupsFile: string, queues: BackupMap): void;
@@ -0,0 +1,122 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { isValidAlias } from '../../../packages/protocol/dist/index.js';
3
+ import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
4
+ const EMPTY = [];
5
+ /** File header every rewrite of config.backups.toml carries; the CLI owns
6
+ * this file's shape, so regenerating it wholesale loses nothing. */
7
+ const FILE_HEADER = `# aweshare hub per-alias failover queues (config.backups.toml).
8
+ # key = alias, value = ordered backup aliases, tried in order when the alias
9
+ # cannot serve (producer offline, daily budget spent, degraded backend,
10
+ # blocked, share window closed, busy). Entries that resolve to nothing are
11
+ # skipped. Edits hot-reload within ~2s, like config.toml.
12
+ # Managed by: aweshare hub backups
13
+
14
+ `;
15
+ /** Parse and validate the file's text: alias -> non-empty ordered list of
16
+ * distinct alias strings, none of them the key itself. Throws naming every
17
+ * problem, so the reload path can keep the previous map and the CLI can fail
18
+ * loudly. */
19
+ export function parseBackups(raw) {
20
+ let parsed;
21
+ try {
22
+ parsed = parseToml(raw);
23
+ }
24
+ catch (err) {
25
+ throw new Error(`config.backups.toml is not valid TOML: ${err instanceof Error ? err.message : err}`);
26
+ }
27
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
28
+ throw new Error('config.backups.toml must be a table of alias = [backup aliases]');
29
+ }
30
+ const out = new Map();
31
+ const problems = [];
32
+ for (const [key, value] of Object.entries(parsed)) {
33
+ if (!isValidAlias(key)) {
34
+ problems.push(`'${key}' is not an alias (namespace/name, lowercase)`);
35
+ continue;
36
+ }
37
+ if (!Array.isArray(value) || value.length === 0) {
38
+ problems.push(`'${key}' must list at least one backup alias (delete the key for none)`);
39
+ continue;
40
+ }
41
+ const queue = [];
42
+ for (const entry of value) {
43
+ if (typeof entry !== 'string' || !isValidAlias(entry)) {
44
+ problems.push(`'${key}' lists a non-alias: ${JSON.stringify(entry ?? null)}`);
45
+ }
46
+ else if (entry === key) {
47
+ problems.push(`'${key}' lists itself as its own backup`);
48
+ }
49
+ else if (queue.includes(entry)) {
50
+ problems.push(`'${key}' repeats '${entry}'`);
51
+ }
52
+ else {
53
+ queue.push(entry);
54
+ }
55
+ }
56
+ if (queue.length > 0)
57
+ out.set(key, queue);
58
+ }
59
+ if (problems.length > 0) {
60
+ throw new Error(`config.backups.toml has problems:\n - ${problems.join('\n - ')}`);
61
+ }
62
+ return out;
63
+ }
64
+ /** Serialize queues back to the file's text: header first, then one
65
+ * `alias = [backups]` line per key in map order. */
66
+ export function serializeBackups(queues) {
67
+ if (queues.size === 0)
68
+ return FILE_HEADER;
69
+ const obj = {};
70
+ for (const [key, queue] of queues)
71
+ obj[key] = [...queue];
72
+ return `${FILE_HEADER}${stringifyToml(obj)}`;
73
+ }
74
+ /** Read and parse the file; a missing file means no backups are configured.
75
+ * Throws on an invalid one (the caller decides: startup fails fast, reload
76
+ * keeps the previous values). */
77
+ export function readBackupsFile(backupsFile) {
78
+ if (typeof backupsFile !== 'string')
79
+ return new Map();
80
+ let raw;
81
+ try {
82
+ raw = readFileSync(backupsFile, 'utf8');
83
+ }
84
+ catch {
85
+ return new Map();
86
+ }
87
+ return parseBackups(raw);
88
+ }
89
+ /** The live per-alias queues: loaded once at startup, reloaded on SIGHUP and
90
+ * on the config watcher's stat-poll — same cadence and keep-previous-on-error
91
+ * rules as config.toml. */
92
+ export class Backups {
93
+ backupsFile;
94
+ queues = new Map();
95
+ constructor(backupsFile) {
96
+ this.backupsFile = backupsFile;
97
+ // startup fails fast on an invalid file (same as config.toml and the
98
+ // produce catalog — serve must not come up half-configured); a missing
99
+ // file is the ordinary empty state. The reload path catches instead and
100
+ // keeps the previous values.
101
+ this.queues = readBackupsFile(backupsFile);
102
+ }
103
+ /** Re-read the file; throws on an invalid one so the caller (the config
104
+ * reloader) keeps the previous values. A file deleted at runtime empties
105
+ * the queues — the file is the single source of truth. */
106
+ reload() {
107
+ this.queues = readBackupsFile(this.backupsFile);
108
+ }
109
+ /** Ordered queue for one alias; empty when none is configured. */
110
+ queueFor(alias) {
111
+ return this.queues.get(alias) ?? EMPTY;
112
+ }
113
+ /** Every configured queue, file order. */
114
+ all() {
115
+ return this.queues;
116
+ }
117
+ }
118
+ /** CLI write path: replace the file's contents wholesale with `queues`. */
119
+ export function writeBackupsFile(backupsFile, queues) {
120
+ writeFileSync(backupsFile, serializeBackups(queues), { mode: 0o600 });
121
+ }
122
+ //# sourceMappingURL=backups.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backups.js","sourceRoot":"","sources":["../src/backups.ts"],"names":[],"mappings":"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"}