Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions plugins/npm/access_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package npm

import (
"context"
"os"
"strings"

"path/filepath"

"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/importer"
"github.com/1Password/shell-plugins/sdk/provision"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
"gopkg.in/ini.v1"
)

func AccessToken() schema.CredentialType {
return schema.CredentialType{
Name: credname.AccessToken,
DocsURL: sdk.URL("https://docs.npmjs.com/creating-and-viewing-access-tokens"),
ManagementURL: sdk.URL("https://www.npmjs.com/settings/<username>/tokens/"),
Fields: []schema.CredentialField{
{
Name: fieldname.Token,
MarkdownDescription: "Token used to authenticate to NPM.",
Secret: true,
Composition: &schema.ValueComposition{
Length: 36,
Prefix: "npm_",
Charset: schema.Charset{
Uppercase: true,
Lowercase: true,
Digits: true,
},
},
},
{
Name: fieldname.Organization,
MarkdownDescription: "The organization the access token is scoped for.",
Optional: true,
},
{
Name: fieldname.Host,
MarkdownDescription: "The registry host for the npm packages.",
Optional: true,
},
},
DefaultProvisioner: provision.TempFile(configFile,
provision.Filename(".npmrc"),
provision.AddArgs(
"--userconfig", "{{ .Path }}",
),
),
Importer: importer.TryAll(
TryNPMConfigFile(""),
TryGlobalNPMConfigFile("NPM_CONFIG_USERCONFIG", "~"),
),
}
}

func configFile(in sdk.ProvisionInput) ([]byte, error) {
contents := ""

if org, ok := in.ItemFields[fieldname.Organization]; ok && org != "" {
contents += "@" + strings.Trim(org, "@") + ":"
}
if host, ok := in.ItemFields[fieldname.Host]; ok && host != "" {
contents += "//" + strings.Trim(host, "/") + "/:"
} else {
contents += "//registry.npmjs.org/:"
}

contents += "_authToken="

if token, ok := in.ItemFields[fieldname.Token]; ok {
contents += token
}

return []byte(contents), nil
}

func TryGlobalNPMConfigFile(env string, defaultPath string) sdk.Importer {
path := os.Getenv(env)
if path == "" {
path = defaultPath
}
return TryNPMConfigFile(path)
}

func TryNPMConfigFile(path string) sdk.Importer {
return importer.TryFile(filepath.Join(path, ".npmrc"), func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) {

// don't use colon as a delimiter, since it is used in the .npmrc file as a delimiter
// between the scope, registry and configuration key
configs, err := ini.LoadSources(ini.LoadOptions{KeyValueDelimiters: "="}, []byte(contents))
if err != nil {
out.AddError(err)
}

// sections are not supported in .npmrc
section, err := configs.GetSection(ini.DefaultSection)
if err != nil {
out.AddError(err)
}
for _, key := range section.Keys() {
if strings.Contains(key.Name(), "_authToken") {

keyParts := strings.Split(key.Name(), ":")

registry := ""
scope := ""
hint := ""
if len(keyParts) == 2 {
registry = strings.Trim(keyParts[0], "/")
hint = registry
} else if len(keyParts) == 3 {
registry = strings.Trim(keyParts[1], "/")
scope = strings.Trim(keyParts[0], "@")
hint = "@" + scope + ":" + registry
}

out.AddCandidate(sdk.ImportCandidate{
Fields: map[sdk.FieldName]string{
fieldname.Token: key.Value(),
fieldname.Host: registry,
fieldname.Organization: scope,
},
NameHint: importer.SanitizeNameHint(hint),
})
}
}
})
}
70 changes: 70 additions & 0 deletions plugins/npm/access_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package npm

import (
"testing"

"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/plugintest"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
)

func TestAccessTokenProvisioner(t *testing.T) {
plugintest.TestProvisioner(t, AccessToken().DefaultProvisioner, map[string]plugintest.ProvisionCase{
"default": {
ItemFields: map[sdk.FieldName]string{
fieldname.Token: "npm_4F1h0xp0lBn0XvZ4RGfpnpoawECBMEXAMPLE",
},
ExpectedOutput: sdk.ProvisionOutput{
Files: map[string]sdk.OutputFile{
"/tmp/.npmrc": {Contents: []byte("//registry.npmjs.org/:_authToken=npm_4F1h0xp0lBn0XvZ4RGfpnpoawECBMEXAMPLE")},
},
CommandLine: []string{"--userconfig", "/tmp/.npmrc"},
},
},
"custom registry": {
ItemFields: map[sdk.FieldName]string{
fieldname.Token: "npm_4F1h0xp0lBn0XvZ4RGfpnpoawECBMEXAMPLE",
fieldname.Host: "my.registry.com",
},
ExpectedOutput: sdk.ProvisionOutput{
Files: map[string]sdk.OutputFile{
"/tmp/.npmrc": {Contents: []byte("//my.registry.com/:_authToken=npm_4F1h0xp0lBn0XvZ4RGfpnpoawECBMEXAMPLE")},
},
CommandLine: []string{"--userconfig", "/tmp/.npmrc"},
},
},
"scoped": {
ItemFields: map[sdk.FieldName]string{
fieldname.Token: "npm_4F1h0xp0lBn0XvZ4RGfpnpoawECBMEXAMPLE",
fieldname.Host: "my.registry.com",
fieldname.Organization: "op",
},
ExpectedOutput: sdk.ProvisionOutput{
Files: map[string]sdk.OutputFile{
"/tmp/.npmrc": {Contents: []byte("@op://my.registry.com/:_authToken=npm_4F1h0xp0lBn0XvZ4RGfpnpoawECBMEXAMPLE")},
},
CommandLine: []string{"--userconfig", "/tmp/.npmrc"},
},
},
})
}

func TestAccessTokenImporter(t *testing.T) {
plugintest.TestImporter(t, AccessToken().Importer, map[string]plugintest.ImportCase{
"config file": {
Files: map[string]string{
"~/.npmrc": plugintest.LoadFixture(t, ".npmrc-default-registry"),
},
ExpectedCandidates: []sdk.ImportCandidate{
{
NameHint: "registry.npmjs.org",
Fields: map[sdk.FieldName]string{
fieldname.Token: "npm_4F1h0xp0lBn0XvZ4RGfpnpoawECBMEXAMPLE",
fieldname.Host: "registry.npmjs.org",
fieldname.Organization: "",
},
},
},
},
})
}
45 changes: 45 additions & 0 deletions plugins/npm/npm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package npm

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/needsauth"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
)

func NPMCLI() schema.Executable {
return schema.Executable{
Name: "NPM CLI",
Runs: []string{"npm"},
DocsURL: sdk.URL("https://docs.npmjs.com/cli"),
NeedsAuth: needsauth.IfAll(
// not a complete list of commands that don't
// need auth, but probably the main ones
needsauth.NotForHelpOrVersion(),
needsauth.NotWhenContainsArgs("init"),
needsauth.NotWhenContainsArgs("?"),
needsauth.NotWhenContainsArgs("config"),
needsauth.NotWhenContainsArgs("help-search"),
needsauth.NotWhenContainsArgs("login"),
needsauth.NotWhenContainsArgs("logout"),
needsauth.NotWhenContainsArgs("prune"),
needsauth.NotWhenContainsArgs("shrinkwrap"),
needsauth.NotWhenContainsArgs("start"),
needsauth.NotWhenContainsArgs("run-script"),
needsauth.NotWhenContainsArgs("run"),
needsauth.NotWhenContainsArgs("rum"),
needsauth.NotWhenContainsArgs("urn"),
needsauth.NotWhenContainsArgs("uninstall"),
needsauth.NotWhenContainsArgs("unlink"),
needsauth.NotWhenContainsArgs("remove"),
needsauth.NotWhenContainsArgs("rm"),
needsauth.NotWhenContainsArgs("r"),
needsauth.NotWhenContainsArgs("un"),
),
Uses: []schema.CredentialUsage{
{
Name: credname.AccessToken,
},
},
}
}
22 changes: 22 additions & 0 deletions plugins/npm/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package npm

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/schema"
)

func New() schema.Plugin {
return schema.Plugin{
Name: "npm",
Platform: schema.PlatformInfo{
Name: "NPM",
Homepage: sdk.URL("https://npmjs.com"),
},
Credentials: []schema.CredentialType{
AccessToken(),
},
Executables: []schema.Executable{
NPMCLI(),
},
}
}
1 change: 1 addition & 0 deletions plugins/npm/test-fixtures/.npmrc-custom-registry
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//my.registry.com/:_authToken=npm_4F1h0xp0lBn0XvZ4RGfpnpoawECBMEXAMPLE
1 change: 1 addition & 0 deletions plugins/npm/test-fixtures/.npmrc-default-registry
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//registry.npmjs.org/:_authToken=npm_4F1h0xp0lBn0XvZ4RGfpnpoawECBMEXAMPLE
1 change: 1 addition & 0 deletions plugins/npm/test-fixtures/.npmrc-scoped
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@op://my.registry.com/:_authToken=npm_4F1h0xp0lBn0XvZ4RGfpnpoawECBMEXAMPLE
Loading