@khanhcan148/mk 0.1.18 → 0.1.19
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/README.md +1 -1
- package/package.json +1 -1
- package/src/commands/auth.js +12 -1
- package/src/commands/update.js +1 -1
- package/src/lib/auth.js +23 -5
- package/src/lib/copy.js +9 -1
- package/src/lib/download.js +18 -0
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ cp -r .claude ~/.claude/
|
|
|
88
88
|
|
|
89
89
|
```
|
|
90
90
|
├── .claude/
|
|
91
|
-
│ ├── agents/ #
|
|
91
|
+
│ ├── agents/ # 36 agents (5 primary + 31 utility: implementers, quality, docs, specialized, concerns, brainstorm critics)
|
|
92
92
|
│ ├── skills/ # 67 skill packages (SKILL.md + scripts/references/assets)
|
|
93
93
|
│ │ ├── mk-*/ # 20 workflow commands (/mk-audit, /mk-brainstorm, /mk-log-analysis, /mk-overview, /mk-wiki, etc.)
|
|
94
94
|
│ │ └── ... # Domain skills (frontend, backend, testing, browser automation, etc.)
|
package/package.json
CHANGED
package/src/commands/auth.js
CHANGED
|
@@ -138,6 +138,17 @@ export async function statusAction(deps = {}) {
|
|
|
138
138
|
process.stdout.write(chalk.green('Repo access: granted\n'));
|
|
139
139
|
} else {
|
|
140
140
|
process.stdout.write(chalk.red('Repo access: denied\n'));
|
|
141
|
-
|
|
141
|
+
const hasRepoScope = access.scopes?.some((s) => s === 'repo');
|
|
142
|
+
if (access.status === 404 && access.scopes && !hasRepoScope) {
|
|
143
|
+
// 404 with no 'repo' scope usually means the token can't see the private
|
|
144
|
+
// KIT_REPO — re-authenticating picks up the new default scope.
|
|
145
|
+
const current = access.scopes.length ? access.scopes.join(', ') : 'none';
|
|
146
|
+
process.stdout.write(
|
|
147
|
+
`Token is missing the 'repo' scope (current scopes: ${current}).\n` +
|
|
148
|
+
"Run 'mk auth logout && mk auth login' to re-authenticate with the required scope.\n"
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
process.stdout.write('Contact the repository owner for collaborator access.\n');
|
|
152
|
+
}
|
|
142
153
|
}
|
|
143
154
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -32,7 +32,7 @@ import { isEmptyDir } from '../lib/fs-utils.js';
|
|
|
32
32
|
* @param {string} str
|
|
33
33
|
* @returns {string}
|
|
34
34
|
*/
|
|
35
|
-
function stripTerminalEscapes(str) {
|
|
35
|
+
export function stripTerminalEscapes(str) {
|
|
36
36
|
return str
|
|
37
37
|
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '') // CSI sequences
|
|
38
38
|
.replace(/\x1b\].*?(\x07|\x1b\\)/gs, '') // OSC sequences (dotAll for multiline)
|
package/src/lib/auth.js
CHANGED
|
@@ -82,8 +82,15 @@ export async function validateToken(token) {
|
|
|
82
82
|
/**
|
|
83
83
|
* Check whether the token has access to the kit repository.
|
|
84
84
|
*
|
|
85
|
+
* Returns the HTTP status and any scopes reported via the `x-oauth-scopes`
|
|
86
|
+
* response header so callers can disambiguate common failure modes:
|
|
87
|
+
* - 404 + no 'repo' scope → token lacks scope (most common for a private KIT_REPO)
|
|
88
|
+
* - 404 + has 'repo' scope → authenticated account is not a collaborator
|
|
89
|
+
* - 403 → SSO or rate-limit block
|
|
90
|
+
* - 0 → network error
|
|
91
|
+
*
|
|
85
92
|
* @param {string} token
|
|
86
|
-
* @returns {Promise<{ accessible: boolean }>}
|
|
93
|
+
* @returns {Promise<{ accessible: boolean, status: number, scopes: string[] }>}
|
|
87
94
|
*/
|
|
88
95
|
export async function checkRepoAccess(token) {
|
|
89
96
|
try {
|
|
@@ -93,12 +100,17 @@ export async function checkRepoAccess(token) {
|
|
|
93
100
|
Accept: 'application/vnd.github.v3+json'
|
|
94
101
|
}
|
|
95
102
|
});
|
|
96
|
-
return { accessible: res.ok };
|
|
103
|
+
return { accessible: res.ok, status: res.status, scopes: parseScopes(res) };
|
|
97
104
|
} catch {
|
|
98
|
-
return { accessible: false };
|
|
105
|
+
return { accessible: false, status: 0, scopes: [] };
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
|
|
109
|
+
function parseScopes(res) {
|
|
110
|
+
const raw = res.headers?.get?.('x-oauth-scopes') || '';
|
|
111
|
+
return raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
|
|
102
114
|
// ---------------------------------------------------------------------------
|
|
103
115
|
// OAuth Device Flow
|
|
104
116
|
// ---------------------------------------------------------------------------
|
|
@@ -127,9 +139,15 @@ export async function startDeviceFlow(opts = {}) {
|
|
|
127
139
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
128
140
|
Accept: 'application/json'
|
|
129
141
|
},
|
|
130
|
-
//
|
|
142
|
+
// KIT_REPO is private, so 'repo' scope is required to read it. Default to 'repo'.
|
|
143
|
+
// Override with MK_OAUTH_SCOPE for public forks (e.g. MK_OAUTH_SCOPE='' for no scope,
|
|
144
|
+
// or MK_OAUTH_SCOPE='public_repo' for public-only access).
|
|
145
|
+
// `??` (not `||`) so an explicit empty string is honored as an opt-out.
|
|
131
146
|
// Use URLSearchParams to prevent parameter injection via env var containing '&' chars.
|
|
132
|
-
body: new URLSearchParams({
|
|
147
|
+
body: new URLSearchParams({
|
|
148
|
+
client_id: GITHUB_CLIENT_ID,
|
|
149
|
+
scope: process.env.MK_OAUTH_SCOPE ?? 'repo'
|
|
150
|
+
}).toString()
|
|
133
151
|
});
|
|
134
152
|
|
|
135
153
|
if (!codeRes.ok) {
|
package/src/lib/copy.js
CHANGED
|
@@ -81,6 +81,14 @@ export function collectDiskFiles(targetDir) {
|
|
|
81
81
|
return results;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @typedef {Object} FileEntry
|
|
86
|
+
* @property {string} relativePath - Path relative to the target .claude/ (POSIX separators, e.g. ".claude/agents/foo.md")
|
|
87
|
+
* @property {string} absolutePath - Destination absolute path under targetDir (where the file is copied to)
|
|
88
|
+
* @property {string} sourceAbsPath - Source absolute path under sourceDir (where the file is copied from)
|
|
89
|
+
* @property {number} size - File size in bytes, captured at copy time
|
|
90
|
+
*/
|
|
91
|
+
|
|
84
92
|
/**
|
|
85
93
|
* Copy kit files from sourceDir (.claude/) to targetDir (.claude/).
|
|
86
94
|
* Only copies KIT_SUBDIRS (agents/, skills/, workflows/).
|
|
@@ -88,7 +96,7 @@ export function collectDiskFiles(targetDir) {
|
|
|
88
96
|
* @param {string} sourceDir - Absolute path to source .claude/
|
|
89
97
|
* @param {string} targetDir - Absolute path to target .claude/
|
|
90
98
|
* @param {{ dryRun: boolean }} options
|
|
91
|
-
* @returns {
|
|
99
|
+
* @returns {FileEntry[]}
|
|
92
100
|
* @remarks Naming convention: `absolutePath` is the destination (under targetDir),
|
|
93
101
|
* `sourceAbsPath` is the source (under sourceDir). The asymmetry is intentional —
|
|
94
102
|
* renaming would break consumers (update.js). See DEBT-016.
|
package/src/lib/download.js
CHANGED
|
@@ -39,6 +39,24 @@ export function assertGitHubHostname(url) {
|
|
|
39
39
|
} catch {
|
|
40
40
|
throw new Error(`SSRF guard: invalid URL "${url}"`);
|
|
41
41
|
}
|
|
42
|
+
// H9/H10: block non-TLS schemes. Plain http:// to github.com can be MITM-redirected,
|
|
43
|
+
// and the kit download path has no reason to accept anything but https.
|
|
44
|
+
if (parsed.protocol !== 'https:') {
|
|
45
|
+
// Truncate to guard against log injection via crafted long / newline-containing schemes.
|
|
46
|
+
const safeScheme = String(parsed.protocol).slice(0, 20);
|
|
47
|
+
throw new Error(
|
|
48
|
+
`SSRF guard: scheme "${safeScheme}" is not allowed. ` +
|
|
49
|
+
`Only https: is permitted for kit downloads.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
// Block userinfo-prefixed URLs (user:pass@host) — they can mask the true
|
|
53
|
+
// hostname in server-side url parsers or confuse logging/auditing.
|
|
54
|
+
if (parsed.username || parsed.password) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`SSRF guard: userinfo-prefixed URL is not allowed. ` +
|
|
57
|
+
`Credentials in the URL (user:pass@host) are rejected for kit downloads.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
42
60
|
const { hostname } = parsed;
|
|
43
61
|
if (!ALLOWED_HOSTS.has(hostname)) {
|
|
44
62
|
throw new Error(
|