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/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]
|
|
@@ -189,11 +202,13 @@ Options:
|
|
|
189
202
|
--limit N Usage --details only: rows to show, 1..1000 (default 100).
|
|
190
203
|
--json Print the raw API rows instead of a table.
|
|
191
204
|
-h, --help Show this message and exit.`,
|
|
192
|
-
limits: `Usage: aweshare hub limits NAME [OPTIONS]
|
|
205
|
+
limits: `Usage: aweshare hub limits NAME... [OPTIONS]
|
|
193
206
|
|
|
194
|
-
Show, set or clear
|
|
195
|
-
|
|
196
|
-
what is already
|
|
207
|
+
Show, set or clear consumers' limit overrides — one consumer or several
|
|
208
|
+
(space- or comma-separated). Unset keys keep the hub-wide defaults
|
|
209
|
+
(AWESHARE_CONSUMER_* env vars); setting merges into what is already
|
|
210
|
+
stored. One failure does not stop the rest; with --json a single name
|
|
211
|
+
keeps the plain object, several become an array.
|
|
197
212
|
|
|
198
213
|
Options:
|
|
199
214
|
--rps N Requests per second (may be fractional).
|
|
@@ -205,6 +220,34 @@ Options:
|
|
|
205
220
|
--clear Drop every override (back to hub-wide defaults).
|
|
206
221
|
--json Print the raw API response instead of text.
|
|
207
222
|
-h, --help Show this message and exit.`,
|
|
223
|
+
backups: `Usage: aweshare hub backups [ALIAS] [OPTIONS]
|
|
224
|
+
|
|
225
|
+
Per-alias failover queues in the data dir's config.backups.toml: when an
|
|
226
|
+
alias cannot serve (producer offline, daily budget spent, degraded
|
|
227
|
+
backend, blocked, share window closed, busy), the hub retries the queue
|
|
228
|
+
in order before answering with the alias's own error. Entries resolve
|
|
229
|
+
to nothing are skipped; a queue is exactly what was written — no
|
|
230
|
+
implicit hops, no expansion. Edits hot-reload within ~2s. The file is
|
|
231
|
+
this machine's, so the command edits it locally: on a remote hub run it
|
|
232
|
+
there (ssh / docker exec), like every other config.toml change.
|
|
233
|
+
|
|
234
|
+
Bare ALIAS shows its queue; no ALIAS lists every queue.
|
|
235
|
+
|
|
236
|
+
Examples:
|
|
237
|
+
aweshare hub backups # every queue
|
|
238
|
+
aweshare hub backups peng/qwen3-coder # one queue
|
|
239
|
+
aweshare hub backups peng/qwen3-coder --add alice/qwen3-coder
|
|
240
|
+
aweshare hub backups peng/qwen3-coder --add hub/glm-4.6 --first
|
|
241
|
+
aweshare hub backups peng/qwen3-coder --remove hub/glm-4.6
|
|
242
|
+
aweshare hub backups peng/qwen3-coder --clear
|
|
243
|
+
|
|
244
|
+
Options:
|
|
245
|
+
--add ALIASES Append (comma-separated aliases accepted); --first prepends instead.
|
|
246
|
+
--first With --add: put the new backups at the front of the queue.
|
|
247
|
+
--remove A Remove one backup alias from ALIAS's queue.
|
|
248
|
+
--clear Drop ALIAS's queue entirely (the key disappears).
|
|
249
|
+
--json Print the queue(s) as JSON.
|
|
250
|
+
-h, --help Show this message and exit.`,
|
|
208
251
|
status: `Usage: aweshare hub status
|
|
209
252
|
|
|
210
253
|
The hub's live dashboard: capacity, the last-5m health, admission
|
|
@@ -393,6 +436,52 @@ function fmtLimits(name, overrides) {
|
|
|
393
436
|
? `${name}: ${set.join(', ')}`
|
|
394
437
|
: `${name}: no overrides — hub-wide defaults apply (AWESHARE_CONSUMER_* env vars)`;
|
|
395
438
|
}
|
|
439
|
+
/** Split a comma-separated alias list ('a/b,c/d') — aliases cannot contain
|
|
440
|
+
* commas, so the split is unambiguous. */
|
|
441
|
+
function splitAliases(raw) {
|
|
442
|
+
return raw
|
|
443
|
+
.split(',')
|
|
444
|
+
.map((entry) => entry.trim())
|
|
445
|
+
.filter((entry) => entry.length > 0);
|
|
446
|
+
}
|
|
447
|
+
/** Several targets on one command line: spaces or commas both work
|
|
448
|
+
* ('18 19 22' or '18,19,22' — ids, names and aliases contain no commas). */
|
|
449
|
+
function parseTargets(raw) {
|
|
450
|
+
return raw
|
|
451
|
+
.flatMap((arg) => arg.split(','))
|
|
452
|
+
.map((t) => t.trim())
|
|
453
|
+
.filter((t) => t.length > 0);
|
|
454
|
+
}
|
|
455
|
+
/** --json across batch commands: one target keeps the plain object, several
|
|
456
|
+
* become an array. */
|
|
457
|
+
function printJsonBatch(entries) {
|
|
458
|
+
console.log(JSON.stringify(entries.length === 1 ? entries[0] : entries, null, 2));
|
|
459
|
+
}
|
|
460
|
+
/** One API call per target, one line each — a failure does not stop the rest
|
|
461
|
+
* (same contract as `produce refresh --all`), and a nonzero exit code closes
|
|
462
|
+
* the run so scripts notice. Failed targets become {target, error} entries,
|
|
463
|
+
* so a --json run stays parseable. */
|
|
464
|
+
async function runBatch(targets, verb, json, each) {
|
|
465
|
+
const entries = [];
|
|
466
|
+
let failed = 0;
|
|
467
|
+
for (const target of targets) {
|
|
468
|
+
try {
|
|
469
|
+
entries.push(await each(target));
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
failed++;
|
|
473
|
+
const error = errorMessage(err);
|
|
474
|
+
entries.push({ target, error });
|
|
475
|
+
if (!json)
|
|
476
|
+
console.log(`✗ ${target} — ${error}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (failed > 0) {
|
|
480
|
+
console.error(`error: ${failed} of ${targets.length} ${verb} failed`);
|
|
481
|
+
process.exitCode = 1;
|
|
482
|
+
}
|
|
483
|
+
return entries;
|
|
484
|
+
}
|
|
396
485
|
/** The five readable tables; bare `list` defaults to the ledger (a read,
|
|
397
486
|
* and the one operators reach for first). */
|
|
398
487
|
const LIST_TARGETS = ['invites', 'producers', 'consumers', 'offerings', 'usage'];
|
|
@@ -444,6 +533,10 @@ async function main() {
|
|
|
444
533
|
writeFileSync(paths.configFile, HUB_CONFIG_TEMPLATE, { mode: 0o600 });
|
|
445
534
|
console.log(`config file: ${paths.configFile} (every key commented out — edit to override)`);
|
|
446
535
|
}
|
|
536
|
+
if (!existsSync(paths.backupsFile)) {
|
|
537
|
+
writeFileSync(paths.backupsFile, serializeBackups(new Map()), { mode: 0o600 });
|
|
538
|
+
console.log(`backups file: ${paths.backupsFile} (empty — per-alias failover queues, see: aweshare hub backups -h)`);
|
|
539
|
+
}
|
|
447
540
|
const hub = createHubServer({ paths }); // creates the db schema
|
|
448
541
|
await hub.close();
|
|
449
542
|
console.log(`data dir: ${paths.dataDir}`);
|
|
@@ -494,46 +587,50 @@ async function main() {
|
|
|
494
587
|
return;
|
|
495
588
|
}
|
|
496
589
|
checkFlags('produce', flags, ['add', 'clear', 'all', 'json']);
|
|
497
|
-
const
|
|
590
|
+
const raws = parseTargets(positional.slice(1));
|
|
498
591
|
const all = flags.get('all') === 'true';
|
|
499
|
-
if (positional[2])
|
|
500
|
-
fail(`unexpected argument '${positional[2]}' (see: aweshare hub produce -h)`);
|
|
501
592
|
if (all) {
|
|
502
|
-
if (
|
|
503
|
-
fail(`--all refreshes every hub/ model — drop the alias '${
|
|
593
|
+
if (raws.length > 0)
|
|
594
|
+
fail(`--all refreshes every hub/ model — drop the alias '${raws[0]}'`);
|
|
504
595
|
if (flags.get('add') !== undefined || flags.get('clear') === 'true')
|
|
505
|
-
fail('--all is a bare refresh of every hub/ model — --add/--clear
|
|
596
|
+
fail('--all is a bare refresh of every hub/ model — --add/--clear are per-alias');
|
|
506
597
|
const api = adminApi();
|
|
507
598
|
await runProduceRefreshAll(api, flags.get('json') === 'true');
|
|
508
599
|
return;
|
|
509
600
|
}
|
|
510
|
-
if (
|
|
601
|
+
if (raws.length === 0)
|
|
511
602
|
fail('produce refresh: alias is required (hub/model — the hub/ prefix is optional), or --all for every hub/ model');
|
|
512
603
|
// hub-hosted offerings live in the reserved hub/ namespace — accept
|
|
513
604
|
// the bare name too; anything else the hub rejects as not its own
|
|
514
|
-
const
|
|
515
|
-
const params = new URLSearchParams({ alias });
|
|
605
|
+
const aliases = raws.map((raw) => (raw.includes('/') ? raw : `hub/${raw}`));
|
|
516
606
|
const add = flags.get('add');
|
|
607
|
+
let addN;
|
|
517
608
|
if (add !== undefined) {
|
|
518
|
-
|
|
519
|
-
if (!Number.isSafeInteger(
|
|
609
|
+
addN = Number(add);
|
|
610
|
+
if (!Number.isSafeInteger(addN) || addN <= 0)
|
|
520
611
|
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
612
|
}
|
|
613
|
+
const clear = flags.get('clear') === 'true';
|
|
614
|
+
if (clear && add !== undefined)
|
|
615
|
+
fail('--clear drops both markers — drop --add too');
|
|
616
|
+
const json = flags.get('json') === 'true';
|
|
528
617
|
const api = adminApi();
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
618
|
+
const entries = await runBatch(aliases, 'refresh(es)', json, async (alias) => {
|
|
619
|
+
const params = new URLSearchParams({ alias });
|
|
620
|
+
if (addN !== undefined)
|
|
621
|
+
params.set('add', String(addN));
|
|
622
|
+
if (clear)
|
|
623
|
+
params.set('clear', 'true');
|
|
624
|
+
const out = (await api('POST', `/admin/v1/offerings/refresh?${params}`));
|
|
625
|
+
if (!json) {
|
|
626
|
+
printRefreshResult(out);
|
|
627
|
+
if (out.reset)
|
|
628
|
+
console.log(` undo: aweshare hub produce refresh ${out.alias} --clear`);
|
|
629
|
+
}
|
|
630
|
+
return out;
|
|
631
|
+
});
|
|
632
|
+
if (json)
|
|
633
|
+
printJsonBatch(entries);
|
|
537
634
|
return;
|
|
538
635
|
}
|
|
539
636
|
case 'admin': {
|
|
@@ -578,8 +675,9 @@ async function main() {
|
|
|
578
675
|
if (role === 'consumer' && count && count !== '1')
|
|
579
676
|
fail('consumer invites are one per name — drop --count and mint one per consumer');
|
|
580
677
|
// absent flag still mints a 7d code — an unexpiring secret should be a
|
|
581
|
-
// deliberate choice (`none` here; API callers may omit expiresAt).
|
|
582
|
-
//
|
|
678
|
+
// deliberate choice (`none` here; API callers may omit expiresAt). The
|
|
679
|
+
// expiry bounds the redemption window only; the minted identity
|
|
680
|
+
// never expires.
|
|
583
681
|
const expiresIn = flags.get('expires-in') ?? '7d';
|
|
584
682
|
if (expiresIn !== 'none') {
|
|
585
683
|
body.expiresAt = new Date(Date.now() + parseDuration(expiresIn)).toISOString();
|
|
@@ -608,24 +706,27 @@ async function main() {
|
|
|
608
706
|
return;
|
|
609
707
|
}
|
|
610
708
|
checkFlags('admin invite', flags, action === 'extend' ? ['expires-in'] : []);
|
|
611
|
-
const
|
|
612
|
-
if (
|
|
709
|
+
const ids = parseTargets(positional.slice(2));
|
|
710
|
+
if (ids.length === 0)
|
|
613
711
|
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
712
|
if (action === 'revoke') {
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
713
|
+
const revokedIds = [];
|
|
714
|
+
await runBatch(ids, 'revoke(s)', false, async (id) => {
|
|
715
|
+
const out = (await api('DELETE', `/admin/v1/invites/${id}`));
|
|
716
|
+
if (!out.revoked && !out.suspended) {
|
|
717
|
+
console.log(`- invite ${id} was already revoked — nothing to do`);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
revokedIds.push(id);
|
|
721
|
+
if (out.suspended) {
|
|
722
|
+
console.log(`✓ invite ${id} revoked — ${out.suspended.role} '${out.suspended.name}' suspended, its token no longer works`);
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
console.log(`✓ invite ${id} revoked — the code no longer pairs`);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
if (revokedIds.length > 0)
|
|
729
|
+
console.log(` undo: aweshare hub admin invite restore ${revokedIds.join(' ')}`);
|
|
629
730
|
return;
|
|
630
731
|
}
|
|
631
732
|
if (action === 'extend') {
|
|
@@ -635,26 +736,33 @@ async function main() {
|
|
|
635
736
|
const expiresAt = expiresIn === 'none'
|
|
636
737
|
? null
|
|
637
738
|
: new Date(Date.now() + parseDuration(expiresIn)).toISOString();
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
739
|
+
await runBatch(ids, 'extend(s)', false, async (id) => {
|
|
740
|
+
const out = (await api('POST', `/admin/v1/invites/${id}/extend`, { expiresAt }));
|
|
741
|
+
if (out.redeemed)
|
|
742
|
+
console.log(`✓ invite ${id} is redeemed — its token never expires, nothing to extend`);
|
|
743
|
+
else
|
|
744
|
+
console.log(out.expiresAt === null
|
|
745
|
+
? `✓ invite ${id} never expires`
|
|
746
|
+
: `✓ invite ${id} expires ${fmtTime(out.expiresAt)}`);
|
|
747
|
+
});
|
|
647
748
|
return;
|
|
648
749
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
750
|
+
await runBatch(ids, 'restore(s)', false, async (id) => {
|
|
751
|
+
const out = (await api('POST', `/admin/v1/invites/${id}/restore`));
|
|
752
|
+
if (!out.restored) {
|
|
753
|
+
console.log(`- invite ${id} was not revoked — nothing to restore`);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (out.restoredProducer) {
|
|
757
|
+
console.log(`✓ invite ${id} restored — producer '${out.restoredProducer.name}' is back`);
|
|
758
|
+
}
|
|
759
|
+
else if (out.restoredConsumer) {
|
|
760
|
+
console.log(`✓ invite ${id} restored — consumer '${out.restoredConsumer.name}' is back`);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
console.log(`✓ invite ${id} restored — the code pairs again (its original expiry still applies)`);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
658
766
|
return;
|
|
659
767
|
}
|
|
660
768
|
// offering: the per-alias scalpel ('block' renamed to 'revoke' — the
|
|
@@ -666,19 +774,24 @@ async function main() {
|
|
|
666
774
|
fail(`unknown offering action '${action}' — expected revoke or restore`);
|
|
667
775
|
}
|
|
668
776
|
checkFlags('admin offering', flags, []);
|
|
669
|
-
const
|
|
670
|
-
if (
|
|
777
|
+
const aliases = parseTargets(positional.slice(2));
|
|
778
|
+
if (aliases.length === 0)
|
|
671
779
|
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
780
|
if (action === 'revoke') {
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
781
|
+
const revokedAliases = [];
|
|
782
|
+
await runBatch(aliases, 'revoke(s)', false, async (alias) => {
|
|
783
|
+
const out = (await api('POST', `/admin/v1/offerings/block?alias=${encodeURIComponent(alias)}`));
|
|
784
|
+
revokedAliases.push(alias);
|
|
785
|
+
console.log(`✓ ${alias} revoked (${out.offerings} protocol row(s)) — new requests get 503 OFFERING_BLOCKED`);
|
|
786
|
+
});
|
|
787
|
+
if (revokedAliases.length > 0)
|
|
788
|
+
console.log(` undo: aweshare hub admin offering restore ${revokedAliases.join(' ')}`);
|
|
678
789
|
return;
|
|
679
790
|
}
|
|
680
|
-
|
|
681
|
-
|
|
791
|
+
await runBatch(aliases, 'restore(s)', false, async (alias) => {
|
|
792
|
+
const out = (await api('POST', `/admin/v1/offerings/restore?alias=${encodeURIComponent(alias)}`));
|
|
793
|
+
console.log(`✓ ${alias} restored (${out.offerings} protocol row(s)) — requests flow again`);
|
|
794
|
+
});
|
|
682
795
|
return;
|
|
683
796
|
}
|
|
684
797
|
case 'list': {
|
|
@@ -816,13 +929,11 @@ async function main() {
|
|
|
816
929
|
}
|
|
817
930
|
case 'limits': {
|
|
818
931
|
checkFlags('limits', flags, [...LIMIT_FLAGS.map(([flag]) => flag), 'clear', 'json']);
|
|
819
|
-
const
|
|
820
|
-
if (
|
|
932
|
+
const names = parseTargets(positional);
|
|
933
|
+
if (names.length === 0)
|
|
821
934
|
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
935
|
const api = adminApi();
|
|
825
|
-
const path = `/admin/v1/consumers/${encodeURIComponent(name)}/limits`;
|
|
936
|
+
const path = (name) => `/admin/v1/consumers/${encodeURIComponent(name)}/limits`;
|
|
826
937
|
const json = flags.get('json') === 'true';
|
|
827
938
|
const set = {};
|
|
828
939
|
for (const [flag, key] of LIMIT_FLAGS) {
|
|
@@ -837,20 +948,141 @@ async function main() {
|
|
|
837
948
|
if (flags.get('clear') === 'true') {
|
|
838
949
|
if (Object.keys(set).length > 0)
|
|
839
950
|
fail('--clear drops every override — drop the value flags too');
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
951
|
+
const entries = await runBatch(names, 'clear(s)', json, async (name) => {
|
|
952
|
+
const out = (await api('DELETE', path(name)));
|
|
953
|
+
if (!json)
|
|
954
|
+
console.log(`✓ ${out.name}: overrides cleared — hub-wide defaults apply`);
|
|
955
|
+
return out;
|
|
956
|
+
});
|
|
957
|
+
if (json)
|
|
958
|
+
printJsonBatch(entries);
|
|
844
959
|
return;
|
|
845
960
|
}
|
|
846
961
|
if (Object.keys(set).length > 0) {
|
|
847
962
|
// integer/precision rules live server-side; its error lists all problems
|
|
848
|
-
const
|
|
849
|
-
|
|
963
|
+
const entries = await runBatch(names, 'update(s)', json, async (name) => {
|
|
964
|
+
const out = (await api('PUT', path(name), set));
|
|
965
|
+
if (!json)
|
|
966
|
+
console.log(`✓ ${fmtLimits(out.name, out.overrides)}`);
|
|
967
|
+
return out;
|
|
968
|
+
});
|
|
969
|
+
if (json)
|
|
970
|
+
printJsonBatch(entries);
|
|
850
971
|
return;
|
|
851
972
|
}
|
|
852
|
-
const
|
|
853
|
-
|
|
973
|
+
const entries = await runBatch(names, 'lookup(s)', json, async (name) => {
|
|
974
|
+
const out = (await api('GET', path(name)));
|
|
975
|
+
if (!json)
|
|
976
|
+
console.log(fmtLimits(out.name, out.overrides));
|
|
977
|
+
return out;
|
|
978
|
+
});
|
|
979
|
+
if (json)
|
|
980
|
+
printJsonBatch(entries);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
case 'backups': {
|
|
984
|
+
checkFlags('backups', flags, ['add', 'first', 'remove', 'clear', 'json']);
|
|
985
|
+
const alias = positional[0];
|
|
986
|
+
if (positional[1])
|
|
987
|
+
fail(`unexpected argument '${positional[1]}' (see: aweshare hub backups -h)`);
|
|
988
|
+
const json = flags.get('json') === 'true';
|
|
989
|
+
const add = flags.get('add');
|
|
990
|
+
const remove = flags.get('remove');
|
|
991
|
+
const clear = flags.get('clear') === 'true';
|
|
992
|
+
const mutating = add !== undefined || remove !== undefined || clear;
|
|
993
|
+
if (mutating && alias === undefined)
|
|
994
|
+
fail('which queue to modify: pass ALIAS (see: aweshare hub backups -h)');
|
|
995
|
+
if (clear && (add !== undefined || remove !== undefined))
|
|
996
|
+
fail('--clear drops the whole queue — drop --add/--remove too');
|
|
997
|
+
if (flags.get('first') === 'true' && add === undefined)
|
|
998
|
+
fail('--first only makes sense with --add');
|
|
999
|
+
let queues;
|
|
1000
|
+
try {
|
|
1001
|
+
queues = readBackupsFile(paths.backupsFile);
|
|
1002
|
+
}
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
1005
|
+
}
|
|
1006
|
+
if (!mutating) {
|
|
1007
|
+
const print = (key, queue) => console.log(queue && queue.length > 0
|
|
1008
|
+
? `${key} → ${queue.join(', ')}`
|
|
1009
|
+
: `${key}: no backups configured`);
|
|
1010
|
+
if (!alias) {
|
|
1011
|
+
if (queues.size === 0) {
|
|
1012
|
+
console.log('no backup queues configured — create one:');
|
|
1013
|
+
console.log(' aweshare hub backups SOME/ALIAS --add OTHER/BACKUP');
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (json)
|
|
1017
|
+
console.log(JSON.stringify(Object.fromEntries(queues), null, 2));
|
|
1018
|
+
else
|
|
1019
|
+
for (const [key, queue] of queues)
|
|
1020
|
+
print(key, queue);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const queue = queues.get(alias);
|
|
1024
|
+
if (json)
|
|
1025
|
+
console.log(JSON.stringify({ [alias]: queue ?? [] }, null, 2));
|
|
1026
|
+
else
|
|
1027
|
+
print(alias, queue);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
// unreachable in plain control flow (mutating implies ALIAS, checked
|
|
1031
|
+
// above) but needed so the compiler sees the string
|
|
1032
|
+
if (alias === undefined)
|
|
1033
|
+
fail('which queue to modify: pass ALIAS (see: aweshare hub backups -h)');
|
|
1034
|
+
if (!isValidAlias(alias))
|
|
1035
|
+
fail(`'${alias}' is not an alias (namespace/name, lowercase)`);
|
|
1036
|
+
// every mutation lands in memory first and the file is written once,
|
|
1037
|
+
// so a rejected flag leaves the file untouched
|
|
1038
|
+
if (clear) {
|
|
1039
|
+
if (!queues.delete(alias))
|
|
1040
|
+
fail(`${alias}: no queue configured (see: aweshare hub backups)`);
|
|
1041
|
+
}
|
|
1042
|
+
if (remove !== undefined) {
|
|
1043
|
+
for (const target of splitAliases(remove)) {
|
|
1044
|
+
const queue = queues.get(alias);
|
|
1045
|
+
if (!queue?.includes(target))
|
|
1046
|
+
fail(`${alias}: '${target}' is not in its queue`);
|
|
1047
|
+
const rest = queue.filter((entry) => entry !== target);
|
|
1048
|
+
if (rest.length > 0)
|
|
1049
|
+
queues.set(alias, rest);
|
|
1050
|
+
else
|
|
1051
|
+
queues.delete(alias);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (add !== undefined) {
|
|
1055
|
+
const additions = splitAliases(add);
|
|
1056
|
+
if (new Set(additions).size !== additions.length)
|
|
1057
|
+
fail(`--add repeats an alias: '${add}'`);
|
|
1058
|
+
for (const entry of additions) {
|
|
1059
|
+
if (!isValidAlias(entry))
|
|
1060
|
+
fail(`'${entry}' is not an alias (namespace/name, lowercase)`);
|
|
1061
|
+
if (entry === alias)
|
|
1062
|
+
fail(`${alias}: it cannot be its own backup`);
|
|
1063
|
+
if (queues.get(alias)?.includes(entry))
|
|
1064
|
+
fail(`${alias}: '${entry}' is already in its queue`);
|
|
1065
|
+
}
|
|
1066
|
+
const queue = queues.get(alias) ?? [];
|
|
1067
|
+
queues.set(alias, flags.get('first') === 'true' ? [...additions, ...queue] : [...queue, ...additions]);
|
|
1068
|
+
}
|
|
1069
|
+
try {
|
|
1070
|
+
// revalidate the whole file the way the hub will parse it
|
|
1071
|
+
parseBackups(serializeBackups(queues));
|
|
1072
|
+
}
|
|
1073
|
+
catch (err) {
|
|
1074
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
1075
|
+
}
|
|
1076
|
+
writeBackupsFile(paths.backupsFile, queues);
|
|
1077
|
+
const queue = queues.get(alias);
|
|
1078
|
+
if (json) {
|
|
1079
|
+
console.log(JSON.stringify(Object.fromEntries(queues), null, 2));
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
const shown = queue && queue.length > 0 ? queue.join(', ') : '(none — queue removed)';
|
|
1083
|
+
console.log(`✓ ${alias} → ${shown}`);
|
|
1084
|
+
console.log('(config.backups.toml written — the hub picks it up within ~2s)');
|
|
1085
|
+
}
|
|
854
1086
|
return;
|
|
855
1087
|
}
|
|
856
1088
|
case 'status': {
|