@vymalo/opencode-repo-auth 0.14.1
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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +156 -0
- package/dist/config.js.map +1 -0
- package/dist/git.d.ts +55 -0
- package/dist/git.js +231 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +4 -0
- package/dist/lib.js +6 -0
- package/dist/lib.js.map +1 -0
- package/dist/opencode.d.ts +13 -0
- package/dist/opencode.js +331 -0
- package/dist/opencode.js.map +1 -0
- package/dist/plugin.d.ts +100 -0
- package/dist/plugin.js +161 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vymalo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# `@vymalo/opencode-repo-auth`
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@vymalo/opencode-repo-auth)
|
|
4
|
+
|
|
5
|
+
**Bill each gateway request to the git project you're working on** — the same repo-as-principal
|
|
6
|
+
attribution CI already has, for local development. The developer logs in once as themselves, and
|
|
7
|
+
every request from an enrolled repo carries a short-lived, **project-scoped** bearer minted by a
|
|
8
|
+
single RFC 8693 token exchange presenting `project_id` (no `audience`, no mint step). See
|
|
9
|
+
[ADR-0011](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/adr/0011-repo-auth-project-id-token-exchange.md).
|
|
10
|
+
|
|
11
|
+
Part of the [OpenCode Toolbelt](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit).
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
1. **Log in once** — `@vymalo/opencode-auth-core`'s `TokenRuntime` runs the configured OAuth flow
|
|
16
|
+
(`device_code` for headless, or `authorization_code` with PKCE) against `issuer`, producing a
|
|
17
|
+
human root token with `offline_access` for silent refresh.
|
|
18
|
+
2. **Exchange per project** — `exchangeTo(projectId, humanToken, { project_id })`, an RFC 8693 token
|
|
19
|
+
exchange. The IdP resolves membership server-side and seals `{account_id, project_id}` into the
|
|
20
|
+
returned JWT. The project token is short-lived and carries no refresh token; renewal is always a
|
|
21
|
+
fresh exchange from the offline human root ("model b").
|
|
22
|
+
3. **Inject per request** — a `chat.headers` hook stamps `Authorization: Bearer <project-token>` on
|
|
23
|
+
the opted-in providers only. **Fail-closed**: an exchange failure injects no header (the gateway
|
|
24
|
+
401s), so a request never runs under the wrong identity.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install @vymalo/opencode-repo-auth
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```jsonc
|
|
33
|
+
// opencode.json
|
|
34
|
+
{
|
|
35
|
+
"plugin": ["@vymalo/opencode-repo-auth"],
|
|
36
|
+
"provider": {
|
|
37
|
+
"gateway": {
|
|
38
|
+
"options": {
|
|
39
|
+
"baseURL": "https://api.example.com/v1",
|
|
40
|
+
"meta": {
|
|
41
|
+
"repoAuth": {
|
|
42
|
+
"projectId": "proj-123",
|
|
43
|
+
"issuer": "https://auth.example.com/realms/lightbridge",
|
|
44
|
+
"clientId": "opencode-cli",
|
|
45
|
+
"scopes": ["openid", "offline_access"],
|
|
46
|
+
"authFlow": "device_code"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The plugin opts in **per provider** via `options.meta.repoAuth`; a missing or malformed block
|
|
56
|
+
warns-and-skips that provider rather than aborting the whole config hook.
|
|
57
|
+
|
|
58
|
+
## `projectId` is declared, not derived
|
|
59
|
+
|
|
60
|
+
`projectId` comes from config — it is **never** derived from the git remote, because a repo may
|
|
61
|
+
belong to many projects. The git `origin` (worktree-aware, hardened against odd `.git` pointers) is
|
|
62
|
+
resolved for **logging only**. The exchanged token is cached per project, `0o600`, atomic-rename.
|
|
63
|
+
|
|
64
|
+
## Don't stack it with `oauth2`
|
|
65
|
+
|
|
66
|
+
`@vymalo/opencode-repo-auth` and `@vymalo/opencode-oauth2` both set `Authorization` — never enable
|
|
67
|
+
both on the **same** provider (the plugin guards against this and logs a conflict). Need one
|
|
68
|
+
credential across the gateway **and** OTEL export? Use the umbrella
|
|
69
|
+
[`@vymalo/opencode-lightbridge`](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/tree/main/packages/opencode-lightbridge)
|
|
70
|
+
instead of stacking plugins.
|
|
71
|
+
|
|
72
|
+
## Full reference
|
|
73
|
+
|
|
74
|
+
- [`docs/repo-auth.md`](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/repo-auth.md)
|
|
75
|
+
— every field, the model-b renewal, the git-hardening cases, troubleshooting.
|
|
76
|
+
- [ADR-0011](https://github.com/ADORSYS-GIS/lightbridge-opencode-toolbeit/blob/main/docs/adr/0011-repo-auth-project-id-token-exchange.md)
|
|
77
|
+
— why `project_id` exchange (not an audience-scoped Source), and the alternatives considered.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AuthServerConfigInput } from "@vymalo/opencode-auth-core/lib";
|
|
2
|
+
export declare const REPO_AUTH_META_KEY = "repoAuth";
|
|
3
|
+
/**
|
|
4
|
+
* Parsed outcome of inspecting one provider's `options.meta.repoAuth`:
|
|
5
|
+
* - `not_opted_in` — no `meta.repoAuth` block at all (skip silently).
|
|
6
|
+
* - `missing_project_id` — block present but no `projectId` (warn; never
|
|
7
|
+
* crash — the no-op matrix in docs/repo-auth.md).
|
|
8
|
+
* - `opted_in` — a valid, self-contained config to manage.
|
|
9
|
+
*/
|
|
10
|
+
export type RepoAuthParseResult = {
|
|
11
|
+
kind: "not_opted_in";
|
|
12
|
+
} | {
|
|
13
|
+
kind: "missing_project_id";
|
|
14
|
+
} | {
|
|
15
|
+
kind: "opted_in";
|
|
16
|
+
config: RepoAuthConfig;
|
|
17
|
+
};
|
|
18
|
+
/** The per-provider opt-in: a `projectId` plus the IdP auth server config. */
|
|
19
|
+
export interface RepoAuthConfig {
|
|
20
|
+
projectId: string;
|
|
21
|
+
auth: AuthServerConfigInput;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse a provider's `options.meta.repoAuth` opt-in block (mirrors models-info's
|
|
25
|
+
* `options.meta.modelsInfoUrl` pattern). Throws malformed-field errors with the
|
|
26
|
+
* canonical config path (`provider.options.meta.repoAuth`) so a typo is
|
|
27
|
+
* debuggable; the missing-`projectId` case is a distinct sentinel, not an
|
|
28
|
+
* error, because the no-op matrix requires it to warn-and-skip rather than
|
|
29
|
+
* crash.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseRepoAuthOptions(providerOptions: Record<string, unknown> | undefined): RepoAuthParseResult;
|
|
32
|
+
type OpenCodeConfigLike = {
|
|
33
|
+
provider?: unknown;
|
|
34
|
+
pluginConfig?: unknown;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Provider ids managed by `@vymalo/opencode-oauth2` via the
|
|
38
|
+
* `pluginConfig.oauth2ModelSync.servers` channel. oauth2 registers those
|
|
39
|
+
* providers under their server `id` without touching `options` at all, so a
|
|
40
|
+
* per-`options` scan alone would miss them.
|
|
41
|
+
*/
|
|
42
|
+
export declare function oauth2ManagedProviderIds(config: OpenCodeConfigLike): Set<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Detect whether a provider is (also) managed by `@vymalo/opencode-oauth2` —
|
|
45
|
+
* either via the provider's own `options.oauth2` / `options.oauth2ModelSync`
|
|
46
|
+
* blocks, or via the `pluginConfig.oauth2ModelSync.servers` channel (whose
|
|
47
|
+
* provider ids are the server ids). The two plugins must never manage the same
|
|
48
|
+
* provider — repo-auth skips (warn) rather than fight over headers.
|
|
49
|
+
*/
|
|
50
|
+
export declare function hasOAuth2Conflict(config: OpenCodeConfigLike, providerOptions: Record<string, unknown> | undefined, providerId?: string): boolean;
|
|
51
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export const REPO_AUTH_META_KEY = "repoAuth";
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function asString(value) {
|
|
9
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
10
|
+
}
|
|
11
|
+
function asStringArray(value) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
const normalized = value.map((entry) => asString(entry)).filter((entry) => Boolean(entry));
|
|
14
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
const normalized = value.split(/[\s,]+/g).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
18
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function asAuthFlow(value, source) {
|
|
23
|
+
if (value === undefined || value === null) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (value === "authorization_code" || value === "device_code" || value === "client_credentials" || value === "jwt_bearer" || value === "token_exchange") {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`${source}.authFlow must be one of "authorization_code" | "device_code" | "client_credentials" | "jwt_bearer" | "token_exchange" (received ${JSON.stringify(value)})`);
|
|
30
|
+
}
|
|
31
|
+
function asClientSecret(value, source) {
|
|
32
|
+
if (value === undefined || value === null) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
36
|
+
throw new Error(`${source}.clientSecret must be a non-empty string when provided`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function asRedirectPort(value, source) {
|
|
41
|
+
if (value === undefined || value === null) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`${source}.redirectPort must be an integer in [1, 65535] (received ${JSON.stringify(value)})`);
|
|
48
|
+
}
|
|
49
|
+
function asBoolean(value, source) {
|
|
50
|
+
if (value === undefined || value === null) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (typeof value !== "boolean") {
|
|
54
|
+
throw new Error(`${source} must be a boolean (received ${JSON.stringify(value)})`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function asSubjectTokenSource(value) {
|
|
59
|
+
// Shallow pass-through; deep validation happens in auth-core's
|
|
60
|
+
// `validateAuthConfig` and its error messages reference the canonical path.
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse a provider's `options.meta.repoAuth` opt-in block (mirrors models-info's
|
|
65
|
+
* `options.meta.modelsInfoUrl` pattern). Throws malformed-field errors with the
|
|
66
|
+
* canonical config path (`provider.options.meta.repoAuth`) so a typo is
|
|
67
|
+
* debuggable; the missing-`projectId` case is a distinct sentinel, not an
|
|
68
|
+
* error, because the no-op matrix requires it to warn-and-skip rather than
|
|
69
|
+
* crash.
|
|
70
|
+
*/
|
|
71
|
+
export function parseRepoAuthOptions(providerOptions) {
|
|
72
|
+
if (!providerOptions) {
|
|
73
|
+
return { kind: "not_opted_in" };
|
|
74
|
+
}
|
|
75
|
+
const meta = asRecord(providerOptions.meta);
|
|
76
|
+
if (!meta) {
|
|
77
|
+
return { kind: "not_opted_in" };
|
|
78
|
+
}
|
|
79
|
+
const raw = asRecord(meta[REPO_AUTH_META_KEY]);
|
|
80
|
+
if (!raw) {
|
|
81
|
+
return { kind: "not_opted_in" };
|
|
82
|
+
}
|
|
83
|
+
const projectId = asString(raw.projectId);
|
|
84
|
+
if (!projectId) {
|
|
85
|
+
return { kind: "missing_project_id" };
|
|
86
|
+
}
|
|
87
|
+
const source = "provider.options.meta.repoAuth";
|
|
88
|
+
const issuer = asString(raw.issuer);
|
|
89
|
+
const clientId = asString(raw.clientId);
|
|
90
|
+
const scopes = asStringArray(raw.scopes);
|
|
91
|
+
if (!issuer || !clientId || !scopes) {
|
|
92
|
+
throw new Error(`${source} requires non-empty issuer, clientId and scopes (projectId=${projectId})`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
kind: "opted_in",
|
|
96
|
+
config: {
|
|
97
|
+
projectId,
|
|
98
|
+
auth: {
|
|
99
|
+
id: REPO_AUTH_META_KEY,
|
|
100
|
+
issuer,
|
|
101
|
+
clientId,
|
|
102
|
+
clientSecret: asClientSecret(raw.clientSecret, source),
|
|
103
|
+
scopes,
|
|
104
|
+
authorizationEndpoint: asString(raw.authorizationEndpoint),
|
|
105
|
+
tokenEndpoint: asString(raw.tokenEndpoint),
|
|
106
|
+
deviceAuthorizationEndpoint: asString(raw.deviceAuthorizationEndpoint),
|
|
107
|
+
jwksUri: asString(raw.jwksUri),
|
|
108
|
+
redirectPort: asRedirectPort(raw.redirectPort, source),
|
|
109
|
+
authFlow: asAuthFlow(raw.authFlow, source),
|
|
110
|
+
pkce: asBoolean(raw.pkce, `${source}.pkce`),
|
|
111
|
+
subjectTokenSource: asSubjectTokenSource(raw.subjectTokenSource)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const OAUTH2_CONFLICT_KEYS = ["oauth2", "oauth2ModelSync"];
|
|
117
|
+
/**
|
|
118
|
+
* Provider ids managed by `@vymalo/opencode-oauth2` via the
|
|
119
|
+
* `pluginConfig.oauth2ModelSync.servers` channel. oauth2 registers those
|
|
120
|
+
* providers under their server `id` without touching `options` at all, so a
|
|
121
|
+
* per-`options` scan alone would miss them.
|
|
122
|
+
*/
|
|
123
|
+
export function oauth2ManagedProviderIds(config) {
|
|
124
|
+
const ids = new Set();
|
|
125
|
+
const pluginConfig = asRecord(config.pluginConfig);
|
|
126
|
+
const oauth2ModelSync = asRecord(pluginConfig?.oauth2ModelSync);
|
|
127
|
+
const servers = asRecord(oauth2ModelSync)?.servers;
|
|
128
|
+
if (Array.isArray(servers)) {
|
|
129
|
+
for (const rawServer of servers) {
|
|
130
|
+
const entry = asRecord(rawServer);
|
|
131
|
+
const id = asString(entry?.id);
|
|
132
|
+
if (id) {
|
|
133
|
+
ids.add(id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return ids;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Detect whether a provider is (also) managed by `@vymalo/opencode-oauth2` —
|
|
141
|
+
* either via the provider's own `options.oauth2` / `options.oauth2ModelSync`
|
|
142
|
+
* blocks, or via the `pluginConfig.oauth2ModelSync.servers` channel (whose
|
|
143
|
+
* provider ids are the server ids). The two plugins must never manage the same
|
|
144
|
+
* provider — repo-auth skips (warn) rather than fight over headers.
|
|
145
|
+
*/
|
|
146
|
+
export function hasOAuth2Conflict(config, providerOptions, providerId) {
|
|
147
|
+
if (providerOptions && OAUTH2_CONFLICT_KEYS.some((key) => asRecord(providerOptions[key]) !== undefined)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (providerId && oauth2ManagedProviderIds(config).has(providerId)) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"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","names":[],"sources":["../src/config.ts"],"version":3,"file":"config.js","sourceRoot":""}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git identity resolution, read directly off disk (`.git/config`, worktree
|
|
3
|
+
* aware) rather than shelling out to `git` — no subprocess, no `git` on
|
|
4
|
+
* `PATH`. Mirrors the house precedent in otel's VCS collector. The remote is
|
|
5
|
+
* log-only in v1: nothing is ever derived from it, so the accepted trade-off
|
|
6
|
+
* (raw config misses `url.<base>.insteadOf` rewrites that `git remote get-url`
|
|
7
|
+
* would apply) is harmless; revisit if resolve-by-remote ever lands.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Walk up from `startDir` to the nearest ancestor that contains a `.git` entry.
|
|
11
|
+
* `.git` may be a directory (a normal checkout) or a file (a linked worktree,
|
|
12
|
+
* pointing at the common dir). Returns the repo root, or `undefined` when no
|
|
13
|
+
* enclosing repository exists.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveRepoRoot(startDir: string): Promise<string | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the git "common dir" (where `config` lives) for a repo root, handling
|
|
18
|
+
* the linked-worktree `.git`-file case:
|
|
19
|
+
* - `.git` is a directory → common dir is `.git` itself.
|
|
20
|
+
* - `.git` is a file → read `gitdir: <path>`; the pointed-at worktree
|
|
21
|
+
* gitdir may carry a `commondir` file (relative to itself) that names the
|
|
22
|
+
* shared object/config dir. Falls back to the worktree gitdir when no
|
|
23
|
+
* `commondir` marker exists (a bare gitdir without worktree layout).
|
|
24
|
+
* Returns the directory containing the `config` file, or `undefined`.
|
|
25
|
+
*
|
|
26
|
+
* The `gitdir:` pointer is attacker-influenced (the `.git` file is part of the
|
|
27
|
+
* tree the user opened), so it is validated defensively: the target must be a
|
|
28
|
+
* real directory (no symlinks), must contain a `HEAD` file (git's own marker
|
|
29
|
+
* that a worktree gitdir exists there), and its `config` must be a regular
|
|
30
|
+
* file — a FIFO/socket/device/symlink is rejected so a crafted tree cannot
|
|
31
|
+
* hang the config hook on a blocking read or reach into unrelated files.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveGitCommonDir(repoRoot: string): Promise<string | undefined>;
|
|
34
|
+
/**
|
|
35
|
+
* Minimal git-config reader for the `[remote "origin"]`/`url` entry — enough
|
|
36
|
+
* for the one key repo-auth (or any embedder) needs. Handles quoted section
|
|
37
|
+
* names and values; comments at line start; case-sensitive section names are
|
|
38
|
+
* not matched, only `remote "<name>"` sections.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseGitConfig(text: string): Map<string, Map<string, string>>;
|
|
41
|
+
/** Extract the raw `origin` remote URL from git-config text, if any. */
|
|
42
|
+
export declare function parseOriginRemote(configText: string): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Normalize a git remote URL to a `<host>/<path>` string safe for logging and
|
|
45
|
+
* repo correlation:
|
|
46
|
+
* - strips userinfo (`user@` / `user:pass@`) in both https and scp forms,
|
|
47
|
+
* - folds scp-style `git@host:org/repo.git` onto the https shape,
|
|
48
|
+
* - drops a trailing `.git` and any trailing slashes,
|
|
49
|
+
* - drops query / fragment.
|
|
50
|
+
* Returns `undefined` for anything that is not a recognizable remote (local
|
|
51
|
+
* paths, garbage) — callers treat that as "no remote".
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizeRemote(url: string): string | undefined;
|
|
54
|
+
/** Read the normalized `origin` remote for a repo root, if any. */
|
|
55
|
+
export declare function resolveOriginRemote(repoRoot: string): Promise<string | undefined>;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { lstat, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
/** Git config files are small; anything beyond this is not a genuine repo. */
|
|
4
|
+
const GIT_CONFIG_MAX_BYTES = 256 * 1024;
|
|
5
|
+
/** Bound the read so a FIFO / stalled file cannot hang the config hook. */
|
|
6
|
+
const GIT_CONFIG_READ_TIMEOUT_MS = 2e3;
|
|
7
|
+
async function isRegularFile(path) {
|
|
8
|
+
try {
|
|
9
|
+
const entry = await lstat(path);
|
|
10
|
+
return entry.isFile();
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Git identity resolution, read directly off disk (`.git/config`, worktree
|
|
17
|
+
* aware) rather than shelling out to `git` — no subprocess, no `git` on
|
|
18
|
+
* `PATH`. Mirrors the house precedent in otel's VCS collector. The remote is
|
|
19
|
+
* log-only in v1: nothing is ever derived from it, so the accepted trade-off
|
|
20
|
+
* (raw config misses `url.<base>.insteadOf` rewrites that `git remote get-url`
|
|
21
|
+
* would apply) is harmless; revisit if resolve-by-remote ever lands.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Walk up from `startDir` to the nearest ancestor that contains a `.git` entry.
|
|
25
|
+
* `.git` may be a directory (a normal checkout) or a file (a linked worktree,
|
|
26
|
+
* pointing at the common dir). Returns the repo root, or `undefined` when no
|
|
27
|
+
* enclosing repository exists.
|
|
28
|
+
*/
|
|
29
|
+
export async function resolveRepoRoot(startDir) {
|
|
30
|
+
let current = resolve(startDir);
|
|
31
|
+
for (;;) {
|
|
32
|
+
const gitEntry = join(current, ".git");
|
|
33
|
+
try {
|
|
34
|
+
const entry = await stat(gitEntry);
|
|
35
|
+
if (entry.isDirectory() || entry.isFile()) {
|
|
36
|
+
return current;
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
const parent = dirname(current);
|
|
40
|
+
if (parent === current) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
current = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the git "common dir" (where `config` lives) for a repo root, handling
|
|
48
|
+
* the linked-worktree `.git`-file case:
|
|
49
|
+
* - `.git` is a directory → common dir is `.git` itself.
|
|
50
|
+
* - `.git` is a file → read `gitdir: <path>`; the pointed-at worktree
|
|
51
|
+
* gitdir may carry a `commondir` file (relative to itself) that names the
|
|
52
|
+
* shared object/config dir. Falls back to the worktree gitdir when no
|
|
53
|
+
* `commondir` marker exists (a bare gitdir without worktree layout).
|
|
54
|
+
* Returns the directory containing the `config` file, or `undefined`.
|
|
55
|
+
*
|
|
56
|
+
* The `gitdir:` pointer is attacker-influenced (the `.git` file is part of the
|
|
57
|
+
* tree the user opened), so it is validated defensively: the target must be a
|
|
58
|
+
* real directory (no symlinks), must contain a `HEAD` file (git's own marker
|
|
59
|
+
* that a worktree gitdir exists there), and its `config` must be a regular
|
|
60
|
+
* file — a FIFO/socket/device/symlink is rejected so a crafted tree cannot
|
|
61
|
+
* hang the config hook on a blocking read or reach into unrelated files.
|
|
62
|
+
*/
|
|
63
|
+
export async function resolveGitCommonDir(repoRoot) {
|
|
64
|
+
const gitEntry = join(repoRoot, ".git");
|
|
65
|
+
let commonDir;
|
|
66
|
+
try {
|
|
67
|
+
const entry = await stat(gitEntry);
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
commonDir = gitEntry;
|
|
70
|
+
} else if (entry.isFile()) {
|
|
71
|
+
const pointer = await readFile(gitEntry, "utf8");
|
|
72
|
+
const match = /^gitdir:\s*(.+)$/m.exec(pointer);
|
|
73
|
+
if (!match) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const gitDir = isAbsolute(match[1].trim()) ? match[1].trim() : join(dirname(gitEntry), match[1].trim());
|
|
77
|
+
try {
|
|
78
|
+
const dirEntry = await lstat(gitDir);
|
|
79
|
+
if (!dirEntry.isDirectory()) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
// git writes a HEAD at the top of every (worktree) gitdir; its presence
|
|
86
|
+
// distinguishes a real git dir from an arbitrary directory the pointer
|
|
87
|
+
// happened to name.
|
|
88
|
+
if (!await isRegularFile(join(gitDir, "HEAD"))) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const marker = await readFile(join(gitDir, "commondir"), {
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
signal: AbortSignal.timeout(GIT_CONFIG_READ_TIMEOUT_MS)
|
|
95
|
+
});
|
|
96
|
+
const common = marker.trim();
|
|
97
|
+
commonDir = isAbsolute(common) ? common : join(gitDir, common);
|
|
98
|
+
} catch {
|
|
99
|
+
commonDir = gitDir;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return commonDir;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Minimal git-config reader for the `[remote "origin"]`/`url` entry — enough
|
|
111
|
+
* for the one key repo-auth (or any embedder) needs. Handles quoted section
|
|
112
|
+
* names and values; comments at line start; case-sensitive section names are
|
|
113
|
+
* not matched, only `remote "<name>"` sections.
|
|
114
|
+
*/
|
|
115
|
+
export function parseGitConfig(text) {
|
|
116
|
+
const sections = new Map();
|
|
117
|
+
let currentRemote;
|
|
118
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
119
|
+
const line = rawLine.trim();
|
|
120
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const sectionMatch = /^\[(.+)]$/.exec(line);
|
|
124
|
+
if (sectionMatch) {
|
|
125
|
+
const header = sectionMatch[1].trim();
|
|
126
|
+
const subsection = /^remote\s+"([^"]+)"$/.exec(header);
|
|
127
|
+
currentRemote = subsection?.[1];
|
|
128
|
+
if (currentRemote) {
|
|
129
|
+
sections.set(`remote.${currentRemote}`, new Map());
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!currentRemote) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const eq = line.indexOf("=");
|
|
137
|
+
if (eq < 0) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const key = line.slice(0, eq).trim();
|
|
141
|
+
let value = line.slice(eq + 1).trim();
|
|
142
|
+
if (value.startsWith("\"") && value.endsWith("\"")) {
|
|
143
|
+
value = value.slice(1, -1);
|
|
144
|
+
}
|
|
145
|
+
sections.get(`remote.${currentRemote}`)?.set(key, value);
|
|
146
|
+
}
|
|
147
|
+
return sections;
|
|
148
|
+
}
|
|
149
|
+
/** Extract the raw `origin` remote URL from git-config text, if any. */
|
|
150
|
+
export function parseOriginRemote(configText) {
|
|
151
|
+
return parseGitConfig(configText).get("remote.origin")?.get("url");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Normalize a git remote URL to a `<host>/<path>` string safe for logging and
|
|
155
|
+
* repo correlation:
|
|
156
|
+
* - strips userinfo (`user@` / `user:pass@`) in both https and scp forms,
|
|
157
|
+
* - folds scp-style `git@host:org/repo.git` onto the https shape,
|
|
158
|
+
* - drops a trailing `.git` and any trailing slashes,
|
|
159
|
+
* - drops query / fragment.
|
|
160
|
+
* Returns `undefined` for anything that is not a recognizable remote (local
|
|
161
|
+
* paths, garbage) — callers treat that as "no remote".
|
|
162
|
+
*/
|
|
163
|
+
export function normalizeRemote(url) {
|
|
164
|
+
const candidate = url.trim();
|
|
165
|
+
if (!candidate) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
// scp-like: [user@]host:path — the path must not start with `/` so a
|
|
169
|
+
// `scheme://…` URL (whose colon sits right before `//`) does not match.
|
|
170
|
+
const scp = /^(?:[^@/]+@)?([^:/]+):([^/].*)$/.exec(candidate);
|
|
171
|
+
if (scp) {
|
|
172
|
+
const path = stripDotGit(scp[2].split(/[?#]/, 1)[0]);
|
|
173
|
+
return path ? `${scp[1]}/${path}` : scp[1];
|
|
174
|
+
}
|
|
175
|
+
if (/^[\w+.-]+:\/\//.test(candidate)) {
|
|
176
|
+
try {
|
|
177
|
+
const parsed = new URL(candidate);
|
|
178
|
+
if (parsed.hostname) {
|
|
179
|
+
const path = stripDotGit(parsed.pathname.replace(/^\/+/, ""));
|
|
180
|
+
return path ? `${parsed.hostname}/${path}` : parsed.hostname;
|
|
181
|
+
}
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
function stripDotGit(path) {
|
|
187
|
+
let cleaned = path;
|
|
188
|
+
while (cleaned.endsWith("/")) {
|
|
189
|
+
cleaned = cleaned.slice(0, -1);
|
|
190
|
+
}
|
|
191
|
+
if (cleaned.endsWith(".git")) {
|
|
192
|
+
cleaned = cleaned.slice(0, -4);
|
|
193
|
+
}
|
|
194
|
+
while (cleaned.endsWith("/")) {
|
|
195
|
+
cleaned = cleaned.slice(0, -1);
|
|
196
|
+
}
|
|
197
|
+
return cleaned;
|
|
198
|
+
}
|
|
199
|
+
/** Read the normalized `origin` remote for a repo root, if any. */
|
|
200
|
+
export async function resolveOriginRemote(repoRoot) {
|
|
201
|
+
const commonDir = await resolveGitCommonDir(repoRoot);
|
|
202
|
+
if (!commonDir) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const configPath = join(commonDir, "config");
|
|
206
|
+
// Only regular files: a crafted `.git` pointer must not let the plugin hang
|
|
207
|
+
// on a FIFO read or follow a symlink to an arbitrary file.
|
|
208
|
+
if (!await isRegularFile(configPath)) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
let configText;
|
|
212
|
+
try {
|
|
213
|
+
const configStats = await stat(configPath);
|
|
214
|
+
if (configStats.size > GIT_CONFIG_MAX_BYTES) {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
configText = await readFile(configPath, {
|
|
218
|
+
encoding: "utf8",
|
|
219
|
+
signal: AbortSignal.timeout(GIT_CONFIG_READ_TIMEOUT_MS)
|
|
220
|
+
});
|
|
221
|
+
} catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
if (!configText) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
const raw = parseOriginRemote(configText);
|
|
228
|
+
return raw ? normalizeRemote(raw) : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"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","names":[],"sources":["../src/git.ts"],"version":3,"file":"git.js","sourceRoot":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./opencode.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"AAAA,SAAS,eAAe","names":[],"sources":["../src/index.ts"],"version":3,"file":"index.js","sourceRoot":""}
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { OpencodeRepoAuthPlugin, createOpencodeRepoAuthPlugin, type OpenCodePluginFactoryOptions } from "./opencode.js";
|
|
2
|
+
export { RepoAuthPlugin, isProjectTokenUsable, HUMAN_IDENTITY, DEFAULT_CACHE_NAMESPACE, repoAuthCacheDir, type RepoAuthPluginOptions } from "./plugin.js";
|
|
3
|
+
export { REPO_AUTH_META_KEY, parseRepoAuthOptions, hasOAuth2Conflict, type RepoAuthConfig, type RepoAuthParseResult } from "./config.js";
|
|
4
|
+
export { normalizeRemote, parseGitConfig, parseOriginRemote, resolveOriginRemote, resolveRepoRoot } from "./git.js";
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { OpencodeRepoAuthPlugin, createOpencodeRepoAuthPlugin } from "./opencode.js";
|
|
2
|
+
export { RepoAuthPlugin, isProjectTokenUsable, HUMAN_IDENTITY, DEFAULT_CACHE_NAMESPACE, repoAuthCacheDir } from "./plugin.js";
|
|
3
|
+
export { REPO_AUTH_META_KEY, parseRepoAuthOptions, hasOAuth2Conflict } from "./config.js";
|
|
4
|
+
export { normalizeRemote, parseGitConfig, parseOriginRemote, resolveOriginRemote, resolveRepoRoot } from "./git.js";
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=lib.js.map
|
package/dist/lib.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"mappings":"AAAA,SACE,wBACA,oCAEK;AACP,SACE,gBACA,sBACA,gBACA,yBACA,wBAEK;AACP,SACE,oBACA,sBACA,yBAGK;AACP,SACE,iBACA,gBACA,mBACA,qBACA,uBACK","names":[],"sources":["../src/lib.ts"],"version":3,"file":"lib.js","sourceRoot":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import { type Logger } from "@vymalo/opencode-auth-core/lib";
|
|
3
|
+
export interface OpenCodePluginFactoryOptions {
|
|
4
|
+
logger?: Logger;
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
onAuthorizationUrl?: (url: string) => Promise<void> | void;
|
|
7
|
+
cacheDir?: string;
|
|
8
|
+
/** Repo root to resolve git identity from (defaults to `process.cwd()`). */
|
|
9
|
+
cwd?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function createOpencodeRepoAuthPlugin(factoryOptions?: OpenCodePluginFactoryOptions): Plugin;
|
|
12
|
+
export declare const OpencodeRepoAuthPlugin: Plugin;
|
|
13
|
+
export default OpencodeRepoAuthPlugin;
|