@cortexkit/antigravity-auth-core 2.2.1 → 3.0.1
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.md +24 -2
- package/dist/abort-forwarding.d.ts +3 -0
- package/dist/abort-forwarding.d.ts.map +1 -0
- package/dist/abort-forwarding.js +19 -0
- package/dist/abort-forwarding.js.map +1 -0
- package/dist/account-identity.d.ts +52 -0
- package/dist/account-identity.d.ts.map +1 -0
- package/dist/account-identity.js +66 -0
- package/dist/account-identity.js.map +1 -0
- package/dist/account-manager.d.ts +228 -43
- package/dist/account-manager.d.ts.map +1 -1
- package/dist/account-manager.js +942 -903
- package/dist/account-manager.js.map +1 -1
- package/dist/account-migration.d.ts +309 -0
- package/dist/account-migration.d.ts.map +1 -0
- package/dist/account-migration.js +2284 -0
- package/dist/account-migration.js.map +1 -0
- package/dist/account-repository-codecs.d.ts +82 -0
- package/dist/account-repository-codecs.d.ts.map +1 -0
- package/dist/account-repository-codecs.js +609 -0
- package/dist/account-repository-codecs.js.map +1 -0
- package/dist/account-repository-types.d.ts +831 -0
- package/dist/account-repository-types.d.ts.map +1 -0
- package/dist/account-repository-types.js +137 -0
- package/dist/account-repository-types.js.map +1 -0
- package/dist/account-repository.d.ts +442 -0
- package/dist/account-repository.d.ts.map +1 -0
- package/dist/account-repository.js +2253 -0
- package/dist/account-repository.js.map +1 -0
- package/dist/account-selector.d.ts +470 -0
- package/dist/account-selector.d.ts.map +1 -0
- package/dist/account-selector.js +1383 -0
- package/dist/account-selector.js.map +1 -0
- package/dist/account-storage.d.ts.map +1 -1
- package/dist/account-storage.js +5 -0
- package/dist/account-storage.js.map +1 -1
- package/dist/agy-request-metadata.d.ts.map +1 -1
- package/dist/agy-request-metadata.js +8 -21
- package/dist/agy-request-metadata.js.map +1 -1
- package/dist/agy-transport.d.ts.map +1 -1
- package/dist/agy-transport.js +21 -35
- package/dist/agy-transport.js.map +1 -1
- package/dist/antigravity-command-menu.d.ts +171 -0
- package/dist/antigravity-command-menu.d.ts.map +1 -0
- package/dist/antigravity-command-menu.js +523 -0
- package/dist/antigravity-command-menu.js.map +1 -0
- package/dist/atomic-write.d.ts +18 -1
- package/dist/atomic-write.d.ts.map +1 -1
- package/dist/atomic-write.js +15 -3
- package/dist/atomic-write.js.map +1 -1
- package/dist/common-auth-embedded/NOTICE.txt +21 -0
- package/dist/common-auth-embedded/auth-menu/accounts.d.ts +97 -0
- package/dist/common-auth-embedded/auth-menu/accounts.js +387 -0
- package/dist/common-auth-embedded/auth-menu/ansi.d.ts +26 -0
- package/dist/common-auth-embedded/auth-menu/ansi.js +98 -0
- package/dist/common-auth-embedded/auth-menu/confirm.d.ts +7 -0
- package/dist/common-auth-embedded/auth-menu/confirm.js +21 -0
- package/dist/common-auth-embedded/auth-menu/doctor.d.ts +52 -0
- package/dist/common-auth-embedded/auth-menu/doctor.js +81 -0
- package/dist/common-auth-embedded/auth-menu/index.d.ts +17 -0
- package/dist/common-auth-embedded/auth-menu/index.js +12 -0
- package/dist/common-auth-embedded/auth-menu/login.d.ts +55 -0
- package/dist/common-auth-embedded/auth-menu/login.js +83 -0
- package/dist/common-auth-embedded/auth-menu/menu.d.ts +59 -0
- package/dist/common-auth-embedded/auth-menu/menu.js +73 -0
- package/dist/common-auth-embedded/auth-menu/opencode-v1.d.ts +49 -0
- package/dist/common-auth-embedded/auth-menu/opencode-v1.js +47 -0
- package/dist/common-auth-embedded/auth-menu/select.d.ts +21 -0
- package/dist/common-auth-embedded/auth-menu/select.js +159 -0
- package/dist/common-auth-embedded/auth-menu/terminal.d.ts +43 -0
- package/dist/common-auth-embedded/auth-menu/terminal.js +19 -0
- package/dist/common-auth-embedded/claustrum/consumer.d.ts +102 -0
- package/dist/common-auth-embedded/claustrum/consumer.js +298 -0
- package/dist/common-auth-embedded/claustrum/custody.d.ts +175 -0
- package/dist/common-auth-embedded/claustrum/custody.js +354 -0
- package/dist/common-auth-embedded/claustrum/enrollment.d.ts +158 -0
- package/dist/common-auth-embedded/claustrum/enrollment.js +759 -0
- package/dist/common-auth-embedded/claustrum/errors.d.ts +16 -0
- package/dist/common-auth-embedded/claustrum/errors.js +8 -0
- package/dist/common-auth-embedded/claustrum/host-slot.d.ts +39 -0
- package/dist/common-auth-embedded/claustrum/host-slot.js +72 -0
- package/dist/common-auth-embedded/claustrum/index.d.ts +18 -0
- package/dist/common-auth-embedded/claustrum/index.js +22 -0
- package/dist/common-auth-embedded/claustrum/interlock.d.ts +27 -0
- package/dist/common-auth-embedded/claustrum/interlock.js +29 -0
- package/dist/common-auth-embedded/claustrum/roster.d.ts +208 -0
- package/dist/common-auth-embedded/claustrum/roster.js +536 -0
- package/dist/common-auth-embedded/commands/builtins.d.ts +71 -0
- package/dist/common-auth-embedded/commands/builtins.js +534 -0
- package/dist/common-auth-embedded/commands/index.d.ts +10 -0
- package/dist/common-auth-embedded/commands/index.js +5 -0
- package/dist/common-auth-embedded/commands/menu.d.ts +61 -0
- package/dist/common-auth-embedded/commands/menu.js +300 -0
- package/dist/common-auth-embedded/commands/model.d.ts +238 -0
- package/dist/common-auth-embedded/commands/model.js +16 -0
- package/dist/common-auth-embedded/commands/pi.d.ts +23 -0
- package/dist/common-auth-embedded/commands/pi.js +206 -0
- package/dist/common-auth-embedded/commands/seam.d.ts +72 -0
- package/dist/common-auth-embedded/commands/seam.js +301 -0
- package/dist/common-auth-embedded/fs/atomic-write.d.ts +20 -0
- package/dist/common-auth-embedded/fs/atomic-write.js +80 -0
- package/dist/common-auth-embedded/fs/index.d.ts +7 -0
- package/dist/common-auth-embedded/fs/index.js +6 -0
- package/dist/common-auth-embedded/fs/lock-constants.d.ts +14 -0
- package/dist/common-auth-embedded/fs/lock-constants.js +14 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +33 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +501 -0
- package/dist/common-auth-embedded/fs/with-lock.d.ts +33 -0
- package/dist/common-auth-embedded/fs/with-lock.js +48 -0
- package/dist/common-auth-embedded/logger/capture-sink.d.ts +14 -0
- package/dist/common-auth-embedded/logger/capture-sink.js +13 -0
- package/dist/common-auth-embedded/logger/engine.d.ts +100 -0
- package/dist/common-auth-embedded/logger/engine.js +246 -0
- package/dist/common-auth-embedded/logger/index.d.ts +6 -0
- package/dist/common-auth-embedded/logger/index.js +3 -0
- package/dist/common-auth-embedded/logger/redact.d.ts +11 -0
- package/dist/common-auth-embedded/logger/redact.js +83 -0
- package/dist/common-auth-embedded/pi-slot/index.d.ts +3 -0
- package/dist/common-auth-embedded/pi-slot/index.js +2 -0
- package/dist/common-auth-embedded/pi-slot/lock.js +212 -0
- package/dist/common-auth-embedded/pi-slot/slot.d.ts +49 -0
- package/dist/common-auth-embedded/pi-slot/slot.js +383 -0
- package/dist/common-auth-embedded/quota/codec.d.ts +11 -0
- package/dist/common-auth-embedded/quota/codec.js +12 -0
- package/dist/common-auth-embedded/quota/format.d.ts +46 -0
- package/dist/common-auth-embedded/quota/format.js +138 -0
- package/dist/common-auth-embedded/quota/index.d.ts +10 -0
- package/dist/common-auth-embedded/quota/index.js +5 -0
- package/dist/common-auth-embedded/quota/map.d.ts +63 -0
- package/dist/common-auth-embedded/quota/map.js +105 -0
- package/dist/common-auth-embedded/quota/merge.d.ts +48 -0
- package/dist/common-auth-embedded/quota/merge.js +162 -0
- package/dist/common-auth-embedded/quota/projection.d.ts +64 -0
- package/dist/common-auth-embedded/quota/projection.js +134 -0
- package/dist/common-auth-embedded/routing/admission.d.ts +93 -0
- package/dist/common-auth-embedded/routing/admission.js +142 -0
- package/dist/common-auth-embedded/routing/index.d.ts +8 -0
- package/dist/common-auth-embedded/routing/index.js +4 -0
- package/dist/common-auth-embedded/routing/ordered.d.ts +47 -0
- package/dist/common-auth-embedded/routing/ordered.js +58 -0
- package/dist/common-auth-embedded/routing/pins.d.ts +22 -0
- package/dist/common-auth-embedded/routing/pins.js +34 -0
- package/dist/common-auth-embedded/routing/sticky.d.ts +158 -0
- package/dist/common-auth-embedded/routing/sticky.js +339 -0
- package/dist/common-auth-embedded/source-output.json +1090 -0
- package/dist/common-auth-embedded/store/attribution.d.ts +28 -0
- package/dist/common-auth-embedded/store/attribution.js +60 -0
- package/dist/common-auth-embedded/store/errors.d.ts +55 -0
- package/dist/common-auth-embedded/store/errors.js +38 -0
- package/dist/common-auth-embedded/store/hooks.d.ts +12 -0
- package/dist/common-auth-embedded/store/hooks.js +34 -0
- package/dist/common-auth-embedded/store/identity.d.ts +39 -0
- package/dist/common-auth-embedded/store/identity.js +79 -0
- package/dist/common-auth-embedded/store/index.d.ts +21 -0
- package/dist/common-auth-embedded/store/index.js +7 -0
- package/dist/common-auth-embedded/store/mutate.d.ts +189 -0
- package/dist/common-auth-embedded/store/mutate.js +500 -0
- package/dist/common-auth-embedded/store/pool.d.ts +196 -0
- package/dist/common-auth-embedded/store/pool.js +120 -0
- package/dist/common-auth-embedded/store/provider-state.d.ts +131 -0
- package/dist/common-auth-embedded/store/provider-state.js +226 -0
- package/dist/common-auth-embedded/store/publication.d.ts +47 -0
- package/dist/common-auth-embedded/store/publication.js +304 -0
- package/dist/common-auth-embedded/store/pull.d.ts +36 -0
- package/dist/common-auth-embedded/store/pull.js +143 -0
- package/dist/common-auth-embedded/store/refresh-lock.d.ts +63 -0
- package/dist/common-auth-embedded/store/refresh-lock.js +136 -0
- package/dist/common-auth-embedded/store/refresh.d.ts +72 -0
- package/dist/common-auth-embedded/store/refresh.js +225 -0
- package/dist/common-auth-embedded/store/reserved.js +13 -0
- package/dist/common-auth-embedded/store/rows.d.ts +313 -0
- package/dist/common-auth-embedded/store/rows.js +1258 -0
- package/dist/common-auth-embedded/store/runtime.d.ts +36 -0
- package/dist/common-auth-embedded/store/runtime.js +68 -0
- package/dist/common-auth-embedded/store/schema.d.ts +443 -0
- package/dist/common-auth-embedded/store/schema.js +754 -0
- package/dist/common-auth-embedded/store/settings.d.ts +63 -0
- package/dist/common-auth-embedded/store/settings.js +120 -0
- package/dist/common-auth-embedded/store/torn.d.ts +78 -0
- package/dist/common-auth-embedded/store/torn.js +294 -0
- package/dist/common-auth-embedded/util/any-signal.js +42 -0
- package/dist/common-auth-runtime.d.ts +73 -0
- package/dist/common-auth-runtime.d.ts.map +1 -0
- package/dist/common-auth-runtime.js +65 -0
- package/dist/common-auth-runtime.js.map +1 -0
- package/dist/credential-redaction.d.ts +13 -0
- package/dist/credential-redaction.d.ts.map +1 -0
- package/dist/credential-redaction.js +230 -0
- package/dist/credential-redaction.js.map +1 -0
- package/dist/fetch-timeout.d.ts +4 -2
- package/dist/fetch-timeout.d.ts.map +1 -1
- package/dist/fetch-timeout.js +121 -14
- package/dist/fetch-timeout.js.map +1 -1
- package/dist/fingerprint.d.ts +3 -3
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +4 -4
- package/dist/fingerprint.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/model-registry.d.ts +16 -0
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +242 -30
- package/dist/model-registry.js.map +1 -1
- package/dist/quota-manager.d.ts.map +1 -1
- package/dist/quota-manager.js +35 -10
- package/dist/quota-manager.js.map +1 -1
- package/dist/quota-presentation.d.ts +10 -0
- package/dist/quota-presentation.d.ts.map +1 -0
- package/dist/quota-presentation.js +60 -0
- package/dist/quota-presentation.js.map +1 -0
- package/dist/quota-types.d.ts +2 -0
- package/dist/quota-types.d.ts.map +1 -1
- package/dist/rotation.d.ts +8 -0
- package/dist/rotation.d.ts.map +1 -1
- package/dist/rotation.js +24 -0
- package/dist/rotation.js.map +1 -1
- package/dist/transform/agy-generation.d.ts +3 -0
- package/dist/transform/agy-generation.d.ts.map +1 -0
- package/dist/transform/agy-generation.js +28 -0
- package/dist/transform/agy-generation.js.map +1 -0
- package/dist/transform/claude.d.ts +7 -3
- package/dist/transform/claude.d.ts.map +1 -1
- package/dist/transform/claude.js +8 -2
- package/dist/transform/claude.js.map +1 -1
- package/dist/transform/index.d.ts +1 -0
- package/dist/transform/index.d.ts.map +1 -1
- package/dist/transform/index.js +1 -0
- package/dist/transform/index.js.map +1 -1
- package/dist/transform/model-resolver.d.ts +2 -1
- package/dist/transform/model-resolver.d.ts.map +1 -1
- package/dist/transform/model-resolver.js +46 -5
- package/dist/transform/model-resolver.js.map +1 -1
- package/dist/transform/types.d.ts +1 -1
- package/dist/transform/types.d.ts.map +1 -1
- package/dist/vault-account-source.d.ts +183 -0
- package/dist/vault-account-source.d.ts.map +1 -0
- package/dist/vault-account-source.js +296 -0
- package/dist/vault-account-source.js.map +1 -0
- package/dist/vault-enrollment-authority.d.ts +16 -0
- package/dist/vault-enrollment-authority.d.ts.map +1 -0
- package/dist/vault-enrollment-authority.js +19 -0
- package/dist/vault-enrollment-authority.js.map +1 -0
- package/dist/vault-family.d.ts +11 -0
- package/dist/vault-family.d.ts.map +1 -0
- package/dist/vault-family.js +24 -0
- package/dist/vault-family.js.map +1 -0
- package/dist/vault-provider-state.d.ts +162 -0
- package/dist/vault-provider-state.d.ts.map +1 -0
- package/dist/vault-provider-state.js +422 -0
- package/dist/vault-provider-state.js.map +1 -0
- package/package.json +39 -2
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { acquireRefreshFileLock, withLock, writeJsonAtomic, } from '../fs/index.js';
|
|
4
|
+
import { isQuotaMap, mergeQuotaObservation, } from '../quota/index.js';
|
|
5
|
+
import { ClaustrumConsumerError } from './errors.js';
|
|
6
|
+
import { acceptAccount, declineAccount, isDeclined, } from './interlock.js';
|
|
7
|
+
export const DEFAULT_ROUTE_PREFIX = 'vault:';
|
|
8
|
+
function hash(value) {
|
|
9
|
+
return createHash('sha256').update(value).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
function defaultLabel(credential) {
|
|
12
|
+
// `oauth:<provider>:<label>` is the vault's conventional spelling; use the
|
|
13
|
+
// trailing label when present and fall back to the account's email.
|
|
14
|
+
const parts = credential.credentialId.split(':');
|
|
15
|
+
const label = parts.length >= 3 ? parts.slice(2).join(':').trim() : undefined;
|
|
16
|
+
return label || credential.email || credential.credentialId;
|
|
17
|
+
}
|
|
18
|
+
function groupKey(member) {
|
|
19
|
+
return member.identity !== undefined
|
|
20
|
+
? `identity:${member.identity}`
|
|
21
|
+
: `credential:${member.credential.credentialId}`;
|
|
22
|
+
}
|
|
23
|
+
function memberIds(row) {
|
|
24
|
+
return [row.credentialId, ...(row.aliases ?? [])];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Project the vault's list onto the previous roster. Pure: callers serialize
|
|
28
|
+
* discovery and commit. Route ids, quota and the time an account was added
|
|
29
|
+
* survive for the same account; a credential that now logs into a different
|
|
30
|
+
* known account gets a new route id and no inherited quota.
|
|
31
|
+
*
|
|
32
|
+
* A record listed without an identity keeps the last account it was known to
|
|
33
|
+
* log into (the row is marked `unclaimed`). Dropping that binding would let a
|
|
34
|
+
* later different account look like an identity learned for the first time
|
|
35
|
+
* and inherit the first account's route and quota.
|
|
36
|
+
*
|
|
37
|
+
* A reply with unusable records is incomplete: a previous member it does not
|
|
38
|
+
* account for (its record was rejected, or a rejected record has no usable id
|
|
39
|
+
* and could be any of them) is kept, as an alias of its live account or as a
|
|
40
|
+
* `stale` row. Only a complete reply removes an account.
|
|
41
|
+
*/
|
|
42
|
+
export function projectVaultRoster(previous, inventory, options = {}) {
|
|
43
|
+
const now = options.now ?? Date.now();
|
|
44
|
+
const prefix = options.routePrefix ?? DEFAULT_ROUTE_PREFIX;
|
|
45
|
+
const reserved = options.reservedRouteIds ?? new Set();
|
|
46
|
+
const previousRows = previous?.rows ?? [];
|
|
47
|
+
// Declines follow the account and outlive its records, so none is pruned.
|
|
48
|
+
const declined = [...(previous?.declined ?? [])];
|
|
49
|
+
const complete = inventory.skipped.length === 0;
|
|
50
|
+
const listed = new Set(inventory.credentials.map((credential) => credential.credentialId));
|
|
51
|
+
const rejectedIds = new Set(inventory.skipped.flatMap((record) => record.credentialId === undefined ? [] : [record.credentialId]));
|
|
52
|
+
const unrecoverable = inventory.skipped.some((record) => record.credentialId === undefined);
|
|
53
|
+
const unaccounted = (id) => !listed.has(id) && (rejectedIds.has(id) || unrecoverable);
|
|
54
|
+
const lastKnown = new Map();
|
|
55
|
+
for (const row of previousRows) {
|
|
56
|
+
if (row.accountIdentity === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
for (const id of memberIds(row))
|
|
59
|
+
if (!lastKnown.has(id))
|
|
60
|
+
lastKnown.set(id, row.accountIdentity);
|
|
61
|
+
}
|
|
62
|
+
const groups = new Map();
|
|
63
|
+
for (const credential of inventory.credentials) {
|
|
64
|
+
const member = credential.accountIdentity !== undefined
|
|
65
|
+
? { credential, identity: credential.accountIdentity, claimed: true }
|
|
66
|
+
: {
|
|
67
|
+
credential,
|
|
68
|
+
identity: lastKnown.get(credential.credentialId),
|
|
69
|
+
claimed: false,
|
|
70
|
+
};
|
|
71
|
+
const key = groupKey(member);
|
|
72
|
+
const group = groups.get(key) ?? [];
|
|
73
|
+
group.push(member);
|
|
74
|
+
groups.set(key, group);
|
|
75
|
+
}
|
|
76
|
+
const used = new Set();
|
|
77
|
+
const taken = new Set(reserved);
|
|
78
|
+
const projected = [];
|
|
79
|
+
const byId = (left, right) => left.credential.credentialId.localeCompare(right.credential.credentialId);
|
|
80
|
+
const sortedGroups = [...groups.values()]
|
|
81
|
+
.map((group) => group.toSorted(byId))
|
|
82
|
+
.sort((left, right) => (left[0] && right[0] ? byId(left[0], right[0]) : 0));
|
|
83
|
+
for (const ordered of sortedGroups) {
|
|
84
|
+
const identity = ordered[0]?.identity;
|
|
85
|
+
const active = ordered.filter((entry) => entry.credential.state === 'active');
|
|
86
|
+
const usable = active.length ? active : ordered;
|
|
87
|
+
// A record that claims the account itself is preferred over one that is
|
|
88
|
+
// only remembered as belonging to it.
|
|
89
|
+
const claimed = usable.filter((entry) => entry.claimed);
|
|
90
|
+
const choices = claimed.length ? claimed : usable;
|
|
91
|
+
const members = new Set(ordered.map((entry) => entry.credential.credentialId));
|
|
92
|
+
// An unknown previous account may be learned now; a known one must match.
|
|
93
|
+
const bound = previousRows.find((row) => !used.has(row) &&
|
|
94
|
+
memberIds(row).some((id) => members.has(id)) &&
|
|
95
|
+
(row.accountIdentity === undefined || row.accountIdentity === identity));
|
|
96
|
+
const chosen = choices.find((entry) => entry.credential.credentialId === bound?.credentialId) ?? choices[0];
|
|
97
|
+
if (!chosen)
|
|
98
|
+
continue;
|
|
99
|
+
const representative = chosen.credential;
|
|
100
|
+
const existing = bound ??
|
|
101
|
+
(identity === undefined
|
|
102
|
+
? undefined
|
|
103
|
+
: previousRows.find((row) => !used.has(row) && row.accountIdentity === identity));
|
|
104
|
+
if (existing)
|
|
105
|
+
used.add(existing);
|
|
106
|
+
let routeId = existing?.routeId;
|
|
107
|
+
if (!routeId || reserved.has(routeId)) {
|
|
108
|
+
const base = `${prefix}${representative.credentialId}${identity === undefined ? '' : `~${hash(identity).slice(0, 10)}`}`;
|
|
109
|
+
routeId = base;
|
|
110
|
+
for (let suffix = 2; taken.has(routeId); suffix++)
|
|
111
|
+
routeId = `${base}~${suffix}`;
|
|
112
|
+
}
|
|
113
|
+
taken.add(routeId);
|
|
114
|
+
const retained = existing ? memberIds(existing).filter(unaccounted) : [];
|
|
115
|
+
const aliases = [
|
|
116
|
+
...new Set([
|
|
117
|
+
...ordered.map((entry) => entry.credential.credentialId),
|
|
118
|
+
...retained,
|
|
119
|
+
]),
|
|
120
|
+
]
|
|
121
|
+
.filter((id) => id !== representative.credentialId)
|
|
122
|
+
.sort((left, right) => left.localeCompare(right));
|
|
123
|
+
const mapped = options.mapAccount?.(representative) ?? {};
|
|
124
|
+
projected.push({
|
|
125
|
+
previousIndex: existing ? previousRows.indexOf(existing) : Infinity,
|
|
126
|
+
row: {
|
|
127
|
+
routeId,
|
|
128
|
+
credentialId: representative.credentialId,
|
|
129
|
+
credentialType: representative.credentialType,
|
|
130
|
+
...(identity !== undefined && { accountIdentity: identity }),
|
|
131
|
+
...(aliases.length && { aliases }),
|
|
132
|
+
state: representative.state,
|
|
133
|
+
label: mapped.label?.trim() || defaultLabel(representative),
|
|
134
|
+
...(representative.email !== undefined && {
|
|
135
|
+
email: representative.email,
|
|
136
|
+
}),
|
|
137
|
+
...(representative.orgName !== undefined && {
|
|
138
|
+
orgName: representative.orgName,
|
|
139
|
+
}),
|
|
140
|
+
enabled: ![representative.credentialId, ...aliases].some((id) => isDeclined(declined, id, identity)),
|
|
141
|
+
addedAt: existing?.addedAt ?? now,
|
|
142
|
+
...(existing?.quota !== undefined && { quota: existing.quota }),
|
|
143
|
+
...(!chosen.claimed && identity !== undefined && { unclaimed: true }),
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// An account the reply does not account for keeps its last good projection:
|
|
148
|
+
// a rejected record must not read as the account having been removed. A
|
|
149
|
+
// credential this reply lists belongs to the live row it was projected into,
|
|
150
|
+
// so it is taken out of the stale row: otherwise one credential could be
|
|
151
|
+
// authorized, and its readings recorded, under two account bindings. A
|
|
152
|
+
// stale row left with no member is dropped.
|
|
153
|
+
previousRows.forEach((row, previousIndex) => {
|
|
154
|
+
if (used.has(row) || !memberIds(row).some(unaccounted))
|
|
155
|
+
return;
|
|
156
|
+
if (taken.has(row.routeId))
|
|
157
|
+
return;
|
|
158
|
+
const [credentialId, ...aliases] = memberIds(row).filter((id) => !listed.has(id));
|
|
159
|
+
if (credentialId === undefined)
|
|
160
|
+
return;
|
|
161
|
+
taken.add(row.routeId);
|
|
162
|
+
const { aliases: _previousAliases, ...rest } = row;
|
|
163
|
+
projected.push({
|
|
164
|
+
previousIndex,
|
|
165
|
+
row: {
|
|
166
|
+
...rest,
|
|
167
|
+
credentialId,
|
|
168
|
+
...(aliases.length && { aliases }),
|
|
169
|
+
stale: true,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
projected.sort((left, right) => left.previousIndex === right.previousIndex
|
|
174
|
+
? left.row.credentialId.localeCompare(right.row.credentialId)
|
|
175
|
+
: left.previousIndex - right.previousIndex);
|
|
176
|
+
return {
|
|
177
|
+
version: 1,
|
|
178
|
+
view: inventory.view,
|
|
179
|
+
complete,
|
|
180
|
+
...(!complete && {
|
|
181
|
+
rejected: inventory.skipped.map((record) => ({
|
|
182
|
+
...(record.credentialId !== undefined && {
|
|
183
|
+
credentialId: record.credentialId,
|
|
184
|
+
}),
|
|
185
|
+
reason: record.reason,
|
|
186
|
+
})),
|
|
187
|
+
}),
|
|
188
|
+
rows: projected.map((entry) => entry.row),
|
|
189
|
+
declined,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The rows `/routing` selects among. Only enabled, active accounts route;
|
|
194
|
+
* a declined or cold vault account stays listed for the menu but never
|
|
195
|
+
* reaches admission.
|
|
196
|
+
*/
|
|
197
|
+
export function vaultRoutingRows(roster) {
|
|
198
|
+
return (roster?.rows ?? [])
|
|
199
|
+
.filter((row) => row.enabled && row.state === 'active')
|
|
200
|
+
.map((row) => ({
|
|
201
|
+
id: row.routeId,
|
|
202
|
+
kind: row.credentialType === 'oauth' ? 'oauth' : 'api-key',
|
|
203
|
+
...(row.quota !== undefined && { quota: row.quota }),
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
function isRecord(value) {
|
|
207
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
208
|
+
}
|
|
209
|
+
function optionalString(value) {
|
|
210
|
+
return value === undefined || typeof value === 'string';
|
|
211
|
+
}
|
|
212
|
+
function decodeRow(value) {
|
|
213
|
+
if (!isRecord(value) ||
|
|
214
|
+
typeof value.routeId !== 'string' ||
|
|
215
|
+
typeof value.credentialId !== 'string' ||
|
|
216
|
+
(value.credentialType !== 'oauth' && value.credentialType !== 'api_key') ||
|
|
217
|
+
typeof value.state !== 'string' ||
|
|
218
|
+
typeof value.label !== 'string' ||
|
|
219
|
+
typeof value.enabled !== 'boolean' ||
|
|
220
|
+
typeof value.addedAt !== 'number' ||
|
|
221
|
+
!optionalString(value.accountIdentity) ||
|
|
222
|
+
!optionalString(value.email) ||
|
|
223
|
+
!optionalString(value.orgName) ||
|
|
224
|
+
(value.aliases !== undefined &&
|
|
225
|
+
(!Array.isArray(value.aliases) ||
|
|
226
|
+
!value.aliases.every((alias) => typeof alias === 'string'))) ||
|
|
227
|
+
(value.quota !== undefined && !isQuotaMap(value.quota)) ||
|
|
228
|
+
(value.stale !== undefined && value.stale !== true) ||
|
|
229
|
+
(value.unclaimed !== undefined && value.unclaimed !== true))
|
|
230
|
+
return undefined;
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
const SKIPPED_REASONS = new Set([
|
|
234
|
+
'empty credential id',
|
|
235
|
+
'duplicate credential id',
|
|
236
|
+
'blank account identity',
|
|
237
|
+
'empty state',
|
|
238
|
+
]);
|
|
239
|
+
function decodeRejected(value) {
|
|
240
|
+
if (!Array.isArray(value))
|
|
241
|
+
return undefined;
|
|
242
|
+
const records = [];
|
|
243
|
+
for (const entry of value) {
|
|
244
|
+
if (!isRecord(entry) ||
|
|
245
|
+
!optionalString(entry.credentialId) ||
|
|
246
|
+
typeof entry.reason !== 'string' ||
|
|
247
|
+
!SKIPPED_REASONS.has(entry.reason))
|
|
248
|
+
return undefined;
|
|
249
|
+
records.push(entry);
|
|
250
|
+
}
|
|
251
|
+
return records;
|
|
252
|
+
}
|
|
253
|
+
function decodeRoster(value) {
|
|
254
|
+
if (!isRecord(value) ||
|
|
255
|
+
value.version !== 1 ||
|
|
256
|
+
!optionalString(value.view) ||
|
|
257
|
+
!Array.isArray(value.rows) ||
|
|
258
|
+
!Array.isArray(value.declined) ||
|
|
259
|
+
(value.complete !== undefined && typeof value.complete !== 'boolean') ||
|
|
260
|
+
(value.rejected !== undefined && !decodeRejected(value.rejected)))
|
|
261
|
+
throw new ClaustrumConsumerError('invalid-state', 'Claustrum roster file is not valid');
|
|
262
|
+
const rows = [];
|
|
263
|
+
for (const entry of value.rows) {
|
|
264
|
+
const row = decodeRow(entry);
|
|
265
|
+
if (!row)
|
|
266
|
+
throw new ClaustrumConsumerError('invalid-state', 'Claustrum roster file is not valid');
|
|
267
|
+
rows.push(row);
|
|
268
|
+
}
|
|
269
|
+
const declined = [];
|
|
270
|
+
for (const entry of value.declined) {
|
|
271
|
+
if (!isRecord(entry) ||
|
|
272
|
+
typeof entry.credentialId !== 'string' ||
|
|
273
|
+
!optionalString(entry.accountIdentity))
|
|
274
|
+
throw new ClaustrumConsumerError('invalid-state', 'Claustrum roster file is not valid');
|
|
275
|
+
declined.push(entry);
|
|
276
|
+
}
|
|
277
|
+
const rejected = decodeRejected(value.rejected);
|
|
278
|
+
return {
|
|
279
|
+
version: 1,
|
|
280
|
+
...(typeof value.view === 'string' && { view: value.view }),
|
|
281
|
+
// A file written before completeness was recorded came from a reply
|
|
282
|
+
// that was used whole, so it reads as complete.
|
|
283
|
+
complete: value.complete !== false,
|
|
284
|
+
...(rejected && { rejected }),
|
|
285
|
+
rows,
|
|
286
|
+
declined,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
export async function readVaultRoster(path) {
|
|
290
|
+
let text;
|
|
291
|
+
try {
|
|
292
|
+
text = await readFile(path, 'utf8');
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (error.code === 'ENOENT')
|
|
296
|
+
return undefined;
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
let value;
|
|
300
|
+
try {
|
|
301
|
+
value = JSON.parse(text);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
throw new ClaustrumConsumerError('invalid-state', 'Claustrum roster file is not valid');
|
|
305
|
+
}
|
|
306
|
+
return decodeRoster(value);
|
|
307
|
+
}
|
|
308
|
+
const WRITE_LOCK = {
|
|
309
|
+
name: 'claustrum-roster-write',
|
|
310
|
+
ttlMs: 10_000,
|
|
311
|
+
timeoutMs: 5_000,
|
|
312
|
+
};
|
|
313
|
+
const DISCOVERY_LEASE = { name: 'claustrum-roster', ttlMs: 30_000 };
|
|
314
|
+
/**
|
|
315
|
+
* Read, change and write the roster under its write lock. `change` returns
|
|
316
|
+
* undefined to leave the file as it is. Call `assertOwned()` immediately
|
|
317
|
+
* before any rename performed inside `change`; if it throws, do not rename.
|
|
318
|
+
*/
|
|
319
|
+
export async function mutateVaultRoster(path, change, options = {}) {
|
|
320
|
+
return withLock(path, WRITE_LOCK, async (lock) => {
|
|
321
|
+
const current = await readVaultRoster(path);
|
|
322
|
+
const { next, result } = await change(current, {
|
|
323
|
+
assertOwned: () => lock.assertOwned(),
|
|
324
|
+
});
|
|
325
|
+
if (next) {
|
|
326
|
+
await lock.assertOwned();
|
|
327
|
+
// Staging the file takes several awaited steps; check both the caller's
|
|
328
|
+
// lease and this lock again at the last moment before the rename.
|
|
329
|
+
await writeJsonAtomic(path, next, {
|
|
330
|
+
beforeRename: async () => {
|
|
331
|
+
await options.beforePublish?.();
|
|
332
|
+
await lock.assertOwned();
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/** Mark a vault account declined; it stays listed but never routes. */
|
|
340
|
+
export function declineVaultRoute(path, routeId) {
|
|
341
|
+
return mutateVaultRoster(path, (current) => {
|
|
342
|
+
const row = current?.rows.find((entry) => entry.routeId === routeId);
|
|
343
|
+
if (!current || !row)
|
|
344
|
+
throw new ClaustrumConsumerError('route-unavailable', 'Claustrum route is not in the roster');
|
|
345
|
+
const declined = declineAccount(current.declined, row.credentialId, row.accountIdentity);
|
|
346
|
+
return {
|
|
347
|
+
next: {
|
|
348
|
+
...current,
|
|
349
|
+
declined,
|
|
350
|
+
rows: current.rows.map((entry) => entry === row ? { ...entry, enabled: false } : entry),
|
|
351
|
+
},
|
|
352
|
+
result: undefined,
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/** Lift the user's decline for a vault account and every alias of it. */
|
|
357
|
+
export function acceptVaultRoute(path, routeId) {
|
|
358
|
+
return mutateVaultRoster(path, (current) => {
|
|
359
|
+
const row = current?.rows.find((entry) => entry.routeId === routeId);
|
|
360
|
+
if (!current || !row)
|
|
361
|
+
throw new ClaustrumConsumerError('route-unavailable', 'Claustrum route is not in the roster');
|
|
362
|
+
return {
|
|
363
|
+
next: {
|
|
364
|
+
...current,
|
|
365
|
+
declined: acceptAccount(current.declined, memberIds(row), row.accountIdentity),
|
|
366
|
+
rows: current.rows.map((entry) => entry === row ? { ...entry, enabled: true } : entry),
|
|
367
|
+
},
|
|
368
|
+
result: undefined,
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* What a receipt may do to a row's quota: `hold` (the row holds the receipt's
|
|
374
|
+
* credential and account), `bind` (the row has never had a known account and
|
|
375
|
+
* the receipt proves one, so the row takes that account with the reading) or
|
|
376
|
+
* `refuse`.
|
|
377
|
+
*/
|
|
378
|
+
function receiptFence(row, receipt) {
|
|
379
|
+
if (!memberIds(row).includes(receipt.credentialId))
|
|
380
|
+
return 'refuse';
|
|
381
|
+
// While the vault makes no claim, the roster's own expectation proves
|
|
382
|
+
// nothing about which account the reading came from.
|
|
383
|
+
if (row.unclaimed &&
|
|
384
|
+
(receipt.accountIdentitySource === 'expected' ||
|
|
385
|
+
receipt.accountIdentitySource === 'none'))
|
|
386
|
+
return 'refuse';
|
|
387
|
+
// Equality, never compatibility: a known account on one side does not match
|
|
388
|
+
// an absent one on the other. The only absent-absent match is a row that
|
|
389
|
+
// has never had a known account and a receipt that names none either.
|
|
390
|
+
if (row.accountIdentity === receipt.accountIdentity)
|
|
391
|
+
return 'hold';
|
|
392
|
+
// A row that has never had a known account, reached by a receipt issued for
|
|
393
|
+
// it in that state whose served or parsed identity names an account. The
|
|
394
|
+
// reading belongs to that account, so keeping it without the account would
|
|
395
|
+
// let the next list that names a different account inherit it as if that
|
|
396
|
+
// account had been learned for the first time. The row takes the account
|
|
397
|
+
// with the reading instead: a later list naming another account is then a
|
|
398
|
+
// known replacement and inherits nothing. Only a single-credential row is
|
|
399
|
+
// bound, since one credential's account says nothing about the others.
|
|
400
|
+
if (row.accountIdentity === undefined &&
|
|
401
|
+
receipt.expectedAccountIdentity === undefined &&
|
|
402
|
+
receipt.accountIdentity !== undefined &&
|
|
403
|
+
(receipt.accountIdentitySource === 'asserted' ||
|
|
404
|
+
receipt.accountIdentitySource === 'parsed') &&
|
|
405
|
+
memberIds(row).length === 1)
|
|
406
|
+
return 'bind';
|
|
407
|
+
return 'refuse';
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Merge a quota observation into a vault row through `/quota`'s merge. The
|
|
411
|
+
* write is fenced on the receipt the reading was taken with: it lands only
|
|
412
|
+
* while the row still holds that receipt's credential (as representative or
|
|
413
|
+
* alias) and account, so a slow read for a replaced account can never land on
|
|
414
|
+
* its successor. Returns whether the observation was kept.
|
|
415
|
+
*/
|
|
416
|
+
export function recordVaultQuota(path, input) {
|
|
417
|
+
return mutateVaultRoster(path, (current) => {
|
|
418
|
+
const row = current?.rows.find((entry) => entry.routeId === input.routeId);
|
|
419
|
+
const fence = !current || !row ? 'refuse' : receiptFence(row, input);
|
|
420
|
+
if (!current || !row || fence === 'refuse')
|
|
421
|
+
return { result: false };
|
|
422
|
+
const quota = mergeQuotaObservation(row.quota, input.observation);
|
|
423
|
+
// A bound account is `unclaimed`: the vault's list has not named it, and
|
|
424
|
+
// the next projection keeps it as the account this credential last
|
|
425
|
+
// logged into until the vault names one.
|
|
426
|
+
const bound = fence === 'bind'
|
|
427
|
+
? { accountIdentity: input.accountIdentity, unclaimed: true }
|
|
428
|
+
: {};
|
|
429
|
+
return {
|
|
430
|
+
next: {
|
|
431
|
+
...current,
|
|
432
|
+
rows: current.rows.map((entry) => entry === row ? { ...entry, ...bound, quota } : entry),
|
|
433
|
+
},
|
|
434
|
+
result: true,
|
|
435
|
+
};
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Discover and commit the roster. The lease covers the list call and the
|
|
440
|
+
* commit, not just the write: an opaque view cannot tell a delayed old reply
|
|
441
|
+
* from a newer one, so a run that lost its lease is refused at commit. A
|
|
442
|
+
* failed list, or custody switching off mid-run, never writes anything; a
|
|
443
|
+
* peer holding the lease means its last committed roster is served instead.
|
|
444
|
+
*/
|
|
445
|
+
export async function refreshVaultRoster(options) {
|
|
446
|
+
options.signal?.throwIfAborted();
|
|
447
|
+
const active = async () => (await options.isActive?.()) ?? true;
|
|
448
|
+
if (!(await active()))
|
|
449
|
+
return undefined;
|
|
450
|
+
const lease = await acquireRefreshFileLock({
|
|
451
|
+
...DISCOVERY_LEASE,
|
|
452
|
+
path: options.path,
|
|
453
|
+
renew: true,
|
|
454
|
+
});
|
|
455
|
+
if (!lease) {
|
|
456
|
+
const persisted = await readVaultRoster(options.path);
|
|
457
|
+
options.signal?.throwIfAborted();
|
|
458
|
+
if (!persisted?.view)
|
|
459
|
+
throw new ClaustrumConsumerError('roster-busy', 'Claustrum account discovery is already in progress');
|
|
460
|
+
return persisted;
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const inventory = await options.custody.discover(options.signal);
|
|
464
|
+
options.signal?.throwIfAborted();
|
|
465
|
+
return await mutateVaultRoster(options.path, async (current) => {
|
|
466
|
+
if (!(await active()))
|
|
467
|
+
return { result: undefined };
|
|
468
|
+
options.signal?.throwIfAborted();
|
|
469
|
+
await lease.assertOwned();
|
|
470
|
+
const projection = typeof options.projection === 'function'
|
|
471
|
+
? options.projection()
|
|
472
|
+
: options.projection;
|
|
473
|
+
const next = projectVaultRoster(current, inventory, projection);
|
|
474
|
+
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
475
|
+
return { ...(changed && { next }), result: next };
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
await lease.release();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* The seam for a plugin that keeps a main account: one fixed account the host
|
|
484
|
+
* owns under a logical route, outside the pool's rotating rows. The library
|
|
485
|
+
* has no main convention of its own: the plugin names the
|
|
486
|
+
* vault record that holds the role (`primaryCredentialId`) and its logical
|
|
487
|
+
* route. Main follows the account that record claims; another credential of
|
|
488
|
+
* that same account may serve it when the record itself is not active, but a
|
|
489
|
+
* credential of any other account is never promoted into main.
|
|
490
|
+
*
|
|
491
|
+
* Inputs are the plugin's filtered inventory, whether that inventory is
|
|
492
|
+
* complete (defaults to the reply having no unusable records; pass the
|
|
493
|
+
* roster's `complete` or false to override), the previous verified binding
|
|
494
|
+
* and, on the request path, the account the chosen request expects.
|
|
495
|
+
*/
|
|
496
|
+
export function resolveVaultPrimary(input) {
|
|
497
|
+
const { inventory, previous } = input;
|
|
498
|
+
const unavailable = (reason) => ({
|
|
499
|
+
status: 'unavailable',
|
|
500
|
+
reason,
|
|
501
|
+
...(previous && { lastVerified: previous }),
|
|
502
|
+
});
|
|
503
|
+
const record = inventory.credentials.find((credential) => credential.credentialId === input.primaryCredentialId);
|
|
504
|
+
if (!record) {
|
|
505
|
+
if (inventory.skipped.some((skipped) => skipped.credentialId === input.primaryCredentialId))
|
|
506
|
+
return unavailable('malformed');
|
|
507
|
+
// A rejected record without a usable id could be the primary record.
|
|
508
|
+
const incomplete = input.complete === false ||
|
|
509
|
+
inventory.skipped.some((skipped) => skipped.credentialId === undefined);
|
|
510
|
+
return incomplete ? unavailable('incomplete') : { status: 'absent' };
|
|
511
|
+
}
|
|
512
|
+
const identity = record.accountIdentity;
|
|
513
|
+
if (identity === undefined)
|
|
514
|
+
return unavailable('unclaimed');
|
|
515
|
+
if (input.expectedAccountIdentity !== undefined &&
|
|
516
|
+
input.expectedAccountIdentity !== identity)
|
|
517
|
+
return unavailable('identity-changed');
|
|
518
|
+
const representative = record.state === 'active'
|
|
519
|
+
? record
|
|
520
|
+
: (inventory.credentials
|
|
521
|
+
.filter((credential) => credential.accountIdentity === identity &&
|
|
522
|
+
credential.state === 'active')
|
|
523
|
+
.toSorted((left, right) => left.credentialId.localeCompare(right.credentialId))[0] ?? record);
|
|
524
|
+
const binding = {
|
|
525
|
+
routeId: input.routeId,
|
|
526
|
+
credentialId: representative.credentialId,
|
|
527
|
+
accountIdentity: identity,
|
|
528
|
+
view: inventory.view,
|
|
529
|
+
};
|
|
530
|
+
return {
|
|
531
|
+
status: 'ready',
|
|
532
|
+
binding,
|
|
533
|
+
...(previous &&
|
|
534
|
+
previous.accountIdentity !== identity && { replaced: previous }),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { AddInput, PoolLockSpec, PoolRow, PoolStore, RemoveOptions } from '../store/index.js';
|
|
2
|
+
import type { CommandInvocation, KnobValues, MenuChoice, MenuKnob } from './model.js';
|
|
3
|
+
import { type ResolvedSection } from './seam.js';
|
|
4
|
+
/**
|
|
5
|
+
* What a plugin's login hands back. `ready` adds the account now. `pending`
|
|
6
|
+
* means the user must finish somewhere else (a browser, a device code):
|
|
7
|
+
* `message` tells them how, and the account is added when `completion`
|
|
8
|
+
* settles, with the result sent to the invocation that started it.
|
|
9
|
+
*/
|
|
10
|
+
export type LoginOutcome = {
|
|
11
|
+
status: 'ready';
|
|
12
|
+
account: AddInput;
|
|
13
|
+
} | {
|
|
14
|
+
status: 'pending';
|
|
15
|
+
message: string;
|
|
16
|
+
/** Resolves with the account to add, or undefined when the user gave up. */
|
|
17
|
+
completion: Promise<AddInput | undefined>;
|
|
18
|
+
} | {
|
|
19
|
+
status: 'cancelled';
|
|
20
|
+
message?: string;
|
|
21
|
+
};
|
|
22
|
+
export interface AccountsSectionOptions {
|
|
23
|
+
/** What to show as an account's identity; defaults to its recorded identity. */
|
|
24
|
+
describeIdentity?(row: PoolRow): string | undefined;
|
|
25
|
+
/** The plugin's login; without it the section offers no add action. */
|
|
26
|
+
login?: {
|
|
27
|
+
label?: string;
|
|
28
|
+
knobs?: MenuKnob[];
|
|
29
|
+
run(values: KnobValues, invocation: CommandInvocation): Promise<LoginOutcome>;
|
|
30
|
+
};
|
|
31
|
+
/** Passed to `store.remove`: a reason refuses removing that id. */
|
|
32
|
+
protect?: RemoveOptions['protect'];
|
|
33
|
+
}
|
|
34
|
+
export interface QuotaSectionOptions {
|
|
35
|
+
/** The quota scope to show (`all` or a model family); defaults to `all`. */
|
|
36
|
+
scope?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Checks quota now for the named rows. Without it, the menu asks the store
|
|
39
|
+
* for a reading of each row and waits for the pulls to settle.
|
|
40
|
+
*/
|
|
41
|
+
check?(ids: readonly string[], invocation: CommandInvocation): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
export interface RoutingSectionOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Further `routing.mode` values the plugin routes as `ordered` with the
|
|
46
|
+
* former main row moved (`main-first`, `fallback-first`), offered between
|
|
47
|
+
* `ordered` and `sticky-balanced`.
|
|
48
|
+
*/
|
|
49
|
+
orderedVariants?: MenuChoice[];
|
|
50
|
+
formerMainId?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface LimitsSectionOptions {
|
|
53
|
+
/**
|
|
54
|
+
* The quota window labels a floor can be set for. Defaults to every label
|
|
55
|
+
* the pool's quota readings carry, or `primary` when there are none.
|
|
56
|
+
*/
|
|
57
|
+
labels?: readonly string[];
|
|
58
|
+
}
|
|
59
|
+
export interface BuiltinOptions {
|
|
60
|
+
store: PoolStore;
|
|
61
|
+
extraLocks?: readonly PoolLockSpec[];
|
|
62
|
+
now: () => number;
|
|
63
|
+
accounts?: AccountsSectionOptions;
|
|
64
|
+
quota?: QuotaSectionOptions;
|
|
65
|
+
routing?: RoutingSectionOptions;
|
|
66
|
+
limits?: LimitsSectionOptions;
|
|
67
|
+
}
|
|
68
|
+
/** `disabledReason` recorded when the user disables an account from the menu. */
|
|
69
|
+
export declare const MENU_DISABLED_REASON = "disabled from the command menu";
|
|
70
|
+
/** The four built-in sections, in their fixed order, from one read of the store. */
|
|
71
|
+
export declare function builtinSections(options: BuiltinOptions): Promise<ResolvedSection[]>;
|