@capybara-hq/kernel 0.1.0-rc.27 → 0.1.0-rc.28

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.
Files changed (88) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/catalog/catalog-content.d.ts +15 -3
  3. package/dist/catalog/catalog-content.js +5 -3
  4. package/dist/catalog/catalog-scope.service.js +3 -2
  5. package/dist/catalog/marketplaces.service.d.ts +1 -1
  6. package/dist/catalog/org-catalog.service.js +10 -3
  7. package/dist/catalog/plugin-install.d.ts +34 -18
  8. package/dist/catalog/plugin-install.js +84 -46
  9. package/dist/catalog/plugin-upgrade.js +1 -0
  10. package/dist/channels/actions.d.ts +4 -1
  11. package/dist/channels/actions.js +5 -2
  12. package/dist/channels/cards.d.ts +7 -1
  13. package/dist/channels/cards.js +28 -1
  14. package/dist/channels/phrases.d.ts +15 -0
  15. package/dist/channels/phrases.js +9 -0
  16. package/dist/config/schema.d.ts +8 -0
  17. package/dist/config/schema.js +69 -0
  18. package/dist/conversations/events.d.ts +14 -0
  19. package/dist/conversations/events.js +20 -0
  20. package/dist/db/kernel-schema.d.ts +19 -4
  21. package/dist/errors.d.ts +5 -0
  22. package/dist/errors.js +5 -0
  23. package/dist/git/gateway.d.ts +93 -0
  24. package/dist/git/gateway.js +750 -0
  25. package/dist/git/gateway.route.d.ts +16 -0
  26. package/dist/git/gateway.route.js +41 -0
  27. package/dist/git/git.module.d.ts +10 -0
  28. package/dist/git/git.module.js +31 -0
  29. package/dist/git/identity.d.ts +80 -0
  30. package/dist/git/identity.js +209 -0
  31. package/dist/git/pkt-line.d.ts +53 -0
  32. package/dist/git/pkt-line.js +146 -0
  33. package/dist/git/run-end.d.ts +29 -0
  34. package/dist/git/run-end.js +121 -0
  35. package/dist/git/sources.d.ts +42 -0
  36. package/dist/git/sources.js +134 -0
  37. package/dist/git/tickets.d.ts +69 -0
  38. package/dist/git/tickets.js +194 -0
  39. package/dist/http/api.module.js +3 -0
  40. package/dist/http/app.d.ts +6 -0
  41. package/dist/http/app.js +3 -0
  42. package/dist/infra/config/kernel-config.service.js +2 -0
  43. package/dist/orgs/settings.service.d.ts +21 -0
  44. package/dist/orgs/settings.service.js +18 -0
  45. package/dist/overrides.d.ts +6 -0
  46. package/dist/overrides.js +4 -0
  47. package/dist/platform/exclusions.js +3 -0
  48. package/dist/runtime/run-finish.d.ts +5 -0
  49. package/dist/runtime/run-finish.js +12 -3
  50. package/dist/runtime/runtime.module.js +2 -0
  51. package/dist/runtime/waits.js +7 -5
  52. package/dist/schedules/fire.d.ts +3 -3
  53. package/dist/schedules/fire.js +7 -4
  54. package/dist/schedules/grant-rules.d.ts +40 -0
  55. package/dist/schedules/grant-rules.js +83 -0
  56. package/dist/schedules/scheduled-tasks.service.d.ts +9 -7
  57. package/dist/schedules/scheduled-tasks.service.js +50 -15
  58. package/dist/schedules/types.d.ts +2 -0
  59. package/dist/tools/approvals.service.d.ts +100 -19
  60. package/dist/tools/approvals.service.js +288 -21
  61. package/dist/tools/builtin-names.d.ts +4 -2
  62. package/dist/tools/builtin-names.js +6 -1
  63. package/dist/tools/builtins/schedules.d.ts +11 -9
  64. package/dist/tools/builtins/schedules.js +202 -44
  65. package/dist/tools/connector-account-policies.d.ts +8 -0
  66. package/dist/tools/connector-account-policies.js +8 -0
  67. package/dist/tools/connectors.controller.d.ts +2 -0
  68. package/dist/tools/connectors.controller.js +5 -1
  69. package/dist/tools/connectors.service.d.ts +46 -3
  70. package/dist/tools/connectors.service.js +98 -5
  71. package/dist/tools/credentials.d.ts +18 -0
  72. package/dist/tools/credentials.js +60 -0
  73. package/dist/tools/credentials.module.d.ts +7 -0
  74. package/dist/tools/credentials.module.js +22 -0
  75. package/dist/tools/git-asks.d.ts +113 -0
  76. package/dist/tools/git-asks.js +174 -0
  77. package/dist/tools/git-push-policy.d.ts +89 -0
  78. package/dist/tools/git-push-policy.js +175 -0
  79. package/dist/tools/info.d.ts +3 -3
  80. package/dist/tools/info.js +3 -3
  81. package/dist/tools/network-asks.d.ts +5 -0
  82. package/dist/tools/network-asks.js +9 -2
  83. package/dist/tools/tools.module.js +4 -3
  84. package/package.json +3 -3
  85. package/src/migrations/0126_git_gateway.sql +10 -0
  86. package/src/migrations/0127_git_push_policy.sql +12 -0
  87. package/src/migrations/0128_scheduled_task_plugin_disabled.sql +10 -0
  88. package/src/migrations/0129_connector_plugin_disabled.sql +5 -0
@@ -0,0 +1,16 @@
1
+ import type { Logger } from "@capybara-hq/plugin-sdk";
2
+ import type { FastifyInstance } from "fastify";
3
+ import type { GitGateway } from "./gateway.js";
4
+ /** The Fastify route template of the git gateway (`GIT_GATEWAY_PREFIX` + `*`). */
5
+ export declare const GIT_GATEWAY_ROUTE = "/git/v1/*";
6
+ /**
7
+ * Registers the git gateway (`GET` / `POST /git/v1/*`, sandbox git credentials spec §5) as a raw
8
+ * Fastify route in its own encapsulated scope, like the channel webhook (P-4): outside Nest, so
9
+ * neither the global `AuthGuard` nor the CSRF check applies (git sends neither; the run ticket is
10
+ * checked by {@link GitGateway}). It is not an API route and not a Platform operation (git
11
+ * smart-HTTP for sandboxes, authenticated by run tickets). Inside this scope only, the two git
12
+ * request types are handed to the handler as the raw stream (no buffering, no body limit: the
13
+ * gateway counts bytes against `tools.git.maxBytesPerRun`); any other type is refused by Fastify.
14
+ * HEAD and every other method answer 404. Registered by the api role; call before `app.init()`.
15
+ */
16
+ export declare function registerGitGatewayRoute(fastify: FastifyInstance, gateway: Pick<GitGateway, "handle">, log: Pick<Logger, "error">): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import { redactPath } from "../http/error.filter.js";
2
+ /** The Fastify route template of the git gateway (`GIT_GATEWAY_PREFIX` + `*`). */
3
+ export const GIT_GATEWAY_ROUTE = "/git/v1/*";
4
+ /** The request content types of git smart-HTTP POSTs. */
5
+ const GIT_REQUEST_TYPES = [
6
+ "application/x-git-upload-pack-request",
7
+ "application/x-git-receive-pack-request",
8
+ ];
9
+ /**
10
+ * Registers the git gateway (`GET` / `POST /git/v1/*`, sandbox git credentials spec §5) as a raw
11
+ * Fastify route in its own encapsulated scope, like the channel webhook (P-4): outside Nest, so
12
+ * neither the global `AuthGuard` nor the CSRF check applies (git sends neither; the run ticket is
13
+ * checked by {@link GitGateway}). It is not an API route and not a Platform operation (git
14
+ * smart-HTTP for sandboxes, authenticated by run tickets). Inside this scope only, the two git
15
+ * request types are handed to the handler as the raw stream (no buffering, no body limit: the
16
+ * gateway counts bytes against `tools.git.maxBytesPerRun`); any other type is refused by Fastify.
17
+ * HEAD and every other method answer 404. Registered by the api role; call before `app.init()`.
18
+ */
19
+ export async function registerGitGatewayRoute(fastify, gateway, log) {
20
+ await fastify.register(async (scope) => {
21
+ scope.removeAllContentTypeParsers();
22
+ scope.addContentTypeParser(GIT_REQUEST_TYPES, (_req, payload, done) => done(null, payload));
23
+ scope.setErrorHandler((err, req, reply) => {
24
+ const status = typeof err.statusCode === "number" ? err.statusCode : 500;
25
+ if (status >= 500)
26
+ log.error("git gateway request failed", { path: redactPath(req.url), err });
27
+ return reply
28
+ .code(status >= 400 && status < 500 ? 404 : 500)
29
+ .header("content-type", "text/plain; charset=utf-8")
30
+ .send(status >= 400 && status < 500
31
+ ? "Capybara: not a git smart-HTTP endpoint.\n"
32
+ : "Capybara: internal error.\n");
33
+ });
34
+ scope.route({
35
+ method: ["GET", "POST"],
36
+ url: GIT_GATEWAY_ROUTE,
37
+ exposeHeadRoute: false,
38
+ handler: (req, reply) => gateway.handle(req, reply),
39
+ });
40
+ });
41
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The git gateway (sandbox git credentials spec §5, §6): run tickets ({@link GitTickets}), whose
3
+ * connector serves a git host for an actor ({@link GitSources}) and the commit identity
4
+ * ({@link GitIdentities}), the gateway's relay ({@link GitGateway}, served by the api role's raw
5
+ * route `/git/v1/*`) and the run-end cleanup ({@link GitRunEnd}). It needs only the global infra
6
+ * and the connector credentials (`CredentialsModule`, not `ToolsModule`), so the api role (the
7
+ * gateway route) and the runtime and tool code (which issue tickets) can import it without a cycle.
8
+ */
9
+ export declare class GitModule {
10
+ }
@@ -0,0 +1,31 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Module } from "@nestjs/common";
8
+ import { CredentialsModule } from "../tools/credentials.module.js";
9
+ import { GitGateway } from "./gateway.js";
10
+ import { GitIdentities } from "./identity.js";
11
+ import { GitRunEnd } from "./run-end.js";
12
+ import { GitSources } from "./sources.js";
13
+ import { GitTickets } from "./tickets.js";
14
+ /**
15
+ * The git gateway (sandbox git credentials spec §5, §6): run tickets ({@link GitTickets}), whose
16
+ * connector serves a git host for an actor ({@link GitSources}) and the commit identity
17
+ * ({@link GitIdentities}), the gateway's relay ({@link GitGateway}, served by the api role's raw
18
+ * route `/git/v1/*`) and the run-end cleanup ({@link GitRunEnd}). It needs only the global infra
19
+ * and the connector credentials (`CredentialsModule`, not `ToolsModule`), so the api role (the
20
+ * gateway route) and the runtime and tool code (which issue tickets) can import it without a cycle.
21
+ */
22
+ let GitModule = class GitModule {
23
+ };
24
+ GitModule = __decorate([
25
+ Module({
26
+ imports: [CredentialsModule],
27
+ providers: [GitTickets, GitSources, GitIdentities, GitGateway, GitRunEnd],
28
+ exports: [GitTickets, GitSources, GitIdentities, GitGateway, GitRunEnd],
29
+ })
30
+ ], GitModule);
31
+ export { GitModule };
@@ -0,0 +1,80 @@
1
+ import type { KernelConfig } from "../config/schema.js";
2
+ import { CacheService } from "../infra/cache/cache.service.js";
3
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
4
+ import { KernelDb } from "../infra/database/kernel-db.js";
5
+ import type { KernelLogger } from "../log.js";
6
+ import type { NetPolicy } from "../net/public-address.js";
7
+ import { type ConnectorAuth, Credentials } from "../tools/credentials.js";
8
+ /**
9
+ * Commit identity for git in sandboxes (sandbox git credentials spec §5.4, decision 8): the
10
+ * person's GitHub display name and noreply address, read once through their connection.
11
+ */
12
+ /** A commit identity (`GIT_AUTHOR_*` / `GIT_COMMITTER_*`). */
13
+ export interface GitIdentity {
14
+ name: string;
15
+ email: string;
16
+ }
17
+ /** What is cached per (org, connector, account, host): never a token. */
18
+ export interface GitHubUser {
19
+ /** The GitHub display name, null when unset. */
20
+ name: string | null;
21
+ login: string;
22
+ id: number;
23
+ }
24
+ /** How long a read GitHub user is cached (24 h). */
25
+ export declare const GIT_IDENTITY_TTL_SECONDS: number;
26
+ /**
27
+ * Test seam (`KernelAppOptions.overrides`): the API base of a git host in place of
28
+ * {@link gitIdentityApiBase}, e.g. a loopback fake for `github.com`.
29
+ */
30
+ export declare const GIT_IDENTITY_API_BASE: unique symbol;
31
+ export type GitIdentityApiBase = (host: string) => string | null;
32
+ /**
33
+ * The REST API base of a git host: `github.com` → `https://api.github.com`, `<sub>.ghe.com` →
34
+ * `https://api.<sub>.ghe.com`; any other host has no identity (null).
35
+ */
36
+ export declare function gitIdentityApiBase(host: string): string | null;
37
+ /**
38
+ * The kernel's outbound rules for git hosts and their APIs: the deployment's egress proxy, its
39
+ * private host exemptions (`marketplaces.allowPrivateHosts`) and loopback http only under
40
+ * `tools.oauthAllowInsecureLoopback` (tests), as for OAuth URLs. Redirects are never followed.
41
+ */
42
+ export declare function gitNetPolicy(config: KernelConfig): NetPolicy;
43
+ /** The fields kept from a `GET /user` answer, or null when it does not look like a user. */
44
+ export declare function parseGitHubUser(body: unknown): GitHubUser | null;
45
+ export declare class GitIdentities {
46
+ private readonly db;
47
+ private readonly credentials;
48
+ private readonly config;
49
+ /** Read by `@Cacheable`. */
50
+ readonly cacheService: CacheService;
51
+ private readonly log;
52
+ private readonly apiBase?;
53
+ constructor(db: KernelDb, credentials: Pick<Credentials, "gitAuthorization" | "gitUnauthorized" | "checkRevision">, config: Pick<KernelConfigService, "get">,
54
+ /** Read by `@Cacheable`. */
55
+ cacheService: CacheService, log: Pick<KernelLogger, "warn">, apiBase?: GitIdentityApiBase | undefined);
56
+ /**
57
+ * The commit identity of `accountId` through the connector's credential on `host`: the GitHub
58
+ * `name` (else the login) and `<id>+<login>@users.noreply.<host>`. Null for a host without an
59
+ * identity API (not github.com or `*.ghe.com`), a connector of another org or without that host,
60
+ * a missing credential or an API failure (git then uses the sandbox's own config).
61
+ *
62
+ * Cached per (org, connector, subject, host, credential revision): the subject is the account
63
+ * for a personal connector and `org` for an org-credential one (the bot's identity is the same
64
+ * for every member, read once); the revision (`Credentials.checkRevision`, hashed) changes when
65
+ * the credential is set, refreshed or cleared, so a reconnect reads the user again.
66
+ */
67
+ gitIdentity(orgId: string, connectorId: string, accountId: string, host: string): Promise<GitIdentity | null>;
68
+ /**
69
+ * `GET /user` on the host's API with the connector's credential (one forced refresh after a
70
+ * 401), cached 24 h under the first five arguments (see {@link gitIdentity}); failures are not
71
+ * cached. The cached value holds no token; the log lines name the connector and the status,
72
+ * never a header. Called by {@link gitIdentity} only.
73
+ */
74
+ gitHubUser(orgId: string, connectorId: string, _subject: string, host: string, _revision: string, req: {
75
+ base: string;
76
+ auth: ConnectorAuth;
77
+ username: string;
78
+ accountId: string;
79
+ }): Promise<GitHubUser | null>;
80
+ }
@@ -0,0 +1,209 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { createHash } from "node:crypto";
11
+ import { Inject, Injectable, Optional } from "@nestjs/common";
12
+ import { CacheService } from "../infra/cache/cache.service.js";
13
+ import { Cacheable } from "../infra/cache/cacheable.decorator.js";
14
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
15
+ import { KernelDb } from "../infra/database/kernel-db.js";
16
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
17
+ import { safeFetch } from "../net/safe-fetch.js";
18
+ import { CredentialMissing, Credentials } from "../tools/credentials.js";
19
+ /** How long a read GitHub user is cached (24 h). */
20
+ export const GIT_IDENTITY_TTL_SECONDS = 24 * 60 * 60;
21
+ /**
22
+ * Test seam (`KernelAppOptions.overrides`): the API base of a git host in place of
23
+ * {@link gitIdentityApiBase}, e.g. a loopback fake for `github.com`.
24
+ */
25
+ export const GIT_IDENTITY_API_BASE = Symbol("GIT_IDENTITY_API_BASE");
26
+ const GHE_COM = /^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.ghe\.com$/;
27
+ /**
28
+ * The REST API base of a git host: `github.com` → `https://api.github.com`, `<sub>.ghe.com` →
29
+ * `https://api.<sub>.ghe.com`; any other host has no identity (null).
30
+ */
31
+ export function gitIdentityApiBase(host) {
32
+ if (host === "github.com")
33
+ return "https://api.github.com";
34
+ const m = GHE_COM.exec(host);
35
+ return m === null ? null : `https://api.${m[1]}.ghe.com`;
36
+ }
37
+ /**
38
+ * The kernel's outbound rules for git hosts and their APIs: the deployment's egress proxy, its
39
+ * private host exemptions (`marketplaces.allowPrivateHosts`) and loopback http only under
40
+ * `tools.oauthAllowInsecureLoopback` (tests), as for OAuth URLs. Redirects are never followed.
41
+ */
42
+ export function gitNetPolicy(config) {
43
+ return {
44
+ allowInsecureLoopback: config.tools.oauthAllowInsecureLoopback,
45
+ allowPrivateHosts: config.marketplaces.allowPrivateHosts,
46
+ trustedOrigins: [],
47
+ ...(config.net.httpsProxy !== undefined && {
48
+ proxy: { url: config.net.httpsProxy, noProxy: config.net.noProxy },
49
+ }),
50
+ };
51
+ }
52
+ /** Bounds of a `GET /user` answer. */
53
+ const USER_MAX_BYTES = 256 * 1024;
54
+ const LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9_-]{0,98}[A-Za-z0-9])?$/;
55
+ const NAME_MAX = 200;
56
+ /** A display name safe for an environment variable: no control characters, bounded. */
57
+ function cleanName(v) {
58
+ if (typeof v !== "string")
59
+ return null;
60
+ const s = [...v]
61
+ .map((ch) => {
62
+ const c = ch.charCodeAt(0);
63
+ return c < 0x20 || c === 0x7f ? " " : ch;
64
+ })
65
+ .join("")
66
+ .trim()
67
+ .slice(0, NAME_MAX)
68
+ .trim();
69
+ return s === "" ? null : s;
70
+ }
71
+ /** The fields kept from a `GET /user` answer, or null when it does not look like a user. */
72
+ export function parseGitHubUser(body) {
73
+ if (typeof body !== "object" || body === null)
74
+ return null;
75
+ const b = body;
76
+ if (typeof b.login !== "string" || !LOGIN_RE.test(b.login))
77
+ return null;
78
+ if (typeof b.id !== "number" || !Number.isSafeInteger(b.id) || b.id <= 0)
79
+ return null;
80
+ return { name: cleanName(b.name), login: b.login, id: b.id };
81
+ }
82
+ let GitIdentities = class GitIdentities {
83
+ db;
84
+ credentials;
85
+ config;
86
+ cacheService;
87
+ log;
88
+ apiBase;
89
+ constructor(db, credentials, config,
90
+ /** Read by `@Cacheable`. */
91
+ cacheService, log, apiBase) {
92
+ this.db = db;
93
+ this.credentials = credentials;
94
+ this.config = config;
95
+ this.cacheService = cacheService;
96
+ this.log = log;
97
+ this.apiBase = apiBase;
98
+ }
99
+ /**
100
+ * The commit identity of `accountId` through the connector's credential on `host`: the GitHub
101
+ * `name` (else the login) and `<id>+<login>@users.noreply.<host>`. Null for a host without an
102
+ * identity API (not github.com or `*.ghe.com`), a connector of another org or without that host,
103
+ * a missing credential or an API failure (git then uses the sandbox's own config).
104
+ *
105
+ * Cached per (org, connector, subject, host, credential revision): the subject is the account
106
+ * for a personal connector and `org` for an org-credential one (the bot's identity is the same
107
+ * for every member, read once); the revision (`Credentials.checkRevision`, hashed) changes when
108
+ * the credential is set, refreshed or cleared, so a reconnect reads the user again.
109
+ */
110
+ async gitIdentity(orgId, connectorId, accountId, host) {
111
+ const base = (this.apiBase ?? gitIdentityApiBase)(host);
112
+ if (base === null)
113
+ return null;
114
+ const row = await this.db.db
115
+ .selectFrom("connectors")
116
+ .select(["auth", "git_hosts"])
117
+ .where("id", "=", connectorId)
118
+ .where("org_id", "=", orgId)
119
+ .where("deleted_at", "is", null)
120
+ .executeTakeFirst();
121
+ const entry = (row?.git_hosts ?? []).find((h) => h.host === host);
122
+ if (row === undefined || entry === undefined)
123
+ return null;
124
+ const auth = row.auth;
125
+ const bot = auth.type !== "none" && auth.ownership === "org";
126
+ const subject = bot ? "org" : accountId;
127
+ const revision = createHash("sha256")
128
+ .update(await this.credentials.checkRevision(orgId, connectorId, bot ? null : accountId))
129
+ .digest("hex")
130
+ .slice(0, 16);
131
+ const user = await this.gitHubUser(orgId, connectorId, subject, host, revision, {
132
+ base,
133
+ auth,
134
+ username: entry.username,
135
+ accountId,
136
+ });
137
+ if (user === null)
138
+ return null;
139
+ return {
140
+ name: user.name ?? user.login,
141
+ email: `${user.id}+${user.login}@users.noreply.${host}`,
142
+ };
143
+ }
144
+ /**
145
+ * `GET /user` on the host's API with the connector's credential (one forced refresh after a
146
+ * 401), cached 24 h under the first five arguments (see {@link gitIdentity}); failures are not
147
+ * cached. The cached value holds no token; the log lines name the connector and the status,
148
+ * never a header. Called by {@link gitIdentity} only.
149
+ */
150
+ async gitHubUser(orgId, connectorId, _subject, host, _revision, req) {
151
+ const { base, auth, accountId, username } = req;
152
+ const config = this.config.get();
153
+ const get = async (authorization) => safeFetch(`${base}/user`, {
154
+ headers: {
155
+ Authorization: authorization,
156
+ Accept: "application/vnd.github+json",
157
+ "User-Agent": "capybara-git-gateway",
158
+ },
159
+ signal: AbortSignal.timeout(Math.min(config.tools.timeoutMs, 30_000)),
160
+ }, gitNetPolicy(config), { maxBytes: USER_MAX_BYTES });
161
+ try {
162
+ const authorization = await this.credentials.gitAuthorization(orgId, connectorId, auth, accountId, username);
163
+ let res = await get(authorization);
164
+ if (res.status === 401) {
165
+ await res.body?.cancel().catch(() => { });
166
+ await this.credentials.gitUnauthorized(orgId, connectorId, auth, accountId, authorization);
167
+ res = await get(await this.credentials.gitAuthorization(orgId, connectorId, auth, accountId, username));
168
+ }
169
+ if (res.status !== 200) {
170
+ await res.body?.cancel().catch(() => { });
171
+ this.log.warn("git identity: the user lookup failed", {
172
+ connectorId,
173
+ host,
174
+ status: res.status,
175
+ });
176
+ return null;
177
+ }
178
+ const user = parseGitHubUser(await res.json());
179
+ if (user === null) {
180
+ this.log.warn("git identity: the user lookup answered no user", { connectorId, host });
181
+ }
182
+ return user;
183
+ }
184
+ catch (e) {
185
+ if (!(e instanceof CredentialMissing)) {
186
+ this.log.warn("git identity: the user lookup failed", {
187
+ connectorId,
188
+ host,
189
+ error: e instanceof Error ? e.name : "unknown",
190
+ });
191
+ }
192
+ return null;
193
+ }
194
+ }
195
+ };
196
+ __decorate([
197
+ Cacheable({ key: "git-identity:#{0}:#{1}:#{2}:#{3}:#{4}", ttl: GIT_IDENTITY_TTL_SECONDS })
198
+ ], GitIdentities.prototype, "gitHubUser", null);
199
+ GitIdentities = __decorate([
200
+ Injectable(),
201
+ __param(0, Inject(KernelDb)),
202
+ __param(1, Inject(Credentials)),
203
+ __param(2, Inject(KernelConfigService)),
204
+ __param(3, Inject(CacheService)),
205
+ __param(4, Inject(KERNEL_LOGGER)),
206
+ __param(5, Optional()),
207
+ __param(5, Inject(GIT_IDENTITY_API_BASE))
208
+ ], GitIdentities);
209
+ export { GitIdentities };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Git pkt-lines (gitprotocol-common: a 4-hex-digit length that counts itself, then the payload;
3
+ * `0000` is the flush-pkt) as far as the git gateway needs them: the ref update commands at the
4
+ * start of a `git-receive-pack` request body, and the refusals git prints to the person running
5
+ * the command (sandbox git credentials spec §5.3).
6
+ */
7
+ /** The flush-pkt. */
8
+ export declare const FLUSH_PKT: Buffer<ArrayBuffer>;
9
+ /** One pkt-line holding `data` (a longer payload is clipped to fit). */
10
+ export declare function pktLine(data: string | Buffer): Buffer;
11
+ /** One ref update of a push: `<old> <new> <ref>`. */
12
+ export interface RefCommand {
13
+ oldId: string;
14
+ newId: string;
15
+ ref: string;
16
+ }
17
+ /** The command list at the start of a receive-pack request. */
18
+ export interface ReceivePackCommands {
19
+ commands: RefCommand[];
20
+ /** The capabilities the client asked for (after the NUL of the first command). */
21
+ capabilities: string[];
22
+ /** Every byte read from the body so far (the commands and whatever followed in the chunk). */
23
+ prefix: Buffer;
24
+ }
25
+ /** Why the command list could not be read: the body is not a push the gateway can decide. */
26
+ export declare class ReceivePackParseError extends Error {
27
+ constructor(message: string);
28
+ }
29
+ /** Bounds of the command list the gateway reads before it decides. */
30
+ export declare const MAX_COMMANDS = 1000;
31
+ export declare const MAX_COMMAND_BYTES: number;
32
+ /**
33
+ * Reads the ref update commands at the start of a receive-pack request body, up to the first
34
+ * flush-pkt, from `chunks` (the body's async iterator; nothing after the chunk that holds the
35
+ * flush-pkt is read). `shallow` lines are skipped; a signed push (`push-cert`), a malformed line,
36
+ * a body that ends early or a list over {@link MAX_COMMANDS} / {@link MAX_COMMAND_BYTES} throws
37
+ * {@link ReceivePackParseError}. An empty list (a bare flush-pkt: git's auth probe) is valid.
38
+ */
39
+ export declare function readReceivePackCommands(chunks: AsyncIterator<Buffer | string>): Promise<ReceivePackCommands>;
40
+ /**
41
+ * A ref advertisement git turns into `fatal: remote error: <message>` (an `ERR` pkt-line as the
42
+ * first packet: git dies on it before it reads anything else).
43
+ */
44
+ export declare function advertisementError(message: string): Buffer;
45
+ /**
46
+ * The answer to a refused receive-pack request, in the form the client asked for, so git prints
47
+ * the message and marks each ref rejected:
48
+ * - side-band(-64k): `message` on band 2 (git prints `remote: <message>`), then the report on
49
+ * band 1 (`unpack ok` + `ng <ref> <reason>`; git prints `! [remote rejected] … (<reason>)`);
50
+ * - report-status(-v2) without side-band: the report alone;
51
+ * - neither: an `ERR` pkt-line (git reports the push failed).
52
+ */
53
+ export declare function receivePackRefusal(capabilities: readonly string[], refs: readonly string[], message: string, reason: string): Buffer;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Git pkt-lines (gitprotocol-common: a 4-hex-digit length that counts itself, then the payload;
3
+ * `0000` is the flush-pkt) as far as the git gateway needs them: the ref update commands at the
4
+ * start of a `git-receive-pack` request body, and the refusals git prints to the person running
5
+ * the command (sandbox git credentials spec §5.3).
6
+ */
7
+ /** The flush-pkt. */
8
+ export const FLUSH_PKT = Buffer.from("0000", "latin1");
9
+ /** The largest pkt-line (header included). */
10
+ const MAX_PKT = 65520;
11
+ /** One pkt-line holding `data` (a longer payload is clipped to fit). */
12
+ export function pktLine(data) {
13
+ const body = typeof data === "string" ? Buffer.from(data, "utf8") : data;
14
+ const clipped = body.subarray(0, MAX_PKT - 4);
15
+ return Buffer.concat([
16
+ Buffer.from((clipped.length + 4).toString(16).padStart(4, "0"), "latin1"),
17
+ clipped,
18
+ ]);
19
+ }
20
+ /** Why the command list could not be read: the body is not a push the gateway can decide. */
21
+ export class ReceivePackParseError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = "ReceivePackParseError";
25
+ }
26
+ }
27
+ /** Bounds of the command list the gateway reads before it decides. */
28
+ export const MAX_COMMANDS = 1000;
29
+ export const MAX_COMMAND_BYTES = 1024 * 1024;
30
+ const OBJECT_ID = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
31
+ const SHALLOW = /^shallow [0-9a-f]{40}(?:[0-9a-f]{24})?$/;
32
+ /**
33
+ * Reads the ref update commands at the start of a receive-pack request body, up to the first
34
+ * flush-pkt, from `chunks` (the body's async iterator; nothing after the chunk that holds the
35
+ * flush-pkt is read). `shallow` lines are skipped; a signed push (`push-cert`), a malformed line,
36
+ * a body that ends early or a list over {@link MAX_COMMANDS} / {@link MAX_COMMAND_BYTES} throws
37
+ * {@link ReceivePackParseError}. An empty list (a bare flush-pkt: git's auth probe) is valid.
38
+ */
39
+ export async function readReceivePackCommands(chunks) {
40
+ let buf = Buffer.alloc(0);
41
+ let pos = 0;
42
+ const commands = [];
43
+ let capabilities = [];
44
+ for (;;) {
45
+ // parse every complete pkt-line in the buffer
46
+ while (buf.length - pos >= 4) {
47
+ const lenHex = buf.toString("latin1", pos, pos + 4);
48
+ if (!/^[0-9a-f]{4}$/i.test(lenHex))
49
+ throw new ReceivePackParseError("bad pkt-line length");
50
+ const len = Number.parseInt(lenHex, 16);
51
+ if (len === 0)
52
+ return { commands, capabilities, prefix: buf };
53
+ if (len < 4)
54
+ throw new ReceivePackParseError("unexpected special pkt-line");
55
+ if (buf.length - pos < len)
56
+ break;
57
+ let line = buf.toString("utf8", pos + 4, pos + len);
58
+ pos += len;
59
+ if (line.endsWith("\n"))
60
+ line = line.slice(0, -1);
61
+ if (commands.length === 0) {
62
+ const nul = line.indexOf("\0");
63
+ if (nul >= 0) {
64
+ capabilities = line
65
+ .slice(nul + 1)
66
+ .split(" ")
67
+ .filter((c) => c !== "");
68
+ line = line.slice(0, nul);
69
+ }
70
+ }
71
+ if (line.startsWith("push-cert")) {
72
+ throw new ReceivePackParseError("signed pushes are not supported through the gateway");
73
+ }
74
+ if (SHALLOW.test(line))
75
+ continue;
76
+ const parts = line.split(" ");
77
+ const [oldId, newId, ref] = parts;
78
+ if (parts.length !== 3 ||
79
+ oldId === undefined ||
80
+ newId === undefined ||
81
+ ref === undefined ||
82
+ !OBJECT_ID.test(oldId) ||
83
+ !OBJECT_ID.test(newId) ||
84
+ !ref.startsWith("refs/")) {
85
+ throw new ReceivePackParseError("malformed ref update command");
86
+ }
87
+ commands.push({ oldId, newId, ref });
88
+ if (commands.length > MAX_COMMANDS)
89
+ throw new ReceivePackParseError("too many ref updates");
90
+ }
91
+ if (buf.length > MAX_COMMAND_BYTES)
92
+ throw new ReceivePackParseError("command list too long");
93
+ const next = await chunks.next();
94
+ if (next.done)
95
+ throw new ReceivePackParseError("the request ended before the command list");
96
+ const chunk = typeof next.value === "string" ? Buffer.from(next.value) : next.value;
97
+ buf = Buffer.concat([buf, chunk]);
98
+ }
99
+ }
100
+ /**
101
+ * A ref advertisement git turns into `fatal: remote error: <message>` (an `ERR` pkt-line as the
102
+ * first packet: git dies on it before it reads anything else).
103
+ */
104
+ export function advertisementError(message) {
105
+ return pktLine(`ERR ${oneLine(message)}\n`);
106
+ }
107
+ /**
108
+ * The answer to a refused receive-pack request, in the form the client asked for, so git prints
109
+ * the message and marks each ref rejected:
110
+ * - side-band(-64k): `message` on band 2 (git prints `remote: <message>`), then the report on
111
+ * band 1 (`unpack ok` + `ng <ref> <reason>`; git prints `! [remote rejected] … (<reason>)`);
112
+ * - report-status(-v2) without side-band: the report alone;
113
+ * - neither: an `ERR` pkt-line (git reports the push failed).
114
+ */
115
+ export function receivePackRefusal(capabilities, refs, message, reason) {
116
+ const caps = new Set(capabilities);
117
+ const report = caps.has("report-status") || caps.has("report-status-v2");
118
+ const sideBand = caps.has("side-band-64k") || caps.has("side-band");
119
+ const why = oneLine(reason);
120
+ const reportPkts = Buffer.concat([
121
+ pktLine("unpack ok\n"),
122
+ ...refs.map((ref) => pktLine(`ng ${ref} ${why}\n`)),
123
+ FLUSH_PKT,
124
+ ]);
125
+ if (sideBand) {
126
+ // side-band (without -64k) carries at most 1000 bytes per packet
127
+ const max = caps.has("side-band-64k") ? MAX_PKT - 5 : 995;
128
+ const band = (n, data) => {
129
+ const out = [];
130
+ for (let i = 0; i < data.length; i += max) {
131
+ out.push(pktLine(Buffer.concat([Buffer.from([n]), data.subarray(i, i + max)])));
132
+ }
133
+ return out;
134
+ };
135
+ return Buffer.concat([
136
+ ...band(2, Buffer.from(`${oneLine(message)}\n`, "utf8")),
137
+ ...(report ? band(1, reportPkts) : []),
138
+ FLUSH_PKT,
139
+ ]);
140
+ }
141
+ if (report)
142
+ return reportPkts;
143
+ return pktLine(`ERR ${oneLine(message)}\n`);
144
+ }
145
+ /** A message on one line (pkt-line payloads git prints are line-based). */
146
+ const oneLine = (s) => s.replace(/[\r\n]+/g, " ").trim();
@@ -0,0 +1,29 @@
1
+ import type { Redis } from "ioredis";
2
+ import { KernelDb } from "../infra/database/kernel-db.js";
3
+ import type { KernelLogger } from "../log.js";
4
+ import { GitTickets } from "./tickets.js";
5
+ /**
6
+ * What a run's end does for the git gateway (sandbox git credentials spec §9): revokes the run's
7
+ * tickets and writes the `git.fetch` summaries. `RunFinish` calls it in `afterCommit` of the
8
+ * transaction that set the terminal status (set first, then revoke: `GitTickets.resolve` already
9
+ * refuses a run that is not active, so revocation is cleanup and defense in depth).
10
+ */
11
+ /** The Redis hash of a run's fetch counters: fields `<host>/<repo>|n` and `<host>/<repo>|bytes`. */
12
+ export declare const gitFetchKey: (runId: string) => string;
13
+ /**
14
+ * Counters of a run that never ends cleanly (a lost end hook) expire after this. A fetch still in
15
+ * flight after the run ended writes a counter nobody summarises: it expires by this TTL too.
16
+ */
17
+ export declare const FETCH_COUNTER_TTL_SECONDS: number;
18
+ /** Adds one fetch request and its bytes to the run's counters (atomic, with a TTL). */
19
+ export declare function recordGitFetch(redis: Redis, runId: string, host: string, repo: string, bytes: number, countRequest: boolean): Promise<void>;
20
+ export declare class GitRunEnd {
21
+ private readonly tickets;
22
+ private readonly db;
23
+ private readonly redis;
24
+ private readonly log;
25
+ constructor(tickets: Pick<GitTickets, "revokeRun">, db: KernelDb, redis: Redis, log: Pick<KernelLogger, "warn">);
26
+ /** Run end: revokes the run's tickets and writes one `git.fetch` audit row per repository. */
27
+ onRunFinished(runId: string): Promise<void>;
28
+ private writeFetchSummaries;
29
+ }