@openparachute/vault 0.4.8 → 0.4.9-rc.10
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/core/src/hooks.test.ts +320 -1
- package/core/src/hooks.ts +243 -38
- package/core/src/mcp.ts +35 -0
- package/core/src/portable-md.test.ts +252 -1
- package/core/src/portable-md.ts +370 -2
- package/core/src/schema.ts +51 -2
- package/core/src/store.ts +68 -2
- package/package.json +1 -1
- package/src/auth.ts +29 -1
- package/src/auto-transcribe.test.ts +7 -2
- package/src/auto-transcribe.ts +6 -2
- package/src/export-watch.test.ts +74 -0
- package/src/export-watch.ts +108 -7
- package/src/github-device-flow.test.ts +404 -0
- package/src/github-device-flow.ts +415 -0
- package/src/mcp-http.ts +24 -36
- package/src/mcp-tools.ts +286 -2
- package/src/mirror-config.test.ts +184 -14
- package/src/mirror-config.ts +220 -24
- package/src/mirror-credentials.test.ts +450 -0
- package/src/mirror-credentials.ts +577 -0
- package/src/mirror-deps.ts +42 -1
- package/src/mirror-import.test.ts +550 -0
- package/src/mirror-import.ts +484 -0
- package/src/mirror-manager.test.ts +423 -12
- package/src/mirror-manager.ts +579 -62
- package/src/mirror-routes.test.ts +966 -10
- package/src/mirror-routes.ts +1096 -5
- package/src/module-config.ts +11 -5
- package/src/routing.test.ts +92 -1
- package/src/routing.ts +165 -1
- package/src/server.ts +21 -8
- package/src/token-store.ts +158 -5
- package/src/transcription-worker.ts +9 -4
- package/src/triggers.ts +16 -3
- package/src/vault.test.ts +380 -1
- package/web/ui/dist/assets/{index-BOa-JJtV.css → index-DBe8Xiah.css} +1 -1
- package/web/ui/dist/assets/index-DE18QJMx.js +60 -0
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-BzA5LgE3.js +0 -60
package/src/mirror-config.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { existsSync, statSync } from "fs";
|
|
|
23
23
|
import { join } from "path";
|
|
24
24
|
|
|
25
25
|
import { DEFAULT_COMMIT_TEMPLATE, isGitRepo } from "./export-watch.ts";
|
|
26
|
+
import { readCredentials, type MirrorCredentials } from "./mirror-credentials.ts";
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
// Types
|
|
@@ -37,6 +38,32 @@ import { DEFAULT_COMMIT_TEMPLATE, isGitRepo } from "./export-watch.ts";
|
|
|
37
38
|
*/
|
|
38
39
|
export type MirrorLocation = "internal" | "external";
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* How the mirror stays current with vault state.
|
|
43
|
+
*
|
|
44
|
+
* - `events` → in-process hooks fire on every note/tag/attachment
|
|
45
|
+
* mutation; the manager debounces them (~500ms) into a single export
|
|
46
|
+
* pass. A background safety-net poll (default 1h) catches anything
|
|
47
|
+
* missed (direct SQL writes, dropped hook dispatches, restart gaps).
|
|
48
|
+
* This is the default for new configs and the post-vault#382 shape.
|
|
49
|
+
* - `manual` → no event subscriptions, no polling. The operator runs
|
|
50
|
+
* "Run export now" (admin SPA button or POST `/.parachute/mirror/run-now`)
|
|
51
|
+
* or `parachute-vault export` from the CLI on their own cadence.
|
|
52
|
+
*
|
|
53
|
+
* The pre-vault#382 `interval_seconds`-driven watch loop has retired —
|
|
54
|
+
* existing configs migrate to `events` (when `watch: true`) or `manual`
|
|
55
|
+
* (when `watch: false`). The legacy field still parses, repurposed as
|
|
56
|
+
* `safety_net_seconds` when explicit.
|
|
57
|
+
*/
|
|
58
|
+
export type MirrorSyncMode = "events" | "manual";
|
|
59
|
+
|
|
60
|
+
/** Default safety-net poll interval in seconds (1 hour). */
|
|
61
|
+
export const DEFAULT_SAFETY_NET_SECONDS = 3600;
|
|
62
|
+
/** Minimum safety-net poll interval. Anything tighter would defeat the point of "safety net" — that's what the event path is for. */
|
|
63
|
+
export const MIN_SAFETY_NET_SECONDS = 60;
|
|
64
|
+
/** Maximum safety-net poll interval (24 hours). */
|
|
65
|
+
export const MAX_SAFETY_NET_SECONDS = 86400;
|
|
66
|
+
|
|
40
67
|
/**
|
|
41
68
|
* The persistent mirror configuration block. Lives under the `mirror:` key
|
|
42
69
|
* in the global config.yaml (one mirror per vault server today — multi-vault
|
|
@@ -46,32 +73,44 @@ export type MirrorLocation = "internal" | "external";
|
|
|
46
73
|
* - `enabled` — master switch. When false (the default for upgrading
|
|
47
74
|
* vaults), no mirror behavior runs at all. The other fields are
|
|
48
75
|
* preserved so the operator can flip enabled back on without losing
|
|
49
|
-
* their location/path/
|
|
76
|
+
* their location/path/sync_mode settings.
|
|
50
77
|
* - `location` — "internal" or "external". Drives `resolveMirrorPath`.
|
|
51
78
|
* - `external_path` — required when location=external. Operator-picked
|
|
52
79
|
* absolute path. Must exist + be a git repo when first validated.
|
|
53
|
-
* - `
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* manual export.
|
|
80
|
+
* - `sync_mode` — "events" (subscribe to in-process hooks; debounce ~500ms;
|
|
81
|
+
* safety-net poll on top) or "manual" (no auto-fire; operator triggers
|
|
82
|
+
* manually). Default "events".
|
|
57
83
|
* - `auto_commit` — after each export pass, `git add -A && git commit`.
|
|
58
84
|
* Reuses the existing `runGitCommitCycle` from vault#346.
|
|
59
85
|
* - `auto_push` — after commit, `git push`. Failures non-fatal.
|
|
60
86
|
* - `commit_template` — passed verbatim to `renderCommitMessage`. Same
|
|
61
87
|
* variable set as the CLI: `{{date}}`, `{{notes_changed}}`,
|
|
62
88
|
* `{{plural}}`, `{{first_note_title}}`, `{{vault_name}}`.
|
|
63
|
-
* - `
|
|
64
|
-
* the
|
|
89
|
+
* - `safety_net_seconds` — `sync_mode: events` runs an hourly background
|
|
90
|
+
* sweep on top of the event-driven path. This field controls the
|
|
91
|
+
* interval (default 3600). Clamped to `[MIN_SAFETY_NET_SECONDS,
|
|
92
|
+
* MAX_SAFETY_NET_SECONDS]` at validation time.
|
|
93
|
+
*
|
|
94
|
+
* Legacy / migration:
|
|
95
|
+
* - `watch` (boolean) — pre-vault#382 field. `true` → `sync_mode: events`,
|
|
96
|
+
* `false` → `sync_mode: manual`. Parsed for back-compat; the canonical
|
|
97
|
+
* form on the wire is `sync_mode`.
|
|
98
|
+
* - `interval_seconds` (positive integer) — pre-vault#382 watch-loop
|
|
99
|
+
* poll interval. Reinterpreted as `safety_net_seconds` when no
|
|
100
|
+
* explicit `safety_net_seconds` is present AND the value lies in the
|
|
101
|
+
* valid range. Out-of-range / missing → default 3600. The field is
|
|
102
|
+
* no longer surfaced in the UI; the SPA stops emitting it but the
|
|
103
|
+
* parser keeps accepting it for hand-edited configs.
|
|
65
104
|
*/
|
|
66
105
|
export interface MirrorConfig {
|
|
67
106
|
enabled: boolean;
|
|
68
107
|
location: MirrorLocation;
|
|
69
108
|
external_path: string | null;
|
|
70
|
-
|
|
109
|
+
sync_mode: MirrorSyncMode;
|
|
71
110
|
auto_commit: boolean;
|
|
72
111
|
auto_push: boolean;
|
|
73
112
|
commit_template: string;
|
|
74
|
-
|
|
113
|
+
safety_net_seconds: number;
|
|
75
114
|
}
|
|
76
115
|
|
|
77
116
|
/**
|
|
@@ -85,11 +124,11 @@ export function defaultMirrorConfig(): MirrorConfig {
|
|
|
85
124
|
enabled: false,
|
|
86
125
|
location: "internal",
|
|
87
126
|
external_path: null,
|
|
88
|
-
|
|
127
|
+
sync_mode: "events",
|
|
89
128
|
auto_commit: true,
|
|
90
129
|
auto_push: false,
|
|
91
130
|
commit_template: DEFAULT_COMMIT_TEMPLATE,
|
|
92
|
-
|
|
131
|
+
safety_net_seconds: DEFAULT_SAFETY_NET_SECONDS,
|
|
93
132
|
};
|
|
94
133
|
}
|
|
95
134
|
|
|
@@ -129,6 +168,13 @@ export function parseMirrorConfig(yaml: string): MirrorConfig | undefined {
|
|
|
129
168
|
const lines = yaml.slice(startIdx).split("\n");
|
|
130
169
|
|
|
131
170
|
const config = defaultMirrorConfig();
|
|
171
|
+
// Track legacy field state for migration: if `sync_mode` is absent and
|
|
172
|
+
// `watch` is set, derive sync_mode from watch. If `safety_net_seconds`
|
|
173
|
+
// is absent and `interval_seconds` is set (legacy field), repurpose it.
|
|
174
|
+
let sawSyncMode = false;
|
|
175
|
+
let sawSafetyNet = false;
|
|
176
|
+
let legacyWatch: boolean | null = null;
|
|
177
|
+
let legacyInterval: number | null = null;
|
|
132
178
|
|
|
133
179
|
for (const line of lines) {
|
|
134
180
|
// Stop at the next top-level key.
|
|
@@ -140,7 +186,7 @@ export function parseMirrorConfig(yaml: string): MirrorConfig | undefined {
|
|
|
140
186
|
const boolField = (
|
|
141
187
|
name: keyof Pick<
|
|
142
188
|
MirrorConfig,
|
|
143
|
-
"enabled" | "
|
|
189
|
+
"enabled" | "auto_commit" | "auto_push"
|
|
144
190
|
>,
|
|
145
191
|
): boolean => {
|
|
146
192
|
const m = trimmed.match(new RegExp(`^${name}:\\s*(true|false)\\s*$`));
|
|
@@ -151,10 +197,22 @@ export function parseMirrorConfig(yaml: string): MirrorConfig | undefined {
|
|
|
151
197
|
return false;
|
|
152
198
|
};
|
|
153
199
|
if (boolField("enabled")) continue;
|
|
154
|
-
if (boolField("watch")) continue;
|
|
155
200
|
if (boolField("auto_commit")) continue;
|
|
156
201
|
if (boolField("auto_push")) continue;
|
|
157
202
|
|
|
203
|
+
const watchMatch = trimmed.match(/^watch:\s*(true|false)\s*$/);
|
|
204
|
+
if (watchMatch) {
|
|
205
|
+
legacyWatch = watchMatch[1] === "true";
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const syncModeMatch = trimmed.match(/^sync_mode:\s*(events|manual)\s*$/);
|
|
210
|
+
if (syncModeMatch) {
|
|
211
|
+
config.sync_mode = syncModeMatch[1] as MirrorSyncMode;
|
|
212
|
+
sawSyncMode = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
158
216
|
const locationMatch = trimmed.match(/^location:\s*(internal|external)\s*$/);
|
|
159
217
|
if (locationMatch) {
|
|
160
218
|
config.location = locationMatch[1] as MirrorLocation;
|
|
@@ -181,17 +239,47 @@ export function parseMirrorConfig(yaml: string): MirrorConfig | undefined {
|
|
|
181
239
|
continue;
|
|
182
240
|
}
|
|
183
241
|
|
|
242
|
+
const safetyNetMatch = trimmed.match(/^safety_net_seconds:\s*(\d+)\s*$/);
|
|
243
|
+
if (safetyNetMatch) {
|
|
244
|
+
const n = parseInt(safetyNetMatch[1]!, 10);
|
|
245
|
+
if (Number.isFinite(n) && n > 0) {
|
|
246
|
+
config.safety_net_seconds = clampSafetyNet(n);
|
|
247
|
+
sawSafetyNet = true;
|
|
248
|
+
}
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
184
252
|
const intervalMatch = trimmed.match(/^interval_seconds:\s*(\d+)\s*$/);
|
|
185
253
|
if (intervalMatch) {
|
|
186
254
|
const n = parseInt(intervalMatch[1]!, 10);
|
|
187
|
-
if (Number.isFinite(n) && n > 0)
|
|
255
|
+
if (Number.isFinite(n) && n > 0) legacyInterval = n;
|
|
188
256
|
continue;
|
|
189
257
|
}
|
|
190
258
|
}
|
|
191
259
|
|
|
260
|
+
// Migration: explicit `sync_mode` wins; otherwise derive from `watch`.
|
|
261
|
+
// A config that carries neither falls through to defaults (events).
|
|
262
|
+
if (!sawSyncMode && legacyWatch !== null) {
|
|
263
|
+
config.sync_mode = legacyWatch ? "events" : "manual";
|
|
264
|
+
}
|
|
265
|
+
// Migration: explicit `safety_net_seconds` wins; otherwise upgrade
|
|
266
|
+
// `interval_seconds` (legacy short-cadence values like 5 get clamped
|
|
267
|
+
// up to MIN_SAFETY_NET_SECONDS — the safety-net path doesn't want
|
|
268
|
+
// 5-second polls). Out-of-range values fall through to the default.
|
|
269
|
+
if (!sawSafetyNet && legacyInterval !== null) {
|
|
270
|
+
config.safety_net_seconds = clampSafetyNet(legacyInterval);
|
|
271
|
+
}
|
|
272
|
+
|
|
192
273
|
return config;
|
|
193
274
|
}
|
|
194
275
|
|
|
276
|
+
/** Clamp safety_net_seconds to the valid range. */
|
|
277
|
+
function clampSafetyNet(n: number): number {
|
|
278
|
+
if (n < MIN_SAFETY_NET_SECONDS) return MIN_SAFETY_NET_SECONDS;
|
|
279
|
+
if (n > MAX_SAFETY_NET_SECONDS) return MAX_SAFETY_NET_SECONDS;
|
|
280
|
+
return n;
|
|
281
|
+
}
|
|
282
|
+
|
|
195
283
|
/**
|
|
196
284
|
* Serialize a MirrorConfig as YAML lines suitable for appending under the
|
|
197
285
|
* top-level keys of `config.yaml`. Returns the lines without a trailing
|
|
@@ -215,12 +303,12 @@ export function serializeMirrorConfig(config: MirrorConfig): string[] {
|
|
|
215
303
|
` external_path: ${needsQuote ? `"${config.external_path}"` : config.external_path}`,
|
|
216
304
|
);
|
|
217
305
|
}
|
|
218
|
-
lines.push(`
|
|
306
|
+
lines.push(` sync_mode: ${config.sync_mode}`);
|
|
219
307
|
lines.push(` auto_commit: ${config.auto_commit}`);
|
|
220
308
|
lines.push(` auto_push: ${config.auto_push}`);
|
|
221
309
|
// Templates contain `{{ }}` and frequently `:` — always quote.
|
|
222
310
|
lines.push(` commit_template: "${config.commit_template.replace(/"/g, '\\"')}"`);
|
|
223
|
-
lines.push(`
|
|
311
|
+
lines.push(` safety_net_seconds: ${config.safety_net_seconds}`);
|
|
224
312
|
return lines;
|
|
225
313
|
}
|
|
226
314
|
|
|
@@ -285,12 +373,21 @@ export type ShapeValidation = ShapeValidationOk | ShapeValidationError;
|
|
|
285
373
|
* `defaultMirrorConfig()`; rejects values that don't conform to the
|
|
286
374
|
* declared types.
|
|
287
375
|
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
376
|
+
* Mostly pure: the one filesystem touch is reading
|
|
377
|
+
* `.mirror-credentials.yaml` to decide whether `auto_push: true +
|
|
378
|
+
* location: internal` is acceptable (credentials carry a remote URL, so
|
|
379
|
+
* "internal mirrors have no remote" no longer holds). The credentials
|
|
380
|
+
* read is injectable via `opts.readCredentials` so tests stay hermetic;
|
|
381
|
+
* production callers omit it and pick up the canonical `readCredentials`
|
|
382
|
+
* from `./mirror-credentials.ts`.
|
|
383
|
+
*
|
|
384
|
+
* Filesystem-level validation of `external_path` (exists, is a git repo)
|
|
385
|
+
* still lives in `validateExternalPath` — that's I/O and stays out of
|
|
386
|
+
* this function.
|
|
291
387
|
*/
|
|
292
388
|
export function validateMirrorConfigShape(
|
|
293
389
|
input: unknown,
|
|
390
|
+
opts: { readCredentials?: () => MirrorCredentials | null } = {},
|
|
294
391
|
): ShapeValidation {
|
|
295
392
|
if (input === null || typeof input !== "object") {
|
|
296
393
|
return {
|
|
@@ -334,11 +431,24 @@ export function validateMirrorConfigShape(
|
|
|
334
431
|
}
|
|
335
432
|
}
|
|
336
433
|
|
|
434
|
+
// Legacy back-compat: `watch: boolean` translates to sync_mode.
|
|
435
|
+
// `sync_mode` takes priority if both are present.
|
|
337
436
|
if ("watch" in blob) {
|
|
338
437
|
if (typeof blob.watch !== "boolean") {
|
|
339
|
-
return { ok: false,
|
|
438
|
+
return { ok: false, error: "`watch` must be boolean." };
|
|
439
|
+
}
|
|
440
|
+
out.sync_mode = blob.watch ? "events" : "manual";
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if ("sync_mode" in blob) {
|
|
444
|
+
if (blob.sync_mode !== "events" && blob.sync_mode !== "manual") {
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
field: "sync_mode",
|
|
448
|
+
error: '`sync_mode` must be "events" or "manual".',
|
|
449
|
+
};
|
|
340
450
|
}
|
|
341
|
-
out.
|
|
451
|
+
out.sync_mode = blob.sync_mode;
|
|
342
452
|
}
|
|
343
453
|
|
|
344
454
|
if ("auto_commit" in blob) {
|
|
@@ -382,7 +492,33 @@ export function validateMirrorConfigShape(
|
|
|
382
492
|
out.commit_template = blob.commit_template;
|
|
383
493
|
}
|
|
384
494
|
|
|
385
|
-
if ("
|
|
495
|
+
if ("safety_net_seconds" in blob) {
|
|
496
|
+
if (
|
|
497
|
+
typeof blob.safety_net_seconds !== "number" ||
|
|
498
|
+
!Number.isFinite(blob.safety_net_seconds) ||
|
|
499
|
+
blob.safety_net_seconds <= 0 ||
|
|
500
|
+
!Number.isInteger(blob.safety_net_seconds)
|
|
501
|
+
) {
|
|
502
|
+
return {
|
|
503
|
+
ok: false,
|
|
504
|
+
field: "safety_net_seconds",
|
|
505
|
+
error: "`safety_net_seconds` must be a positive integer.",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (
|
|
509
|
+
blob.safety_net_seconds < MIN_SAFETY_NET_SECONDS ||
|
|
510
|
+
blob.safety_net_seconds > MAX_SAFETY_NET_SECONDS
|
|
511
|
+
) {
|
|
512
|
+
return {
|
|
513
|
+
ok: false,
|
|
514
|
+
field: "safety_net_seconds",
|
|
515
|
+
error: `\`safety_net_seconds\` must be between ${MIN_SAFETY_NET_SECONDS} and ${MAX_SAFETY_NET_SECONDS}.`,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
out.safety_net_seconds = blob.safety_net_seconds;
|
|
519
|
+
} else if ("interval_seconds" in blob) {
|
|
520
|
+
// Legacy field still accepted for hand-edited configs. Migrate by
|
|
521
|
+
// clamping into the safety-net range.
|
|
386
522
|
if (
|
|
387
523
|
typeof blob.interval_seconds !== "number" ||
|
|
388
524
|
!Number.isFinite(blob.interval_seconds) ||
|
|
@@ -391,11 +527,15 @@ export function validateMirrorConfigShape(
|
|
|
391
527
|
) {
|
|
392
528
|
return {
|
|
393
529
|
ok: false,
|
|
394
|
-
field: "interval_seconds",
|
|
395
530
|
error: "`interval_seconds` must be a positive integer.",
|
|
396
531
|
};
|
|
397
532
|
}
|
|
398
|
-
|
|
533
|
+
const clamped = blob.interval_seconds < MIN_SAFETY_NET_SECONDS
|
|
534
|
+
? MIN_SAFETY_NET_SECONDS
|
|
535
|
+
: blob.interval_seconds > MAX_SAFETY_NET_SECONDS
|
|
536
|
+
? MAX_SAFETY_NET_SECONDS
|
|
537
|
+
: blob.interval_seconds;
|
|
538
|
+
out.safety_net_seconds = clamped;
|
|
399
539
|
}
|
|
400
540
|
|
|
401
541
|
// Cross-field rule: external requires external_path — but ONLY when
|
|
@@ -414,6 +554,62 @@ export function validateMirrorConfigShape(
|
|
|
414
554
|
};
|
|
415
555
|
}
|
|
416
556
|
|
|
557
|
+
// Cross-field rule: auto_push + internal location was historically
|
|
558
|
+
// rejected outright — the assumption being "internal mirrors live under
|
|
559
|
+
// vault's data dir with no configured remote, so push would always
|
|
560
|
+
// fail." That assumption no longer holds once credentials are wired:
|
|
561
|
+
// the credential save path (handleAuthPat / handleAuthGithubSelectRepo)
|
|
562
|
+
// sets `origin` on the internal repo to the embedded-credential URL,
|
|
563
|
+
// so `git push` to GitHub/GitLab/etc. is meaningful even when the
|
|
564
|
+
// working tree lives under `~/.parachute/vault/data/<name>/mirror/`.
|
|
565
|
+
//
|
|
566
|
+
// New rule: auto_push + internal is rejected ONLY when no credentials
|
|
567
|
+
// are configured. If credentials ARE wired (PAT or GitHub OAuth),
|
|
568
|
+
// accept the combination — vault has a remote to push to. Operators
|
|
569
|
+
// hitting Aaron's three-stacking-gaps bug (History preset + PAT saved,
|
|
570
|
+
// pushes never fire) get unblocked.
|
|
571
|
+
//
|
|
572
|
+
// Asymmetry note (reviewer-flagged on vault#392): external + auto_push +
|
|
573
|
+
// no vault-stored credentials is INTENTIONALLY not rejected here.
|
|
574
|
+
// External mirrors are operator-managed paths — operators may have
|
|
575
|
+
// configured push credentials via system-level git config (SSH agent,
|
|
576
|
+
// ~/.git-credentials, GH_TOKEN env, `gh auth login`), none of which
|
|
577
|
+
// vault can detect by reading `.mirror-credentials.yaml`. Rejecting on
|
|
578
|
+
// "vault doesn't see credentials" would refuse legitimate
|
|
579
|
+
// operator-managed setups. Push failures on missing credentials surface
|
|
580
|
+
// via the non-fatal warning path in gitPush — operators see them in
|
|
581
|
+
// `last_push_error` rather than being blocked at save time. Internal
|
|
582
|
+
// location is different: internal mirrors live under vault's data
|
|
583
|
+
// dir, so vault IS the only thing that can wire a remote, which is
|
|
584
|
+
// why the gate below requires vault-stored credentials specifically.
|
|
585
|
+
if (out.enabled && out.auto_push && out.location === "internal") {
|
|
586
|
+
const readCreds = opts.readCredentials ?? readCredentials;
|
|
587
|
+
let creds: MirrorCredentials | null = null;
|
|
588
|
+
try {
|
|
589
|
+
creds = readCreds();
|
|
590
|
+
} catch {
|
|
591
|
+
// If the credentials file is unreadable we conservatively fail
|
|
592
|
+
// closed — same outcome as "no credentials." Better to surface the
|
|
593
|
+
// actionable "configure credentials first" error than to accept a
|
|
594
|
+
// config that won't actually push.
|
|
595
|
+
creds = null;
|
|
596
|
+
}
|
|
597
|
+
const hasCredentials = !!(
|
|
598
|
+
creds &&
|
|
599
|
+
creds.active_method &&
|
|
600
|
+
((creds.active_method === "pat" && creds.pat) ||
|
|
601
|
+
(creds.active_method === "github_oauth" && creds.github_oauth))
|
|
602
|
+
);
|
|
603
|
+
if (!hasCredentials) {
|
|
604
|
+
return {
|
|
605
|
+
ok: false,
|
|
606
|
+
field: "auto_push",
|
|
607
|
+
error:
|
|
608
|
+
"Configure git credentials before enabling auto-push on an internal mirror — vault has no remote to push to without them. Connect GitHub or paste a Personal Access Token in the Git remote section, or set auto_push to false.",
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
417
613
|
return { ok: true, config: out };
|
|
418
614
|
}
|
|
419
615
|
|