@lobu/connector-sdk 7.2.0 → 8.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/dist/connector-runtime.d.ts +10 -2
- package/dist/connector-runtime.d.ts.map +1 -1
- package/dist/connector-runtime.js +21 -1
- package/dist/connector-runtime.js.map +1 -1
- package/dist/connector-types.d.ts +0 -6
- package/dist/connector-types.d.ts.map +1 -1
- package/dist/connector-types.js +0 -7
- package/dist/connector-types.js.map +1 -1
- package/dist/file-source.d.ts +112 -0
- package/dist/file-source.d.ts.map +1 -0
- package/dist/file-source.js +40 -0
- package/dist/file-source.js.map +1 -0
- package/dist/index.d.ts +12 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/sources/cache.d.ts +82 -0
- package/dist/sources/cache.d.ts.map +1 -0
- package/dist/sources/cache.js +169 -0
- package/dist/sources/cache.js.map +1 -0
- package/dist/sources/git-file-source.d.ts +33 -0
- package/dist/sources/git-file-source.d.ts.map +1 -0
- package/dist/sources/git-file-source.js +207 -0
- package/dist/sources/git-file-source.js.map +1 -0
- package/dist/sources/git-http.d.ts +48 -0
- package/dist/sources/git-http.d.ts.map +1 -0
- package/dist/sources/git-http.js +179 -0
- package/dist/sources/git-http.js.map +1 -0
- package/dist/sources/git-snapshot.d.ts +14 -0
- package/dist/sources/git-snapshot.d.ts.map +1 -0
- package/dist/sources/git-snapshot.js +96 -0
- package/dist/sources/git-snapshot.js.map +1 -0
- package/dist/sources/glob.d.ts +31 -0
- package/dist/sources/glob.d.ts.map +1 -0
- package/dist/sources/glob.js +129 -0
- package/dist/sources/glob.js.map +1 -0
- package/dist/sources/local-file-source.d.ts +29 -0
- package/dist/sources/local-file-source.d.ts.map +1 -0
- package/dist/sources/local-file-source.js +343 -0
- package/dist/sources/local-file-source.js.map +1 -0
- package/dist/sources/resolver.d.ts +6 -0
- package/dist/sources/resolver.d.ts.map +1 -0
- package/dist/sources/resolver.js +47 -0
- package/dist/sources/resolver.js.map +1 -0
- package/dist/sources/snapshot.d.ts +19 -0
- package/dist/sources/snapshot.d.ts.map +1 -0
- package/dist/sources/snapshot.js +91 -0
- package/dist/sources/snapshot.js.map +1 -0
- package/dist/sources/tarball-file-source.d.ts +28 -0
- package/dist/sources/tarball-file-source.d.ts.map +1 -0
- package/dist/sources/tarball-file-source.js +273 -0
- package/dist/sources/tarball-file-source.js.map +1 -0
- package/dist/types.d.ts +3 -65
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -3
- package/dist/event-taxonomy.d.ts +0 -3
- package/dist/event-taxonomy.d.ts.map +0 -1
- package/dist/event-taxonomy.js +0 -30
- package/dist/event-taxonomy.js.map +0 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-layout helpers shared by all FileSystemSource implementations.
|
|
3
|
+
*
|
|
4
|
+
* ${WORKSPACE_DIR}/.lobu-cache/sources/<sha256(uri)[:32]>/
|
|
5
|
+
* ├── snapshot/ ← actual files (connector-visible via Snapshot.readFile)
|
|
6
|
+
* ├── manifest.json ← { ref, files: [{ path, sha256 }], fetched_at }
|
|
7
|
+
* └── meta.json ← { uri, kind }
|
|
8
|
+
*
|
|
9
|
+
* - `WORKSPACE_DIR` env var is the cache root; falls back to `process.cwd()`.
|
|
10
|
+
* - URI is hashed (sha256, first 32 hex chars = 128 bits) for filesystem-safe
|
|
11
|
+
* naming. 128 bits is collision-resistant even against adversarial URIs
|
|
12
|
+
* (1e19 URIs before 50% collision odds).
|
|
13
|
+
* - One cache directory per URI — same URI yields the same directory across
|
|
14
|
+
* runs so re-fetch is incremental for git, manifest-comparable for the rest.
|
|
15
|
+
* On reuse, `meta.json`'s `uri` field is verified to match — a mismatch
|
|
16
|
+
* (theoretical hash collision OR cache-root reuse across schemes) throws.
|
|
17
|
+
* - The cache layout is intentionally NOT exported on the public API; this
|
|
18
|
+
* module is internal to the SDK.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
/** Build the cache paths for `uri`, anchored under `cacheRoot`. */
|
|
24
|
+
export function cachePathsFor(uri, cacheRoot = defaultCacheRoot()) {
|
|
25
|
+
const hash = createHash('sha256').update(uri).digest('hex').slice(0, 32);
|
|
26
|
+
const root = join(cacheRoot, '.lobu-cache', 'sources', hash);
|
|
27
|
+
return {
|
|
28
|
+
root,
|
|
29
|
+
snapshotDir: join(root, 'snapshot'),
|
|
30
|
+
manifestPath: join(root, 'manifest.json'),
|
|
31
|
+
metaPath: join(root, 'meta.json'),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read `meta.json` if present and assert it matches `uri`. Throws on URI
|
|
36
|
+
* mismatch (defends against an adversarial collision OR an operator copying
|
|
37
|
+
* a cache dir across sources). Returns `null` for a fresh cache.
|
|
38
|
+
*/
|
|
39
|
+
export async function readAndVerifyMeta(metaPath, expectedUri) {
|
|
40
|
+
const meta = await readMeta(metaPath);
|
|
41
|
+
if (!meta)
|
|
42
|
+
return null;
|
|
43
|
+
if (meta.uri !== expectedUri) {
|
|
44
|
+
throw new Error(`FileSystemSource cache mismatch: ${metaPath} belongs to ${meta.uri}, ` +
|
|
45
|
+
`but ${expectedUri} was requested. Refusing to reuse cache directory.`);
|
|
46
|
+
}
|
|
47
|
+
return meta;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Like `readAndVerifyMeta`, but rejects when the cache is uninitialised.
|
|
51
|
+
* Use this from `diffSinceRef()` paths — calling diff before fetch is a
|
|
52
|
+
* caller bug, and a missing meta also means a stale/collided cache dir
|
|
53
|
+
* should not be silently consumed.
|
|
54
|
+
*/
|
|
55
|
+
export async function requireMeta(metaPath, expectedUri) {
|
|
56
|
+
const meta = await readAndVerifyMeta(metaPath, expectedUri);
|
|
57
|
+
if (!meta) {
|
|
58
|
+
throw new Error(`FileSystemSource: source not fetched yet — call fetch() before diffSinceRef() (${expectedUri})`);
|
|
59
|
+
}
|
|
60
|
+
return meta;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Per-source mutex. Same URI → shared `Promise` chain so concurrent
|
|
64
|
+
* `fetch()` calls serialize. Process-local only — fine for the embedded
|
|
65
|
+
* worker model where one worker subprocess owns its cache.
|
|
66
|
+
*
|
|
67
|
+
* v1 limitation: two processes sharing the same
|
|
68
|
+
* `${WORKSPACE_DIR}/.lobu-cache` are NOT coordinated by this lock —
|
|
69
|
+
* each gets its own in-memory `_sourceLocks` map, and they can
|
|
70
|
+
* race-prune each other's per-ref dirs (see `pruneOldRefDirs` in each
|
|
71
|
+
* source impl). v1 assumes one cache owner per workspace. If we ever
|
|
72
|
+
* need multi-process sharing, replace this with a filesystem advisory
|
|
73
|
+
* lock (e.g. `proper-lockfile` against `${root}/.lock`) around
|
|
74
|
+
* fetch+prune.
|
|
75
|
+
*
|
|
76
|
+
* The map stores the *guarded* (error-swallowed) promise so a rejection in
|
|
77
|
+
* `fn` doesn't poison the chain. Identity comparison in `finally` uses the
|
|
78
|
+
* same stored reference, so cleanup actually removes the entry — an
|
|
79
|
+
* earlier draft created a fresh `.catch()` inside `finally` and leaked one
|
|
80
|
+
* entry per distinct URI.
|
|
81
|
+
*/
|
|
82
|
+
const _sourceLocks = new Map();
|
|
83
|
+
export async function withSourceLock(uri, fn) {
|
|
84
|
+
const prev = _sourceLocks.get(uri) ?? Promise.resolve();
|
|
85
|
+
const next = prev.then(fn, fn);
|
|
86
|
+
const guarded = next.catch(() => undefined);
|
|
87
|
+
_sourceLocks.set(uri, guarded);
|
|
88
|
+
try {
|
|
89
|
+
return await next;
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
if (_sourceLocks.get(uri) === guarded) {
|
|
93
|
+
_sourceLocks.delete(uri);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Default cache root: `WORKSPACE_DIR` env, else `process.cwd()`. */
|
|
98
|
+
export function defaultCacheRoot() {
|
|
99
|
+
return process.env.WORKSPACE_DIR ?? process.cwd();
|
|
100
|
+
}
|
|
101
|
+
export async function readManifest(path) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = await readFile(path, 'utf8');
|
|
104
|
+
return JSON.parse(raw);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
if (err.code === 'ENOENT')
|
|
108
|
+
return null;
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export async function writeManifest(path, manifest) {
|
|
113
|
+
await writeFile(path, JSON.stringify(manifest, null, 2), 'utf8');
|
|
114
|
+
}
|
|
115
|
+
export async function readMeta(path) {
|
|
116
|
+
try {
|
|
117
|
+
const raw = await readFile(path, 'utf8');
|
|
118
|
+
return JSON.parse(raw);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (err.code === 'ENOENT')
|
|
122
|
+
return null;
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function writeMeta(path, meta) {
|
|
127
|
+
await writeFile(path, JSON.stringify(meta, null, 2), 'utf8');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Diff two manifests by `(path, sha256)`. Order-independent.
|
|
131
|
+
*/
|
|
132
|
+
export function diffManifests(prev, next) {
|
|
133
|
+
const prevMap = new Map(prev.files.map((f) => [f.path, f.sha256]));
|
|
134
|
+
const nextMap = new Map(next.files.map((f) => [f.path, f.sha256]));
|
|
135
|
+
const added = [];
|
|
136
|
+
const modified = [];
|
|
137
|
+
const removed = [];
|
|
138
|
+
for (const [path, sha] of nextMap) {
|
|
139
|
+
const prevSha = prevMap.get(path);
|
|
140
|
+
if (prevSha === undefined)
|
|
141
|
+
added.push(path);
|
|
142
|
+
else if (prevSha !== sha)
|
|
143
|
+
modified.push(path);
|
|
144
|
+
}
|
|
145
|
+
for (const path of prevMap.keys()) {
|
|
146
|
+
if (!nextMap.has(path))
|
|
147
|
+
removed.push(path);
|
|
148
|
+
}
|
|
149
|
+
return { added, modified, removed };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Canonicalize a manifest's `ref` from its file list:
|
|
153
|
+
*
|
|
154
|
+
* sha256 over lines of `<path>\0<sha256>\n`, sorted by path.
|
|
155
|
+
*
|
|
156
|
+
* Deterministic across runs and platforms.
|
|
157
|
+
*/
|
|
158
|
+
export function canonicalManifestRef(files) {
|
|
159
|
+
const sorted = [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
160
|
+
const h = createHash('sha256');
|
|
161
|
+
for (const f of sorted) {
|
|
162
|
+
h.update(f.path);
|
|
163
|
+
h.update('\0');
|
|
164
|
+
h.update(f.sha256);
|
|
165
|
+
h.update('\n');
|
|
166
|
+
}
|
|
167
|
+
return h.digest('hex');
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/sources/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitFileSource — shallow git clone backed by `isomorphic-git`.
|
|
3
|
+
*
|
|
4
|
+
* - URI shape: `git+https://github.com/owner/repo.git[@<ref>]`. Ref may be
|
|
5
|
+
* a branch name, tag, or full commit SHA. If omitted, defaults to `main`.
|
|
6
|
+
* - Initial fetch: shallow clone (`depth: 1`, `singleBranch: true`).
|
|
7
|
+
* - Subsequent fetch: `git.fetch` against the same single branch with
|
|
8
|
+
* `depth: 1` — pulls only the new tip if upstream advanced.
|
|
9
|
+
* - `ref` = `resolveRef('HEAD')` (full commit SHA).
|
|
10
|
+
* - `diffSinceRef(prevRef)` walks two trees with `git.walk` and classifies
|
|
11
|
+
* each path by OID equality (`added`/`modified`/`removed`).
|
|
12
|
+
*
|
|
13
|
+
* Caveats called out in JSDoc rather than hidden:
|
|
14
|
+
*
|
|
15
|
+
* - With `depth: 1`, history before the current tip is NOT in the local
|
|
16
|
+
* repo. `git.walk` requires both trees to be reachable; if the caller
|
|
17
|
+
* passes a `prevRef` we no longer have on disk we throw a clear error.
|
|
18
|
+
* - Snapshot reads point at the working tree inside the cache. Git itself
|
|
19
|
+
* is not exposed.
|
|
20
|
+
*/
|
|
21
|
+
import type { FileDelta, FileSystemSource, Snapshot } from '../file-source.js';
|
|
22
|
+
export interface ParsedGitUri {
|
|
23
|
+
url: string;
|
|
24
|
+
ref: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function parseGitUri(uri: string): ParsedGitUri;
|
|
27
|
+
export declare class GitFileSource implements FileSystemSource {
|
|
28
|
+
#private;
|
|
29
|
+
constructor(uri: string);
|
|
30
|
+
fetch(): Promise<Snapshot>;
|
|
31
|
+
diffSinceRef(prevRef: string): Promise<FileDelta>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=git-file-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-file-source.d.ts","sourceRoot":"","sources":["../../src/sources/git-file-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAiB/E,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAkBrD;AAED,qBAAa,aAAc,YAAW,gBAAgB;;gBAKxC,GAAG,EAAE,MAAM;IAMvB,KAAK,IAAI,OAAO,CAAC,QAAQ,CAAC;IAmD1B,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;CA2DlD"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitFileSource — shallow git clone backed by `isomorphic-git`.
|
|
3
|
+
*
|
|
4
|
+
* - URI shape: `git+https://github.com/owner/repo.git[@<ref>]`. Ref may be
|
|
5
|
+
* a branch name, tag, or full commit SHA. If omitted, defaults to `main`.
|
|
6
|
+
* - Initial fetch: shallow clone (`depth: 1`, `singleBranch: true`).
|
|
7
|
+
* - Subsequent fetch: `git.fetch` against the same single branch with
|
|
8
|
+
* `depth: 1` — pulls only the new tip if upstream advanced.
|
|
9
|
+
* - `ref` = `resolveRef('HEAD')` (full commit SHA).
|
|
10
|
+
* - `diffSinceRef(prevRef)` walks two trees with `git.walk` and classifies
|
|
11
|
+
* each path by OID equality (`added`/`modified`/`removed`).
|
|
12
|
+
*
|
|
13
|
+
* Caveats called out in JSDoc rather than hidden:
|
|
14
|
+
*
|
|
15
|
+
* - With `depth: 1`, history before the current tip is NOT in the local
|
|
16
|
+
* repo. `git.walk` requires both trees to be reachable; if the caller
|
|
17
|
+
* passes a `prevRef` we no longer have on disk we throw a clear error.
|
|
18
|
+
* - Snapshot reads point at the working tree inside the cache. Git itself
|
|
19
|
+
* is not exposed.
|
|
20
|
+
*/
|
|
21
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import * as git from 'isomorphic-git';
|
|
24
|
+
// `fs` is passed as a plain Node fs module — isomorphic-git accepts it.
|
|
25
|
+
// We import the callback-style module so isomorphic-git's promisified
|
|
26
|
+
// adapter works out of the box (it auto-detects promises if present).
|
|
27
|
+
import nodeFs from 'node:fs';
|
|
28
|
+
import { cachePathsFor, readAndVerifyMeta, requireMeta, withSourceLock, writeMeta, } from './cache.js';
|
|
29
|
+
import { GitSnapshot } from './git-snapshot.js';
|
|
30
|
+
// Custom https-only http client — rejects plaintext redirects that the
|
|
31
|
+
// stock `isomorphic-git/http/node` (simple-get under the hood) would
|
|
32
|
+
// silently follow.
|
|
33
|
+
import { gitHttpsOnlyClient as http } from './git-http.js';
|
|
34
|
+
const DEFAULT_BRANCH = 'main';
|
|
35
|
+
export function parseGitUri(uri) {
|
|
36
|
+
if (uri.startsWith('git+http://')) {
|
|
37
|
+
throw new Error('GitFileSource: plaintext HTTP rejected, use git+https://');
|
|
38
|
+
}
|
|
39
|
+
if (!uri.startsWith('git+https://')) {
|
|
40
|
+
throw new Error(`GitFileSource: expected git+https:// URI, got ${uri}`);
|
|
41
|
+
}
|
|
42
|
+
const stripped = uri.slice('git+'.length); // → https://...
|
|
43
|
+
// Split on the LAST `@` that follows the host's `/`, not the user@host form.
|
|
44
|
+
const slashIdx = stripped.indexOf('/', stripped.indexOf('://') + 3);
|
|
45
|
+
const atIdx = slashIdx === -1 ? -1 : stripped.lastIndexOf('@');
|
|
46
|
+
if (atIdx > slashIdx) {
|
|
47
|
+
return {
|
|
48
|
+
url: stripped.slice(0, atIdx),
|
|
49
|
+
ref: stripped.slice(atIdx + 1) || DEFAULT_BRANCH,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return { url: stripped, ref: DEFAULT_BRANCH };
|
|
53
|
+
}
|
|
54
|
+
export class GitFileSource {
|
|
55
|
+
#uri;
|
|
56
|
+
#parsed;
|
|
57
|
+
#paths;
|
|
58
|
+
constructor(uri) {
|
|
59
|
+
this.#uri = uri;
|
|
60
|
+
this.#parsed = parseGitUri(uri);
|
|
61
|
+
this.#paths = cachePathsFor(uri);
|
|
62
|
+
}
|
|
63
|
+
fetch() {
|
|
64
|
+
return withSourceLock(this.#uri, () => this.#fetchLocked());
|
|
65
|
+
}
|
|
66
|
+
async #fetchLocked() {
|
|
67
|
+
await mkdir(this.#paths.root, { recursive: true });
|
|
68
|
+
await readAndVerifyMeta(this.#paths.metaPath, this.#uri);
|
|
69
|
+
const dir = this.#paths.snapshotDir;
|
|
70
|
+
// Detect whether we already have a clone.
|
|
71
|
+
const alreadyCloned = await pathExists(join(dir, '.git'));
|
|
72
|
+
if (!alreadyCloned) {
|
|
73
|
+
await mkdir(dir, { recursive: true });
|
|
74
|
+
await git.clone({
|
|
75
|
+
fs: nodeFs,
|
|
76
|
+
http,
|
|
77
|
+
dir,
|
|
78
|
+
url: this.#parsed.url,
|
|
79
|
+
ref: this.#parsed.ref,
|
|
80
|
+
singleBranch: true,
|
|
81
|
+
depth: 1,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Existing repo — fetch the same branch shallow.
|
|
86
|
+
await git.fetch({
|
|
87
|
+
fs: nodeFs,
|
|
88
|
+
http,
|
|
89
|
+
dir,
|
|
90
|
+
ref: this.#parsed.ref,
|
|
91
|
+
singleBranch: true,
|
|
92
|
+
depth: 1,
|
|
93
|
+
tags: false,
|
|
94
|
+
});
|
|
95
|
+
// Move HEAD/working tree to the fetched tip.
|
|
96
|
+
await git.checkout({
|
|
97
|
+
fs: nodeFs,
|
|
98
|
+
dir,
|
|
99
|
+
ref: this.#parsed.ref,
|
|
100
|
+
force: true,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const ref = await git.resolveRef({ fs: nodeFs, dir, ref: 'HEAD' });
|
|
104
|
+
await writeMeta(this.#paths.metaPath, { uri: this.#uri, kind: 'git' });
|
|
105
|
+
// Read from the captured commit's tree/blobs — immutable view even if a
|
|
106
|
+
// later fetch() moves HEAD or rewrites the working tree on disk.
|
|
107
|
+
return new GitSnapshot(dir, ref, nodeFs, { exclude: isGitInternalPath });
|
|
108
|
+
}
|
|
109
|
+
diffSinceRef(prevRef) {
|
|
110
|
+
return withSourceLock(this.#uri, () => this.#diffLocked(prevRef));
|
|
111
|
+
}
|
|
112
|
+
async #diffLocked(prevRef) {
|
|
113
|
+
await requireMeta(this.#paths.metaPath, this.#uri);
|
|
114
|
+
const dir = this.#paths.snapshotDir;
|
|
115
|
+
const currentRef = await git.resolveRef({ fs: nodeFs, dir, ref: 'HEAD' });
|
|
116
|
+
if (currentRef === prevRef)
|
|
117
|
+
return { added: [], modified: [], removed: [] };
|
|
118
|
+
// If `prevRef` isn't reachable in the local (shallow) repo, return a
|
|
119
|
+
// full-reingest delta — every current file as `added`. Matches the
|
|
120
|
+
// tarball and local-source contract: an unknown prevRef yields a
|
|
121
|
+
// re-ingest, never a thrown error.
|
|
122
|
+
let prevReachable = true;
|
|
123
|
+
try {
|
|
124
|
+
await git.readCommit({ fs: nodeFs, dir, oid: prevRef });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
prevReachable = false;
|
|
128
|
+
}
|
|
129
|
+
if (!prevReachable) {
|
|
130
|
+
const allFiles = await listCurrentFiles(dir, currentRef);
|
|
131
|
+
return { added: allFiles, modified: [], removed: [] };
|
|
132
|
+
}
|
|
133
|
+
const added = [];
|
|
134
|
+
const modified = [];
|
|
135
|
+
const removed = [];
|
|
136
|
+
await git.walk({
|
|
137
|
+
fs: nodeFs,
|
|
138
|
+
dir,
|
|
139
|
+
trees: [git.TREE({ ref: prevRef }), git.TREE({ ref: currentRef })],
|
|
140
|
+
map: async (filepath, entries) => {
|
|
141
|
+
// The root entry has filepath '.'. Don't classify it but DO let the
|
|
142
|
+
// walk descend (returning null prunes the subtree).
|
|
143
|
+
if (filepath === '.')
|
|
144
|
+
return undefined;
|
|
145
|
+
if (!entries)
|
|
146
|
+
return undefined;
|
|
147
|
+
const [a, b] = entries;
|
|
148
|
+
// Skip directories — they're emitted as their own map() calls; we
|
|
149
|
+
// only classify file (blob) entries.
|
|
150
|
+
const aType = a ? await a.type() : null;
|
|
151
|
+
const bType = b ? await b.type() : null;
|
|
152
|
+
if (aType === 'tree' || bType === 'tree')
|
|
153
|
+
return undefined;
|
|
154
|
+
const aOid = aType ? await a.oid() : null;
|
|
155
|
+
const bOid = bType ? await b.oid() : null;
|
|
156
|
+
if (aOid === null && bOid !== null)
|
|
157
|
+
added.push(filepath);
|
|
158
|
+
else if (aOid !== null && bOid === null)
|
|
159
|
+
removed.push(filepath);
|
|
160
|
+
else if (aOid !== null && bOid !== null && aOid !== bOid)
|
|
161
|
+
modified.push(filepath);
|
|
162
|
+
return undefined;
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return { added, modified, removed };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Walk a commit's tree and return every blob path (POSIX-separated). */
|
|
169
|
+
async function listCurrentFiles(dir, ref) {
|
|
170
|
+
const collected = [];
|
|
171
|
+
await git.walk({
|
|
172
|
+
fs: nodeFs,
|
|
173
|
+
dir,
|
|
174
|
+
trees: [git.TREE({ ref })],
|
|
175
|
+
map: async (filepath, entries) => {
|
|
176
|
+
if (filepath === '.')
|
|
177
|
+
return undefined;
|
|
178
|
+
if (!entries)
|
|
179
|
+
return undefined;
|
|
180
|
+
const [entry] = entries;
|
|
181
|
+
if (!entry)
|
|
182
|
+
return undefined;
|
|
183
|
+
const type = await entry.type();
|
|
184
|
+
if (type === 'blob')
|
|
185
|
+
collected.push(filepath);
|
|
186
|
+
return undefined;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
return collected;
|
|
190
|
+
}
|
|
191
|
+
/** Filter for git internals — matches `.git` itself and anything under it. */
|
|
192
|
+
function isGitInternalPath(rel) {
|
|
193
|
+
return rel === '.git' || rel.startsWith('.git/');
|
|
194
|
+
}
|
|
195
|
+
async function pathExists(p) {
|
|
196
|
+
try {
|
|
197
|
+
await readFile(p).catch(async () => {
|
|
198
|
+
const { stat } = await import('node:fs/promises');
|
|
199
|
+
await stat(p);
|
|
200
|
+
});
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=git-file-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-file-source.js","sourceRoot":"","sources":["../../src/sources/git-file-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,KAAK,GAAG,MAAM,gBAAgB,CAAC;AACtC,wEAAwE;AACxE,sEAAsE;AACtE,sEAAsE;AACtE,OAAO,MAAM,MAAM,SAAS,CAAC;AAG7B,OAAO,EAEL,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,SAAS,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,uEAAuE;AACvE,qEAAqE;AACrE,mBAAmB;AACnB,OAAO,EAAE,kBAAkB,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAE3D,MAAM,cAAc,GAAG,MAAM,CAAC;AAO9B,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,iDAAiD,GAAG,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB;IAC3D,6EAA6E;IAC7E,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACpE,MAAM,KAAK,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/D,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;QACrB,OAAO;YACL,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC;YAC7B,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,cAAc;SACjD,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,OAAO,aAAa;IACf,IAAI,CAAS;IACb,OAAO,CAAe;IACtB,MAAM,CAAa;IAE5B,YAAY,GAAW;QACrB,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,KAAK;QACH,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,MAAM,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACzD,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;QAEpC,0CAA0C;QAC1C,MAAM,aAAa,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;QAE1D,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACtC,MAAM,GAAG,CAAC,KAAK,CAAC;gBACd,EAAE,EAAE,MAAM;gBACV,IAAI;gBACJ,GAAG;gBACH,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG;gBACrB,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG;gBACrB,YAAY,EAAE,IAAI;gBAClB,KAAK,EAAE,CAAC;aACT,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,iDAAiD;YACjD,MAAM,GAAG,CAAC,KAAK,CAAC;gBACd,EAAE,EAAE,MAAM;gBACV,IAAI;gBACJ,GAAG;gBACH,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG;gBACrB,YAAY,EAAE,IAAI;gBAClB,KAAK,EAAE,CAAC;gBACR,IAAI,EAAE,KAAK;aACZ,CAAC,CAAC;YACH,6CAA6C;YAC7C,MAAM,GAAG,CAAC,QAAQ,CAAC;gBACjB,EAAE,EAAE,MAAM;gBACV,GAAG;gBACH,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG;gBACrB,KAAK,EAAE,IAAI;aACZ,CAAC,CAAC;QACL,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;QACnE,MAAM,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAEvE,wEAAwE;QACxE,iEAAiE;QACjE,OAAO,IAAI,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,YAAY,CAAC,OAAe;QAC1B,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;IACpE,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAe;QAC/B,MAAM,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;QACpC,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;QAC1E,IAAI,UAAU,KAAK,OAAO;YAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAE5E,qEAAqE;QACrE,mEAAmE;QACnE,iEAAiE;QACjE,mCAAmC;QACnC,IAAI,aAAa,GAAG,IAAI,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;QAC1D,CAAC;QAAC,MAAM,CAAC;YACP,aAAa,GAAG,KAAK,CAAC;QACxB,CAAC;QACD,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;YACzD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QACxD,CAAC;QAED,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAa,EAAE,CAAC;QAE7B,MAAM,GAAG,CAAC,IAAI,CAAC;YACb,EAAE,EAAE,MAAM;YACV,GAAG;YACH,KAAK,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC;YAClE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;gBAC/B,oEAAoE;gBACpE,oDAAoD;gBACpD,IAAI,QAAQ,KAAK,GAAG;oBAAE,OAAO,SAAS,CAAC;gBACvC,IAAI,CAAC,OAAO;oBAAE,OAAO,SAAS,CAAC;gBAC/B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC;gBAEvB,kEAAkE;gBAClE,qCAAqC;gBACrC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;gBACxC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;gBACxC,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM;oBAAE,OAAO,SAAS,CAAC;gBAE3D,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;gBAE3C,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;oBAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;qBACpD,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;oBAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;qBAC3D,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;oBAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAElF,OAAO,SAAS,CAAC;YACnB,CAAC;SACF,CAAC,CAAC;QAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IACtC,CAAC;CACF;AAED,yEAAyE;AACzE,KAAK,UAAU,gBAAgB,CAAC,GAAW,EAAE,GAAW;IACtD,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,MAAM,GAAG,CAAC,IAAI,CAAC;QACb,EAAE,EAAE,MAAM;QACV,GAAG;QACH,KAAK,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1B,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;YAC/B,IAAI,QAAQ,KAAK,GAAG;gBAAE,OAAO,SAAS,CAAC;YACvC,IAAI,CAAC,OAAO;gBAAE,OAAO,SAAS,CAAC;YAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC;YACxB,IAAI,CAAC,KAAK;gBAAE,OAAO,SAAS,CAAC;YAC7B,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;YAChC,IAAI,IAAI,KAAK,MAAM;gBAAE,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC9C,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC,CAAC;IACH,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,8EAA8E;AAC9E,SAAS,iBAAiB,CAAC,GAAW;IACpC,OAAO,GAAG,KAAK,MAAM,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;AACnD,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,CAAS;IACjC,IAAI,CAAC;QACH,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE;YACjC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;YAClD,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpClient for isomorphic-git that:
|
|
3
|
+
*
|
|
4
|
+
* - Talks https only — refuses to issue any request whose URL is not
|
|
5
|
+
* `https://...`.
|
|
6
|
+
* - Follows redirects manually (max 5 hops). Every `Location` is re-validated
|
|
7
|
+
* https before the next request — closes the https-to-http downgrade hole
|
|
8
|
+
* that an https endpoint replying with `Location: http://...` would
|
|
9
|
+
* otherwise open against the default `isomorphic-git/http/node` impl
|
|
10
|
+
* (which uses `simple-get`'s built-in redirect follower and does NOT
|
|
11
|
+
* enforce a scheme on the next hop).
|
|
12
|
+
*
|
|
13
|
+
* Implemented on top of Node's `https.request` so the SDK doesn't pull in
|
|
14
|
+
* an extra HTTP client. The body-as-async-iterator contract matches
|
|
15
|
+
* isomorphic-git's `GitHttpRequest` / `GitHttpResponse` types.
|
|
16
|
+
*/
|
|
17
|
+
interface GitHttpRequest {
|
|
18
|
+
url: string;
|
|
19
|
+
method?: string;
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
body?: AsyncIterable<Uint8Array> | Uint8Array | Buffer;
|
|
22
|
+
}
|
|
23
|
+
interface GitHttpResponse {
|
|
24
|
+
url: string;
|
|
25
|
+
method: string;
|
|
26
|
+
statusCode: number;
|
|
27
|
+
statusMessage: string;
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
body: AsyncIterableIterator<Uint8Array>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Send `req`, manually following 3xx redirects up to MAX_REDIRECTS. Every
|
|
33
|
+
* hop validates the next URL is https. On a 30x→non-https Location, throws.
|
|
34
|
+
*
|
|
35
|
+
* Per RFC 7231 §6.4: 301/302/303 turn POST into GET and drop the request
|
|
36
|
+
* body; 307/308 preserve the method and body. isomorphic-git's traffic is
|
|
37
|
+
* GET (info/refs) and POST (upload-pack / receive-pack). For a POST that
|
|
38
|
+
* gets redirected with a method-changing status we drop the body — git
|
|
39
|
+
* smart servers don't typically issue method-changing redirects mid-flow,
|
|
40
|
+
* but if they did, the resulting GET would surface as a server-side error
|
|
41
|
+
* rather than silently completing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function gitHttpRequest(req: GitHttpRequest): Promise<GitHttpResponse>;
|
|
44
|
+
export declare const gitHttpsOnlyClient: {
|
|
45
|
+
request: typeof gitHttpRequest;
|
|
46
|
+
};
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=git-http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-http.d.ts","sourceRoot":"","sources":["../../src/sources/git-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,UAAU,GAAG,MAAM,CAAC;CACxD;AAED,UAAU,eAAe;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,qBAAqB,CAAC,UAAU,CAAC,CAAC;CACzC;AAgGD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAkElF;AAED,eAAO,MAAM,kBAAkB;;CAA8B,CAAC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpClient for isomorphic-git that:
|
|
3
|
+
*
|
|
4
|
+
* - Talks https only — refuses to issue any request whose URL is not
|
|
5
|
+
* `https://...`.
|
|
6
|
+
* - Follows redirects manually (max 5 hops). Every `Location` is re-validated
|
|
7
|
+
* https before the next request — closes the https-to-http downgrade hole
|
|
8
|
+
* that an https endpoint replying with `Location: http://...` would
|
|
9
|
+
* otherwise open against the default `isomorphic-git/http/node` impl
|
|
10
|
+
* (which uses `simple-get`'s built-in redirect follower and does NOT
|
|
11
|
+
* enforce a scheme on the next hop).
|
|
12
|
+
*
|
|
13
|
+
* Implemented on top of Node's `https.request` so the SDK doesn't pull in
|
|
14
|
+
* an extra HTTP client. The body-as-async-iterator contract matches
|
|
15
|
+
* isomorphic-git's `GitHttpRequest` / `GitHttpResponse` types.
|
|
16
|
+
*/
|
|
17
|
+
import { request as httpsRequest } from 'node:https';
|
|
18
|
+
import { Readable } from 'node:stream';
|
|
19
|
+
const MAX_REDIRECTS = 5;
|
|
20
|
+
async function bodyToBuffer(body) {
|
|
21
|
+
if (!body)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (Buffer.isBuffer(body))
|
|
24
|
+
return body;
|
|
25
|
+
if (body instanceof Uint8Array)
|
|
26
|
+
return Buffer.from(body);
|
|
27
|
+
const chunks = [];
|
|
28
|
+
for await (const chunk of body) {
|
|
29
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
30
|
+
}
|
|
31
|
+
return Buffer.concat(chunks);
|
|
32
|
+
}
|
|
33
|
+
function flattenHeaders(raw) {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
36
|
+
if (v === undefined)
|
|
37
|
+
continue;
|
|
38
|
+
out[k] = Array.isArray(v) ? v.join(', ') : v;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function nodeStreamToAsyncIterableIterator(stream) {
|
|
43
|
+
// IncomingMessage is already AsyncIterable<Buffer>. Wrap it as an
|
|
44
|
+
// AsyncIterableIterator since that's what isomorphic-git's type
|
|
45
|
+
// demands (it has a `next()` method).
|
|
46
|
+
const iter = stream[Symbol.asyncIterator]();
|
|
47
|
+
return {
|
|
48
|
+
next: () => iter.next(),
|
|
49
|
+
return: iter.return ? (v) => iter.return(v) : undefined,
|
|
50
|
+
throw: iter.throw ? (e) => iter.throw(e) : undefined,
|
|
51
|
+
[Symbol.asyncIterator]() {
|
|
52
|
+
return this;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function singleRequest(req) {
|
|
57
|
+
if (!req.url.startsWith('https://')) {
|
|
58
|
+
throw new Error(`GitFileSource: refusing non-https request: ${req.url}`);
|
|
59
|
+
}
|
|
60
|
+
const parsed = new URL(req.url);
|
|
61
|
+
const bodyBuf = await bodyToBuffer(req.body);
|
|
62
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
63
|
+
const headers = { ...(req.headers ?? {}) };
|
|
64
|
+
if (bodyBuf) {
|
|
65
|
+
headers['content-length'] = bodyBuf.length;
|
|
66
|
+
}
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const r = httpsRequest({
|
|
69
|
+
protocol: parsed.protocol,
|
|
70
|
+
hostname: parsed.hostname,
|
|
71
|
+
port: parsed.port || undefined,
|
|
72
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
73
|
+
method,
|
|
74
|
+
headers,
|
|
75
|
+
}, (res) => {
|
|
76
|
+
resolve({
|
|
77
|
+
statusCode: res.statusCode ?? 0,
|
|
78
|
+
statusMessage: res.statusMessage ?? '',
|
|
79
|
+
headers: flattenHeaders(res.headers),
|
|
80
|
+
bodyStream: res,
|
|
81
|
+
url: req.url,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
r.on('error', reject);
|
|
85
|
+
if (bodyBuf)
|
|
86
|
+
r.write(bodyBuf);
|
|
87
|
+
r.end();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function drain(stream) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
stream.on('data', () => undefined);
|
|
93
|
+
stream.on('end', () => resolve());
|
|
94
|
+
stream.on('error', () => resolve());
|
|
95
|
+
stream.resume();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Send `req`, manually following 3xx redirects up to MAX_REDIRECTS. Every
|
|
100
|
+
* hop validates the next URL is https. On a 30x→non-https Location, throws.
|
|
101
|
+
*
|
|
102
|
+
* Per RFC 7231 §6.4: 301/302/303 turn POST into GET and drop the request
|
|
103
|
+
* body; 307/308 preserve the method and body. isomorphic-git's traffic is
|
|
104
|
+
* GET (info/refs) and POST (upload-pack / receive-pack). For a POST that
|
|
105
|
+
* gets redirected with a method-changing status we drop the body — git
|
|
106
|
+
* smart servers don't typically issue method-changing redirects mid-flow,
|
|
107
|
+
* but if they did, the resulting GET would surface as a server-side error
|
|
108
|
+
* rather than silently completing.
|
|
109
|
+
*/
|
|
110
|
+
export async function gitHttpRequest(req) {
|
|
111
|
+
let cur = req;
|
|
112
|
+
let lastBuffered;
|
|
113
|
+
for (let i = 0; i <= MAX_REDIRECTS; i++) {
|
|
114
|
+
if (cur.body && !Buffer.isBuffer(cur.body) && !(cur.body instanceof Uint8Array)) {
|
|
115
|
+
// Eagerly buffer the body so a redirect can replay it without
|
|
116
|
+
// re-consuming an already-iterated source.
|
|
117
|
+
lastBuffered = await bodyToBuffer(cur.body);
|
|
118
|
+
cur = { ...cur, body: lastBuffered };
|
|
119
|
+
}
|
|
120
|
+
const res = await singleRequest(cur);
|
|
121
|
+
const status = res.statusCode;
|
|
122
|
+
const isRedirect = status >= 300 && status < 400 && res.headers['location'];
|
|
123
|
+
if (!isRedirect) {
|
|
124
|
+
return {
|
|
125
|
+
url: res.url,
|
|
126
|
+
method: cur.method ?? 'GET',
|
|
127
|
+
statusCode: status,
|
|
128
|
+
statusMessage: res.statusMessage,
|
|
129
|
+
headers: res.headers,
|
|
130
|
+
body: nodeStreamToAsyncIterableIterator(res.bodyStream),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const locHeader = res.headers['location'];
|
|
134
|
+
if (!locHeader) {
|
|
135
|
+
return {
|
|
136
|
+
url: res.url,
|
|
137
|
+
method: cur.method ?? 'GET',
|
|
138
|
+
statusCode: status,
|
|
139
|
+
statusMessage: res.statusMessage,
|
|
140
|
+
headers: res.headers,
|
|
141
|
+
body: nodeStreamToAsyncIterableIterator(res.bodyStream),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
let nextUrl;
|
|
145
|
+
try {
|
|
146
|
+
nextUrl = new URL(locHeader, cur.url).toString();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
throw new Error(`GitFileSource: invalid redirect location: ${locHeader}`);
|
|
150
|
+
}
|
|
151
|
+
if (!nextUrl.startsWith('https://')) {
|
|
152
|
+
throw new Error(`GitFileSource: redirect to plaintext URL rejected: ${nextUrl}`);
|
|
153
|
+
}
|
|
154
|
+
// Drain the redirect's body so the socket can be reused.
|
|
155
|
+
await drain(res.bodyStream);
|
|
156
|
+
// RFC 7231 §6.4.2/6.4.3: 301/302/303 strip the body and change the
|
|
157
|
+
// method to GET. 307/308 preserve method + body.
|
|
158
|
+
let nextMethod = cur.method ?? 'GET';
|
|
159
|
+
let nextBody = lastBuffered;
|
|
160
|
+
if (status === 301 || status === 302 || status === 303) {
|
|
161
|
+
if (nextMethod !== 'GET' && nextMethod !== 'HEAD') {
|
|
162
|
+
nextMethod = 'GET';
|
|
163
|
+
nextBody = undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
cur = {
|
|
167
|
+
url: nextUrl,
|
|
168
|
+
method: nextMethod,
|
|
169
|
+
headers: { ...(cur.headers ?? {}) },
|
|
170
|
+
body: nextBody,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
throw new Error(`GitFileSource: too many redirects (>${MAX_REDIRECTS}) for ${req.url}`);
|
|
174
|
+
}
|
|
175
|
+
export const gitHttpsOnlyClient = { request: gitHttpRequest };
|
|
176
|
+
// Force Readable import to be retained (avoids unused-import elision and
|
|
177
|
+
// keeps the contract explicit for future stream-conversion changes).
|
|
178
|
+
void Readable;
|
|
179
|
+
//# sourceMappingURL=git-http.js.map
|