@cortexkit/antigravity-auth-core 2.2.1 → 3.0.0
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 +2282 -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-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/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/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/claude.d.ts +7 -3
- package/dist/transform/claude.d.ts.map +1 -1
- package/dist/transform/claude.js +6 -2
- package/dist/transform/claude.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,206 @@
|
|
|
1
|
+
// The Pi renderer: walks the same menu model the OpenCode dialog draws, with
|
|
2
|
+
// Pi's extension UI (`select`, `confirm`, `input`, `notify`). It sees only the
|
|
3
|
+
// payloads the menu's seam produces, and applies actions through the menu,
|
|
4
|
+
// so the confirmation and input checks are the same as on OpenCode.
|
|
5
|
+
/**
|
|
6
|
+
* Shows `entries` with `select` and returns the chosen value. Labels are made
|
|
7
|
+
* unique, since `select` answers with the label.
|
|
8
|
+
*/
|
|
9
|
+
async function pick(ui, title, entries) {
|
|
10
|
+
const byLabel = new Map();
|
|
11
|
+
const labels = [];
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
let label = entry.label;
|
|
14
|
+
for (let n = 2; byLabel.has(label); n++)
|
|
15
|
+
label = `${entry.label} (${n})`;
|
|
16
|
+
byLabel.set(label, entry.value);
|
|
17
|
+
labels.push(label);
|
|
18
|
+
}
|
|
19
|
+
const chosen = await ui.select(title, labels);
|
|
20
|
+
return chosen === undefined ? undefined : byLabel.get(chosen);
|
|
21
|
+
}
|
|
22
|
+
function withLines(title, lines) {
|
|
23
|
+
return lines.length > 0 ? `${title}\n${lines.join('\n')}` : title;
|
|
24
|
+
}
|
|
25
|
+
/** A row as one line of text: its label, then its status when it has one. */
|
|
26
|
+
function rowText(item) {
|
|
27
|
+
return item.status ? `${item.label} · ${item.status}` : item.label;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The read-only part of a section's select title: its lines, then each item
|
|
31
|
+
* that has no actions, under its group's header. Such an item does nothing
|
|
32
|
+
* when chosen, so it is shown as text rather than offered as an option. Pi's
|
|
33
|
+
* `select` cannot put text between options, so the headers of groups whose
|
|
34
|
+
* rows are all selectable are not shown.
|
|
35
|
+
*/
|
|
36
|
+
function sectionText(section) {
|
|
37
|
+
const text = [...section.lines];
|
|
38
|
+
let header;
|
|
39
|
+
for (const item of section.items) {
|
|
40
|
+
if (item.actions.length > 0)
|
|
41
|
+
continue;
|
|
42
|
+
if (item.group !== undefined && item.group !== header)
|
|
43
|
+
text.push(item.group);
|
|
44
|
+
header = item.group;
|
|
45
|
+
text.push(item.group !== undefined ? ` ${rowText(item)}` : rowText(item));
|
|
46
|
+
}
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
function itemTitle(item) {
|
|
50
|
+
const lines = item.detail ? [item.detail] : [];
|
|
51
|
+
for (const [name, value] of Object.entries(item.facts ?? {}))
|
|
52
|
+
lines.push(`${name}: ${String(value)}`);
|
|
53
|
+
return withLines(rowText(item), lines);
|
|
54
|
+
}
|
|
55
|
+
/** One knob's value, or undefined when the user backed out or typed nonsense. */
|
|
56
|
+
async function askKnob(ui, knob) {
|
|
57
|
+
switch (knob.kind) {
|
|
58
|
+
case 'choice': {
|
|
59
|
+
const value = await pick(ui, knob.label, knob.choices.map((choice) => ({
|
|
60
|
+
label: choice.value === knob.value
|
|
61
|
+
? `${choice.label} (current)`
|
|
62
|
+
: choice.label,
|
|
63
|
+
value: choice.value,
|
|
64
|
+
})));
|
|
65
|
+
return value === undefined ? undefined : { value };
|
|
66
|
+
}
|
|
67
|
+
case 'toggle': {
|
|
68
|
+
const value = await pick(ui, knob.label, [
|
|
69
|
+
{ label: knob.value ? 'On (current)' : 'On', value: true },
|
|
70
|
+
{ label: knob.value ? 'Off' : 'Off (current)', value: false },
|
|
71
|
+
]);
|
|
72
|
+
return value === undefined ? undefined : { value };
|
|
73
|
+
}
|
|
74
|
+
case 'number': {
|
|
75
|
+
const range = knob.min !== undefined && knob.max !== undefined
|
|
76
|
+
? ` (${knob.min}-${knob.max}${knob.required ? '' : ', empty clears'})`
|
|
77
|
+
: knob.required
|
|
78
|
+
? ''
|
|
79
|
+
: ' (empty clears)';
|
|
80
|
+
const typed = await ui.input(`${knob.label}${range}`, knob.value !== undefined ? String(knob.value) : undefined);
|
|
81
|
+
if (typed === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
const text = typed.trim();
|
|
84
|
+
if (text === '')
|
|
85
|
+
return knob.required && knob.value !== undefined
|
|
86
|
+
? { value: knob.value }
|
|
87
|
+
: { value: null };
|
|
88
|
+
const number = Number(text);
|
|
89
|
+
if (!Number.isFinite(number)) {
|
|
90
|
+
ui.notify(`${knob.label} needs a number.`, 'error');
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return { value: number };
|
|
94
|
+
}
|
|
95
|
+
case 'text': {
|
|
96
|
+
const typed = await ui.input(knob.label, knob.value ?? knob.placeholder);
|
|
97
|
+
if (typed === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
const text = typed.trim();
|
|
100
|
+
if (text === '')
|
|
101
|
+
return knob.required && knob.value !== undefined
|
|
102
|
+
? { value: knob.value }
|
|
103
|
+
: { value: null };
|
|
104
|
+
return { value: text };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function askValues(ui, action) {
|
|
109
|
+
const values = {};
|
|
110
|
+
for (const knob of action.knobs) {
|
|
111
|
+
const answer = await askKnob(ui, knob);
|
|
112
|
+
if (!answer)
|
|
113
|
+
return undefined;
|
|
114
|
+
values[knob.id] = answer.value;
|
|
115
|
+
}
|
|
116
|
+
return values;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Runs one invocation of the slash command on Pi: sections, then an item or
|
|
120
|
+
* a section action, then the action's inputs and confirmation; after each
|
|
121
|
+
* apply the user is back in the same section with the refreshed menu. Each
|
|
122
|
+
* section's list ends with Back, which returns to the section list; backing
|
|
123
|
+
* out of the section list ends the invocation.
|
|
124
|
+
*/
|
|
125
|
+
export async function runPiCommandMenu(menu, ui, options = {}) {
|
|
126
|
+
const invocation = {
|
|
127
|
+
...(options.sessionId !== undefined
|
|
128
|
+
? { sessionId: options.sessionId }
|
|
129
|
+
: {}),
|
|
130
|
+
notify: (message, kind) => ui.notify(message, kind),
|
|
131
|
+
};
|
|
132
|
+
let model = (await menu.open(invocation)).menu;
|
|
133
|
+
for (;;) {
|
|
134
|
+
const chosen = await pick(ui, model.title, model.sections.map((section) => ({
|
|
135
|
+
label: section.lines[0]
|
|
136
|
+
? `${section.title}: ${section.lines[0]}`
|
|
137
|
+
: section.title,
|
|
138
|
+
value: section.id,
|
|
139
|
+
})));
|
|
140
|
+
if (chosen === undefined)
|
|
141
|
+
return;
|
|
142
|
+
for (;;) {
|
|
143
|
+
const section = model.sections.find((entry) => entry.id === chosen);
|
|
144
|
+
if (!section)
|
|
145
|
+
break;
|
|
146
|
+
// Only rows that do something are options; the rest is title text.
|
|
147
|
+
const entries = [
|
|
148
|
+
...section.items
|
|
149
|
+
.filter((item) => item.actions.length > 0)
|
|
150
|
+
.map((item) => ({
|
|
151
|
+
label: rowText(item),
|
|
152
|
+
value: { kind: 'item', item },
|
|
153
|
+
})),
|
|
154
|
+
...section.actions.map((action) => ({
|
|
155
|
+
label: action.label,
|
|
156
|
+
value: { kind: 'action', action },
|
|
157
|
+
})),
|
|
158
|
+
];
|
|
159
|
+
const text = sectionText(section);
|
|
160
|
+
if (entries.length === 0 && text.length === 0) {
|
|
161
|
+
ui.notify('Nothing to do here.');
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
const choice = await pick(ui, withLines(section.title, text), [
|
|
165
|
+
...entries,
|
|
166
|
+
{ label: 'Back', value: { kind: 'back' } },
|
|
167
|
+
]);
|
|
168
|
+
if (!choice || choice.kind === 'back')
|
|
169
|
+
break;
|
|
170
|
+
let action;
|
|
171
|
+
let item;
|
|
172
|
+
if (choice.kind === 'item') {
|
|
173
|
+
item = choice.item;
|
|
174
|
+
action = await pick(ui, itemTitle(item), item.actions.map((entry) => ({ label: entry.label, value: entry })));
|
|
175
|
+
if (!action)
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
action = choice.action;
|
|
180
|
+
}
|
|
181
|
+
const values = await askValues(ui, action);
|
|
182
|
+
if (!values)
|
|
183
|
+
continue;
|
|
184
|
+
if (action.confirm) {
|
|
185
|
+
const yes = await ui.confirm(action.label, action.confirm.message);
|
|
186
|
+
if (!yes) {
|
|
187
|
+
ui.notify('Cancelled.');
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const result = await menu.apply({
|
|
192
|
+
command: model.command,
|
|
193
|
+
sectionId: section.id,
|
|
194
|
+
...(item ? { itemId: item.id } : {}),
|
|
195
|
+
actionId: action.id,
|
|
196
|
+
values,
|
|
197
|
+
...(action.confirm ? { confirmed: true } : {}),
|
|
198
|
+
...(options.sessionId !== undefined
|
|
199
|
+
? { sessionId: options.sessionId }
|
|
200
|
+
: {}),
|
|
201
|
+
}, invocation);
|
|
202
|
+
ui.notify(result.text, result.ok ? 'info' : 'error');
|
|
203
|
+
model = result.menu;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type RedactionOptions } from '../logger/index.js';
|
|
2
|
+
import type { ActionDefinition, CommandApplyResult, CommandDialogPayload, ItemDefinition, MenuAccount, MenuConfirmation, SectionContent, SectionSlot } from './model.js';
|
|
3
|
+
/** Where a dropped field is reported. Names only, never values. */
|
|
4
|
+
export interface SeamLogger {
|
|
5
|
+
warn(message: string, data?: unknown): void;
|
|
6
|
+
}
|
|
7
|
+
/** Masks the secret-shaped parts of one string. */
|
|
8
|
+
export type TextRedactor = (text: string) => string;
|
|
9
|
+
/** The string redactor every value crossing the seam goes through. */
|
|
10
|
+
export declare function createTextRedactor(options?: RedactionOptions): TextRedactor;
|
|
11
|
+
/**
|
|
12
|
+
* A failure whose message was written for the user. An action throws it to
|
|
13
|
+
* show `message` with the stable `code`; any other thrown value is shown only
|
|
14
|
+
* as a generic message, because its text may quote a request, a response or
|
|
15
|
+
* a credential. `code` is lowercase letters, digits and dashes; anything else
|
|
16
|
+
* is reported as `action-failed`.
|
|
17
|
+
*/
|
|
18
|
+
export declare class CommandError extends Error {
|
|
19
|
+
readonly code: string;
|
|
20
|
+
constructor(code: string, message: string, options?: {
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** What a failed action shows: a stable code and a message safe to display. */
|
|
25
|
+
export interface ProjectedFailure {
|
|
26
|
+
code: string;
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
/** The code and message shown for an exception the seam cannot vouch for. */
|
|
30
|
+
export declare const ACTION_FAILED: ProjectedFailure;
|
|
31
|
+
/** The code and display text for a thrown value; never its raw text. */
|
|
32
|
+
export declare function projectFailure(error: unknown): ProjectedFailure;
|
|
33
|
+
/** A built-in item may carry the account it shows; plugin items cannot. */
|
|
34
|
+
export interface ResolvedItem extends ItemDefinition {
|
|
35
|
+
account?: MenuAccount;
|
|
36
|
+
}
|
|
37
|
+
/** A section with its bodies, as the menu holds it between builds. */
|
|
38
|
+
export interface ResolvedSection {
|
|
39
|
+
id: string;
|
|
40
|
+
slot: SectionSlot;
|
|
41
|
+
title: string;
|
|
42
|
+
content: Omit<SectionContent, 'items'> & {
|
|
43
|
+
items?: ResolvedItem[];
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The confirmation shown when an irreversible action names none. */
|
|
47
|
+
export declare const DEFAULT_IRREVERSIBLE_CONFIRMATION = "This cannot be undone. Continue?";
|
|
48
|
+
/** What every payload builder needs besides the payload itself. */
|
|
49
|
+
export interface SeamContext {
|
|
50
|
+
logger: SeamLogger;
|
|
51
|
+
redact: TextRedactor;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The confirmation an action must pass before it runs, or undefined. An
|
|
55
|
+
* irreversible action always has one, even when its definition (built
|
|
56
|
+
* outside the type checker) names none.
|
|
57
|
+
*/
|
|
58
|
+
export declare function confirmationOf(action: ActionDefinition): MenuConfirmation | undefined;
|
|
59
|
+
/** The account fields a renderer may show; see `MenuAccount`. */
|
|
60
|
+
export declare function projectAccount(account: MenuAccount): MenuAccount;
|
|
61
|
+
/** The payload a host's TUI receives when the slash command opens. */
|
|
62
|
+
export declare function dialogPayload(command: string, title: string, sections: readonly ResolvedSection[], seam: SeamContext): CommandDialogPayload;
|
|
63
|
+
/** How an apply ended, before the seam turns it into a payload. */
|
|
64
|
+
export interface ApplyOutcome {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
text: string;
|
|
67
|
+
/** Present on every failure. */
|
|
68
|
+
code?: string;
|
|
69
|
+
needsConfirmation?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/** An apply's result: the message and the refreshed menu. */
|
|
72
|
+
export declare function applyResult(command: string, title: string, sections: readonly ResolvedSection[], outcome: ApplyOutcome, seam: SeamContext): CommandApplyResult;
|
|
@@ -0,0 +1,301 @@
|
|
|
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 as a backstop: credential-shaped property names are
|
|
6
|
+
// dropped and every string value is passed through the redactor. Nothing
|
|
7
|
+
// outside this module can build a payload without passing through
|
|
8
|
+
// `dialogPayload` or `applyResult`. A failed action never shows its raw
|
|
9
|
+
// exception text: `projectFailure` turns it into a stable code and a message
|
|
10
|
+
// the library or the plugin wrote for the user.
|
|
11
|
+
import { createRedactor } from '../logger/index.js';
|
|
12
|
+
import { PoolOperationError } from '../store/index.js';
|
|
13
|
+
/**
|
|
14
|
+
* Secret shapes the seam masks on top of the logger's (bearer tokens, `sk-`
|
|
15
|
+
* keys, JWTs, vault host tokens): a value assigned to a secret-named field
|
|
16
|
+
* (`client_secret=…`, `"api_key": "…"`, `Authorization: …`), and a run of 32
|
|
17
|
+
* or more hex digits, the shape of a minted request secret. A plugin adds its
|
|
18
|
+
* provider's key shapes through `RedactionOptions.extraValuePatterns`.
|
|
19
|
+
*/
|
|
20
|
+
const SEAM_VALUE_PATTERNS = [
|
|
21
|
+
/\b[\w-]*(?:secret|token|password|passwd|api[_-]?key|authorization|verifier)["']?\s*[:=]\s*["']?(?:(?:Bearer|Basic)\s+)?[^\s"'&,;}]+/gi,
|
|
22
|
+
/\b[0-9a-fA-F]{32,}\b/g,
|
|
23
|
+
];
|
|
24
|
+
/** The string redactor every value crossing the seam goes through. */
|
|
25
|
+
export function createTextRedactor(options = {}) {
|
|
26
|
+
const { redactStrings } = createRedactor({
|
|
27
|
+
...options,
|
|
28
|
+
extraValuePatterns: [
|
|
29
|
+
...SEAM_VALUE_PATTERNS,
|
|
30
|
+
...(options.extraValuePatterns ?? []),
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
return (text) => redactStrings(text);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A failure whose message was written for the user. An action throws it to
|
|
37
|
+
* show `message` with the stable `code`; any other thrown value is shown only
|
|
38
|
+
* as a generic message, because its text may quote a request, a response or
|
|
39
|
+
* a credential. `code` is lowercase letters, digits and dashes; anything else
|
|
40
|
+
* is reported as `action-failed`.
|
|
41
|
+
*/
|
|
42
|
+
export class CommandError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
constructor(code, message, options) {
|
|
45
|
+
super(message, options);
|
|
46
|
+
this.name = 'CommandError';
|
|
47
|
+
this.code = code;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const CODE_SHAPE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
51
|
+
/** The code and message shown for an exception the seam cannot vouch for. */
|
|
52
|
+
export const ACTION_FAILED = {
|
|
53
|
+
code: 'action-failed',
|
|
54
|
+
text: 'That action failed.',
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Store failure kinds whose message the store composes itself from row ids
|
|
58
|
+
* and the plugin's own refusal reasons (`protect`). Other kinds can carry a
|
|
59
|
+
* lock or filesystem error's text, or an arbitrary exception from a hook, so
|
|
60
|
+
* they show a generic message with the kind as the code.
|
|
61
|
+
*/
|
|
62
|
+
const STORE_KINDS_WITH_OWN_MESSAGE = new Set([
|
|
63
|
+
'unknown-row',
|
|
64
|
+
'invalid-row',
|
|
65
|
+
'invalid-input',
|
|
66
|
+
'id-exists',
|
|
67
|
+
'id-removed',
|
|
68
|
+
'type-mismatch',
|
|
69
|
+
'no-credential',
|
|
70
|
+
'row-disabled',
|
|
71
|
+
'row-protected',
|
|
72
|
+
'duplicate-identity',
|
|
73
|
+
'row-key-changed',
|
|
74
|
+
'invalid-order',
|
|
75
|
+
'pending-migration',
|
|
76
|
+
'provider',
|
|
77
|
+
'pull',
|
|
78
|
+
'attribution',
|
|
79
|
+
]);
|
|
80
|
+
/** The code and display text for a thrown value; never its raw text. */
|
|
81
|
+
export function projectFailure(error) {
|
|
82
|
+
if (error instanceof CommandError)
|
|
83
|
+
return {
|
|
84
|
+
code: CODE_SHAPE.test(error.code) ? error.code : ACTION_FAILED.code,
|
|
85
|
+
text: error.message,
|
|
86
|
+
};
|
|
87
|
+
if (error instanceof PoolOperationError)
|
|
88
|
+
return {
|
|
89
|
+
code: `pool-${error.kind}`,
|
|
90
|
+
text: STORE_KINDS_WITH_OWN_MESSAGE.has(error.kind)
|
|
91
|
+
? error.message
|
|
92
|
+
: `The account pool could not do that (${error.kind}).`,
|
|
93
|
+
};
|
|
94
|
+
return ACTION_FAILED;
|
|
95
|
+
}
|
|
96
|
+
/** The confirmation shown when an irreversible action names none. */
|
|
97
|
+
export const DEFAULT_IRREVERSIBLE_CONFIRMATION = 'This cannot be undone. Continue?';
|
|
98
|
+
const CREDENTIAL_NAMES = new Set([
|
|
99
|
+
'access',
|
|
100
|
+
'refresh',
|
|
101
|
+
'apikey',
|
|
102
|
+
'password',
|
|
103
|
+
'authheader',
|
|
104
|
+
'credential',
|
|
105
|
+
'credentials',
|
|
106
|
+
]);
|
|
107
|
+
/**
|
|
108
|
+
* True for a property name that looks like it holds a secret. Case, `-` and
|
|
109
|
+
* `_` are ignored, so `api_key`, `API-Key` and `apiKey` all match. Any name
|
|
110
|
+
* ending in `token`, `key` or `secret` matches: no menu field is named that
|
|
111
|
+
* way, so a match is a leak whatever the rest of the name says.
|
|
112
|
+
*/
|
|
113
|
+
function isCredentialName(name) {
|
|
114
|
+
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
115
|
+
return (CREDENTIAL_NAMES.has(normalized) ||
|
|
116
|
+
normalized.endsWith('token') ||
|
|
117
|
+
normalized.endsWith('key') ||
|
|
118
|
+
normalized.endsWith('secret'));
|
|
119
|
+
}
|
|
120
|
+
function isPlainRecord(value) {
|
|
121
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A copy of `value` without credential-shaped properties and with every
|
|
125
|
+
* string redacted; the paths of both go to `report`.
|
|
126
|
+
*/
|
|
127
|
+
function scrub(value, path, report) {
|
|
128
|
+
if (typeof value === 'string') {
|
|
129
|
+
const clean = report.redact(value);
|
|
130
|
+
if (clean !== value)
|
|
131
|
+
report.redacted.push(path);
|
|
132
|
+
return clean;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(value))
|
|
135
|
+
return value.map((entry, index) => scrub(entry, `${path}[${index}]`, report));
|
|
136
|
+
if (!isPlainRecord(value))
|
|
137
|
+
return value;
|
|
138
|
+
const out = {};
|
|
139
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
140
|
+
if (isCredentialName(name)) {
|
|
141
|
+
report.dropped.push(`${path}.${name}`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
Object.defineProperty(out, name, {
|
|
145
|
+
value: scrub(entry, `${path}.${name}`, report),
|
|
146
|
+
enumerable: true,
|
|
147
|
+
writable: true,
|
|
148
|
+
configurable: true,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
function sealed(payload, command, seam) {
|
|
154
|
+
const report = {
|
|
155
|
+
redact: seam.redact,
|
|
156
|
+
dropped: [],
|
|
157
|
+
redacted: [],
|
|
158
|
+
};
|
|
159
|
+
const clean = scrub(payload, 'payload', report);
|
|
160
|
+
if (report.dropped.length > 0)
|
|
161
|
+
seam.logger.warn('credential-shaped field dropped from a command payload', {
|
|
162
|
+
command,
|
|
163
|
+
fields: report.dropped,
|
|
164
|
+
});
|
|
165
|
+
if (report.redacted.length > 0)
|
|
166
|
+
seam.logger.warn('secret-shaped text masked in a command payload', {
|
|
167
|
+
command,
|
|
168
|
+
fields: report.redacted,
|
|
169
|
+
});
|
|
170
|
+
return clean;
|
|
171
|
+
}
|
|
172
|
+
function projectChoice(choice) {
|
|
173
|
+
return { value: String(choice.value), label: String(choice.label) };
|
|
174
|
+
}
|
|
175
|
+
function projectKnob(knob) {
|
|
176
|
+
switch (knob.kind) {
|
|
177
|
+
case 'choice':
|
|
178
|
+
return {
|
|
179
|
+
kind: 'choice',
|
|
180
|
+
id: knob.id,
|
|
181
|
+
label: knob.label,
|
|
182
|
+
choices: (knob.choices ?? []).map(projectChoice),
|
|
183
|
+
...(knob.value !== undefined ? { value: knob.value } : {}),
|
|
184
|
+
};
|
|
185
|
+
case 'toggle':
|
|
186
|
+
return {
|
|
187
|
+
kind: 'toggle',
|
|
188
|
+
id: knob.id,
|
|
189
|
+
label: knob.label,
|
|
190
|
+
value: knob.value === true,
|
|
191
|
+
};
|
|
192
|
+
case 'number':
|
|
193
|
+
return {
|
|
194
|
+
kind: 'number',
|
|
195
|
+
id: knob.id,
|
|
196
|
+
label: knob.label,
|
|
197
|
+
...(knob.value !== undefined ? { value: knob.value } : {}),
|
|
198
|
+
...(knob.min !== undefined ? { min: knob.min } : {}),
|
|
199
|
+
...(knob.max !== undefined ? { max: knob.max } : {}),
|
|
200
|
+
...(knob.required ? { required: true } : {}),
|
|
201
|
+
};
|
|
202
|
+
case 'text':
|
|
203
|
+
// A masked input holds a secret, so its current value is never shown;
|
|
204
|
+
// the action still receives it when the user leaves the input alone.
|
|
205
|
+
return {
|
|
206
|
+
kind: 'text',
|
|
207
|
+
id: knob.id,
|
|
208
|
+
label: knob.label,
|
|
209
|
+
...(knob.value !== undefined && !knob.masked
|
|
210
|
+
? { value: knob.value }
|
|
211
|
+
: {}),
|
|
212
|
+
...(knob.placeholder !== undefined
|
|
213
|
+
? { placeholder: knob.placeholder }
|
|
214
|
+
: {}),
|
|
215
|
+
...(knob.masked ? { masked: true } : {}),
|
|
216
|
+
...(knob.required ? { required: true } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The confirmation an action must pass before it runs, or undefined. An
|
|
222
|
+
* irreversible action always has one, even when its definition (built
|
|
223
|
+
* outside the type checker) names none.
|
|
224
|
+
*/
|
|
225
|
+
export function confirmationOf(action) {
|
|
226
|
+
if (action.irreversible === true)
|
|
227
|
+
return {
|
|
228
|
+
message: action.confirm || DEFAULT_IRREVERSIBLE_CONFIRMATION,
|
|
229
|
+
irreversible: true,
|
|
230
|
+
};
|
|
231
|
+
if (action.confirm)
|
|
232
|
+
return { message: action.confirm, irreversible: false };
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
function projectAction(action) {
|
|
236
|
+
const confirm = confirmationOf(action);
|
|
237
|
+
return {
|
|
238
|
+
id: action.id,
|
|
239
|
+
label: action.label,
|
|
240
|
+
...(action.description !== undefined
|
|
241
|
+
? { description: action.description }
|
|
242
|
+
: {}),
|
|
243
|
+
...(action.group !== undefined ? { group: String(action.group) } : {}),
|
|
244
|
+
knobs: (action.knobs ?? []).map(projectKnob),
|
|
245
|
+
...(confirm ? { confirm } : {}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/** The account fields a renderer may show; see `MenuAccount`. */
|
|
249
|
+
export function projectAccount(account) {
|
|
250
|
+
return {
|
|
251
|
+
id: account.id,
|
|
252
|
+
...(account.label !== undefined ? { label: account.label } : {}),
|
|
253
|
+
enabled: account.enabled,
|
|
254
|
+
type: account.type,
|
|
255
|
+
...(account.identity !== undefined ? { identity: account.identity } : {}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function projectItem(item) {
|
|
259
|
+
return {
|
|
260
|
+
id: item.id,
|
|
261
|
+
label: item.label,
|
|
262
|
+
...(item.detail !== undefined ? { detail: item.detail } : {}),
|
|
263
|
+
...(item.group !== undefined ? { group: String(item.group) } : {}),
|
|
264
|
+
...(item.status !== undefined ? { status: String(item.status) } : {}),
|
|
265
|
+
...(item.account ? { account: projectAccount(item.account) } : {}),
|
|
266
|
+
...(item.facts !== undefined ? { facts: item.facts } : {}),
|
|
267
|
+
actions: (item.actions ?? []).map(projectAction),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function projectSection(section) {
|
|
271
|
+
const { content } = section;
|
|
272
|
+
return {
|
|
273
|
+
id: section.id,
|
|
274
|
+
slot: section.slot,
|
|
275
|
+
title: section.title,
|
|
276
|
+
lines: (content.lines ?? []).map(String),
|
|
277
|
+
items: (content.items ?? []).map(projectItem),
|
|
278
|
+
actions: (content.actions ?? []).map(projectAction),
|
|
279
|
+
...(content.facts !== undefined ? { facts: content.facts } : {}),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function projectMenu(command, title, sections) {
|
|
283
|
+
return { command, title, sections: sections.map(projectSection) };
|
|
284
|
+
}
|
|
285
|
+
/** The payload a host's TUI receives when the slash command opens. */
|
|
286
|
+
export function dialogPayload(command, title, sections, seam) {
|
|
287
|
+
return sealed({ command, menu: projectMenu(command, title, sections) }, command, seam);
|
|
288
|
+
}
|
|
289
|
+
/** An apply's result: the message and the refreshed menu. */
|
|
290
|
+
export function applyResult(command, title, sections, outcome, seam) {
|
|
291
|
+
return sealed({
|
|
292
|
+
command,
|
|
293
|
+
ok: outcome.ok,
|
|
294
|
+
text: String(outcome.text),
|
|
295
|
+
...(!outcome.ok && outcome.code !== undefined
|
|
296
|
+
? { code: CODE_SHAPE.test(outcome.code) ? outcome.code : 'refused' }
|
|
297
|
+
: {}),
|
|
298
|
+
...(outcome.needsConfirmation ? { needsConfirmation: true } : {}),
|
|
299
|
+
menu: projectMenu(command, title, sections),
|
|
300
|
+
}, command, seam);
|
|
301
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface AtomicWriteOptions {
|
|
2
|
+
serialize?: (value: unknown) => string;
|
|
3
|
+
beforeRename?: () => Promise<void>;
|
|
4
|
+
/** Test seam for forcing staging-name collisions; defaults to randomUUID. */
|
|
5
|
+
stageName?: () => string;
|
|
6
|
+
/** Sync the staged file and, after rename, its parent directory. Opt-in. */
|
|
7
|
+
durable?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Internal classification of fsync failures, including a failure after rename. */
|
|
10
|
+
export declare class AtomicSyncError extends Error {
|
|
11
|
+
constructor(cause: unknown);
|
|
12
|
+
}
|
|
13
|
+
export declare function writeJsonAtomic(path: string, value: unknown, options?: AtomicWriteOptions): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Notify internal callers at the rename. The store records committed writes
|
|
16
|
+
* and removed ids here, because a later directory sync cannot undo the rename.
|
|
17
|
+
*/
|
|
18
|
+
export declare function writeJsonAtomicTracked(path: string, value: unknown, options: AtomicWriteOptions, onRenamed?: () => void): Promise<void>;
|
|
19
|
+
/** Make an already renamed file and its directory durable without rewriting bytes. */
|
|
20
|
+
export declare function syncJsonFile(path: string): Promise<void>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
/** Internal classification of fsync failures, including a failure after rename. */
|
|
5
|
+
export class AtomicSyncError extends Error {
|
|
6
|
+
constructor(cause) {
|
|
7
|
+
super('file or directory sync failed', { cause });
|
|
8
|
+
this.name = 'AtomicSyncError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export async function writeJsonAtomic(path, value, options = {}) {
|
|
12
|
+
await writeJsonAtomicTracked(path, value, options);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Notify internal callers at the rename. The store records committed writes
|
|
16
|
+
* and removed ids here, because a later directory sync cannot undo the rename.
|
|
17
|
+
*/
|
|
18
|
+
export async function writeJsonAtomicTracked(path, value, options, onRenamed) {
|
|
19
|
+
await mkdir(dirname(path), { recursive: true });
|
|
20
|
+
const tempPath = `${path}.${(options.stageName ?? randomUUID)()}.tmp`;
|
|
21
|
+
let handle;
|
|
22
|
+
let created = false;
|
|
23
|
+
try {
|
|
24
|
+
handle = await open(tempPath, 'wx', 0o600);
|
|
25
|
+
created = true;
|
|
26
|
+
await handle.writeFile(options.serialize
|
|
27
|
+
? options.serialize(value)
|
|
28
|
+
: `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
29
|
+
await handle.chmod(0o600);
|
|
30
|
+
if (options.durable) {
|
|
31
|
+
try {
|
|
32
|
+
await handle.sync();
|
|
33
|
+
}
|
|
34
|
+
catch (cause) {
|
|
35
|
+
throw new AtomicSyncError(cause);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
await handle.close();
|
|
39
|
+
handle = undefined;
|
|
40
|
+
await options.beforeRename?.();
|
|
41
|
+
await rename(tempPath, path);
|
|
42
|
+
created = false;
|
|
43
|
+
onRenamed?.();
|
|
44
|
+
if (options.durable) {
|
|
45
|
+
const directory = await open(dirname(path), 'r');
|
|
46
|
+
try {
|
|
47
|
+
await directory.sync();
|
|
48
|
+
}
|
|
49
|
+
catch (cause) {
|
|
50
|
+
throw new AtomicSyncError(cause);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await directory.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await handle?.close().catch(() => { });
|
|
59
|
+
// A failed write can leave partial bytes, but a collision is not ours to remove.
|
|
60
|
+
if (created)
|
|
61
|
+
await rm(tempPath, { force: true }).catch(() => { });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Make an already renamed file and its directory durable without rewriting bytes. */
|
|
65
|
+
export async function syncJsonFile(path) {
|
|
66
|
+
const file = await open(path, 'r');
|
|
67
|
+
try {
|
|
68
|
+
await file.sync();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await file.close();
|
|
72
|
+
}
|
|
73
|
+
const directory = await open(dirname(path), 'r');
|
|
74
|
+
try {
|
|
75
|
+
await directory.sync();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await directory.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { AtomicWriteOptions } from './atomic-write.js';
|
|
2
|
+
export { writeJsonAtomic } from './atomic-write.js';
|
|
3
|
+
export { WRITER_LOCK_CONSTANTS } from './lock-constants.js';
|
|
4
|
+
export type { LockLoss, RefreshFileLock } from './refresh-file-lock.js';
|
|
5
|
+
export { acquireRefreshFileLock, isLostMarkerRaceError, } from './refresh-file-lock.js';
|
|
6
|
+
export type { LockOptions, LockOwnershipDetails } from './with-lock.js';
|
|
7
|
+
export { LockContentionError, LockOwnershipError, lockPathFor, withLock, } from './with-lock.js';
|