just-git 1.2.12 → 1.3.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.
@@ -1,19 +1,222 @@
1
- import { f as GitRepo, X as Rejection } from '../hooks-4DvkF2xT.js';
1
+ import { a3 as RawObject, a5 as Ref, f as GitRepo, X as Rejection, N as NetworkPolicy } from '../hooks-BtbyLyYE.js';
2
2
 
3
- interface GitServerConfig {
3
+ type MaybeAsync<T> = T | Promise<T>;
4
+ /** Options for {@link StorageAdapter.createRepo}. */
5
+ interface CreateRepoOptions {
6
+ /** Name of the default branch (default: `"main"`). Used for HEAD initialization. */
7
+ defaultBranch?: string;
8
+ }
9
+ /** Unresolved ref entry as stored by the storage backend. */
10
+ interface RawRefEntry {
11
+ name: string;
12
+ ref: Ref;
13
+ }
14
+ /**
15
+ * Ref operations available inside an {@link Storage.atomicRefUpdate} callback.
16
+ * The storage backend provides isolation; the shared adapter runs git-aware CAS logic inside.
17
+ */
18
+ interface RefOps {
19
+ getRef(name: string): MaybeAsync<Ref | null>;
20
+ putRef(name: string, ref: Ref): MaybeAsync<void>;
21
+ removeRef(name: string): MaybeAsync<void>;
22
+ }
23
+ /**
24
+ * Storage backend interface. Implementations provide raw key-value
25
+ * CRUD for objects and refs, plus an atomic ref operation primitive.
26
+ *
27
+ * All git-aware logic (object hashing, pack ingestion, symref resolution,
28
+ * CAS semantics) lives in the shared adapter built by {@link createStorageAdapter}.
29
+ *
30
+ * All methods may return synchronously or asynchronously.
31
+ */
32
+ interface Storage {
33
+ hasRepo(repoId: string): MaybeAsync<boolean>;
34
+ insertRepo(repoId: string): MaybeAsync<void>;
35
+ /** Delete the repo record and all associated objects and refs. */
36
+ deleteRepo(repoId: string): MaybeAsync<void>;
37
+ getObject(repoId: string, hash: string): MaybeAsync<RawObject | null>;
38
+ putObject(repoId: string, hash: string, type: string, content: Uint8Array): MaybeAsync<void>;
39
+ /** Bulk insert. Implementations should use their optimal batch strategy. */
40
+ putObjects(repoId: string, objects: ReadonlyArray<{
41
+ hash: string;
42
+ type: string;
43
+ content: Uint8Array;
44
+ }>): MaybeAsync<void>;
45
+ hasObject(repoId: string, hash: string): MaybeAsync<boolean>;
46
+ findObjectsByPrefix(repoId: string, prefix: string): MaybeAsync<string[]>;
47
+ getRef(repoId: string, name: string): MaybeAsync<Ref | null>;
48
+ putRef(repoId: string, name: string, ref: Ref): MaybeAsync<void>;
49
+ removeRef(repoId: string, name: string): MaybeAsync<void>;
50
+ /** Return all refs under a prefix, unresolved (symrefs not followed). */
51
+ listRefs(repoId: string, prefix?: string): MaybeAsync<RawRefEntry[]>;
52
+ /**
53
+ * Run ref operations atomically. The storage backend wraps the callback in
54
+ * whatever isolation mechanism it supports (transaction, lock, etc.).
55
+ * The shared adapter uses this for compare-and-swap with symref resolution.
56
+ */
57
+ atomicRefUpdate<T>(repoId: string, fn: (ops: RefOps) => MaybeAsync<T>): MaybeAsync<T>;
58
+ }
59
+
60
+ /**
61
+ * Default session type, produced by the built-in session builder when
62
+ * no custom `session` config is provided to `createServer`.
63
+ *
64
+ * HTTP requests produce `{ transport: "http", request }`.
65
+ * SSH sessions produce `{ transport: "ssh", username }`.
66
+ */
67
+ interface Session {
68
+ transport: "http" | "ssh";
69
+ /** Authenticated username, when available. */
70
+ username?: string;
71
+ /** The HTTP request, present only when `transport` is `"http"`. */
72
+ request?: Request;
73
+ }
74
+ /**
75
+ * User-provided session builder that transforms raw transport input
76
+ * into a typed session object threaded through all hooks.
77
+ *
78
+ * TypeScript infers `S` from the return types of the builder functions,
79
+ * so hooks receive the custom type without explicit generic annotations.
80
+ *
81
+ * ```ts
82
+ * const server = createServer({
83
+ * storage: new BunSqliteStorage(db),
84
+ * session: {
85
+ * http: (req) => ({
86
+ * userId: parseJwt(req).sub,
87
+ * roles: parseJwt(req).roles,
88
+ * }),
89
+ * ssh: (info) => ({
90
+ * userId: info.username ?? "anonymous",
91
+ * roles: (info.metadata?.roles as string[]) ?? [],
92
+ * }),
93
+ * },
94
+ * hooks: {
95
+ * preReceive: ({ session }) => {
96
+ * // session is { userId: string, roles: string[] } — inferred!
97
+ * if (!session?.roles.includes("push"))
98
+ * return { reject: true, message: "forbidden" };
99
+ * },
100
+ * },
101
+ * });
102
+ * ```
103
+ */
104
+ interface SessionBuilder<S> {
105
+ /**
106
+ * Build a session from an HTTP request.
107
+ *
108
+ * Return `S` to proceed, or return a `Response` to short-circuit
109
+ * the request (e.g. 401 with `WWW-Authenticate` header). This is
110
+ * the primary mechanism for HTTP auth — no separate middleware needed.
111
+ */
112
+ http: (request: Request) => S | Response | Promise<S | Response>;
113
+ /** Build a session from SSH session info. */
114
+ ssh: (info: SshSessionInfo) => S | Promise<S>;
115
+ }
116
+ /** Information about the SSH session passed to `handleSession`. */
117
+ interface SshSessionInfo {
118
+ /** SSH username from authentication. */
119
+ username?: string;
120
+ /**
121
+ * Arbitrary metadata from the SSH auth layer.
122
+ * Stash key fingerprints, client IPs, roles, etc. here —
123
+ * the session builder can extract and type them.
124
+ */
125
+ metadata?: Record<string, unknown>;
126
+ }
127
+ /**
128
+ * Bidirectional channel for SSH session I/O.
129
+ *
130
+ * Adapters create this from their SSH library's channel/stream.
131
+ * The handler reads the client request from `readable` and writes
132
+ * the server response to `writable`.
133
+ *
134
+ * For receive-pack (push), `readable` must close when the client
135
+ * finishes sending. For upload-pack (fetch/clone), the handler
136
+ * reads protocol-aware pkt-lines and does not require EOF.
137
+ */
138
+ interface SshChannel {
139
+ /** Client data (from client stdout via SSH channel). */
140
+ readonly readable: ReadableStream<Uint8Array>;
141
+ /** Server response (to client stdin via SSH channel). */
142
+ readonly writable: WritableStream<Uint8Array>;
143
+ /** Write a diagnostic/error message to the client's stderr. */
144
+ writeStderr?(data: Uint8Array): void;
145
+ }
146
+ /** Node.js `http.IncomingMessage`-compatible request interface. */
147
+ interface NodeHttpRequest {
148
+ method?: string;
149
+ url?: string;
150
+ headers: Record<string, string | string[] | undefined>;
151
+ on(event: string, listener: (...args: any[]) => void): any;
152
+ }
153
+ /** Node.js `http.ServerResponse`-compatible response interface. */
154
+ interface NodeHttpResponse {
155
+ writeHead(statusCode: number, headers?: Record<string, string | string[]>): any;
156
+ write(chunk: any): any;
157
+ end(data?: string): any;
158
+ }
159
+ /**
160
+ * Declarative push rules applied before user-provided hooks.
161
+ *
162
+ * These are git-level constraints that don't depend on the session.
163
+ * For session-dependent logic (auth, logging), use hooks directly.
164
+ */
165
+ interface ServerPolicy {
166
+ /** Branches that cannot be force-pushed to or deleted. */
167
+ protectedBranches?: string[];
168
+ /** Reject all non-fast-forward pushes globally. */
169
+ denyNonFastForward?: boolean;
170
+ /** Reject all ref deletions globally. */
171
+ denyDeletes?: boolean;
172
+ /** Reject deletion and overwrite of tags. Tags are treated as immutable. */
173
+ denyDeleteTags?: boolean;
174
+ }
175
+ interface GitServerConfig<S = Session> {
4
176
  /**
5
- * Resolve an incoming request path to a repository.
177
+ * Storage backend for git object and ref persistence.
6
178
  *
7
- * Return values:
8
- * - `GitRepo` use this repo for the request
9
- * - `null` — respond with 404
10
- * - `Response` — send this response as-is (useful for 401/403 with
11
- * custom headers like `WWW-Authenticate`)
179
+ * The server calls `createStorageAdapter(storage)` internally to build the
180
+ * git-aware adapter. Users provide the storage backend; they never see
181
+ * the `StorageAdapter` interface.
12
182
  */
13
- resolveRepo: (repoPath: string, request: Request) => GitRepo | Response | null | Promise<GitRepo | Response | null>;
183
+ storage: Storage;
184
+ /**
185
+ * Map a request path to a repo ID.
186
+ *
187
+ * Called for both HTTP and SSH requests. Return a string repo ID
188
+ * to serve, or `null` to respond with 404 / reject.
189
+ *
190
+ * Default: identity — the URL path segment is the repo ID.
191
+ */
192
+ resolve?: (path: string) => string | null | Promise<string | null>;
193
+ /**
194
+ * Automatically create repos on first access.
195
+ *
196
+ * When `true`, uses `"main"` as the default branch.
197
+ * When `{ defaultBranch }`, uses the specified branch name.
198
+ * When `false` or omitted, unknown repos return 404.
199
+ */
200
+ autoCreate?: boolean | {
201
+ defaultBranch?: string;
202
+ };
14
203
  /** Server-side hooks. All optional. */
15
- hooks?: ServerHooks;
16
- /** Base path prefix to strip from URLs (e.g. "/git"). */
204
+ hooks?: ServerHooks<S>;
205
+ /**
206
+ * Declarative push policy. Rules run before user-provided hooks.
207
+ *
208
+ * For session-dependent logic (auth, post-push actions), use `hooks`.
209
+ */
210
+ policy?: ServerPolicy;
211
+ /**
212
+ * Custom session builder. When provided, the server calls
213
+ * `session.http(request)` for HTTP and `session.ssh(info)` for SSH
214
+ * to produce the session object threaded through all hooks.
215
+ *
216
+ * When omitted, the built-in `Session` type is used.
217
+ */
218
+ session?: SessionBuilder<S>;
219
+ /** Base path prefix to strip from HTTP URLs (e.g. "/git"). */
17
220
  basePath?: string;
18
221
  /**
19
222
  * Cache generated packfiles for identical full-clone requests.
@@ -44,36 +247,116 @@ interface GitServerConfig {
44
247
  * Override to integrate with your own logging, or set to `false` to
45
248
  * suppress all error output.
46
249
  */
47
- onError?: false | ((err: unknown, request: Request) => void);
250
+ onError?: false | ((err: unknown, session?: S) => void);
48
251
  }
49
252
  interface GitServer {
50
- /** Standard fetch-API handler: (Request) => Response */
51
- fetch: (request: Request) => Promise<Response>;
253
+ /** Standard fetch-API handler for HTTP: (Request) => Response */
254
+ fetch(request: Request): Promise<Response>;
255
+ /**
256
+ * Handle a single git-over-SSH session.
257
+ *
258
+ * Call this when the SSH client execs a git command (typically
259
+ * `git-upload-pack` or `git-receive-pack`). Returns the exit code
260
+ * to send to the client.
261
+ *
262
+ * ```ts
263
+ * import { Server } from "ssh2";
264
+ *
265
+ * new Server({ hostKeys: [key] }, (client) => {
266
+ * client.on("authentication", (ctx) => { ctx.accept(); });
267
+ * client.on("session", (accept) => {
268
+ * accept().on("exec", (accept, reject, info) => {
269
+ * const stream = accept();
270
+ * const channel: SshChannel = {
271
+ * readable: new ReadableStream({
272
+ * start(c) {
273
+ * stream.on("data", (d: Buffer) => c.enqueue(new Uint8Array(d)));
274
+ * stream.on("end", () => c.close());
275
+ * },
276
+ * }),
277
+ * writable: new WritableStream({ write(chunk) { stream.write(chunk); } }),
278
+ * writeStderr(data) { stream.stderr.write(data); },
279
+ * };
280
+ * server.handleSession(info.command, channel, { username: ctx.username })
281
+ * .then((code) => { stream.exit(code); stream.close(); });
282
+ * });
283
+ * });
284
+ * });
285
+ * ```
286
+ */
287
+ handleSession(command: string, channel: SshChannel, session?: SshSessionInfo): Promise<number>;
288
+ /**
289
+ * Node.js `http.createServer` compatible handler.
290
+ *
291
+ * ```ts
292
+ * import http from "node:http";
293
+ * http.createServer(server.nodeHandler).listen(4280);
294
+ * ```
295
+ */
296
+ nodeHandler(req: NodeHttpRequest, res: NodeHttpResponse): void;
297
+ /** Create a new repo. Throws if the repo already exists. */
298
+ createRepo(id: string, options?: CreateRepoOptions): Promise<GitRepo>;
299
+ /** Get a repo by ID, or `null` if it doesn't exist. */
300
+ repo(id: string): Promise<GitRepo | null>;
301
+ /** Delete a repo and all its data. */
302
+ deleteRepo(id: string): Promise<void>;
303
+ /**
304
+ * Graceful shutdown. After calling, new HTTP requests receive 503
305
+ * and new SSH sessions get exit 128. Resolves when all in-flight
306
+ * operations complete and the pack cache is released.
307
+ *
308
+ * Pass an `AbortSignal` to set a timeout — when aborted, the
309
+ * promise resolves immediately even if operations are still running.
310
+ * Idempotent: subsequent calls return the same drain promise.
311
+ */
312
+ close(options?: {
313
+ signal?: AbortSignal;
314
+ }): Promise<void>;
315
+ /** Whether `close()` has been called. */
316
+ readonly closed: boolean;
317
+ /**
318
+ * Build a {@link NetworkPolicy} that routes HTTP requests to this
319
+ * server in-process, bypassing the network stack entirely.
320
+ *
321
+ * Pass the returned policy as `network` to {@link createGit}:
322
+ *
323
+ * ```ts
324
+ * const git = createGit({ network: server.asNetwork() });
325
+ * await git.exec("clone http://git/my-repo /work");
326
+ * ```
327
+ *
328
+ * @param baseUrl - Base URL used in clone/push/fetch commands.
329
+ * Only the hostname matters (for the `allowed` list). The URL
330
+ * never hits the network — it's resolved by the server's
331
+ * `resolve` function. Defaults to `"http://git"`.
332
+ */
333
+ asNetwork(baseUrl?: string): NetworkPolicy;
52
334
  }
53
- interface ServerHooks {
335
+ interface ServerHooks<S = Session> {
54
336
  /**
55
337
  * Called after objects are unpacked but before any refs update.
56
338
  * Receives ALL ref updates as a batch. Return a Rejection to abort
57
339
  * the entire push. Auth, branch protection, and repo-wide policy
58
340
  * belong here.
59
341
  */
60
- preReceive?: (event: PreReceiveEvent) => void | Rejection | Promise<void | Rejection>;
342
+ preReceive?: (event: PreReceiveEvent<S>) => void | Rejection | Promise<void | Rejection>;
61
343
  /**
62
344
  * Called per-ref, after preReceive passes.
63
345
  * Return a Rejection to block this specific ref update while
64
346
  * allowing others. Per-branch rules belong here.
65
347
  */
66
- update?: (event: UpdateEvent) => void | Rejection | Promise<void | Rejection>;
348
+ update?: (event: UpdateEvent<S>) => void | Rejection | Promise<void | Rejection>;
67
349
  /**
68
350
  * Called after all ref updates succeed. Cannot reject.
69
351
  * CI triggers, webhooks, notifications belong here.
70
352
  */
71
- postReceive?: (event: PostReceiveEvent) => void | Promise<void>;
353
+ postReceive?: (event: PostReceiveEvent<S>) => void | Promise<void>;
72
354
  /**
73
355
  * Called when a client wants to fetch or push (during ref advertisement).
74
- * Return a filtered ref list to hide branches, or void to advertise all.
356
+ * Return a filtered ref list to hide branches, a Rejection to deny
357
+ * access entirely, or void to advertise all refs.
75
358
  */
76
- advertiseRefs?: (event: AdvertiseRefsEvent) => RefAdvertisement[] | void | Promise<RefAdvertisement[] | void>;
359
+ advertiseRefs?: (event: AdvertiseRefsEvent<S>) => RefAdvertisement[] | void | Rejection | Promise<RefAdvertisement[] | void | Rejection>;
77
360
  }
78
361
  /** A single ref update within a push. */
79
362
  interface RefUpdate {
@@ -91,33 +374,41 @@ interface RefUpdate {
91
374
  isDelete: boolean;
92
375
  }
93
376
  /** Fired after objects are unpacked but before refs are updated. */
94
- interface PreReceiveEvent {
377
+ interface PreReceiveEvent<S = Session> {
95
378
  repo: GitRepo;
96
- repoPath: string;
379
+ /** Resolved repo ID (the value returned by `resolve`, or the raw path when `resolve` is not set). */
380
+ repoId: string;
97
381
  updates: readonly RefUpdate[];
98
- request: Request;
382
+ /** Session info. Present for HTTP and SSH; absent for in-process pushes. */
383
+ session?: S;
99
384
  }
100
385
  /** Fired per-ref after preReceive passes. */
101
- interface UpdateEvent {
386
+ interface UpdateEvent<S = Session> {
102
387
  repo: GitRepo;
103
- repoPath: string;
388
+ /** Resolved repo ID (the value returned by `resolve`, or the raw path when `resolve` is not set). */
389
+ repoId: string;
104
390
  update: RefUpdate;
105
- request: Request;
391
+ /** Session info. Present for HTTP and SSH; absent for in-process pushes. */
392
+ session?: S;
106
393
  }
107
394
  /** Fired after all ref updates succeed. */
108
- interface PostReceiveEvent {
395
+ interface PostReceiveEvent<S = Session> {
109
396
  repo: GitRepo;
110
- repoPath: string;
397
+ /** Resolved repo ID (the value returned by `resolve`, or the raw path when `resolve` is not set). */
398
+ repoId: string;
111
399
  updates: readonly RefUpdate[];
112
- request: Request;
400
+ /** Session info. Present for HTTP and SSH; absent for in-process pushes. */
401
+ session?: S;
113
402
  }
114
403
  /** Fired during ref advertisement (info/refs). */
115
- interface AdvertiseRefsEvent {
404
+ interface AdvertiseRefsEvent<S = Session> {
116
405
  repo: GitRepo;
117
- repoPath: string;
406
+ /** Resolved repo ID (the value returned by `resolve`, or the raw path when `resolve` is not set). */
407
+ repoId: string;
118
408
  refs: RefAdvertisement[];
119
409
  service: "git-upload-pack" | "git-receive-pack";
120
- request: Request;
410
+ /** Session info. Present for HTTP and SSH; absent for in-process requests. */
411
+ session?: S;
121
412
  }
122
413
  /** A ref name and hash advertised to clients during fetch/push discovery. */
123
414
  interface RefAdvertisement {
@@ -126,47 +417,43 @@ interface RefAdvertisement {
126
417
  }
127
418
 
128
419
  /**
129
- * Framework-agnostic Git Smart HTTP request handler.
420
+ * Unified Git server: Smart HTTP + SSH session handling.
130
421
  *
131
- * Uses web-standard Request/Response, works with Bun.serve, Hono,
132
- * Cloudflare Workers, or any framework that speaks fetch API.
133
- */
134
-
135
- /**
136
- * Create a Git Smart HTTP server handler.
422
+ * Uses web-standard Request/Response for HTTP, and web-standard
423
+ * ReadableStream/WritableStream for SSH. Works with Bun.serve, Hono,
424
+ * Cloudflare Workers, or any framework that speaks fetch API. SSH
425
+ * works with any SSH library (ssh2, etc.) through a thin adapter.
137
426
  *
138
427
  * ```ts
139
- * const server = createGitServer({
140
- * resolveRepo: async (repoPath, request) => storage.repo(repoPath),
428
+ * const server = createServer({
429
+ * storage: new MemoryStorage(),
430
+ * autoCreate: true,
141
431
  * });
432
+ * await server.createRepo("my-repo");
433
+ *
434
+ * // HTTP
142
435
  * Bun.serve({ fetch: server.fetch });
143
436
  * ```
144
437
  */
145
- declare function createGitServer(config: GitServerConfig): GitServer;
146
- interface NodeHttpRequest {
147
- method?: string;
148
- url?: string;
149
- headers: Record<string, string | string[] | undefined>;
150
- on(event: string, listener: (...args: any[]) => void): any;
151
- }
152
- interface NodeHttpResponse {
153
- writeHead(statusCode: number, headers?: Record<string, string | string[]>): any;
154
- write(chunk: any): any;
155
- end(data?: string): any;
156
- }
438
+
157
439
  /**
158
- * Adapt a `GitServer` to Node.js's `http.createServer` callback.
159
- *
160
- * Converts between Node's `IncomingMessage`/`ServerResponse` and the
161
- * web-standard `Request`/`Response` used by the server handler.
440
+ * Create a unified Git server that handles both HTTP and SSH.
162
441
  *
163
442
  * ```ts
164
- * import http from "node:http";
165
- * const httpServer = http.createServer(toNodeHandler(server));
166
- * httpServer.listen(4280);
443
+ * const server = createServer({
444
+ * storage: new MemoryStorage(),
445
+ * autoCreate: true,
446
+ * });
447
+ * await server.createRepo("my-repo");
448
+ *
449
+ * // HTTP — pass to Bun.serve, Hono, Cloudflare Workers, etc.
450
+ * Bun.serve({ fetch: server.fetch });
451
+ *
452
+ * // SSH — wire up with ssh2 or any SSH library
453
+ * server.handleSession(command, channel, { username });
167
454
  * ```
168
455
  */
169
- declare function toNodeHandler(server: GitServer): (req: NodeHttpRequest, res: NodeHttpResponse) => void;
456
+ declare function createServer<S = Session>(config: GitServerConfig<S>): GitServer;
170
457
  /**
171
458
  * Compose multiple hook sets into a single `ServerHooks` object.
172
459
  *
@@ -175,94 +462,241 @@ declare function toNodeHandler(server: GitServer): (req: NodeHttpRequest, res: N
175
462
  * - **Post-hooks** (`postReceive`): run all in order. Each is individually
176
463
  * try/caught so one failure doesn't prevent the rest from running.
177
464
  * - **Filter hooks** (`advertiseRefs`): chain — each hook receives the
178
- * refs returned by the previous one. Returning void passes through
179
- * unchanged.
465
+ * refs returned by the previous one. Short-circuits on `Rejection`.
466
+ * Returning void passes through unchanged.
180
467
  */
181
- declare function composeHooks(...hookSets: (ServerHooks | undefined)[]): ServerHooks;
468
+ declare function composeHooks<S = Session>(...hookSets: (ServerHooks<S> | undefined)[]): ServerHooks<S>;
182
469
 
183
470
  /**
184
- * Opinionated hook presets built on top of the minimal ServerHooks interface.
471
+ * Server-side Git protocol helpers.
472
+ *
473
+ * Transport-agnostic ref advertisement, upload-pack response building,
474
+ * and receive-pack request/response parsing. The HTTP-specific service
475
+ * header wrapping is layered on top of the shared ref list builder.
185
476
  */
186
477
 
187
- type ResolveRepo = GitServerConfig["resolveRepo"];
478
+ interface AdvertisedRef {
479
+ name: string;
480
+ hash: string;
481
+ }
188
482
  /**
189
- * Wrap a `resolveRepo` function with an authorization check that
190
- * gates **all** access (clone, fetch, and push).
483
+ * Build the pkt-line ref list with capabilities. Transport-agnostic
484
+ * used directly by SSH/in-process transports and wrapped by
485
+ * `buildRefAdvertisement` for HTTP.
191
486
  *
192
- * The `authorize` callback receives the raw `Request` and returns:
193
- * - `true` request is allowed, delegate to the inner `resolveRepo`
194
- * - `false` respond with 403 Forbidden
195
- * - `Response` — send as-is (e.g. 401 with `WWW-Authenticate` header)
487
+ * Format:
488
+ * pkt-line("<hash> <refname>\0<capabilities>\n") // first ref
489
+ * pkt-line("<hash> <refname>\n") // subsequent refs
490
+ * flush
491
+ */
492
+ declare function buildRefListPktLines(refs: AdvertisedRef[], capabilities: string[], headTarget?: string): Uint8Array;
493
+ interface PushCommand {
494
+ oldHash: string;
495
+ newHash: string;
496
+ refName: string;
497
+ }
498
+
499
+ /**
500
+ * High-level server operations.
196
501
  *
197
- * For push-only authorization, use `createStandardHooks({ authorizePush })`.
198
- * The two compose naturally:
502
+ * Transport-agnostic: each operation accepts a `GitRepo` and returns
503
+ * structured results. `applyReceivePack` encapsulates the full push
504
+ * lifecycle (hooks + ref application) for use by any transport adapter.
505
+ */
506
+
507
+ interface PackCacheEntry {
508
+ packData: Uint8Array;
509
+ objectCount: number;
510
+ deltaCount: number;
511
+ }
512
+ /**
513
+ * Bounded LRU-ish cache for generated packfiles.
199
514
  *
200
- * ```ts
201
- * const server = createGitServer({
202
- * resolveRepo: withAuth(
203
- * (req) => req.headers.get("Authorization") === `Bearer ${token}`,
204
- * (repoPath) => storage.repo(repoPath),
205
- * ),
206
- * hooks: createStandardHooks({ protectedBranches: ["main"] }),
207
- * });
208
- * ```
515
+ * Keyed on `(repoId, sorted wants)` — only caches full clones
516
+ * (requests with no `have` lines). Incremental fetches always
517
+ * compute fresh packs.
518
+ *
519
+ * Entries are automatically invalidated when refs change: since the
520
+ * cache key includes the exact want hashes, a ref update changes
521
+ * the want set on the next client request, producing a cache miss.
209
522
  */
210
- declare function withAuth(authorize: (request: Request) => boolean | Response | Promise<boolean | Response>, resolveRepo: ResolveRepo): ResolveRepo;
211
- interface StandardHooksConfig {
212
- /** Branches that cannot be force-pushed to or deleted. */
213
- protectedBranches?: string[];
214
- /** Reject all non-fast-forward pushes globally. */
215
- denyNonFastForward?: boolean;
216
- /** Reject all ref deletions globally. */
217
- denyDeletes?: boolean;
218
- /** Reject deletion and overwrite of tags. Tags are treated as immutable. */
219
- denyDeleteTags?: boolean;
220
- /** Return false to reject the entire push (e.g. check Authorization header). */
221
- authorizePush?: (request: Request) => boolean | Promise<boolean>;
222
- /** Called after refs are updated. */
223
- onPush?: (event: PostReceiveEvent) => void | Promise<void>;
523
+ declare class PackCache {
524
+ private entries;
525
+ private currentBytes;
526
+ private maxBytes;
527
+ private hits;
528
+ private misses;
529
+ constructor(maxBytes?: number);
530
+ /** Build a cache key. Returns null for requests with haves (not cacheable). */
531
+ static key(repoId: string, wants: string[], haves: string[]): string | null;
532
+ get(key: string): PackCacheEntry | undefined;
533
+ set(key: string, entry: PackCacheEntry): void;
534
+ clear(): void;
535
+ get stats(): {
536
+ entries: number;
537
+ bytes: number;
538
+ hits: number;
539
+ misses: number;
540
+ };
541
+ }
542
+ interface RefsData {
543
+ refs: RefAdvertisement[];
544
+ headTarget?: string;
224
545
  }
225
546
  /**
226
- * Build a standard set of server hooks from a simple config.
547
+ * Collect the structured ref list from a repo (no wire encoding).
548
+ * The handler can pass this through an advertiseRefs hook to filter,
549
+ * then call `buildRefAdvertisementBytes` to produce the wire format.
550
+ */
551
+ declare function collectRefs(repo: GitRepo): Promise<RefsData>;
552
+ /**
553
+ * Build the HTTP-wrapped ref advertisement (includes `# service=...` header).
554
+ */
555
+ declare function buildRefAdvertisementBytes(refs: RefAdvertisement[], service: "git-upload-pack" | "git-receive-pack", headTarget?: string): Uint8Array;
556
+ /**
557
+ * Build the transport-agnostic ref list (no HTTP service header).
558
+ * Used by SSH and in-process transports.
559
+ */
560
+ declare function buildRefListBytes(refs: RefAdvertisement[], service: "git-upload-pack" | "git-receive-pack", headTarget?: string): Uint8Array;
561
+ interface AdvertiseResult {
562
+ refs: RefAdvertisement[];
563
+ headTarget?: string;
564
+ }
565
+ /**
566
+ * Collect refs and run the `advertiseRefs` hook. Returns either the
567
+ * (possibly filtered) ref list, or a `Rejection` if the hook denied access.
227
568
  *
228
- * Covers the most common policies (branch protection, fast-forward
229
- * enforcement, authorization, post-push callbacks) so users don't
230
- * have to wire hooks manually for typical setups.
569
+ * Both HTTP and SSH code paths use this — the caller handles the
570
+ * transport-specific response (HTTP 403 vs SSH exit 128).
231
571
  */
232
- declare function createStandardHooks(config: StandardHooksConfig): ServerHooks;
233
-
572
+ declare function advertiseRefsWithHooks<S>(repo: GitRepo, repoId: string, service: "git-upload-pack" | "git-receive-pack", hooks?: ServerHooks<S>, session?: S): Promise<AdvertiseResult | Rejection>;
573
+ interface UploadPackOptions {
574
+ /** Pack cache instance. When provided, full clones (no haves) are cached. */
575
+ cache?: PackCache;
576
+ /** Repo path used as part of the cache key. Required when cache is set. */
577
+ cacheKey?: string;
578
+ /** Skip delta compression — faster pack generation, larger output. */
579
+ noDelta?: boolean;
580
+ /** Delta window size (default 10). Ignored when noDelta is true. */
581
+ deltaWindow?: number;
582
+ }
234
583
  /**
235
- * Abstract storage backend for multi-repo git object and ref storage.
584
+ * Handle a `POST /git-upload-pack` request.
236
585
  *
237
- * Implemented by `BunSqliteStorage`, `BetterSqlite3Storage`, and `PgStorage`.
586
+ * Returns `Uint8Array` for buffered responses (cache hits, deltified packs)
587
+ * or `ReadableStream<Uint8Array>` for streaming no-delta responses.
238
588
  */
239
- interface Storage {
240
- /** Get a `GitRepo` scoped to a specific repo. */
241
- repo(repoId: string): GitRepo;
242
- /** Delete all objects and refs for a repo. */
243
- deleteRepo(repoId: string): Promise<void>;
589
+ declare function handleUploadPack(repo: GitRepo, requestBody: Uint8Array, options?: UploadPackOptions): Promise<Uint8Array | ReadableStream<Uint8Array>>;
590
+ interface ReceivePackResult {
591
+ updates: RefUpdate[];
592
+ unpackOk: boolean;
593
+ capabilities: string[];
594
+ /** Whether the request body contained a valid pkt-line flush packet. */
595
+ sawFlush: boolean;
244
596
  }
597
+ /**
598
+ * Ingest a receive-pack request: parse commands, ingest the packfile,
599
+ * and compute enriched RefUpdate objects. Does NOT apply ref updates —
600
+ * call `applyReceivePack` to run hooks and apply refs.
601
+ */
602
+ declare function ingestReceivePack(repo: GitRepo, requestBody: Uint8Array): Promise<ReceivePackResult>;
603
+ /**
604
+ * Streaming variant of `ingestReceivePack`. Accepts pre-parsed push
605
+ * commands and a raw pack byte stream. Uses `readPackStreaming` →
606
+ * `ingestPackStream` so pack bytes are consumed incrementally without
607
+ * buffering the entire pack in memory.
608
+ *
609
+ * The HTTP handler continues using `ingestReceivePack` (runtime buffers
610
+ * POST bodies anyway). The SSH handler calls this directly after parsing
611
+ * pkt-line commands.
612
+ */
613
+ declare function ingestReceivePackFromStream(repo: GitRepo, commands: PushCommand[], capabilities: string[], packStream: AsyncIterable<Uint8Array>, sawFlush?: boolean): Promise<ReceivePackResult>;
614
+ interface ApplyReceivePackOptions<S = unknown> {
615
+ repo: GitRepo;
616
+ repoId: string;
617
+ ingestResult: ReceivePackResult;
618
+ hooks?: ServerHooks<S>;
619
+ /** Session info threaded through to hooks. */
620
+ session?: S;
621
+ }
622
+ interface RefResult {
623
+ ref: string;
624
+ ok: boolean;
625
+ error?: string;
626
+ }
627
+ interface ApplyReceivePackResult {
628
+ refResults: RefResult[];
629
+ applied: RefUpdate[];
630
+ }
631
+ /**
632
+ * Run the full receive-pack lifecycle: preReceive hook, per-ref update
633
+ * hook with ref format validation, CAS ref application, and postReceive
634
+ * hook. Transport-agnostic — works for HTTP, SSH, or in-process pushes.
635
+ *
636
+ * Returns per-ref results and the list of successfully applied updates.
637
+ * Does NOT handle unpack failures — the caller should check
638
+ * `ingestResult.unpackOk` and short-circuit before calling this.
639
+ */
640
+ declare function applyReceivePack<S = unknown>(options: ApplyReceivePackOptions<S>): Promise<ApplyReceivePackResult>;
641
+
642
+ /**
643
+ * SSH protocol helpers for the unified Git server.
644
+ *
645
+ * Provides the pkt-line stream reader, command parser, and
646
+ * receive-pack streaming logic used by `createServer`'s
647
+ * `handleSession` method. Not a public entry point — see
648
+ * `createServer` for usage.
649
+ */
650
+
651
+ type GitSshService = "git-upload-pack" | "git-receive-pack";
652
+ /**
653
+ * Parse a git SSH exec command into service and repo path.
654
+ *
655
+ * Handles `git-upload-pack '/path'`, `git upload-pack '/path'`,
656
+ * and unquoted variants.
657
+ */
658
+ declare function parseGitSshCommand(command: string): {
659
+ service: GitSshService;
660
+ repoPath: string;
661
+ } | null;
245
662
 
246
663
  /**
247
- * In-memory git storage with multi-repo support.
664
+ * In-memory storage backend with multi-repo support.
248
665
  *
249
666
  * Useful for tests, ephemeral servers, and benchmarking.
250
667
  * Data is lost when the process exits.
251
668
  *
252
669
  * ```ts
253
- * const storage = new MemoryStorage();
254
- * const server = createGitServer({
255
- * resolveRepo: async (repoPath) => storage.repo(repoPath),
670
+ * const server = createServer({
671
+ * storage: new MemoryStorage(),
256
672
  * });
673
+ * await server.createRepo("my-repo");
257
674
  * ```
258
675
  */
259
676
  declare class MemoryStorage implements Storage {
677
+ private repos;
260
678
  private objects;
261
679
  private refs;
262
- repo(repoId: string): GitRepo;
263
- deleteRepo(repoId: string): Promise<void>;
264
- private getObjects;
265
- private getRefs;
680
+ hasRepo(repoId: string): boolean;
681
+ insertRepo(repoId: string): void;
682
+ deleteRepo(repoId: string): void;
683
+ getObject(repoId: string, hash: string): RawObject | null;
684
+ putObject(repoId: string, hash: string, type: string, content: Uint8Array): void;
685
+ putObjects(repoId: string, objects: ReadonlyArray<{
686
+ hash: string;
687
+ type: string;
688
+ content: Uint8Array;
689
+ }>): void;
690
+ hasObject(repoId: string, hash: string): boolean;
691
+ findObjectsByPrefix(repoId: string, prefix: string): string[];
692
+ getRef(repoId: string, name: string): Ref | null;
693
+ putRef(repoId: string, name: string, ref: Ref): void;
694
+ removeRef(repoId: string, name: string): void;
695
+ listRefs(repoId: string, prefix?: string): RawRefEntry[];
696
+ atomicRefUpdate<T>(repoId: string, fn: (ops: RefOps) => T): T;
697
+ repoIds(): string[];
698
+ private getObjMap;
699
+ private getRefMap;
266
700
  }
267
701
 
268
702
  /** Minimal prepared statement interface matching `bun:sqlite`. */
@@ -278,20 +712,35 @@ interface BunSqliteDatabase {
278
712
  transaction<F extends (...args: any[]) => any>(fn: F): (...args: Parameters<F>) => ReturnType<F>;
279
713
  }
280
714
  /**
281
- * SQLite-backed git storage using `bun:sqlite`.
715
+ * SQLite-backed storage using `bun:sqlite`.
282
716
  *
283
717
  * ```ts
284
718
  * import { Database } from "bun:sqlite";
285
- * const storage = new BunSqliteStorage(new Database("repos.db"));
719
+ * const storage = createStorageAdapter(new BunSqliteStorage(new Database("repos.db")));
286
720
  * ```
287
721
  */
288
722
  declare class BunSqliteStorage implements Storage {
289
723
  private db;
290
724
  private stmts;
291
- private ingestTx;
725
+ private batchInsertTx;
292
726
  constructor(db: BunSqliteDatabase);
293
- repo(repoId: string): GitRepo;
294
- deleteRepo(repoId: string): Promise<void>;
727
+ hasRepo(repoId: string): boolean;
728
+ insertRepo(repoId: string): void;
729
+ deleteRepo(repoId: string): void;
730
+ getObject(repoId: string, hash: string): RawObject | null;
731
+ putObject(repoId: string, hash: string, type: string, content: Uint8Array): void;
732
+ putObjects(repoId: string, objects: ReadonlyArray<{
733
+ hash: string;
734
+ type: string;
735
+ content: Uint8Array;
736
+ }>): void;
737
+ hasObject(repoId: string, hash: string): boolean;
738
+ findObjectsByPrefix(repoId: string, prefix: string): string[];
739
+ getRef(repoId: string, name: string): Ref | null;
740
+ putRef(repoId: string, name: string, ref: Ref): void;
741
+ removeRef(repoId: string, name: string): void;
742
+ listRefs(repoId: string, prefix?: string): RawRefEntry[];
743
+ atomicRefUpdate<T>(repoId: string, fn: (ops: RefOps) => T): T;
295
744
  }
296
745
 
297
746
  /** Minimal prepared statement interface matching `better-sqlite3`. */
@@ -307,20 +756,35 @@ interface BetterSqlite3Database {
307
756
  transaction<F extends (...args: any[]) => any>(fn: F): (...args: Parameters<F>) => ReturnType<F>;
308
757
  }
309
758
  /**
310
- * SQLite-backed git storage using `better-sqlite3`.
759
+ * SQLite-backed storage using `better-sqlite3`.
311
760
  *
312
761
  * ```ts
313
762
  * import Database from "better-sqlite3";
314
- * const storage = new BetterSqlite3Storage(new Database("repos.db"));
763
+ * const storage = createStorageAdapter(new BetterSqlite3Storage(new Database("repos.db")));
315
764
  * ```
316
765
  */
317
766
  declare class BetterSqlite3Storage implements Storage {
318
767
  private db;
319
768
  private stmts;
320
- private ingestTx;
769
+ private batchInsertTx;
321
770
  constructor(db: BetterSqlite3Database);
322
- repo(repoId: string): GitRepo;
323
- deleteRepo(repoId: string): Promise<void>;
771
+ hasRepo(repoId: string): boolean;
772
+ insertRepo(repoId: string): void;
773
+ deleteRepo(repoId: string): void;
774
+ getObject(repoId: string, hash: string): RawObject | null;
775
+ putObject(repoId: string, hash: string, type: string, content: Uint8Array): void;
776
+ putObjects(repoId: string, objects: ReadonlyArray<{
777
+ hash: string;
778
+ type: string;
779
+ content: Uint8Array;
780
+ }>): void;
781
+ hasObject(repoId: string, hash: string): boolean;
782
+ findObjectsByPrefix(repoId: string, prefix: string): string[];
783
+ getRef(repoId: string, name: string): Ref | null;
784
+ putRef(repoId: string, name: string, ref: Ref): void;
785
+ removeRef(repoId: string, name: string): void;
786
+ listRefs(repoId: string, prefix?: string): RawRefEntry[];
787
+ atomicRefUpdate<T>(repoId: string, fn: (ops: RefOps) => T): T;
324
788
  }
325
789
 
326
790
  /** Minimal database interface for PostgreSQL. Use {@link wrapPgPool} to adapt a `pg` Pool. */
@@ -357,10 +821,7 @@ interface PgPoolClient {
357
821
  */
358
822
  declare function wrapPgPool(pool: PgPool): PgDatabase;
359
823
  /**
360
- * PostgreSQL-backed git storage with multi-repo support.
361
- *
362
- * Creates and manages `git_objects` and `git_refs` tables in the
363
- * provided database. Multiple repos are partitioned by `repo_id`.
824
+ * PostgreSQL-backed storage.
364
825
  *
365
826
  * Use the static `create` factory (schema setup is async):
366
827
  *
@@ -368,19 +829,29 @@ declare function wrapPgPool(pool: PgPool): PgDatabase;
368
829
  * import { Pool } from "pg";
369
830
  * const pool = new Pool({ connectionString: "..." });
370
831
  * const storage = await PgStorage.create(wrapPgPool(pool));
371
- * const server = createGitServer({
372
- * resolveRepo: async (repoPath) => storage.repo(repoPath),
373
- * });
374
832
  * ```
375
833
  */
376
834
  declare class PgStorage implements Storage {
377
835
  private db;
378
836
  private constructor();
379
837
  static create(db: PgDatabase): Promise<PgStorage>;
380
- /** Get a `GitRepo` scoped to a specific repo. */
381
- repo(repoId: string): GitRepo;
382
- /** Delete all objects and refs for a repo. */
838
+ hasRepo(repoId: string): Promise<boolean>;
839
+ insertRepo(repoId: string): Promise<void>;
383
840
  deleteRepo(repoId: string): Promise<void>;
841
+ getObject(repoId: string, hash: string): Promise<RawObject | null>;
842
+ putObject(repoId: string, hash: string, type: string, content: Uint8Array): Promise<void>;
843
+ putObjects(repoId: string, objects: ReadonlyArray<{
844
+ hash: string;
845
+ type: string;
846
+ content: Uint8Array;
847
+ }>): Promise<void>;
848
+ hasObject(repoId: string, hash: string): Promise<boolean>;
849
+ findObjectsByPrefix(repoId: string, prefix: string): Promise<string[]>;
850
+ getRef(repoId: string, name: string): Promise<Ref | null>;
851
+ putRef(repoId: string, name: string, ref: Ref): Promise<void>;
852
+ removeRef(repoId: string, name: string): Promise<void>;
853
+ listRefs(repoId: string, prefix?: string): Promise<RawRefEntry[]>;
854
+ atomicRefUpdate<T>(repoId: string, fn: (ops: RefOps) => Promise<T> | T): Promise<T>;
384
855
  }
385
856
 
386
- export { type AdvertiseRefsEvent, type BetterSqlite3Database, type BetterSqlite3Statement, BetterSqlite3Storage, type BunSqliteDatabase, type BunSqliteStatement, BunSqliteStorage, type GitServer, type GitServerConfig, MemoryStorage, type PgDatabase, type PgPool, type PgPoolClient, PgStorage, type PostReceiveEvent, type PreReceiveEvent, type RefAdvertisement, type RefUpdate, Rejection, type ServerHooks, type StandardHooksConfig, type Storage, type UpdateEvent, composeHooks, createGitServer, createStandardHooks, toNodeHandler, withAuth, wrapPgPool };
857
+ export { type AdvertiseRefsEvent, type AdvertiseResult, type ApplyReceivePackOptions, type ApplyReceivePackResult, type BetterSqlite3Database, type BetterSqlite3Statement, BetterSqlite3Storage, type BunSqliteDatabase, type BunSqliteStatement, BunSqliteStorage, type CreateRepoOptions, type GitServer, type GitServerConfig, type MaybeAsync, MemoryStorage, type NodeHttpRequest, type NodeHttpResponse, type PgDatabase, type PgPool, type PgPoolClient, PgStorage, type PostReceiveEvent, type PreReceiveEvent, type RawRefEntry, type ReceivePackResult, type RefAdvertisement, type RefOps, type RefResult, type RefUpdate, Rejection, type ServerHooks, type ServerPolicy, type Session, type SessionBuilder, type SshChannel, type SshSessionInfo, type Storage, type UpdateEvent, advertiseRefsWithHooks, applyReceivePack, buildRefAdvertisementBytes, buildRefListBytes, buildRefListPktLines, collectRefs, composeHooks, createServer, handleUploadPack, ingestReceivePack, ingestReceivePackFromStream, parseGitSshCommand, wrapPgPool };