@cortexkit/common-auth 0.2.5 → 0.2.6
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/dist/commands/builtins.d.ts +71 -0
- package/dist/commands/builtins.js +508 -0
- package/dist/commands/index.d.ts +10 -1
- package/dist/commands/index.js +5 -2
- package/dist/commands/menu.d.ts +39 -0
- package/dist/commands/menu.js +249 -0
- package/dist/commands/model.d.ts +188 -0
- package/dist/commands/model.js +16 -0
- package/dist/commands/pi.d.ts +22 -0
- package/dist/commands/pi.js +178 -0
- package/dist/commands/seam.d.ts +36 -0
- package/dist/commands/seam.js +185 -0
- package/dist/store/errors.d.ts +1 -1
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +1 -0
- package/dist/store/pool.d.ts +12 -0
- package/dist/store/pool.js +3 -0
- package/dist/store/settings.d.ts +63 -0
- package/dist/store/settings.js +120 -0
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// The one way out of the process for anything the command menu shows. Every
|
|
2
|
+
// payload a renderer receives (the dialog payload and every apply result) is
|
|
3
|
+
// produced here: each part is projected field by field from its definition,
|
|
4
|
+
// so action bodies and stray properties never travel, and the whole payload
|
|
5
|
+
// is then scrubbed of credential-shaped property names as a backstop. Nothing
|
|
6
|
+
// outside this module can build a payload without passing through
|
|
7
|
+
// `dialogPayload` or `applyResult`.
|
|
8
|
+
/** The confirmation shown when an irreversible action names none. */
|
|
9
|
+
export const DEFAULT_IRREVERSIBLE_CONFIRMATION = 'This cannot be undone. Continue?';
|
|
10
|
+
const CREDENTIAL_NAMES = new Set([
|
|
11
|
+
'access',
|
|
12
|
+
'refresh',
|
|
13
|
+
'apikey',
|
|
14
|
+
'password',
|
|
15
|
+
'authheader',
|
|
16
|
+
'credential',
|
|
17
|
+
'credentials',
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* True for a property name that looks like it holds a secret. Case, `-` and
|
|
21
|
+
* `_` are ignored, so `api_key`, `API-Key` and `apiKey` all match. Any name
|
|
22
|
+
* ending in `token`, `key` or `secret` matches: no menu field is named that
|
|
23
|
+
* way, so a match is a leak whatever the rest of the name says.
|
|
24
|
+
*/
|
|
25
|
+
function isCredentialName(name) {
|
|
26
|
+
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
27
|
+
return (CREDENTIAL_NAMES.has(normalized) ||
|
|
28
|
+
normalized.endsWith('token') ||
|
|
29
|
+
normalized.endsWith('key') ||
|
|
30
|
+
normalized.endsWith('secret'));
|
|
31
|
+
}
|
|
32
|
+
function isPlainRecord(value) {
|
|
33
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
/** A copy of `value` without credential-shaped properties; their paths go to `found`. */
|
|
36
|
+
function scrub(value, path, found) {
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return value.map((entry, index) => scrub(entry, `${path}[${index}]`, found));
|
|
39
|
+
if (!isPlainRecord(value))
|
|
40
|
+
return value;
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
43
|
+
if (isCredentialName(name)) {
|
|
44
|
+
found.push(`${path}.${name}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
Object.defineProperty(out, name, {
|
|
48
|
+
value: scrub(entry, `${path}.${name}`, found),
|
|
49
|
+
enumerable: true,
|
|
50
|
+
writable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function sealed(payload, command, logger) {
|
|
57
|
+
const found = [];
|
|
58
|
+
const clean = scrub(payload, 'payload', found);
|
|
59
|
+
if (found.length > 0)
|
|
60
|
+
logger.warn('credential-shaped field dropped from a command payload', {
|
|
61
|
+
command,
|
|
62
|
+
fields: found,
|
|
63
|
+
});
|
|
64
|
+
return clean;
|
|
65
|
+
}
|
|
66
|
+
function projectChoice(choice) {
|
|
67
|
+
return { value: String(choice.value), label: String(choice.label) };
|
|
68
|
+
}
|
|
69
|
+
function projectKnob(knob) {
|
|
70
|
+
switch (knob.kind) {
|
|
71
|
+
case 'choice':
|
|
72
|
+
return {
|
|
73
|
+
kind: 'choice',
|
|
74
|
+
id: knob.id,
|
|
75
|
+
label: knob.label,
|
|
76
|
+
choices: (knob.choices ?? []).map(projectChoice),
|
|
77
|
+
...(knob.value !== undefined ? { value: knob.value } : {}),
|
|
78
|
+
};
|
|
79
|
+
case 'toggle':
|
|
80
|
+
return {
|
|
81
|
+
kind: 'toggle',
|
|
82
|
+
id: knob.id,
|
|
83
|
+
label: knob.label,
|
|
84
|
+
value: knob.value === true,
|
|
85
|
+
};
|
|
86
|
+
case 'number':
|
|
87
|
+
return {
|
|
88
|
+
kind: 'number',
|
|
89
|
+
id: knob.id,
|
|
90
|
+
label: knob.label,
|
|
91
|
+
...(knob.value !== undefined ? { value: knob.value } : {}),
|
|
92
|
+
...(knob.min !== undefined ? { min: knob.min } : {}),
|
|
93
|
+
...(knob.max !== undefined ? { max: knob.max } : {}),
|
|
94
|
+
...(knob.required ? { required: true } : {}),
|
|
95
|
+
};
|
|
96
|
+
case 'text':
|
|
97
|
+
return {
|
|
98
|
+
kind: 'text',
|
|
99
|
+
id: knob.id,
|
|
100
|
+
label: knob.label,
|
|
101
|
+
...(knob.value !== undefined ? { value: knob.value } : {}),
|
|
102
|
+
...(knob.placeholder !== undefined
|
|
103
|
+
? { placeholder: knob.placeholder }
|
|
104
|
+
: {}),
|
|
105
|
+
...(knob.masked ? { masked: true } : {}),
|
|
106
|
+
...(knob.required ? { required: true } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The confirmation an action must pass before it runs, or undefined. An
|
|
112
|
+
* irreversible action always has one, even when its definition (built
|
|
113
|
+
* outside the type checker) names none.
|
|
114
|
+
*/
|
|
115
|
+
export function confirmationOf(action) {
|
|
116
|
+
if (action.irreversible === true)
|
|
117
|
+
return {
|
|
118
|
+
message: action.confirm || DEFAULT_IRREVERSIBLE_CONFIRMATION,
|
|
119
|
+
irreversible: true,
|
|
120
|
+
};
|
|
121
|
+
if (action.confirm)
|
|
122
|
+
return { message: action.confirm, irreversible: false };
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function projectAction(action) {
|
|
126
|
+
const confirm = confirmationOf(action);
|
|
127
|
+
return {
|
|
128
|
+
id: action.id,
|
|
129
|
+
label: action.label,
|
|
130
|
+
...(action.description !== undefined
|
|
131
|
+
? { description: action.description }
|
|
132
|
+
: {}),
|
|
133
|
+
knobs: (action.knobs ?? []).map(projectKnob),
|
|
134
|
+
...(confirm ? { confirm } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** The account fields a renderer may show; see `MenuAccount`. */
|
|
138
|
+
export function projectAccount(account) {
|
|
139
|
+
return {
|
|
140
|
+
id: account.id,
|
|
141
|
+
...(account.label !== undefined ? { label: account.label } : {}),
|
|
142
|
+
enabled: account.enabled,
|
|
143
|
+
type: account.type,
|
|
144
|
+
...(account.identity !== undefined ? { identity: account.identity } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function projectItem(item) {
|
|
148
|
+
return {
|
|
149
|
+
id: item.id,
|
|
150
|
+
label: item.label,
|
|
151
|
+
...(item.detail !== undefined ? { detail: item.detail } : {}),
|
|
152
|
+
...(item.account ? { account: projectAccount(item.account) } : {}),
|
|
153
|
+
...(item.facts !== undefined ? { facts: item.facts } : {}),
|
|
154
|
+
actions: (item.actions ?? []).map(projectAction),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function projectSection(section) {
|
|
158
|
+
const { content } = section;
|
|
159
|
+
return {
|
|
160
|
+
id: section.id,
|
|
161
|
+
slot: section.slot,
|
|
162
|
+
title: section.title,
|
|
163
|
+
lines: (content.lines ?? []).map(String),
|
|
164
|
+
items: (content.items ?? []).map(projectItem),
|
|
165
|
+
actions: (content.actions ?? []).map(projectAction),
|
|
166
|
+
...(content.facts !== undefined ? { facts: content.facts } : {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function projectMenu(command, title, sections) {
|
|
170
|
+
return { command, title, sections: sections.map(projectSection) };
|
|
171
|
+
}
|
|
172
|
+
/** The payload a host's TUI receives when the slash command opens. */
|
|
173
|
+
export function dialogPayload(command, title, sections, logger) {
|
|
174
|
+
return sealed({ command, menu: projectMenu(command, title, sections) }, command, logger);
|
|
175
|
+
}
|
|
176
|
+
/** An apply's result: the message and the refreshed menu. */
|
|
177
|
+
export function applyResult(command, title, sections, outcome, logger) {
|
|
178
|
+
return sealed({
|
|
179
|
+
command,
|
|
180
|
+
ok: outcome.ok,
|
|
181
|
+
text: outcome.text,
|
|
182
|
+
...(outcome.needsConfirmation ? { needsConfirmation: true } : {}),
|
|
183
|
+
menu: projectMenu(command, title, sections),
|
|
184
|
+
}, command, logger);
|
|
185
|
+
}
|
package/dist/store/errors.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StoredCredential } from './schema.js';
|
|
2
2
|
/** Every library operation that can fail, as named in the failure value. */
|
|
3
|
-
export type PoolOperation = 'initialize' | 'add' | 'replace' | 'rotate' | 'disable' | 'enable' | 'remove' | 'reorder' | 'recordIdentity' | 'refresh' | 'pull';
|
|
3
|
+
export type PoolOperation = 'initialize' | 'add' | 'replace' | 'rotate' | 'disable' | 'enable' | 'remove' | 'reorder' | 'updateSettings' | 'recordIdentity' | 'refresh' | 'pull';
|
|
4
4
|
/**
|
|
5
5
|
* How far an operation got before it failed.
|
|
6
6
|
*
|
package/dist/store/index.d.ts
CHANGED
|
@@ -14,3 +14,5 @@ export type { AddInput, AddResult, FailureHook, RemoveOptions, RemoveResult, Rem
|
|
|
14
14
|
export type { PullReason } from './runtime.js';
|
|
15
15
|
export type { ApiKeyCredential, OAuthCredential, PoolCredential, PoolRow, QuotaCodec, StoredCredential, } from './schema.js';
|
|
16
16
|
export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
|
|
17
|
+
export type { PoolSettings, SettingsMutator, SettingsRead, UpdateSettingsOptions, UpdateSettingsResult, } from './settings.js';
|
|
18
|
+
export { POOL_OWNED_KEYS } from './settings.js';
|
package/dist/store/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { countUnknownIdentityRows, DUPLICATE_IDENTITY_REASON, } from './identity
|
|
|
3
3
|
export { openPoolStore } from './pool.js';
|
|
4
4
|
export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
|
|
5
5
|
export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
|
|
6
|
+
export { POOL_OWNED_KEYS } from './settings.js';
|
package/dist/store/pool.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type ProviderRefresh, type RefreshOptions, type RefreshOutcome } from '
|
|
|
7
7
|
import { type LockEnvironment, type PoolLockOptions, type PoolLockSpec } from './refresh-lock.js';
|
|
8
8
|
import { type AddInput, type AddResult, type RemoveOptions, type RemoveResult, type ReorderOptions, type ReorderResult, type RowOperationOptions, type RowToggleOptions } from './rows.js';
|
|
9
9
|
import { type PoolCredential, type PoolRow, type QuotaCodec, type StoredCredential } from './schema.js';
|
|
10
|
+
import { type SettingsMutator, type SettingsRead, type UpdateSettingsOptions, type UpdateSettingsResult } from './settings.js';
|
|
10
11
|
export interface OpenPoolStoreOptions {
|
|
11
12
|
/** The provider every row of this pool belongs to; keys the provider-wide lock. */
|
|
12
13
|
provider: string;
|
|
@@ -108,6 +109,17 @@ export interface PoolStore {
|
|
|
108
109
|
* provider-wide lock. Roster rows and their entries are left unchanged.
|
|
109
110
|
*/
|
|
110
111
|
reorder(ids: readonly string[], options?: ReorderOptions): Promise<ReorderResult>;
|
|
112
|
+
/**
|
|
113
|
+
* The plugin's settings (since 0.2.6): every top-level key of the config
|
|
114
|
+
* file except the pool-owned ones (`POOL_OWNED_KEYS`). Never writes.
|
|
115
|
+
*/
|
|
116
|
+
readSettings(): Promise<SettingsRead>;
|
|
117
|
+
/**
|
|
118
|
+
* One locked write of the plugin's settings (since 0.2.6) beside the pool,
|
|
119
|
+
* in the config file. Refuses a result that sets a pool-owned key
|
|
120
|
+
* (`invalid-input`). Takes `extraLocks`, then the store locks.
|
|
121
|
+
*/
|
|
122
|
+
updateSettings(mutator: SettingsMutator, options?: UpdateSettingsOptions): Promise<UpdateSettingsResult>;
|
|
111
123
|
recordIdentity(id: string, identity: string, options?: RowOperationOptions): Promise<{
|
|
112
124
|
id: string;
|
|
113
125
|
disabled: string[];
|
package/dist/store/pool.js
CHANGED
|
@@ -5,6 +5,7 @@ import { refreshRow, } from './refresh.js';
|
|
|
5
5
|
import { POOL_LOCK_DEFAULTS, } from './refresh-lock.js';
|
|
6
6
|
import { addRow, disableRow, enableRow, recordRowIdentity, removeRow, reorderRows, replaceRow, rotateRow, } from './rows.js';
|
|
7
7
|
import { POOL_SCHEMA_VERSION, } from './schema.js';
|
|
8
|
+
import { readPoolSettings, updatePoolSettings, } from './settings.js';
|
|
8
9
|
/**
|
|
9
10
|
* Process-wide memory per config file: ids whose per-row entry a library
|
|
10
11
|
* write dropped (not reused in this process), and rows a load-time pull
|
|
@@ -95,6 +96,8 @@ export function openPoolStore(options) {
|
|
|
95
96
|
enable: (id, callOptions) => enableRow(rt, id, callOptions),
|
|
96
97
|
remove: (id, callOptions) => removeRow(rt, id, callOptions),
|
|
97
98
|
reorder: (ids, callOptions) => reorderRows(rt, ids, callOptions),
|
|
99
|
+
readSettings: () => readPoolSettings(rt),
|
|
100
|
+
updateSettings: (mutator, callOptions) => updatePoolSettings(rt, mutator, callOptions),
|
|
98
101
|
recordIdentity: (id, identity, callOptions) => recordRowIdentity(rt, id, identity, callOptions),
|
|
99
102
|
refresh: (id, provider, callOptions) => refreshRow(rt, id, provider, callOptions),
|
|
100
103
|
recordQuota: (id, attribution, observation) => recordQuota(rt, id, attribution, observation),
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { PoolOperationError } from './errors.js';
|
|
2
|
+
import type { PoolLockSpec } from './refresh-lock.js';
|
|
3
|
+
import type { StoreRuntime } from './runtime.js';
|
|
4
|
+
/**
|
|
5
|
+
* Top-level config keys the pool owns: the legacy `version`, the legacy
|
|
6
|
+
* roster (`accounts`) and the pool's own key. A settings write never reads
|
|
7
|
+
* them into the settings it hands out and refuses a result that sets them,
|
|
8
|
+
* so the roster and the per-row entries change only through row operations.
|
|
9
|
+
*/
|
|
10
|
+
export declare const POOL_OWNED_KEYS: readonly string[];
|
|
11
|
+
/** The plugin's settings: every top-level config key except the pool-owned ones. */
|
|
12
|
+
export type PoolSettings = Record<string, unknown>;
|
|
13
|
+
export type SettingsRead = {
|
|
14
|
+
status: 'ready';
|
|
15
|
+
settings: PoolSettings;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The config still holds a legacy roster with no pool key. Its settings
|
|
19
|
+
* read the same way; `updateSettings` refuses until the pool is initialized.
|
|
20
|
+
*/
|
|
21
|
+
| {
|
|
22
|
+
status: 'pending-migration';
|
|
23
|
+
settings: PoolSettings;
|
|
24
|
+
} | {
|
|
25
|
+
status: 'error';
|
|
26
|
+
file: 'config' | 'state';
|
|
27
|
+
reason: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Receives a private copy of the current settings and either edits it in
|
|
31
|
+
* place (returning nothing) or returns the complete next settings object.
|
|
32
|
+
* It runs under the store locks, so it must not call back into the store:
|
|
33
|
+
* a store operation called from inside it is refused (`PoolReentryError`).
|
|
34
|
+
*/
|
|
35
|
+
export type SettingsMutator = (settings: PoolSettings) => PoolSettings | undefined | Promise<PoolSettings | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* Options of `updateSettings`. It names no row, so its failure hook is
|
|
38
|
+
* handed only the failure; like `reorder` it takes the extra locks, then the
|
|
39
|
+
* store locks, and no row or provider-wide lock.
|
|
40
|
+
*/
|
|
41
|
+
export interface UpdateSettingsOptions {
|
|
42
|
+
/** Called once, awaited, on every non-success path, before the extra locks release. */
|
|
43
|
+
onFailure?: (error: PoolOperationError) => void | Promise<void>;
|
|
44
|
+
/** Locks taken, in this order, before the store locks. */
|
|
45
|
+
extraLocks?: readonly PoolLockSpec[];
|
|
46
|
+
}
|
|
47
|
+
export type UpdateSettingsResult = {
|
|
48
|
+
/** The settings now on disk. */
|
|
49
|
+
settings: PoolSettings;
|
|
50
|
+
/** `unchanged` when the mutator left the settings as they were; nothing was written. */
|
|
51
|
+
outcome: 'updated' | 'unchanged';
|
|
52
|
+
};
|
|
53
|
+
/** Reads the settings without taking a lock or writing anything. */
|
|
54
|
+
export declare function readPoolSettings(rt: StoreRuntime): Promise<SettingsRead>;
|
|
55
|
+
/**
|
|
56
|
+
* One locked read-modify-write of the plugin's settings in the config file
|
|
57
|
+
* the pool lives in. The pool must be ready (a pending migration or a load
|
|
58
|
+
* error refuses before the mutator runs). A mutator result that sets a
|
|
59
|
+
* pool-owned key refuses (`invalid-input`) with nothing written; a result
|
|
60
|
+
* equal to the current settings writes nothing. The state file is never
|
|
61
|
+
* touched, and the pool-owned keys are written back exactly as read.
|
|
62
|
+
*/
|
|
63
|
+
export declare function updatePoolSettings(rt: StoreRuntime, mutator: SettingsMutator, options?: UpdateSettingsOptions): Promise<UpdateSettingsResult>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { writeJsonAtomic } from '../fs/atomic-write.js';
|
|
3
|
+
import { PoolOperationError } from './errors.js';
|
|
4
|
+
import { assertNotInsideHook, runInsideHook } from './hooks.js';
|
|
5
|
+
import { notReadyError, readPool, runOperation } from './mutate.js';
|
|
6
|
+
import { isRecord, POOL_KEY } from './schema.js';
|
|
7
|
+
/**
|
|
8
|
+
* Top-level config keys the pool owns: the legacy `version`, the legacy
|
|
9
|
+
* roster (`accounts`) and the pool's own key. A settings write never reads
|
|
10
|
+
* them into the settings it hands out and refuses a result that sets them,
|
|
11
|
+
* so the roster and the per-row entries change only through row operations.
|
|
12
|
+
*/
|
|
13
|
+
export const POOL_OWNED_KEYS = Object.freeze([
|
|
14
|
+
'version',
|
|
15
|
+
'accounts',
|
|
16
|
+
POOL_KEY,
|
|
17
|
+
]);
|
|
18
|
+
function settingsOf(config) {
|
|
19
|
+
const settings = {};
|
|
20
|
+
for (const [key, value] of Object.entries(config))
|
|
21
|
+
if (!POOL_OWNED_KEYS.includes(key))
|
|
22
|
+
defineKey(settings, key, value);
|
|
23
|
+
return settings;
|
|
24
|
+
}
|
|
25
|
+
/** Sets a key as an own data property, so a `__proto__` key stays a plain key. */
|
|
26
|
+
function defineKey(target, key, value) {
|
|
27
|
+
Object.defineProperty(target, key, {
|
|
28
|
+
value,
|
|
29
|
+
enumerable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The next config: the pool-owned keys exactly as read, every settings key
|
|
36
|
+
* from `next` in its existing position, settings keys `next` dropped left
|
|
37
|
+
* out, and new settings keys appended.
|
|
38
|
+
*/
|
|
39
|
+
function composeConfig(config, next) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [key, value] of Object.entries(config)) {
|
|
42
|
+
if (POOL_OWNED_KEYS.includes(key))
|
|
43
|
+
defineKey(out, key, value);
|
|
44
|
+
else if (Object.hasOwn(next, key))
|
|
45
|
+
defineKey(out, key, next[key]);
|
|
46
|
+
}
|
|
47
|
+
for (const [key, value] of Object.entries(next))
|
|
48
|
+
if (!Object.hasOwn(config, key))
|
|
49
|
+
defineKey(out, key, value);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function settingsRefusal(message) {
|
|
53
|
+
return new PoolOperationError({
|
|
54
|
+
operation: 'updateSettings',
|
|
55
|
+
phase: 'before-first-write',
|
|
56
|
+
retryable: false,
|
|
57
|
+
kind: 'invalid-input',
|
|
58
|
+
message,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Reads the settings without taking a lock or writing anything. */
|
|
62
|
+
export async function readPoolSettings(rt) {
|
|
63
|
+
const result = await readPool(rt.ctx);
|
|
64
|
+
if (result.status === 'error')
|
|
65
|
+
return result;
|
|
66
|
+
return { status: result.status, settings: settingsOf(result.config) };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* One locked read-modify-write of the plugin's settings in the config file
|
|
70
|
+
* the pool lives in. The pool must be ready (a pending migration or a load
|
|
71
|
+
* error refuses before the mutator runs). A mutator result that sets a
|
|
72
|
+
* pool-owned key refuses (`invalid-input`) with nothing written; a result
|
|
73
|
+
* equal to the current settings writes nothing. The state file is never
|
|
74
|
+
* touched, and the pool-owned keys are written back exactly as read.
|
|
75
|
+
*/
|
|
76
|
+
export async function updatePoolSettings(rt, mutator, options = {}) {
|
|
77
|
+
assertNotInsideHook('updateSettings');
|
|
78
|
+
const { ctx } = rt;
|
|
79
|
+
const onFailure = options.onFailure;
|
|
80
|
+
return runOperation(ctx, 'updateSettings', undefined, onFailure && ((_rowId, error) => onFailure(error)), async (locks, progress) => {
|
|
81
|
+
for (const extra of options.extraLocks ?? [])
|
|
82
|
+
await locks.acquire(extra);
|
|
83
|
+
// `mark` is where the store locks start on the lock stack. They are
|
|
84
|
+
// released at the end of this block, so `onFailure` runs holding only
|
|
85
|
+
// the caller's extra locks, as it does for every other store operation.
|
|
86
|
+
const mark = locks.held.length;
|
|
87
|
+
try {
|
|
88
|
+
for (const spec of ctx.storeLocks)
|
|
89
|
+
await locks.acquire(spec);
|
|
90
|
+
const result = await readPool(ctx);
|
|
91
|
+
if (result.status !== 'ready')
|
|
92
|
+
throw notReadyError(result, 'updateSettings', undefined);
|
|
93
|
+
const current = settingsOf(result.config);
|
|
94
|
+
const draft = structuredClone(current);
|
|
95
|
+
const returned = await runInsideHook('updateSettings', () => mutator(draft));
|
|
96
|
+
const next = returned === undefined ? draft : returned;
|
|
97
|
+
if (!isRecord(next))
|
|
98
|
+
throw settingsRefusal('the settings mutator must produce an object');
|
|
99
|
+
const owned = POOL_OWNED_KEYS.filter((key) => Object.hasOwn(next, key));
|
|
100
|
+
if (owned.length > 0)
|
|
101
|
+
throw settingsRefusal(`settings cannot set the pool-owned key(s) ${owned.join(', ')}`);
|
|
102
|
+
if (isDeepStrictEqual(next, current))
|
|
103
|
+
return { settings: current, outcome: 'unchanged' };
|
|
104
|
+
const config = composeConfig(result.config, next);
|
|
105
|
+
const info = { operation: 'updateSettings', rowId: undefined };
|
|
106
|
+
await writeJsonAtomic(ctx.configPath, config, {
|
|
107
|
+
beforeRename: async () => {
|
|
108
|
+
await ctx.onStep?.('before-config-write', info);
|
|
109
|
+
await locks.assertAll();
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
progress.writes++;
|
|
113
|
+
await ctx.onStep?.('after-config-write', info);
|
|
114
|
+
return { settings: settingsOf(config), outcome: 'updated' };
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await locks.releaseTo(mark);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/common-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Shared code for the CortexKit auth plugins: account pool, quota and routing, commands and auth menu, OpenCode 2 hooks, Claustrum custody, and plumbing (loopback RPC, file locks, logger, sidebar state, TUI preferences and build).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|