aweshare 0.7.3 → 0.7.5
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 +43 -13
- package/apps/agent/dist/cli.js +17 -10
- package/apps/agent/dist/cli.js.map +1 -1
- package/apps/agent/dist/hub.d.ts +2 -0
- package/apps/agent/dist/hub.js +2 -0
- package/apps/agent/dist/hub.js.map +1 -1
- package/apps/agent/dist/table.d.ts +2 -2
- package/apps/agent/dist/table.js +48 -14
- package/apps/agent/dist/table.js.map +1 -1
- package/apps/hub/dist/admin.js +26 -18
- package/apps/hub/dist/admin.js.map +1 -1
- 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 +371 -131
- 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 +169 -54
- package/apps/hub/dist/consumer.js.map +1 -1
- package/apps/hub/dist/db.d.ts +9 -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 +3 -4
- package/apps/hub/dist/table.js +52 -20
- 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 +3 -2
- package/packages/producer-core/dist/catalog.js +9 -1
- package/packages/producer-core/dist/catalog.js.map +1 -1
- package/packages/producer-core/dist/job.js +1 -1
- package/packages/producer-core/dist/job.js.map +1 -1
- package/packages/protocol/dist/models.js +6 -1
- package/packages/protocol/dist/models.js.map +1 -1
- package/packages/protocol/dist/schedule.d.ts +8 -3
- package/packages/protocol/dist/schedule.js +124 -11
- package/packages/protocol/dist/schedule.js.map +1 -1
package/apps/hub/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { fmtDate, fmtTime, fmtTokens } from '../../../packages/protocol/dist/index.js';
|
|
3
|
+
import { fmtDate, fmtTime, fmtTokens, isValidAlias } from '../../../packages/protocol/dist/index.js';
|
|
4
|
+
import { parseBackups, readBackupsFile, serializeBackups, writeBackupsFile } from './backups.js';
|
|
4
5
|
import { ensureDataDir, HUB_CONFIG_TEMPLATE, initHubProduceFiles, loadHubConfig, readAdminToken, resolvePaths, } from './config.js';
|
|
5
6
|
import { createHubServer, installConfigReloader } from './server.js';
|
|
6
7
|
import { refreshTargets, renderConsumers, renderHubStatus, renderInvites, renderOfferings, renderProducers, renderUsage, renderUsageSummary, } from './table.js';
|
|
@@ -22,7 +23,8 @@ Commands:
|
|
|
22
23
|
admin Govern: invite mint|revoke|restore, offering revoke|restore.
|
|
23
24
|
list Read hub state: invites (default), producers, consumers, offerings, usage.
|
|
24
25
|
status Live dashboard: capacity, 5m health, admission pressure, defaults.
|
|
25
|
-
limits Show, set or clear one
|
|
26
|
+
limits Show, set or clear one or more consumers' limit overrides.
|
|
27
|
+
backups Per-alias failover queues: show, add, remove or clear.`;
|
|
26
28
|
const COMMAND_HELP = {
|
|
27
29
|
init: `Usage: aweshare hub init
|
|
28
30
|
|
|
@@ -61,19 +63,20 @@ Options:
|
|
|
61
63
|
initializes the Hub data directory, database, pepper and admin token.
|
|
62
64
|
|
|
63
65
|
` +
|
|
64
|
-
'`refresh ALIAS
|
|
66
|
+
'`refresh ALIAS...`' +
|
|
65
67
|
` works on a running hub, no restart: re-anchor the daily token
|
|
66
|
-
budget of one hub/...
|
|
67
|
-
counting), or raise it for the rest of today
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
budget of one or several hub/... models at this moment (past usage today
|
|
69
|
+
stops counting), or raise it for the rest of today — several aliases per
|
|
70
|
+
call, space- or comma-separated, one failure does not stop the rest.
|
|
71
|
+
Producer-registered models (ns/...) are not the hub's to refresh — that
|
|
72
|
+
producer runs 'aweshare producer refresh' itself. A permanent raise
|
|
73
|
+
belongs in config.produce.toml (dailyTokens), which hot-reloads. --all
|
|
71
74
|
bare-refreshes every hub/ model with a daily cap in one run; unlimited
|
|
72
|
-
ones (dailyTokens = 0) are reported and skipped
|
|
73
|
-
not stop the rest.
|
|
75
|
+
ones (dailyTokens = 0) are reported and skipped.
|
|
74
76
|
|
|
75
77
|
Examples:
|
|
76
78
|
aweshare hub produce refresh glm-5.3 # today restarts now
|
|
79
|
+
aweshare hub produce refresh glm-5.3 glm-4.6 # several at once
|
|
77
80
|
aweshare hub produce refresh --all # every hub/ model restarts today
|
|
78
81
|
aweshare hub produce refresh glm-5.3 --add 2000000 # today's cap +2M
|
|
79
82
|
aweshare hub produce refresh glm-5.3 --clear # drop both markers
|
|
@@ -92,15 +95,19 @@ Options:
|
|
|
92
95
|
|
|
93
96
|
Commands:
|
|
94
97
|
invite mint [...] Mint one-time invite codes.
|
|
95
|
-
invite revoke N
|
|
98
|
+
invite revoke N... Kill invites — the sledgehammer: a pending
|
|
96
99
|
code stops pairing, a redeemed one suspends
|
|
97
100
|
the whole producer or consumer it minted.
|
|
98
|
-
invite restore N
|
|
99
|
-
invite extend N [...] Set
|
|
100
|
-
|
|
101
|
+
invite restore N... Undo an invite revoke.
|
|
102
|
+
invite extend N [...] Set a pending invite's expiry; a redeemed
|
|
103
|
+
invite's token never expires.
|
|
104
|
+
offering revoke ALIAS... The scalpel: 503 OFFERING_BLOCKED for new
|
|
101
105
|
requests to one alias while the producer's
|
|
102
106
|
other offerings keep serving.
|
|
103
|
-
offering restore ALIAS
|
|
107
|
+
offering restore ALIAS... Undo an offering revoke.
|
|
108
|
+
|
|
109
|
+
Every N/ALIAS argument accepts several targets, space- or comma-separated;
|
|
110
|
+
one failure does not stop the rest.
|
|
104
111
|
|
|
105
112
|
Run 'aweshare hub admin invite -h' or 'aweshare hub admin offering -h'
|
|
106
113
|
for the full semantics of each group.
|
|
@@ -112,20 +119,24 @@ Options:
|
|
|
112
119
|
The invite lifecycle. One code, one identity — and the invite is the
|
|
113
120
|
operator's handle from mint to suspension: revoking a redeemed code
|
|
114
121
|
suspends the producer or consumer it minted (a producer's tunnel closes,
|
|
115
|
-
a consumer's key stops working); restoring brings both back.
|
|
122
|
+
a consumer's key stops working); restoring brings both back. The code's
|
|
123
|
+
expiry bounds only the redemption window — the token a redeem mints never
|
|
124
|
+
expires; revoke/restore is its only lifecycle handle.
|
|
116
125
|
|
|
117
126
|
Commands:
|
|
118
|
-
mint
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
revoke N
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
mint Mint one-time invite codes. Printed once; recover
|
|
128
|
+
with: aweshare hub list invites --reveal. Listing lives under
|
|
129
|
+
'list invites' on purpose — minting sends real codes, reading
|
|
130
|
+
the ledger never should.
|
|
131
|
+
revoke N... Kill invites: a pending code stops pairing, a redeemed one
|
|
132
|
+
suspends its identity (find ids with: aweshare hub list
|
|
133
|
+
invites).
|
|
134
|
+
restore N... Undo a revoke: pending pairs again, redeemed revives its
|
|
135
|
+
identity. Restoring a redeemed producer needs a free slot
|
|
136
|
+
(403 HUB_FULL at AWESHARE_MAX_PRODUCERS).
|
|
137
|
+
extend N... Reset a pending code's expiry from now ('none' = no
|
|
138
|
+
deadline). A redeemed invite has nothing to extend — its
|
|
139
|
+
token never expires — and says so.
|
|
129
140
|
|
|
130
141
|
Options:
|
|
131
142
|
--role ROLE producer (default) or consumer — a consumer code redeems into
|
|
@@ -133,15 +144,14 @@ Options:
|
|
|
133
144
|
--name NAME Bind the codes to one name (required for --role consumer;
|
|
134
145
|
producers may leave it for redeem-time name + email).
|
|
135
146
|
--count N Mint N codes, 1..100 (default 1; producer codes only).
|
|
136
|
-
--expires-in D How long
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
expiry from now.
|
|
147
|
+
--expires-in D How long the code stays redeemable: 30m, 12h or 7d
|
|
148
|
+
(default 7d); 'none' = no deadline. It bounds the code
|
|
149
|
+
only — the minted token never expires. With extend, this
|
|
150
|
+
is required.
|
|
141
151
|
-h, --help Show this message and exit.`,
|
|
142
|
-
'admin offering': `Usage: aweshare hub admin offering revoke|restore ALIAS
|
|
152
|
+
'admin offering': `Usage: aweshare hub admin offering revoke|restore ALIAS...
|
|
143
153
|
|
|
144
|
-
Revoke
|
|
154
|
+
Revoke offering aliases (every protocol row of them) or bring them back —
|
|
145
155
|
the scalpel between doing nothing and revoking the whole producer
|
|
146
156
|
('admin invite revoke'). A revoke 503s new requests with
|
|
147
157
|
OFFERING_BLOCKED while the producer's other offerings keep serving;
|
|
@@ -150,6 +160,9 @@ Options:
|
|
|
150
160
|
autoBlockModelMismatch is enabled) clear once the producer re-declares
|
|
151
161
|
a different upstreamModel.
|
|
152
162
|
|
|
163
|
+
Several aliases per call, space- or comma-separated; one failure does
|
|
164
|
+
not stop the rest.
|
|
165
|
+
|
|
153
166
|
Options:
|
|
154
167
|
-h, --help Show this message and exit.`,
|
|
155
168
|
list: `Usage: aweshare hub list [invites|producers|consumers|offerings|usage] [OPTIONS]
|
|
@@ -167,10 +180,11 @@ Options:
|
|
|
167
180
|
other side), with observed model, caps, live occupancy and
|
|
168
181
|
today's remaining tokens.
|
|
169
182
|
usage Who used how much: aggregated on the hub's SQLite, one row per
|
|
170
|
-
consumer × model (default), most recently used first
|
|
171
|
-
switches to the request log
|
|
172
|
-
|
|
173
|
-
that made it.
|
|
183
|
+
consumer × model (default), most recently used first, closing
|
|
184
|
+
with a TOTAL row. --details switches to the request log
|
|
185
|
+
instead: newest first, one row per request, zero content
|
|
186
|
+
stored, each row naming the consumer that made it. Both take
|
|
187
|
+
the same --since window (default all time).
|
|
174
188
|
|
|
175
189
|
Options:
|
|
176
190
|
--reveal Invites only: show the full invite codes (asi_...).
|
|
@@ -181,7 +195,8 @@ Options:
|
|
|
181
195
|
--producer NAME Usage only: only rows for this producer.
|
|
182
196
|
--alias ALIAS Usage only: only rows for this alias (ns/model).
|
|
183
197
|
--group-by KEY Usage only: consumer × model (default), consumer or alias.
|
|
184
|
-
--since DUR Usage only: window
|
|
198
|
+
--since DUR Usage only: window (summary and --details): all
|
|
199
|
+
(default), or 30m, 12h, 7d, 30d…
|
|
185
200
|
--sort KEY Usage summary only: time (default, newest first), consumer,
|
|
186
201
|
producer or model (alphabetical, newest first within), or
|
|
187
202
|
tokens / requests (busiest first; tokens keeps a person's
|
|
@@ -189,11 +204,13 @@ Options:
|
|
|
189
204
|
--limit N Usage --details only: rows to show, 1..1000 (default 100).
|
|
190
205
|
--json Print the raw API rows instead of a table.
|
|
191
206
|
-h, --help Show this message and exit.`,
|
|
192
|
-
limits: `Usage: aweshare hub limits NAME [OPTIONS]
|
|
207
|
+
limits: `Usage: aweshare hub limits NAME... [OPTIONS]
|
|
193
208
|
|
|
194
|
-
Show, set or clear
|
|
195
|
-
|
|
196
|
-
what is already
|
|
209
|
+
Show, set or clear consumers' limit overrides — one consumer or several
|
|
210
|
+
(space- or comma-separated). Unset keys keep the hub-wide defaults
|
|
211
|
+
(AWESHARE_CONSUMER_* env vars); setting merges into what is already
|
|
212
|
+
stored. One failure does not stop the rest; with --json a single name
|
|
213
|
+
keeps the plain object, several become an array.
|
|
197
214
|
|
|
198
215
|
Options:
|
|
199
216
|
--rps N Requests per second (may be fractional).
|
|
@@ -205,6 +222,34 @@ Options:
|
|
|
205
222
|
--clear Drop every override (back to hub-wide defaults).
|
|
206
223
|
--json Print the raw API response instead of text.
|
|
207
224
|
-h, --help Show this message and exit.`,
|
|
225
|
+
backups: `Usage: aweshare hub backups [ALIAS] [OPTIONS]
|
|
226
|
+
|
|
227
|
+
Per-alias failover queues in the data dir's config.backups.toml: when an
|
|
228
|
+
alias cannot serve (producer offline, daily budget spent, degraded
|
|
229
|
+
backend, blocked, share window closed, busy), the hub retries the queue
|
|
230
|
+
in order before answering with the alias's own error. Entries resolve
|
|
231
|
+
to nothing are skipped; a queue is exactly what was written — no
|
|
232
|
+
implicit hops, no expansion. Edits hot-reload within ~2s. The file is
|
|
233
|
+
this machine's, so the command edits it locally: on a remote hub run it
|
|
234
|
+
there (ssh / docker exec), like every other config.toml change.
|
|
235
|
+
|
|
236
|
+
Bare ALIAS shows its queue; no ALIAS lists every queue.
|
|
237
|
+
|
|
238
|
+
Examples:
|
|
239
|
+
aweshare hub backups # every queue
|
|
240
|
+
aweshare hub backups peng/qwen3-coder # one queue
|
|
241
|
+
aweshare hub backups peng/qwen3-coder --add alice/qwen3-coder
|
|
242
|
+
aweshare hub backups peng/qwen3-coder --add hub/glm-4.6 --first
|
|
243
|
+
aweshare hub backups peng/qwen3-coder --remove hub/glm-4.6
|
|
244
|
+
aweshare hub backups peng/qwen3-coder --clear
|
|
245
|
+
|
|
246
|
+
Options:
|
|
247
|
+
--add ALIASES Append (comma-separated aliases accepted); --first prepends instead.
|
|
248
|
+
--first With --add: put the new backups at the front of the queue.
|
|
249
|
+
--remove A Remove one backup alias from ALIAS's queue.
|
|
250
|
+
--clear Drop ALIAS's queue entirely (the key disappears).
|
|
251
|
+
--json Print the queue(s) as JSON.
|
|
252
|
+
-h, --help Show this message and exit.`,
|
|
208
253
|
status: `Usage: aweshare hub status
|
|
209
254
|
|
|
210
255
|
The hub's live dashboard: capacity, the last-5m health, admission
|
|
@@ -393,6 +438,52 @@ function fmtLimits(name, overrides) {
|
|
|
393
438
|
? `${name}: ${set.join(', ')}`
|
|
394
439
|
: `${name}: no overrides — hub-wide defaults apply (AWESHARE_CONSUMER_* env vars)`;
|
|
395
440
|
}
|
|
441
|
+
/** Split a comma-separated alias list ('a/b,c/d') — aliases cannot contain
|
|
442
|
+
* commas, so the split is unambiguous. */
|
|
443
|
+
function splitAliases(raw) {
|
|
444
|
+
return raw
|
|
445
|
+
.split(',')
|
|
446
|
+
.map((entry) => entry.trim())
|
|
447
|
+
.filter((entry) => entry.length > 0);
|
|
448
|
+
}
|
|
449
|
+
/** Several targets on one command line: spaces or commas both work
|
|
450
|
+
* ('18 19 22' or '18,19,22' — ids, names and aliases contain no commas). */
|
|
451
|
+
function parseTargets(raw) {
|
|
452
|
+
return raw
|
|
453
|
+
.flatMap((arg) => arg.split(','))
|
|
454
|
+
.map((t) => t.trim())
|
|
455
|
+
.filter((t) => t.length > 0);
|
|
456
|
+
}
|
|
457
|
+
/** --json across batch commands: one target keeps the plain object, several
|
|
458
|
+
* become an array. */
|
|
459
|
+
function printJsonBatch(entries) {
|
|
460
|
+
console.log(JSON.stringify(entries.length === 1 ? entries[0] : entries, null, 2));
|
|
461
|
+
}
|
|
462
|
+
/** One API call per target, one line each — a failure does not stop the rest
|
|
463
|
+
* (same contract as `produce refresh --all`), and a nonzero exit code closes
|
|
464
|
+
* the run so scripts notice. Failed targets become {target, error} entries,
|
|
465
|
+
* so a --json run stays parseable. */
|
|
466
|
+
async function runBatch(targets, verb, json, each) {
|
|
467
|
+
const entries = [];
|
|
468
|
+
let failed = 0;
|
|
469
|
+
for (const target of targets) {
|
|
470
|
+
try {
|
|
471
|
+
entries.push(await each(target));
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
failed++;
|
|
475
|
+
const error = errorMessage(err);
|
|
476
|
+
entries.push({ target, error });
|
|
477
|
+
if (!json)
|
|
478
|
+
console.log(`✗ ${target} — ${error}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (failed > 0) {
|
|
482
|
+
console.error(`error: ${failed} of ${targets.length} ${verb} failed`);
|
|
483
|
+
process.exitCode = 1;
|
|
484
|
+
}
|
|
485
|
+
return entries;
|
|
486
|
+
}
|
|
396
487
|
/** The five readable tables; bare `list` defaults to the ledger (a read,
|
|
397
488
|
* and the one operators reach for first). */
|
|
398
489
|
const LIST_TARGETS = ['invites', 'producers', 'consumers', 'offerings', 'usage'];
|
|
@@ -444,6 +535,10 @@ async function main() {
|
|
|
444
535
|
writeFileSync(paths.configFile, HUB_CONFIG_TEMPLATE, { mode: 0o600 });
|
|
445
536
|
console.log(`config file: ${paths.configFile} (every key commented out — edit to override)`);
|
|
446
537
|
}
|
|
538
|
+
if (!existsSync(paths.backupsFile)) {
|
|
539
|
+
writeFileSync(paths.backupsFile, serializeBackups(new Map()), { mode: 0o600 });
|
|
540
|
+
console.log(`backups file: ${paths.backupsFile} (empty — per-alias failover queues, see: aweshare hub backups -h)`);
|
|
541
|
+
}
|
|
447
542
|
const hub = createHubServer({ paths }); // creates the db schema
|
|
448
543
|
await hub.close();
|
|
449
544
|
console.log(`data dir: ${paths.dataDir}`);
|
|
@@ -494,46 +589,50 @@ async function main() {
|
|
|
494
589
|
return;
|
|
495
590
|
}
|
|
496
591
|
checkFlags('produce', flags, ['add', 'clear', 'all', 'json']);
|
|
497
|
-
const
|
|
592
|
+
const raws = parseTargets(positional.slice(1));
|
|
498
593
|
const all = flags.get('all') === 'true';
|
|
499
|
-
if (positional[2])
|
|
500
|
-
fail(`unexpected argument '${positional[2]}' (see: aweshare hub produce -h)`);
|
|
501
594
|
if (all) {
|
|
502
|
-
if (
|
|
503
|
-
fail(`--all refreshes every hub/ model — drop the alias '${
|
|
595
|
+
if (raws.length > 0)
|
|
596
|
+
fail(`--all refreshes every hub/ model — drop the alias '${raws[0]}'`);
|
|
504
597
|
if (flags.get('add') !== undefined || flags.get('clear') === 'true')
|
|
505
|
-
fail('--all is a bare refresh of every hub/ model — --add/--clear
|
|
598
|
+
fail('--all is a bare refresh of every hub/ model — --add/--clear are per-alias');
|
|
506
599
|
const api = adminApi();
|
|
507
600
|
await runProduceRefreshAll(api, flags.get('json') === 'true');
|
|
508
601
|
return;
|
|
509
602
|
}
|
|
510
|
-
if (
|
|
603
|
+
if (raws.length === 0)
|
|
511
604
|
fail('produce refresh: alias is required (hub/model — the hub/ prefix is optional), or --all for every hub/ model');
|
|
512
605
|
// hub-hosted offerings live in the reserved hub/ namespace — accept
|
|
513
606
|
// the bare name too; anything else the hub rejects as not its own
|
|
514
|
-
const
|
|
515
|
-
const params = new URLSearchParams({ alias });
|
|
607
|
+
const aliases = raws.map((raw) => (raw.includes('/') ? raw : `hub/${raw}`));
|
|
516
608
|
const add = flags.get('add');
|
|
609
|
+
let addN;
|
|
517
610
|
if (add !== undefined) {
|
|
518
|
-
|
|
519
|
-
if (!Number.isSafeInteger(
|
|
611
|
+
addN = Number(add);
|
|
612
|
+
if (!Number.isSafeInteger(addN) || addN <= 0)
|
|
520
613
|
fail(`--add must be a positive integer, got '${add}'`);
|
|
521
|
-
params.set('add', String(n));
|
|
522
|
-
}
|
|
523
|
-
if (flags.get('clear') === 'true') {
|
|
524
|
-
if (add !== undefined)
|
|
525
|
-
fail('--clear drops both markers — drop --add too');
|
|
526
|
-
params.set('clear', 'true');
|
|
527
614
|
}
|
|
615
|
+
const clear = flags.get('clear') === 'true';
|
|
616
|
+
if (clear && add !== undefined)
|
|
617
|
+
fail('--clear drops both markers — drop --add too');
|
|
618
|
+
const json = flags.get('json') === 'true';
|
|
528
619
|
const api = adminApi();
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
620
|
+
const entries = await runBatch(aliases, 'refresh(es)', json, async (alias) => {
|
|
621
|
+
const params = new URLSearchParams({ alias });
|
|
622
|
+
if (addN !== undefined)
|
|
623
|
+
params.set('add', String(addN));
|
|
624
|
+
if (clear)
|
|
625
|
+
params.set('clear', 'true');
|
|
626
|
+
const out = (await api('POST', `/admin/v1/offerings/refresh?${params}`));
|
|
627
|
+
if (!json) {
|
|
628
|
+
printRefreshResult(out);
|
|
629
|
+
if (out.reset)
|
|
630
|
+
console.log(` undo: aweshare hub produce refresh ${out.alias} --clear`);
|
|
631
|
+
}
|
|
632
|
+
return out;
|
|
633
|
+
});
|
|
634
|
+
if (json)
|
|
635
|
+
printJsonBatch(entries);
|
|
537
636
|
return;
|
|
538
637
|
}
|
|
539
638
|
case 'admin': {
|
|
@@ -578,8 +677,9 @@ async function main() {
|
|
|
578
677
|
if (role === 'consumer' && count && count !== '1')
|
|
579
678
|
fail('consumer invites are one per name — drop --count and mint one per consumer');
|
|
580
679
|
// absent flag still mints a 7d code — an unexpiring secret should be a
|
|
581
|
-
// deliberate choice (`none` here; API callers may omit expiresAt).
|
|
582
|
-
//
|
|
680
|
+
// deliberate choice (`none` here; API callers may omit expiresAt). The
|
|
681
|
+
// expiry bounds the redemption window only; the minted identity
|
|
682
|
+
// never expires.
|
|
583
683
|
const expiresIn = flags.get('expires-in') ?? '7d';
|
|
584
684
|
if (expiresIn !== 'none') {
|
|
585
685
|
body.expiresAt = new Date(Date.now() + parseDuration(expiresIn)).toISOString();
|
|
@@ -608,24 +708,27 @@ async function main() {
|
|
|
608
708
|
return;
|
|
609
709
|
}
|
|
610
710
|
checkFlags('admin invite', flags, action === 'extend' ? ['expires-in'] : []);
|
|
611
|
-
const
|
|
612
|
-
if (
|
|
711
|
+
const ids = parseTargets(positional.slice(2));
|
|
712
|
+
if (ids.length === 0)
|
|
613
713
|
fail(`invite ${action}: id is required (find ids with: aweshare hub list invites)`);
|
|
614
|
-
if (positional[3])
|
|
615
|
-
fail(`unexpected argument '${positional[3]}' (see: aweshare hub admin invite -h)`);
|
|
616
714
|
if (action === 'revoke') {
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
715
|
+
const revokedIds = [];
|
|
716
|
+
await runBatch(ids, 'revoke(s)', false, async (id) => {
|
|
717
|
+
const out = (await api('DELETE', `/admin/v1/invites/${id}`));
|
|
718
|
+
if (!out.revoked && !out.suspended) {
|
|
719
|
+
console.log(`- invite ${id} was already revoked — nothing to do`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
revokedIds.push(id);
|
|
723
|
+
if (out.suspended) {
|
|
724
|
+
console.log(`✓ invite ${id} revoked — ${out.suspended.role} '${out.suspended.name}' suspended, its token no longer works`);
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
console.log(`✓ invite ${id} revoked — the code no longer pairs`);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
if (revokedIds.length > 0)
|
|
731
|
+
console.log(` undo: aweshare hub admin invite restore ${revokedIds.join(' ')}`);
|
|
629
732
|
return;
|
|
630
733
|
}
|
|
631
734
|
if (action === 'extend') {
|
|
@@ -635,26 +738,33 @@ async function main() {
|
|
|
635
738
|
const expiresAt = expiresIn === 'none'
|
|
636
739
|
? null
|
|
637
740
|
: new Date(Date.now() + parseDuration(expiresIn)).toISOString();
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
741
|
+
await runBatch(ids, 'extend(s)', false, async (id) => {
|
|
742
|
+
const out = (await api('POST', `/admin/v1/invites/${id}/extend`, { expiresAt }));
|
|
743
|
+
if (out.redeemed)
|
|
744
|
+
console.log(`✓ invite ${id} is redeemed — its token never expires, nothing to extend`);
|
|
745
|
+
else
|
|
746
|
+
console.log(out.expiresAt === null
|
|
747
|
+
? `✓ invite ${id} never expires`
|
|
748
|
+
: `✓ invite ${id} expires ${fmtTime(out.expiresAt)}`);
|
|
749
|
+
});
|
|
647
750
|
return;
|
|
648
751
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
752
|
+
await runBatch(ids, 'restore(s)', false, async (id) => {
|
|
753
|
+
const out = (await api('POST', `/admin/v1/invites/${id}/restore`));
|
|
754
|
+
if (!out.restored) {
|
|
755
|
+
console.log(`- invite ${id} was not revoked — nothing to restore`);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (out.restoredProducer) {
|
|
759
|
+
console.log(`✓ invite ${id} restored — producer '${out.restoredProducer.name}' is back`);
|
|
760
|
+
}
|
|
761
|
+
else if (out.restoredConsumer) {
|
|
762
|
+
console.log(`✓ invite ${id} restored — consumer '${out.restoredConsumer.name}' is back`);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
console.log(`✓ invite ${id} restored — the code pairs again (its original expiry still applies)`);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
658
768
|
return;
|
|
659
769
|
}
|
|
660
770
|
// offering: the per-alias scalpel ('block' renamed to 'revoke' — the
|
|
@@ -666,19 +776,24 @@ async function main() {
|
|
|
666
776
|
fail(`unknown offering action '${action}' — expected revoke or restore`);
|
|
667
777
|
}
|
|
668
778
|
checkFlags('admin offering', flags, []);
|
|
669
|
-
const
|
|
670
|
-
if (
|
|
779
|
+
const aliases = parseTargets(positional.slice(2));
|
|
780
|
+
if (aliases.length === 0)
|
|
671
781
|
fail(`offering ${action}: alias is required (ns/model)`);
|
|
672
|
-
if (positional[3])
|
|
673
|
-
fail(`unexpected argument '${positional[3]}' (see: aweshare hub admin offering -h)`);
|
|
674
782
|
if (action === 'revoke') {
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
783
|
+
const revokedAliases = [];
|
|
784
|
+
await runBatch(aliases, 'revoke(s)', false, async (alias) => {
|
|
785
|
+
const out = (await api('POST', `/admin/v1/offerings/block?alias=${encodeURIComponent(alias)}`));
|
|
786
|
+
revokedAliases.push(alias);
|
|
787
|
+
console.log(`✓ ${alias} revoked (${out.offerings} protocol row(s)) — new requests get 503 OFFERING_BLOCKED`);
|
|
788
|
+
});
|
|
789
|
+
if (revokedAliases.length > 0)
|
|
790
|
+
console.log(` undo: aweshare hub admin offering restore ${revokedAliases.join(' ')}`);
|
|
678
791
|
return;
|
|
679
792
|
}
|
|
680
|
-
|
|
681
|
-
|
|
793
|
+
await runBatch(aliases, 'restore(s)', false, async (alias) => {
|
|
794
|
+
const out = (await api('POST', `/admin/v1/offerings/restore?alias=${encodeURIComponent(alias)}`));
|
|
795
|
+
console.log(`✓ ${alias} restored (${out.offerings} protocol row(s)) — requests flow again`);
|
|
796
|
+
});
|
|
682
797
|
return;
|
|
683
798
|
}
|
|
684
799
|
case 'list': {
|
|
@@ -747,7 +862,7 @@ async function main() {
|
|
|
747
862
|
const group = flags.get('group-by') ?? 'consumer-alias';
|
|
748
863
|
if (group !== 'consumer-alias' && group !== 'consumer' && group !== 'alias')
|
|
749
864
|
fail(`--group-by must be 'consumer', 'alias' or 'consumer-alias' (default), got '${group}'`);
|
|
750
|
-
const since = flags.get('since') ?? '
|
|
865
|
+
const since = flags.get('since') ?? 'all';
|
|
751
866
|
if (since !== 'all')
|
|
752
867
|
parseDuration(since); // validate early with a local error
|
|
753
868
|
const sort = flags.get('sort') ?? 'time';
|
|
@@ -784,20 +899,26 @@ async function main() {
|
|
|
784
899
|
fail(`--limit must be an integer between 1 and 1000, got '${limit}'`);
|
|
785
900
|
params.set('limit', String(n));
|
|
786
901
|
}
|
|
787
|
-
// the details
|
|
788
|
-
//
|
|
789
|
-
if (flags.get('since') !== undefined)
|
|
790
|
-
fail('--since applies to the summary, not --details');
|
|
902
|
+
// the details log is a timeline — newest first, nothing to re-sort;
|
|
903
|
+
// --since works like on the summary (default all time)
|
|
791
904
|
if (flags.get('group-by') !== undefined)
|
|
792
905
|
fail('--group-by applies to the summary, not --details');
|
|
793
906
|
if (flags.get('sort') !== undefined)
|
|
794
907
|
fail('--sort applies to the summary, not --details (the log is newest first)');
|
|
908
|
+
const since = flags.get('since') ?? 'all';
|
|
909
|
+
if (since !== 'all')
|
|
910
|
+
parseDuration(since); // validate early with a local error
|
|
911
|
+
params.set('since', since);
|
|
795
912
|
const qs = params.toString();
|
|
796
913
|
const rows = (await api('GET', `/admin/v1/usage${qs ? `?${qs}` : ''}`));
|
|
797
914
|
if (flags.get('json') === 'true') {
|
|
798
915
|
console.log(JSON.stringify(rows, null, 2));
|
|
799
916
|
return;
|
|
800
917
|
}
|
|
918
|
+
const window = since === 'all'
|
|
919
|
+
? 'all time'
|
|
920
|
+
: `since ${fmtDate(new Date(Date.now() - parseDuration(since)))} (${since})`;
|
|
921
|
+
console.log(`usage ${window} — details, newest first`);
|
|
801
922
|
// current hubs join consumer names into the rows; the roster is only
|
|
802
923
|
// fetched when an older remote hub answered without them
|
|
803
924
|
const names = new Map();
|
|
@@ -816,13 +937,11 @@ async function main() {
|
|
|
816
937
|
}
|
|
817
938
|
case 'limits': {
|
|
818
939
|
checkFlags('limits', flags, [...LIMIT_FLAGS.map(([flag]) => flag), 'clear', 'json']);
|
|
819
|
-
const
|
|
820
|
-
if (
|
|
940
|
+
const names = parseTargets(positional);
|
|
941
|
+
if (names.length === 0)
|
|
821
942
|
fail('consumer name is required (see: aweshare hub limits -h)');
|
|
822
|
-
if (positional[1])
|
|
823
|
-
fail(`unexpected argument '${positional[1]}' (see: aweshare hub limits -h)`);
|
|
824
943
|
const api = adminApi();
|
|
825
|
-
const path = `/admin/v1/consumers/${encodeURIComponent(name)}/limits`;
|
|
944
|
+
const path = (name) => `/admin/v1/consumers/${encodeURIComponent(name)}/limits`;
|
|
826
945
|
const json = flags.get('json') === 'true';
|
|
827
946
|
const set = {};
|
|
828
947
|
for (const [flag, key] of LIMIT_FLAGS) {
|
|
@@ -837,20 +956,141 @@ async function main() {
|
|
|
837
956
|
if (flags.get('clear') === 'true') {
|
|
838
957
|
if (Object.keys(set).length > 0)
|
|
839
958
|
fail('--clear drops every override — drop the value flags too');
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
959
|
+
const entries = await runBatch(names, 'clear(s)', json, async (name) => {
|
|
960
|
+
const out = (await api('DELETE', path(name)));
|
|
961
|
+
if (!json)
|
|
962
|
+
console.log(`✓ ${out.name}: overrides cleared — hub-wide defaults apply`);
|
|
963
|
+
return out;
|
|
964
|
+
});
|
|
965
|
+
if (json)
|
|
966
|
+
printJsonBatch(entries);
|
|
844
967
|
return;
|
|
845
968
|
}
|
|
846
969
|
if (Object.keys(set).length > 0) {
|
|
847
970
|
// integer/precision rules live server-side; its error lists all problems
|
|
848
|
-
const
|
|
849
|
-
|
|
971
|
+
const entries = await runBatch(names, 'update(s)', json, async (name) => {
|
|
972
|
+
const out = (await api('PUT', path(name), set));
|
|
973
|
+
if (!json)
|
|
974
|
+
console.log(`✓ ${fmtLimits(out.name, out.overrides)}`);
|
|
975
|
+
return out;
|
|
976
|
+
});
|
|
977
|
+
if (json)
|
|
978
|
+
printJsonBatch(entries);
|
|
850
979
|
return;
|
|
851
980
|
}
|
|
852
|
-
const
|
|
853
|
-
|
|
981
|
+
const entries = await runBatch(names, 'lookup(s)', json, async (name) => {
|
|
982
|
+
const out = (await api('GET', path(name)));
|
|
983
|
+
if (!json)
|
|
984
|
+
console.log(fmtLimits(out.name, out.overrides));
|
|
985
|
+
return out;
|
|
986
|
+
});
|
|
987
|
+
if (json)
|
|
988
|
+
printJsonBatch(entries);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
case 'backups': {
|
|
992
|
+
checkFlags('backups', flags, ['add', 'first', 'remove', 'clear', 'json']);
|
|
993
|
+
const alias = positional[0];
|
|
994
|
+
if (positional[1])
|
|
995
|
+
fail(`unexpected argument '${positional[1]}' (see: aweshare hub backups -h)`);
|
|
996
|
+
const json = flags.get('json') === 'true';
|
|
997
|
+
const add = flags.get('add');
|
|
998
|
+
const remove = flags.get('remove');
|
|
999
|
+
const clear = flags.get('clear') === 'true';
|
|
1000
|
+
const mutating = add !== undefined || remove !== undefined || clear;
|
|
1001
|
+
if (mutating && alias === undefined)
|
|
1002
|
+
fail('which queue to modify: pass ALIAS (see: aweshare hub backups -h)');
|
|
1003
|
+
if (clear && (add !== undefined || remove !== undefined))
|
|
1004
|
+
fail('--clear drops the whole queue — drop --add/--remove too');
|
|
1005
|
+
if (flags.get('first') === 'true' && add === undefined)
|
|
1006
|
+
fail('--first only makes sense with --add');
|
|
1007
|
+
let queues;
|
|
1008
|
+
try {
|
|
1009
|
+
queues = readBackupsFile(paths.backupsFile);
|
|
1010
|
+
}
|
|
1011
|
+
catch (err) {
|
|
1012
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
1013
|
+
}
|
|
1014
|
+
if (!mutating) {
|
|
1015
|
+
const print = (key, queue) => console.log(queue && queue.length > 0
|
|
1016
|
+
? `${key} → ${queue.join(', ')}`
|
|
1017
|
+
: `${key}: no backups configured`);
|
|
1018
|
+
if (!alias) {
|
|
1019
|
+
if (queues.size === 0) {
|
|
1020
|
+
console.log('no backup queues configured — create one:');
|
|
1021
|
+
console.log(' aweshare hub backups SOME/ALIAS --add OTHER/BACKUP');
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (json)
|
|
1025
|
+
console.log(JSON.stringify(Object.fromEntries(queues), null, 2));
|
|
1026
|
+
else
|
|
1027
|
+
for (const [key, queue] of queues)
|
|
1028
|
+
print(key, queue);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const queue = queues.get(alias);
|
|
1032
|
+
if (json)
|
|
1033
|
+
console.log(JSON.stringify({ [alias]: queue ?? [] }, null, 2));
|
|
1034
|
+
else
|
|
1035
|
+
print(alias, queue);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
// unreachable in plain control flow (mutating implies ALIAS, checked
|
|
1039
|
+
// above) but needed so the compiler sees the string
|
|
1040
|
+
if (alias === undefined)
|
|
1041
|
+
fail('which queue to modify: pass ALIAS (see: aweshare hub backups -h)');
|
|
1042
|
+
if (!isValidAlias(alias))
|
|
1043
|
+
fail(`'${alias}' is not an alias (namespace/name, lowercase)`);
|
|
1044
|
+
// every mutation lands in memory first and the file is written once,
|
|
1045
|
+
// so a rejected flag leaves the file untouched
|
|
1046
|
+
if (clear) {
|
|
1047
|
+
if (!queues.delete(alias))
|
|
1048
|
+
fail(`${alias}: no queue configured (see: aweshare hub backups)`);
|
|
1049
|
+
}
|
|
1050
|
+
if (remove !== undefined) {
|
|
1051
|
+
for (const target of splitAliases(remove)) {
|
|
1052
|
+
const queue = queues.get(alias);
|
|
1053
|
+
if (!queue?.includes(target))
|
|
1054
|
+
fail(`${alias}: '${target}' is not in its queue`);
|
|
1055
|
+
const rest = queue.filter((entry) => entry !== target);
|
|
1056
|
+
if (rest.length > 0)
|
|
1057
|
+
queues.set(alias, rest);
|
|
1058
|
+
else
|
|
1059
|
+
queues.delete(alias);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (add !== undefined) {
|
|
1063
|
+
const additions = splitAliases(add);
|
|
1064
|
+
if (new Set(additions).size !== additions.length)
|
|
1065
|
+
fail(`--add repeats an alias: '${add}'`);
|
|
1066
|
+
for (const entry of additions) {
|
|
1067
|
+
if (!isValidAlias(entry))
|
|
1068
|
+
fail(`'${entry}' is not an alias (namespace/name, lowercase)`);
|
|
1069
|
+
if (entry === alias)
|
|
1070
|
+
fail(`${alias}: it cannot be its own backup`);
|
|
1071
|
+
if (queues.get(alias)?.includes(entry))
|
|
1072
|
+
fail(`${alias}: '${entry}' is already in its queue`);
|
|
1073
|
+
}
|
|
1074
|
+
const queue = queues.get(alias) ?? [];
|
|
1075
|
+
queues.set(alias, flags.get('first') === 'true' ? [...additions, ...queue] : [...queue, ...additions]);
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
// revalidate the whole file the way the hub will parse it
|
|
1079
|
+
parseBackups(serializeBackups(queues));
|
|
1080
|
+
}
|
|
1081
|
+
catch (err) {
|
|
1082
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
1083
|
+
}
|
|
1084
|
+
writeBackupsFile(paths.backupsFile, queues);
|
|
1085
|
+
const queue = queues.get(alias);
|
|
1086
|
+
if (json) {
|
|
1087
|
+
console.log(JSON.stringify(Object.fromEntries(queues), null, 2));
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
const shown = queue && queue.length > 0 ? queue.join(', ') : '(none — queue removed)';
|
|
1091
|
+
console.log(`✓ ${alias} → ${shown}`);
|
|
1092
|
+
console.log('(config.backups.toml written — the hub picks it up within ~2s)');
|
|
1093
|
+
}
|
|
854
1094
|
return;
|
|
855
1095
|
}
|
|
856
1096
|
case 'status': {
|