@futdevpro/nts-dynamo 1.15.359 → 1.15.362
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/build/_modules/git-sync/_models/git-sync.interface.d.ts +94 -0
- package/build/_modules/git-sync/_models/git-sync.interface.d.ts.map +1 -0
- package/build/_modules/git-sync/_models/git-sync.interface.js +3 -0
- package/build/_modules/git-sync/_models/git-sync.interface.js.map +1 -0
- package/build/_modules/git-sync/git-sync-input.util.d.ts +28 -0
- package/build/_modules/git-sync/git-sync-input.util.d.ts.map +1 -0
- package/build/_modules/git-sync/git-sync-input.util.js +146 -0
- package/build/_modules/git-sync/git-sync-input.util.js.map +1 -0
- package/build/_modules/git-sync/git-sync.service.d.ts +54 -0
- package/build/_modules/git-sync/git-sync.service.d.ts.map +1 -0
- package/build/_modules/git-sync/git-sync.service.js +274 -0
- package/build/_modules/git-sync/git-sync.service.js.map +1 -0
- package/build/_modules/git-sync/index.d.ts +8 -0
- package/build/_modules/git-sync/index.d.ts.map +1 -0
- package/build/_modules/git-sync/index.js +11 -0
- package/build/_modules/git-sync/index.js.map +1 -0
- package/build/_modules/imap/_models/imap-mailbox-status.interface.d.ts +10 -0
- package/build/_modules/imap/_models/imap-mailbox-status.interface.d.ts.map +1 -0
- package/build/_modules/imap/_models/imap-mailbox-status.interface.js +3 -0
- package/build/_modules/imap/_models/imap-mailbox-status.interface.js.map +1 -0
- package/build/_modules/imap/_models/imap-message-meta.interface.d.ts +18 -0
- package/build/_modules/imap/_models/imap-message-meta.interface.d.ts.map +1 -0
- package/build/_modules/imap/_models/imap-message-meta.interface.js +3 -0
- package/build/_modules/imap/_models/imap-message-meta.interface.js.map +1 -0
- package/build/_modules/imap/_models/imap-sync.interface.d.ts +22 -0
- package/build/_modules/imap/_models/imap-sync.interface.d.ts.map +1 -0
- package/build/_modules/imap/_models/imap-sync.interface.js +3 -0
- package/build/_modules/imap/_models/imap-sync.interface.js.map +1 -0
- package/build/_modules/imap/_models/imap-transport.interface.d.ts +16 -0
- package/build/_modules/imap/_models/imap-transport.interface.d.ts.map +1 -0
- package/build/_modules/imap/_models/imap-transport.interface.js +3 -0
- package/build/_modules/imap/_models/imap-transport.interface.js.map +1 -0
- package/build/_modules/imap/imap-flow.transport.d.ts +58 -0
- package/build/_modules/imap/imap-flow.transport.d.ts.map +1 -0
- package/build/_modules/imap/imap-flow.transport.js +156 -0
- package/build/_modules/imap/imap-flow.transport.js.map +1 -0
- package/build/_modules/imap/imap-inbox.service.d.ts +36 -0
- package/build/_modules/imap/imap-inbox.service.d.ts.map +1 -0
- package/build/_modules/imap/imap-inbox.service.js +81 -0
- package/build/_modules/imap/imap-inbox.service.js.map +1 -0
- package/build/_modules/imap/index.d.ts +11 -0
- package/build/_modules/imap/index.d.ts.map +1 -0
- package/build/_modules/imap/index.js +14 -0
- package/build/_modules/imap/index.js.map +1 -0
- package/package.json +24 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** BFR-OLDLIGHTSITE-003 — one file the caller wants in the repository, byte-exact. */
|
|
2
|
+
export interface DyNTS_GitSyncFile_Interface {
|
|
3
|
+
/** A repository-relative path with `/` separators (no `..`, no absolute path, nothing under `.git`). */
|
|
4
|
+
path: string;
|
|
5
|
+
/** Written exactly as given (UTF-8, no line-ending conversion). */
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
/** BFR-OLDLIGHTSITE-003 — one `sync` call. */
|
|
9
|
+
export interface DyNTS_GitSyncRequest_Interface {
|
|
10
|
+
files: DyNTS_GitSyncFile_Interface[];
|
|
11
|
+
/** The commit message of the ONE commit this call may create. */
|
|
12
|
+
message: string;
|
|
13
|
+
/**
|
|
14
|
+
* Directories the caller OWNS completely (e.g. `story-points`): a tracked file under them that is NOT in `files` is
|
|
15
|
+
* removed in the same commit. Nothing outside these directories is ever removed.
|
|
16
|
+
*/
|
|
17
|
+
prune?: string[];
|
|
18
|
+
}
|
|
19
|
+
/** BFR-OLDLIGHTSITE-003 — one git command of the run log (no secret: the token never travels in arguments). */
|
|
20
|
+
export interface DyNTS_GitSyncLogEntry_Interface {
|
|
21
|
+
step: string;
|
|
22
|
+
command: string;
|
|
23
|
+
exitCode: number;
|
|
24
|
+
ms: number;
|
|
25
|
+
}
|
|
26
|
+
/** BFR-OLDLIGHTSITE-003 — the outcome of a successful `sync`. */
|
|
27
|
+
export interface DyNTS_GitSyncResult_Interface {
|
|
28
|
+
/** `pushed` = one new commit is on the remote branch; `unchanged` = the files already matched, nothing committed. */
|
|
29
|
+
status: 'pushed' | 'unchanged';
|
|
30
|
+
/** The remote branch head after the call (the new commit, or the unchanged head). */
|
|
31
|
+
commit: string;
|
|
32
|
+
/** Added or modified paths, sorted. */
|
|
33
|
+
changedFiles: string[];
|
|
34
|
+
/** Paths removed through `prune`, sorted. */
|
|
35
|
+
removedFiles: string[];
|
|
36
|
+
/** 1, or 2 when the first push was rejected because the remote moved meanwhile. */
|
|
37
|
+
attempts: number;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
log: DyNTS_GitSyncLogEntry_Interface[];
|
|
40
|
+
}
|
|
41
|
+
/** What a git command returned. */
|
|
42
|
+
export interface DyNTS_GitRunResult_Interface {
|
|
43
|
+
exitCode: number;
|
|
44
|
+
stdout: string;
|
|
45
|
+
stderr: string;
|
|
46
|
+
}
|
|
47
|
+
/** Runs `git <args>` — the default spawns the git binary; a test can wrap or replace it. */
|
|
48
|
+
export type DyNTS_GitRunner_Type = (args: string[], options: {
|
|
49
|
+
cwd: string;
|
|
50
|
+
env: NodeJS.ProcessEnv;
|
|
51
|
+
timeoutMs: number;
|
|
52
|
+
}) => Promise<DyNTS_GitRunResult_Interface>;
|
|
53
|
+
/** BFR-OLDLIGHTSITE-003 — one synchronised repository. */
|
|
54
|
+
export interface DyNTS_GitSync_Settings_Interface {
|
|
55
|
+
/** `https://…` (then `credentialRef` is required) or `file://…`. ⛔ Never with credentials inside the URL. */
|
|
56
|
+
repoUrl: string;
|
|
57
|
+
/** The branch to follow and push (it must already exist on the remote). */
|
|
58
|
+
branch: string;
|
|
59
|
+
/**
|
|
60
|
+
* A directory OWNED by this service: it is cloned on first use and force-reset to the remote branch on every call.
|
|
61
|
+
* An existing directory that is not a clone of `repoUrl` is refused, never touched.
|
|
62
|
+
*/
|
|
63
|
+
workdir: string;
|
|
64
|
+
/** The env-key prefix of the token: `OLS_ARCHIVE_GIT` → `OLS_ARCHIVE_GIT_TOKEN` (injected by the FDP Keystore). */
|
|
65
|
+
credentialRef?: string;
|
|
66
|
+
/** The HTTP Basic user sent with the token (default `x-access-token` — GitHub accepts it for PATs and app tokens). */
|
|
67
|
+
tokenUser?: string;
|
|
68
|
+
author: {
|
|
69
|
+
name: string;
|
|
70
|
+
email: string;
|
|
71
|
+
};
|
|
72
|
+
/** The environment to read the token from (default `process.env`) — a test seam. */
|
|
73
|
+
env?: NodeJS.ProcessEnv;
|
|
74
|
+
runner?: DyNTS_GitRunner_Type;
|
|
75
|
+
/** Per git command (default 120 000 ms). */
|
|
76
|
+
timeoutMs?: number;
|
|
77
|
+
}
|
|
78
|
+
/** What `credentialRef` resolved to. Carries key NAMES only — never a value. */
|
|
79
|
+
export type DyNTS_GitSyncCredential_Type = {
|
|
80
|
+
status: 'none';
|
|
81
|
+
} | {
|
|
82
|
+
status: 'configured';
|
|
83
|
+
credentialRef: string;
|
|
84
|
+
tokenKey: string;
|
|
85
|
+
} | {
|
|
86
|
+
status: 'absent';
|
|
87
|
+
credentialRef: string;
|
|
88
|
+
missingKeys: string[];
|
|
89
|
+
} | {
|
|
90
|
+
status: 'invalid';
|
|
91
|
+
credentialRef: string;
|
|
92
|
+
problems: string[];
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=git-sync.interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-sync.interface.d.ts","sourceRoot":"","sources":["../../../../src/_modules/git-sync/_models/git-sync.interface.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,WAAW,2BAA2B;IAC1C,wGAAwG;IACxG,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,8CAA8C;AAC9C,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,2BAA2B,EAAE,CAAC;IACrC,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,+GAA+G;AAC/G,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,iEAAiE;AACjE,MAAM,WAAW,6BAA6B;IAC5C,qHAAqH;IACrH,MAAM,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC/B,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,+BAA+B,EAAE,CAAC;CACxC;AAED,mCAAmC;AACnC,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,4FAA4F;AAC5F,MAAM,MAAM,oBAAoB,GAAG,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,KAChE,OAAO,CAAC,4BAA4B,CAAC,CAAC;AAE3C,0DAA0D;AAC1D,MAAM,WAAW,gCAAgC;IAC/C,6GAA6G;IAC7G,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,mHAAmH;IACnH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sHAAsH;IACtH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,gFAAgF;AAChF,MAAM,MAAM,4BAA4B,GACpC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACjE;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,GAClE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-sync.interface.js","sourceRoot":"","sources":["../../../../src/_modules/git-sync/_models/git-sync.interface.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DyNTS_GitSync_Settings_Interface, DyNTS_GitSyncCredential_Type, DyNTS_GitSyncFile_Interface, DyNTS_GitSyncRequest_Interface } from './_models/git-sync.interface';
|
|
2
|
+
/** A validated request: the message trimmed, the prune directories normalised and deduplicated. */
|
|
3
|
+
export interface DyNTS_GitSyncPlan_Interface {
|
|
4
|
+
files: DyNTS_GitSyncFile_Interface[];
|
|
5
|
+
message: string;
|
|
6
|
+
prune: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* BFR-OLDLIGHTSITE-003 — everything checked BEFORE the first git command: the request (paths, message, prune
|
|
10
|
+
* directories), the settings (URL, branch, author) and the Keystore reference of the token.
|
|
11
|
+
*
|
|
12
|
+
* @description A bad input is a caller error (400) and never reaches git or the filesystem.
|
|
13
|
+
*/
|
|
14
|
+
export declare class DyNTS_GitSyncInput_Util {
|
|
15
|
+
/** The env-key a reference expands to. */
|
|
16
|
+
static keysOf(credentialRef: string): string[];
|
|
17
|
+
/** Resolves `credentialRef`; never throws and never returns the value. */
|
|
18
|
+
static resolveCredential(settings: DyNTS_GitSync_Settings_Interface): DyNTS_GitSyncCredential_Type;
|
|
19
|
+
/** Checks the settings; returns the problems (empty = usable). */
|
|
20
|
+
static settingsProblems(settings: DyNTS_GitSync_Settings_Interface): string[];
|
|
21
|
+
/** Validates and normalises a request, or throws a 400. */
|
|
22
|
+
static plan(request: DyNTS_GitSyncRequest_Interface): DyNTS_GitSyncPlan_Interface;
|
|
23
|
+
/** Why a repository-relative path is unsafe (null = safe). */
|
|
24
|
+
static pathProblem(path: unknown): string | null;
|
|
25
|
+
/** A conservative branch-name check (no option-like or ref-breaking names). */
|
|
26
|
+
static isSafeBranch(branch: string): boolean;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=git-sync-input.util.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-sync-input.util.d.ts","sourceRoot":"","sources":["../../../src/_modules/git-sync/git-sync-input.util.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,gCAAgC,EAAE,4BAA4B,EAAE,2BAA2B,EAC3F,8BAA8B,EAC/B,MAAM,8BAA8B,CAAC;AAEtC,mGAAmG;AACnG,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,2BAA2B,EAAE,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;GAKG;AACH,qBAAa,uBAAuB;IAElC,0CAA0C;IAC1C,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE;IAI9C,0EAA0E;IAC1E,MAAM,CAAC,iBAAiB,CAAC,QAAQ,EAAE,gCAAgC,GAAG,4BAA4B;IAyBlG,kEAAkE;IAClE,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,gCAAgC,GAAG,MAAM,EAAE;IAqC7E,2DAA2D;IAC3D,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,8BAA8B,GAAG,2BAA2B;IAyDjF,8DAA8D;IAC9D,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI;IAuBhD,+EAA+E;IAC/E,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;CAK7C"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DyNTS_GitSyncInput_Util = void 0;
|
|
4
|
+
const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
|
|
5
|
+
const global_settings_const_1 = require("../../_collections/global-settings.const");
|
|
6
|
+
/**
|
|
7
|
+
* BFR-OLDLIGHTSITE-003 — everything checked BEFORE the first git command: the request (paths, message, prune
|
|
8
|
+
* directories), the settings (URL, branch, author) and the Keystore reference of the token.
|
|
9
|
+
*
|
|
10
|
+
* @description A bad input is a caller error (400) and never reaches git or the filesystem.
|
|
11
|
+
*/
|
|
12
|
+
class DyNTS_GitSyncInput_Util {
|
|
13
|
+
/** The env-key a reference expands to. */
|
|
14
|
+
static keysOf(credentialRef) {
|
|
15
|
+
return [`${credentialRef}_TOKEN`];
|
|
16
|
+
}
|
|
17
|
+
/** Resolves `credentialRef`; never throws and never returns the value. */
|
|
18
|
+
static resolveCredential(settings) {
|
|
19
|
+
if (settings.credentialRef === undefined) {
|
|
20
|
+
return { status: 'none' };
|
|
21
|
+
}
|
|
22
|
+
const credentialRef = String(settings.credentialRef);
|
|
23
|
+
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(credentialRef)) {
|
|
24
|
+
return {
|
|
25
|
+
status: 'invalid',
|
|
26
|
+
credentialRef: credentialRef,
|
|
27
|
+
problems: ['the credentialRef must be an UPPER_SNAKE env-key prefix (e.g. OLS_ARCHIVE_GIT)'],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const tokenKey = DyNTS_GitSyncInput_Util.keysOf(credentialRef)[0];
|
|
31
|
+
const env = settings.env ?? process.env;
|
|
32
|
+
if (!String(env[tokenKey] ?? '').trim()) {
|
|
33
|
+
return { status: 'absent', credentialRef: credentialRef, missingKeys: [tokenKey] };
|
|
34
|
+
}
|
|
35
|
+
return { status: 'configured', credentialRef: credentialRef, tokenKey: tokenKey };
|
|
36
|
+
}
|
|
37
|
+
/** Checks the settings; returns the problems (empty = usable). */
|
|
38
|
+
static settingsProblems(settings) {
|
|
39
|
+
const problems = [];
|
|
40
|
+
let url = null;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(String(settings?.repoUrl ?? ''));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
problems.push('repoUrl is not a URL');
|
|
46
|
+
}
|
|
47
|
+
if (url) {
|
|
48
|
+
if (url.username || url.password) {
|
|
49
|
+
problems.push('repoUrl must not contain credentials — pass the token through credentialRef (the FDP Keystore)');
|
|
50
|
+
}
|
|
51
|
+
if (url.protocol === 'https:' && settings.credentialRef === undefined) {
|
|
52
|
+
problems.push('an https repoUrl needs a credentialRef');
|
|
53
|
+
}
|
|
54
|
+
if (url.protocol !== 'https:' && url.protocol !== 'file:') {
|
|
55
|
+
problems.push(`repoUrl protocol ${url.protocol} is not supported (https: or file:)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!DyNTS_GitSyncInput_Util.isSafeBranch(String(settings?.branch ?? ''))) {
|
|
59
|
+
problems.push('branch is not a safe branch name');
|
|
60
|
+
}
|
|
61
|
+
if (!String(settings?.workdir ?? '').trim()) {
|
|
62
|
+
problems.push('workdir is required');
|
|
63
|
+
}
|
|
64
|
+
const authorName = String(settings?.author?.name ?? '').trim();
|
|
65
|
+
if (!authorName || !/^[^\s@]+@[^\s@]+$/.test(String(settings?.author?.email ?? ''))) {
|
|
66
|
+
problems.push('author needs a name and an e-mail address');
|
|
67
|
+
}
|
|
68
|
+
return problems;
|
|
69
|
+
}
|
|
70
|
+
/** Validates and normalises a request, or throws a 400. */
|
|
71
|
+
static plan(request) {
|
|
72
|
+
const problems = [];
|
|
73
|
+
const files = Array.isArray(request?.files) ? request.files : [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
if (!Array.isArray(request?.files)) {
|
|
76
|
+
problems.push('files must be an array');
|
|
77
|
+
}
|
|
78
|
+
files.forEach((file, index) => {
|
|
79
|
+
const problem = DyNTS_GitSyncInput_Util.pathProblem(file?.path);
|
|
80
|
+
if (problem) {
|
|
81
|
+
problems.push(`files[${index}]: ${problem}`);
|
|
82
|
+
}
|
|
83
|
+
else if (seen.has(file.path.toLowerCase())) {
|
|
84
|
+
// case-insensitive: two paths that differ only in case collide on Windows and macOS checkouts
|
|
85
|
+
problems.push(`files[${index}]: duplicate path ${file.path}`);
|
|
86
|
+
}
|
|
87
|
+
if (typeof file?.content !== 'string') {
|
|
88
|
+
problems.push(`files[${index}]: content must be a string`);
|
|
89
|
+
}
|
|
90
|
+
seen.add(String(file?.path ?? '').toLowerCase());
|
|
91
|
+
});
|
|
92
|
+
const prune = (request?.prune ?? []).map((dir) => String(dir ?? '').replace(/\/+$/, ''));
|
|
93
|
+
prune.forEach((dir, index) => {
|
|
94
|
+
const problem = DyNTS_GitSyncInput_Util.pathProblem(dir);
|
|
95
|
+
if (problem) {
|
|
96
|
+
problems.push(`prune[${index}]: ${problem}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const message = String(request?.message ?? '').trim();
|
|
100
|
+
if (!message || message.length > 10000) {
|
|
101
|
+
problems.push('message must be 1–10000 characters');
|
|
102
|
+
}
|
|
103
|
+
if (problems.length) {
|
|
104
|
+
throw new fsm_dynamo_1.DyFM_Error({
|
|
105
|
+
status: 400,
|
|
106
|
+
errorCode: `${global_settings_const_1.DyNTS_global_settings.systemShortCodeName}|DyNTS-GITSYNC-IN0`,
|
|
107
|
+
message: `GitSync request rejected: ${problems.join('; ')}`,
|
|
108
|
+
userMessage: 'The files to synchronise are invalid.',
|
|
109
|
+
additionalContent: { problems: problems },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
files: files,
|
|
114
|
+
message: message,
|
|
115
|
+
prune: [...new Set(prune)].sort(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Why a repository-relative path is unsafe (null = safe). */
|
|
119
|
+
static pathProblem(path) {
|
|
120
|
+
if (typeof path !== 'string' || !path) {
|
|
121
|
+
return 'the path is empty';
|
|
122
|
+
}
|
|
123
|
+
if (path.length > 400) {
|
|
124
|
+
return 'the path is longer than 400 characters';
|
|
125
|
+
}
|
|
126
|
+
if (/[\\:\0]/.test(path) || path.startsWith('/')) {
|
|
127
|
+
return `${path} is not a relative path with / separators`;
|
|
128
|
+
}
|
|
129
|
+
const segments = path.split('/');
|
|
130
|
+
if (segments.some((segment) => !segment || segment === '.' || segment === '..')) {
|
|
131
|
+
return `${path} has an empty, . or .. segment`;
|
|
132
|
+
}
|
|
133
|
+
if (segments.some((segment) => segment.toLowerCase() === '.git')) {
|
|
134
|
+
return `${path} is inside .git`;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
/** A conservative branch-name check (no option-like or ref-breaking names). */
|
|
139
|
+
static isSafeBranch(branch) {
|
|
140
|
+
return /^[A-Za-z0-9._/-]{1,200}$/.test(branch)
|
|
141
|
+
&& !branch.startsWith('-') && !branch.startsWith('/') && !branch.endsWith('/')
|
|
142
|
+
&& !branch.includes('..') && !branch.includes('//') && !branch.endsWith('.lock') && !branch.endsWith('.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.DyNTS_GitSyncInput_Util = DyNTS_GitSyncInput_Util;
|
|
146
|
+
//# sourceMappingURL=git-sync-input.util.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-sync-input.util.js","sourceRoot":"","sources":["../../../src/_modules/git-sync/git-sync-input.util.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { DyNTS_GitRunner_Type, DyNTS_GitSync_Settings_Interface, DyNTS_GitSyncRequest_Interface, DyNTS_GitSyncResult_Interface } from './_models/git-sync.interface';
|
|
2
|
+
/**
|
|
3
|
+
* BFR-OLDLIGHTSITE-003 — synchronises a caller-built file set into a git repository as ONE commit, pushed.
|
|
4
|
+
*
|
|
5
|
+
* @description Per call: clone (first use) or fetch → force-reset the owned workdir to the remote branch → write the
|
|
6
|
+
* files byte-exact → remove tracked files the caller no longer lists under its `prune` directories → ONE commit →
|
|
7
|
+
* push (fast-forward only: a push is never forced). Nothing changed → no commit (`unchanged`). The remote moved
|
|
8
|
+
* between fetch and push → the whole step runs once more on the new head.
|
|
9
|
+
*
|
|
10
|
+
* The token (`<credentialRef>_TOKEN`, FDP Keystore) reaches git ONLY as an environment-scoped `http.extraHeader`
|
|
11
|
+
* (`GIT_CONFIG_*`): never in an argument (visible in the process list), never in the URL, never in `.git/config`.
|
|
12
|
+
* Every error text is redacted — the token literal included.
|
|
13
|
+
*/
|
|
14
|
+
export declare class DyNTS_GitSync_Service {
|
|
15
|
+
private readonly settings;
|
|
16
|
+
private readonly runner;
|
|
17
|
+
private readonly timeoutMs;
|
|
18
|
+
/** The calls of this instance run one after another — they share one workdir. */
|
|
19
|
+
private queue;
|
|
20
|
+
/** Validates the settings (a 400 on a configuration error); the token is only read when `sync` runs. */
|
|
21
|
+
constructor(settings: DyNTS_GitSync_Settings_Interface);
|
|
22
|
+
/** The default runner: the `git` binary, no shell, bounded time and output. */
|
|
23
|
+
static readonly defaultRunner: DyNTS_GitRunner_Type;
|
|
24
|
+
/**
|
|
25
|
+
* One synchronisation step — see the class description. Concurrent calls on the same instance are queued (one
|
|
26
|
+
* workdir); ⚠️ two INSTANCES or processes must not share a workdir.
|
|
27
|
+
*/
|
|
28
|
+
sync(request: DyNTS_GitSyncRequest_Interface): Promise<DyNTS_GitSyncResult_Interface>;
|
|
29
|
+
private syncNow;
|
|
30
|
+
/** Clone on first use; otherwise verify the clone's origin, fetch and force-reset to the remote branch. */
|
|
31
|
+
private prepareWorkdir;
|
|
32
|
+
/** Writes every file byte-exact; a symbolic link on the way is refused (it could point outside the workdir). */
|
|
33
|
+
private writeFiles;
|
|
34
|
+
/** Removes the tracked files under the prune directories that the caller no longer lists. */
|
|
35
|
+
private pruneFiles;
|
|
36
|
+
/** The absolute path of a repository-relative one — and a last check that it stays inside the workdir. */
|
|
37
|
+
private inside;
|
|
38
|
+
/** Refuses a path whose existing parent is a symbolic link. */
|
|
39
|
+
private assertNoLink;
|
|
40
|
+
/** `git diff --name-status -z` → added/modified and deleted paths, sorted. */
|
|
41
|
+
private parseNameStatus;
|
|
42
|
+
private head;
|
|
43
|
+
/** The child environment: no prompt, and the token — if any — as an URL-scoped extra header. */
|
|
44
|
+
private gitEnv;
|
|
45
|
+
/** Values that must never appear in a log or error text (the token and its header form). */
|
|
46
|
+
private secrets;
|
|
47
|
+
/** Runs one git command, logs it, and throws a rich, redacted error on failure (unless `allowFailure`). */
|
|
48
|
+
private git;
|
|
49
|
+
private failure;
|
|
50
|
+
private refusal;
|
|
51
|
+
/** The generic secret redaction, plus the literal token values of this sync. */
|
|
52
|
+
private redact;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=git-sync.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-sync.service.d.ts","sourceRoot":"","sources":["../../../src/_modules/git-sync/git-sync.service.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,oBAAoB,EAAgC,gCAAgC,EACtB,8BAA8B,EAC5F,6BAA6B,EAC9B,MAAM,8BAA8B,CAAC;AAWtC;;;;;;;;;;;GAWG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,iFAAiF;IACjF,OAAO,CAAC,KAAK,CAAuC;IAEpD,wGAAwG;gBAC5F,QAAQ,EAAE,gCAAgC;IAkBtD,+EAA+E;IAC/E,MAAM,CAAC,QAAQ,CAAC,aAAa,EAAE,oBAAoB,CAcjD;IAEF;;;OAGG;IACG,IAAI,CAAC,OAAO,EAAE,8BAA8B,GAAG,OAAO,CAAC,6BAA6B,CAAC;YA0B7E,OAAO;IAuDrB,2GAA2G;YAC7F,cAAc;IAkC5B,gHAAgH;YAClG,UAAU;IAUxB,6FAA6F;YAC/E,UAAU;IAsBxB,0GAA0G;IAC1G,OAAO,CAAC,MAAM;IAUd,+DAA+D;YACjD,YAAY;IAc1B,8EAA8E;IAC9E,OAAO,CAAC,eAAe;YAYT,IAAI;IAIlB,gGAAgG;IAChG,OAAO,CAAC,MAAM;IA8Bd,4FAA4F;IAC5F,OAAO,CAAC,OAAO,CAAgB;IAE/B,2GAA2G;YAC7F,GAAG;IAoBjB,OAAO,CAAC,OAAO;IAiBf,OAAO,CAAC,OAAO;IAUf,gFAAgF;IAChF,OAAO,CAAC,MAAM;CAMf"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DyNTS_GitSync_Service = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
|
|
8
|
+
const global_settings_const_1 = require("../../_collections/global-settings.const");
|
|
9
|
+
const secret_redaction_util_1 = require("../../_collections/secret-redaction.util");
|
|
10
|
+
const git_sync_input_util_1 = require("./git-sync-input.util");
|
|
11
|
+
/** Config applied to EVERY git command: byte-exact content, no prompt, no host credential helper. */
|
|
12
|
+
const BASE_CONFIG = [
|
|
13
|
+
'-c', 'core.autocrlf=false', '-c', 'core.safecrlf=false', '-c', 'credential.helper=',
|
|
14
|
+
];
|
|
15
|
+
/** One rejected push is retried (the remote moved between fetch and push); more means contention — report it. */
|
|
16
|
+
const MAX_ATTEMPTS = 2;
|
|
17
|
+
/**
|
|
18
|
+
* BFR-OLDLIGHTSITE-003 — synchronises a caller-built file set into a git repository as ONE commit, pushed.
|
|
19
|
+
*
|
|
20
|
+
* @description Per call: clone (first use) or fetch → force-reset the owned workdir to the remote branch → write the
|
|
21
|
+
* files byte-exact → remove tracked files the caller no longer lists under its `prune` directories → ONE commit →
|
|
22
|
+
* push (fast-forward only: a push is never forced). Nothing changed → no commit (`unchanged`). The remote moved
|
|
23
|
+
* between fetch and push → the whole step runs once more on the new head.
|
|
24
|
+
*
|
|
25
|
+
* The token (`<credentialRef>_TOKEN`, FDP Keystore) reaches git ONLY as an environment-scoped `http.extraHeader`
|
|
26
|
+
* (`GIT_CONFIG_*`): never in an argument (visible in the process list), never in the URL, never in `.git/config`.
|
|
27
|
+
* Every error text is redacted — the token literal included.
|
|
28
|
+
*/
|
|
29
|
+
class DyNTS_GitSync_Service {
|
|
30
|
+
settings;
|
|
31
|
+
runner;
|
|
32
|
+
timeoutMs;
|
|
33
|
+
/** The calls of this instance run one after another — they share one workdir. */
|
|
34
|
+
queue = Promise.resolve();
|
|
35
|
+
/** Validates the settings (a 400 on a configuration error); the token is only read when `sync` runs. */
|
|
36
|
+
constructor(settings) {
|
|
37
|
+
const problems = git_sync_input_util_1.DyNTS_GitSyncInput_Util.settingsProblems(settings);
|
|
38
|
+
if (problems.length) {
|
|
39
|
+
throw new fsm_dynamo_1.DyFM_Error({
|
|
40
|
+
status: 400,
|
|
41
|
+
errorCode: `${global_settings_const_1.DyNTS_global_settings.systemShortCodeName}|DyNTS-GITSYNC-CF0`,
|
|
42
|
+
message: `GitSync settings rejected: ${problems.join('; ')}`,
|
|
43
|
+
userMessage: 'The repository synchronisation is misconfigured.',
|
|
44
|
+
additionalContent: { problems: problems },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
this.settings = { ...settings, repoUrl: settings.repoUrl.trim(), workdir: (0, path_1.resolve)(settings.workdir) };
|
|
48
|
+
this.runner = settings.runner ?? DyNTS_GitSync_Service.defaultRunner;
|
|
49
|
+
this.timeoutMs = Math.max(1000, settings.timeoutMs ?? 120000);
|
|
50
|
+
}
|
|
51
|
+
/** The default runner: the `git` binary, no shell, bounded time and output. */
|
|
52
|
+
static defaultRunner = (args, options) => (new Promise((done) => {
|
|
53
|
+
(0, child_process_1.execFile)('git', args, {
|
|
54
|
+
cwd: options.cwd, env: options.env, timeout: options.timeoutMs, maxBuffer: 64 * 1024 * 1024, windowsHide: true,
|
|
55
|
+
}, (error, stdout, stderr) => {
|
|
56
|
+
const code = error ? Reflect.get(error, 'code') : 0;
|
|
57
|
+
done({
|
|
58
|
+
exitCode: typeof code === 'number' ? code : error ? -1 : 0,
|
|
59
|
+
stdout: String(stdout ?? ''),
|
|
60
|
+
stderr: String(stderr ?? '') || (error ? error.message : ''),
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}));
|
|
64
|
+
/**
|
|
65
|
+
* One synchronisation step — see the class description. Concurrent calls on the same instance are queued (one
|
|
66
|
+
* workdir); ⚠️ two INSTANCES or processes must not share a workdir.
|
|
67
|
+
*/
|
|
68
|
+
async sync(request) {
|
|
69
|
+
const run = this.queue.then(() => this.syncNow(request));
|
|
70
|
+
this.queue = run.catch(() => undefined);
|
|
71
|
+
try {
|
|
72
|
+
return await run;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof fsm_dynamo_1.DyFM_Error) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
// a filesystem failure (permission, disk full, a file where a directory is needed) — rich and redacted
|
|
79
|
+
throw new fsm_dynamo_1.DyFM_Error({
|
|
80
|
+
status: 500,
|
|
81
|
+
errorCode: `${global_settings_const_1.DyNTS_global_settings.systemShortCodeName}|DyNTS-GITSYNC-FS0`,
|
|
82
|
+
message: `GitSync failed in ${this.settings.workdir}: `
|
|
83
|
+
+ this.redact(fsm_dynamo_1.DyFM_Error.getErrorMessage(error)),
|
|
84
|
+
userMessage: 'The repository synchronisation failed. Please try again later.',
|
|
85
|
+
error: secret_redaction_util_1.DyNTS_SecretRedaction_Util.redactErrorForLog(error),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async syncNow(request) {
|
|
90
|
+
const plan = git_sync_input_util_1.DyNTS_GitSyncInput_Util.plan(request);
|
|
91
|
+
const env = this.gitEnv(git_sync_input_util_1.DyNTS_GitSyncInput_Util.resolveCredential(this.settings));
|
|
92
|
+
const log = [];
|
|
93
|
+
const started = Date.now();
|
|
94
|
+
for (let attempt = 1;; attempt++) {
|
|
95
|
+
await this.prepareWorkdir(env, log);
|
|
96
|
+
await this.writeFiles(plan.files);
|
|
97
|
+
const removedFiles = await this.pruneFiles(plan, env, log);
|
|
98
|
+
const paths = [
|
|
99
|
+
...plan.files.map((file) => file.path), ...removedFiles,
|
|
100
|
+
];
|
|
101
|
+
if (paths.length) {
|
|
102
|
+
await this.git('stage', ['add', '--all', '--', ...paths], env, log);
|
|
103
|
+
}
|
|
104
|
+
const changes = this.parseNameStatus((await this.git('diff', ['diff', '--cached', '--no-renames', '--name-status', '-z'], env, log)).stdout);
|
|
105
|
+
if (!changes.changed.length && !changes.removed.length) {
|
|
106
|
+
return {
|
|
107
|
+
status: 'unchanged', commit: await this.head(env, log), changedFiles: [], removedFiles: [],
|
|
108
|
+
attempts: attempt, durationMs: Date.now() - started, log: log,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
await this.git('commit', [
|
|
112
|
+
'-c', `user.name=${this.settings.author.name}`, '-c', `user.email=${this.settings.author.email}`,
|
|
113
|
+
'commit', '--quiet', '-m', plan.message,
|
|
114
|
+
], env, log);
|
|
115
|
+
const push = await this.git('push', ['push', '--porcelain', 'origin', `HEAD:refs/heads/${this.settings.branch}`], env, log, true);
|
|
116
|
+
if (!push.exitCode) {
|
|
117
|
+
return {
|
|
118
|
+
status: 'pushed', commit: await this.head(env, log), changedFiles: changes.changed,
|
|
119
|
+
removedFiles: changes.removed, attempts: attempt, durationMs: Date.now() - started, log: log,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const rejected = /rejected|non-fast-forward|fetch first|stale info/i.test(push.stdout + push.stderr);
|
|
123
|
+
if (!rejected || attempt >= MAX_ATTEMPTS) {
|
|
124
|
+
throw this.failure('push', push, log, rejected ? 409 : 502);
|
|
125
|
+
}
|
|
126
|
+
fsm_dynamo_1.DyFM_Log.warn(`[DyNTS GitSync] the remote branch ${this.settings.branch} moved during the sync — retrying once`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Clone on first use; otherwise verify the clone's origin, fetch and force-reset to the remote branch. */
|
|
130
|
+
async prepareWorkdir(env, log) {
|
|
131
|
+
const workdir = this.settings.workdir;
|
|
132
|
+
const entries = (0, fs_1.existsSync)(workdir) ? await fs_1.promises.readdir(workdir) : [];
|
|
133
|
+
if (!entries.length) {
|
|
134
|
+
await fs_1.promises.mkdir((0, path_1.dirname)(workdir), { recursive: true });
|
|
135
|
+
await this.git('clone', [
|
|
136
|
+
'clone', '--quiet', '--no-tags', '--single-branch', '--branch', this.settings.branch, '--',
|
|
137
|
+
this.settings.repoUrl, workdir,
|
|
138
|
+
], env, log, false, (0, path_1.dirname)(workdir));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!entries.includes('.git')) {
|
|
142
|
+
throw this.refusal(`the workdir ${workdir} exists, is not empty and is not a git clone — refusing to touch it`, log);
|
|
143
|
+
}
|
|
144
|
+
const origin = (await this.git('origin', ['remote', 'get-url', 'origin'], env, log)).stdout.trim();
|
|
145
|
+
if (origin.replace(/\/+$/, '') !== this.settings.repoUrl.replace(/\/+$/, '')) {
|
|
146
|
+
throw this.refusal(`the workdir ${workdir} is a clone of a DIFFERENT repository — refusing to touch it`, log);
|
|
147
|
+
}
|
|
148
|
+
const remoteRef = `refs/remotes/origin/${this.settings.branch}`;
|
|
149
|
+
await this.git('fetch', [
|
|
150
|
+
'fetch', '--quiet', '--no-tags', 'origin', `+refs/heads/${this.settings.branch}:${remoteRef}`,
|
|
151
|
+
], env, log);
|
|
152
|
+
await this.git('reset', ['checkout', '--quiet', '--force', '-B', this.settings.branch, remoteRef], env, log);
|
|
153
|
+
}
|
|
154
|
+
/** Writes every file byte-exact; a symbolic link on the way is refused (it could point outside the workdir). */
|
|
155
|
+
async writeFiles(files) {
|
|
156
|
+
for (const file of files) {
|
|
157
|
+
const target = this.inside(file.path);
|
|
158
|
+
await this.assertNoLink(file.path);
|
|
159
|
+
await fs_1.promises.mkdir((0, path_1.dirname)(target), { recursive: true });
|
|
160
|
+
await fs_1.promises.writeFile(target, file.content, 'utf8');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Removes the tracked files under the prune directories that the caller no longer lists. */
|
|
164
|
+
async pruneFiles(plan, env, log) {
|
|
165
|
+
if (!plan.prune.length) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const wanted = new Set(plan.files.map((file) => file.path));
|
|
169
|
+
const tracked = (await this.git('list', ['ls-files', '-z', '--', ...plan.prune.map((dir) => `${dir}/`)], env, log)).stdout.split('\0').filter((path) => !!path);
|
|
170
|
+
const removed = tracked.filter((path) => !wanted.has(path)).sort();
|
|
171
|
+
for (const path of removed) {
|
|
172
|
+
await fs_1.promises.rm(this.inside(path), { force: true });
|
|
173
|
+
}
|
|
174
|
+
return removed;
|
|
175
|
+
}
|
|
176
|
+
/** The absolute path of a repository-relative one — and a last check that it stays inside the workdir. */
|
|
177
|
+
inside(path) {
|
|
178
|
+
const target = (0, path_1.join)(this.settings.workdir, ...path.split('/'));
|
|
179
|
+
if (!target.startsWith(this.settings.workdir + path_1.sep)) {
|
|
180
|
+
throw this.refusal(`${path} resolves outside the workdir`, []);
|
|
181
|
+
}
|
|
182
|
+
return target;
|
|
183
|
+
}
|
|
184
|
+
/** Refuses a path whose existing parent is a symbolic link. */
|
|
185
|
+
async assertNoLink(path) {
|
|
186
|
+
const segments = path.split('/');
|
|
187
|
+
for (let depth = 1; depth < segments.length; depth++) {
|
|
188
|
+
const stat = await fs_1.promises.lstat(this.inside(segments.slice(0, depth).join('/'))).catch(() => null);
|
|
189
|
+
if (stat?.isSymbolicLink()) {
|
|
190
|
+
throw this.refusal(`${segments.slice(0, depth).join('/')} is a symbolic link — refusing to write through it`, []);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** `git diff --name-status -z` → added/modified and deleted paths, sorted. */
|
|
195
|
+
parseNameStatus(output) {
|
|
196
|
+
const parts = output.split('\0').filter((part) => !!part);
|
|
197
|
+
const changed = [];
|
|
198
|
+
const removed = [];
|
|
199
|
+
for (let index = 0; index + 1 < parts.length; index += 2) {
|
|
200
|
+
(parts[index] === 'D' ? removed : changed).push(parts[index + 1]);
|
|
201
|
+
}
|
|
202
|
+
return { changed: changed.sort(), removed: removed.sort() };
|
|
203
|
+
}
|
|
204
|
+
async head(env, log) {
|
|
205
|
+
return (await this.git('head', ['rev-parse', 'HEAD'], env, log)).stdout.trim();
|
|
206
|
+
}
|
|
207
|
+
/** The child environment: no prompt, and the token — if any — as an URL-scoped extra header. */
|
|
208
|
+
gitEnv(credential) {
|
|
209
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' };
|
|
210
|
+
if (credential.status === 'absent' || credential.status === 'invalid') {
|
|
211
|
+
const detail = credential.status === 'absent'
|
|
212
|
+
? `set ${credential.missingKeys.join(', ')} in the FDP Keystore`
|
|
213
|
+
: credential.problems.join('; ');
|
|
214
|
+
throw new fsm_dynamo_1.DyFM_Error({
|
|
215
|
+
status: 503,
|
|
216
|
+
errorCode: `${global_settings_const_1.DyNTS_global_settings.systemShortCodeName}|DyNTS-GITSYNC-CR0`,
|
|
217
|
+
message: `GitSync credential ${credential.credentialRef} is not usable: ${detail}`,
|
|
218
|
+
userMessage: 'The repository synchronisation is not configured on this server.',
|
|
219
|
+
additionalContent: { credential: credential },
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (credential.status === 'configured') {
|
|
223
|
+
const token = String((this.settings.env ?? process.env)[credential.tokenKey]).trim();
|
|
224
|
+
const basic = Buffer.from(`${this.settings.tokenUser ?? 'x-access-token'}:${token}`).toString('base64');
|
|
225
|
+
env.GIT_CONFIG_COUNT = '1';
|
|
226
|
+
env.GIT_CONFIG_KEY_0 = `http.${new URL(this.settings.repoUrl).origin}/.extraHeader`;
|
|
227
|
+
env.GIT_CONFIG_VALUE_0 = `Authorization: Basic ${basic}`;
|
|
228
|
+
this.secrets = [token, basic];
|
|
229
|
+
}
|
|
230
|
+
return env;
|
|
231
|
+
}
|
|
232
|
+
/** Values that must never appear in a log or error text (the token and its header form). */
|
|
233
|
+
secrets = [];
|
|
234
|
+
/** Runs one git command, logs it, and throws a rich, redacted error on failure (unless `allowFailure`). */
|
|
235
|
+
async git(step, args, env, log, allowFailure = false, cwd = this.settings.workdir) {
|
|
236
|
+
const started = Date.now();
|
|
237
|
+
const result = await this.runner([...BASE_CONFIG, ...args], { cwd: cwd, env: env, timeoutMs: this.timeoutMs });
|
|
238
|
+
log.push({
|
|
239
|
+
step: step, command: this.redact(`git ${args.join(' ')}`), exitCode: result.exitCode, ms: Date.now() - started,
|
|
240
|
+
});
|
|
241
|
+
if (result.exitCode && !allowFailure) {
|
|
242
|
+
throw this.failure(step, result, log, 502);
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
failure(step, result, log, status) {
|
|
247
|
+
const output = this.redact(`${result.stderr} ${result.stdout}`.trim()).slice(0, 2000);
|
|
248
|
+
return new fsm_dynamo_1.DyFM_Error({
|
|
249
|
+
status: status,
|
|
250
|
+
errorCode: `${global_settings_const_1.DyNTS_global_settings.systemShortCodeName}|DyNTS-GITSYNC-${status === 409 ? 'RJ0' : 'GT0'}`,
|
|
251
|
+
message: `GitSync ${step} failed (exit ${result.exitCode}) for ${this.redact(this.settings.repoUrl)}`
|
|
252
|
+
+ ` ${this.settings.branch}: ${output}`,
|
|
253
|
+
userMessage: status === 409
|
|
254
|
+
? 'The repository changed during the synchronisation. Please try again.'
|
|
255
|
+
: 'The repository synchronisation failed. Please try again later.',
|
|
256
|
+
additionalContent: { step: step, exitCode: result.exitCode, log: log },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
refusal(reason, log) {
|
|
260
|
+
return new fsm_dynamo_1.DyFM_Error({
|
|
261
|
+
status: 409,
|
|
262
|
+
errorCode: `${global_settings_const_1.DyNTS_global_settings.systemShortCodeName}|DyNTS-GITSYNC-WD0`,
|
|
263
|
+
message: `GitSync refused: ${reason}`,
|
|
264
|
+
userMessage: 'The repository synchronisation is misconfigured.',
|
|
265
|
+
additionalContent: { log: log },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/** The generic secret redaction, plus the literal token values of this sync. */
|
|
269
|
+
redact(text) {
|
|
270
|
+
return this.secrets.reduce((current, secret) => (secret ? current.split(secret).join('[redacted]') : current), secret_redaction_util_1.DyNTS_SecretRedaction_Util.redactText(text));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
exports.DyNTS_GitSync_Service = DyNTS_GitSync_Service;
|
|
274
|
+
//# sourceMappingURL=git-sync.service.js.map
|