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.
- package/README.ai.md +1 -1
- package/README.md +39 -9
- package/apps/hub/dist/auth.js +0 -12
- package/apps/hub/dist/auth.js.map +1 -1
- package/apps/hub/dist/backups.d.ts +43 -0
- package/apps/hub/dist/backups.js +122 -0
- package/apps/hub/dist/backups.js.map +1 -0
- package/apps/hub/dist/cli.js +353 -121
- package/apps/hub/dist/cli.js.map +1 -1
- package/apps/hub/dist/config.d.ts +2 -0
- package/apps/hub/dist/config.js +1 -0
- package/apps/hub/dist/config.js.map +1 -1
- package/apps/hub/dist/consumer.d.ts +3 -0
- package/apps/hub/dist/consumer.js +164 -54
- package/apps/hub/dist/consumer.js.map +1 -1
- package/apps/hub/dist/db.d.ts +7 -10
- package/apps/hub/dist/db.js +20 -22
- package/apps/hub/dist/db.js.map +1 -1
- package/apps/hub/dist/http.d.ts +7 -1
- package/apps/hub/dist/http.js +6 -1
- package/apps/hub/dist/http.js.map +1 -1
- package/apps/hub/dist/invites.js +9 -2
- package/apps/hub/dist/invites.js.map +1 -1
- package/apps/hub/dist/server.d.ts +7 -3
- package/apps/hub/dist/server.js +23 -8
- package/apps/hub/dist/server.js.map +1 -1
- package/apps/hub/dist/table.d.ts +1 -3
- package/apps/hub/dist/table.js +2 -6
- package/apps/hub/dist/table.js.map +1 -1
- package/apps/hub/dist/tunnel.d.ts +0 -2
- package/apps/hub/dist/tunnel.js +2 -16
- package/apps/hub/dist/tunnel.js.map +1 -1
- package/package.json +1 -1
- package/packages/producer-core/dist/catalog.js +9 -1
- package/packages/producer-core/dist/catalog.js.map +1 -1
- package/packages/protocol/dist/models.js +6 -1
- 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.
|
|
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
|
|
374
|
-
| `aweshare hub admin invite revoke N
|
|
375
|
-
| `aweshare hub admin invite extend N --expires-in D\|none` |
|
|
376
|
-
| `aweshare hub admin offering revoke ALIAS
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/apps/hub/dist/auth.js
CHANGED
|
@@ -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,
|
|
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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"}
|