Merge remote-tracking branch 'upstream' into forgejo
Some checks failed
Integration tests for the release process / release-simulation (push) Has been cancelled
/ release (push) Has been cancelled
testing / backend-checks (push) Has been cancelled
testing / frontend-checks (push) Has been cancelled
testing / test-unit (push) Has been cancelled
testing / test-e2e (push) Has been cancelled
testing / test-remote-cacher (redis) (push) Has been cancelled
testing / test-remote-cacher (valkey) (push) Has been cancelled
testing / test-remote-cacher (garnet) (push) Has been cancelled
testing / test-remote-cacher (redict) (push) Has been cancelled
testing / test-mysql (push) Has been cancelled
testing / test-pgsql (push) Has been cancelled
testing / test-sqlite (push) Has been cancelled
testing / security-check (push) Has been cancelled

This commit is contained in:
Minecon724 2025-06-09 19:24:48 +02:00
commit 89613f09a7
Signed by: Minecon724
GPG key ID: A02E6E67AB961189
31 changed files with 1123 additions and 545 deletions

View file

@ -28,7 +28,7 @@ jobs:
runs-on: docker
container:
image: data.forgejo.org/renovate/renovate:40.40.0
image: data.forgejo.org/renovate/renovate:40.48.4
steps:
- name: Load renovate repo cache

View file

@ -48,7 +48,7 @@ GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasour
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.34.0 # renovate: datasource=go
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.2 # renovate: datasource=go
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.1 # renovate: datasource=go
RENOVATE_NPM_PACKAGE ?= renovate@40.40.0 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
RENOVATE_NPM_PACKAGE ?= renovate@40.48.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...

View file

@ -408,7 +408,7 @@ local addIssueLabelsOverrides(labels) =
regex: '',
type: 'query',
multi: true,
allValue: '.+'
allValue: '.+',
},
)
.addTemplate(
@ -423,7 +423,7 @@ local addIssueLabelsOverrides(labels) =
regex: '',
type: 'query',
multi: true,
allValue: '.+'
allValue: '.+',
},
)
.addTemplate(

View file

@ -183,7 +183,7 @@ RUN_USER = ; git
;;
;; For the built-in SSH server, choose the key exchange algorithms to support for SSH connections,
;; for system SSH this setting has no effect
;SSH_SERVER_KEY_EXCHANGES = curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1
;SSH_SERVER_KEY_EXCHANGES = mlkem768x25519-sha256, curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1
;;
;; For the built-in SSH server, choose the MACs to support for SSH connections,
;; for system SSH this setting has no effect

2
go.mod
View file

@ -17,7 +17,7 @@ require (
code.gitea.io/actions-proto-go v0.4.0
code.gitea.io/sdk/gitea v0.21.0
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.17.0
connectrpc.com/connect v1.18.1
github.com/42wim/httpsig v1.2.3
github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358

4
go.sum
View file

@ -32,8 +32,8 @@ code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY=
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM=
connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk=
connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=

View file

@ -239,3 +239,15 @@
num_members: 2
includes_all_repositories: false
can_create_org_repo: false
-
id: 25
org_id: 17
lower_name: super-user
name: super-user
description: ""
authorize: 3
num_repos: 0
num_members: 0
includes_all_repositories: 0
can_create_org_repo: 0

View file

@ -329,3 +329,10 @@
team_id: 22
type: 3
access_mode: 1
-
id: 84
org_id: 17
team_id: 25
type: 3
access_mode: 3

View file

@ -642,7 +642,7 @@
num_following: 0
num_stars: 0
num_repos: 2
num_teams: 3
num_teams: 4
num_members: 4
visibility: 0
repo_admin_change_team_access: false

View file

@ -235,7 +235,7 @@ func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...str
if attach.UUID == "" {
return errors.New("attachment uuid should be not blank")
}
if attach.ExternalURL != "" && !validation.IsValidExternalURL(attach.ExternalURL) {
if attach.ExternalURL != "" && !validation.IsValidReleaseAssetURL(attach.ExternalURL) {
return ErrInvalidExternalURL{ExternalURL: attach.ExternalURL}
}
_, err := db.GetEngine(ctx).Where("uuid=?", attach.UUID).Cols(cols...).Update(attach)
@ -244,7 +244,7 @@ func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...str
// UpdateAttachment updates the given attachment in database
func UpdateAttachment(ctx context.Context, atta *Attachment) error {
if atta.ExternalURL != "" && !validation.IsValidExternalURL(atta.ExternalURL) {
if atta.ExternalURL != "" && !validation.IsValidReleaseAssetURL(atta.ExternalURL) {
return ErrInvalidExternalURL{ExternalURL: atta.ExternalURL}
}
sess := db.GetEngine(ctx).Cols("name", "issue_id", "release_id", "comment_id", "download_count")

View file

@ -35,6 +35,8 @@ type ServeHeaderOptions struct {
Filename string
CacheDuration time.Duration // defaults to 5 minutes
LastModified time.Time
AdditionalHeaders http.Header
RedirectStatusCode int
}
// ServeSetHeaders sets necessary content serve headers
@ -82,6 +84,12 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
}
if opts.AdditionalHeaders != nil {
for k, v := range opts.AdditionalHeaders {
header[k] = v
}
}
}
// ServeData download file from io.Reader

View file

@ -84,6 +84,13 @@ func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) {
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
var image oci.Image
if err := json.NewDecoder(r).Decode(&image); err != nil {
// Handle empty config blobs (common in OCI artifacts)
if err == io.EOF {
return &Metadata{
Type: TypeOCI,
Platform: DefaultPlatform,
}, nil
}
return nil, err
}

View file

@ -4,6 +4,7 @@
package container
import (
"io"
"strings"
"testing"
@ -60,3 +61,49 @@ func TestParseImageConfig(t *testing.T) {
assert.Equal(t, projectURL, metadata.ProjectURL)
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
}
func TestParseImageConfigEmptyBlob(t *testing.T) {
t.Run("Empty config blob (EOF)", func(t *testing.T) {
// Test empty reader (simulates empty config blob common in OCI artifacts)
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(""))
require.NoError(t, err)
assert.Equal(t, TypeOCI, metadata.Type)
assert.Equal(t, DefaultPlatform, metadata.Platform)
assert.Empty(t, metadata.Description)
assert.Empty(t, metadata.Authors)
assert.Empty(t, metadata.Labels)
assert.Empty(t, metadata.Manifests)
})
t.Run("Empty JSON object", func(t *testing.T) {
// Test minimal valid JSON config
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader("{}"))
require.NoError(t, err)
assert.Equal(t, TypeOCI, metadata.Type)
assert.Equal(t, DefaultPlatform, metadata.Platform)
assert.Empty(t, metadata.Description)
assert.Empty(t, metadata.Authors)
})
t.Run("Invalid JSON still returns error", func(t *testing.T) {
// Test that actual JSON errors (not EOF) are still returned
_, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader("{invalid json"))
require.Error(t, err)
assert.NotEqual(t, io.EOF, err)
})
t.Run("OCI artifact with empty config", func(t *testing.T) {
// Test OCI artifact scenario with minimal config
configOCI := `{"config": {}}`
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI))
require.NoError(t, err)
assert.Equal(t, TypeOCI, metadata.Type)
assert.Equal(t, DefaultPlatform, metadata.Platform)
assert.Empty(t, metadata.Description)
assert.Empty(t, metadata.Authors)
assert.Empty(t, metadata.ImageLayers)
})
}

View file

@ -56,7 +56,7 @@ var SSH = struct {
Domain: "",
Port: 22,
ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"},
ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"},
ServerKeyExchanges: []string{"mlkem768x25519-sha256", "curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"},
ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"},
KeygenPath: "",
MinimumKeySizeCheck: true,

View file

@ -75,6 +75,11 @@ func IsValidExternalURL(uri string) bool {
return true
}
// IsValidReleaseAssetURL checks if the URL is valid for external release assets
func IsValidReleaseAssetURL(uri string) bool {
return IsValidURL(uri)
}
// IsValidExternalTrackerURLFormat checks if URL matches required syntax for external trackers
func IsValidExternalTrackerURLFormat(uri string) bool {
if !IsValidExternalURL(uri) {

View file

@ -2985,8 +2985,6 @@ teams.invite_team_member.list = Pending invitations
teams.delete_team_title = Delete team
teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue?
teams.delete_team_success = The team has been deleted.
teams.read_permission_desc = This team grants <strong>Read</strong> access: members can view and clone team repositories.
teams.write_permission_desc = This team grants <strong>Write</strong> access: members can read from and push to team repositories.
teams.admin_permission_desc = This team grants <strong>Administrator</strong> access: members can read from, push to and add collaborators to team repositories.
teams.create_repo_permission_desc = Additionally, this team grants <strong>Create repository</strong> permission: members can create new repositories in organization.
teams.repositories = Team repositories

928
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -66,7 +66,7 @@
"@stylistic/eslint-plugin-js": "4.4.1",
"@stylistic/stylelint-plugin": "3.1.2",
"@vitejs/plugin-vue": "5.2.4",
"@vitest/coverage-v8": "3.2.2",
"@vitest/coverage-v8": "3.2.3",
"@vitest/eslint-plugin": "1.2.1",
"@vue/test-utils": "2.4.6",
"eslint": "9.28.0",
@ -76,7 +76,7 @@
"eslint-plugin-no-jquery": "3.1.1",
"eslint-plugin-no-use-extend-native": "0.7.2",
"eslint-plugin-playwright": "2.2.0",
"eslint-plugin-regexp": "2.8.0",
"eslint-plugin-regexp": "2.9.0",
"eslint-plugin-sonarjs": "3.0.2",
"eslint-plugin-unicorn": "59.0.1",
"eslint-plugin-toml": "0.12.0",
@ -87,7 +87,7 @@
"globals": "16.1.0",
"happy-dom": "17.6.3",
"license-checker-rseidelsohn": "4.4.2",
"markdownlint-cli": "0.44.0",
"markdownlint-cli": "0.45.0",
"postcss-html": "1.8.0",
"sharp": "0.34.2",
"stylelint": "16.20.0",
@ -98,7 +98,7 @@
"typescript": "5.8.3",
"typescript-eslint": "8.33.1",
"vite-string-plugin": "1.3.4",
"vitest": "3.2.2"
"vitest": "3.2.3"
},
"browserslist": [
"defaults"

View file

@ -4,6 +4,7 @@
package container
import (
"bytes"
"errors"
"fmt"
"io"
@ -62,9 +63,6 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
if h.ContentType != "" {
resp.Header().Set("Content-Type", h.ContentType)
}
if h.ContentLength != 0 {
resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
}
if h.UploadUUID != "" {
resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
}
@ -72,17 +70,29 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
resp.Header().Set("Docker-Content-Digest", h.ContentDigest)
resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest))
}
if h.ContentLength >= 0 {
resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
}
resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
resp.WriteHeader(h.Status)
}
func jsonResponse(ctx *context.Context, status int, obj any) {
setResponseHeaders(ctx.Resp, &containerHeaders{
Status: status,
ContentType: "application/json",
})
if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
// Buffer the JSON content first to calculate correct Content-Length
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(obj); err != nil {
log.Error("JSON encode: %v", err)
return
}
setResponseHeaders(ctx.Resp, &containerHeaders{
Status: status,
ContentType: "application/json",
ContentLength: int64(buf.Len()),
})
if _, err := buf.WriteTo(ctx.Resp); err != nil {
log.Error("JSON write: %v", err)
}
}
@ -691,33 +701,30 @@ func DeleteManifest(ctx *context.Context) {
func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
serveDirectReqParams := make(url.Values)
serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType))
s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams)
s, u, pf, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams)
if err != nil {
if errors.Is(err, packages_model.ErrPackageFileNotExist) {
apiError(ctx, http.StatusNotFound, err)
return
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
headers := &containerHeaders{
ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
ContentLength: pfd.Blob.Size,
Status: http.StatusOK,
opts := &context.ServeHeaderOptions{
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
RedirectStatusCode: http.StatusTemporaryRedirect,
AdditionalHeaders: map[string][]string{
"Docker-Distribution-Api-Version": {"registry/2.0"},
},
}
if u != nil {
headers.Status = http.StatusTemporaryRedirect
headers.Location = u.String()
setResponseHeaders(ctx.Resp, headers)
return
if d := pfd.Properties.GetByName(container_module.PropertyDigest); d != "" {
opts.AdditionalHeaders["Docker-Content-Digest"] = []string{d}
opts.AdditionalHeaders["ETag"] = []string{fmt.Sprintf(`"%s"`, d)}
}
defer s.Close()
setResponseHeaders(ctx.Resp, headers)
if _, err := io.Copy(ctx.Resp, s); err != nil {
log.Error("Error whilst copying content to response: %v", err)
}
helper.ServePackageFile(ctx, s, u, pf, opts)
}
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
@ -725,7 +732,7 @@ func GetTagList(ctx *context.Context) {
image := ctx.Params("image")
if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
if err == packages_model.ErrPackageNotExist {
if errors.Is(err, packages_model.ErrPackageNotExist) {
apiErrorDefined(ctx, errNameUnknown)
} else {
apiError(ctx, http.StatusInternalServerError, err)

View file

@ -0,0 +1,124 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSetResponseHeaders(t *testing.T) {
t.Run("Content-Length for empty content", func(t *testing.T) {
recorder := httptest.NewRecorder()
setResponseHeaders(recorder, &containerHeaders{
Status: http.StatusOK,
ContentLength: 0, // Empty blob
ContentDigest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
})
assert.Equal(t, "0", recorder.Header().Get("Content-Length"))
assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", recorder.Header().Get("Docker-Content-Digest"))
assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version"))
assert.Equal(t, http.StatusOK, recorder.Code)
})
t.Run("Content-Length for non-empty content", func(t *testing.T) {
recorder := httptest.NewRecorder()
setResponseHeaders(recorder, &containerHeaders{
Status: http.StatusOK,
ContentLength: 1024,
ContentDigest: "sha256:abcd1234",
})
assert.Equal(t, "1024", recorder.Header().Get("Content-Length"))
assert.Equal(t, "sha256:abcd1234", recorder.Header().Get("Docker-Content-Digest"))
})
t.Run("All headers set correctly", func(t *testing.T) {
recorder := httptest.NewRecorder()
setResponseHeaders(recorder, &containerHeaders{
Status: http.StatusAccepted,
ContentLength: 512,
ContentDigest: "sha256:test123",
ContentType: "application/vnd.oci.image.manifest.v1+json",
Location: "/v2/test/repo/blobs/uploads/uuid123",
Range: "0-511",
UploadUUID: "uuid123",
})
assert.Equal(t, "512", recorder.Header().Get("Content-Length"))
assert.Equal(t, "sha256:test123", recorder.Header().Get("Docker-Content-Digest"))
assert.Equal(t, "application/vnd.oci.image.manifest.v1+json", recorder.Header().Get("Content-Type"))
assert.Equal(t, "/v2/test/repo/blobs/uploads/uuid123", recorder.Header().Get("Location"))
assert.Equal(t, "0-511", recorder.Header().Get("Range"))
assert.Equal(t, "uuid123", recorder.Header().Get("Docker-Upload-Uuid"))
assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version"))
assert.Equal(t, `"sha256:test123"`, recorder.Header().Get("ETag"))
assert.Equal(t, http.StatusAccepted, recorder.Code)
})
}
// TestResponseHeadersForEmptyBlobs tests the core fix for ORAS empty blob support
func TestResponseHeadersForEmptyBlobs(t *testing.T) {
t.Run("Content-Length set for empty blob", func(t *testing.T) {
recorder := httptest.NewRecorder()
// This tests the main fix: empty blobs should have Content-Length: 0
setResponseHeaders(recorder, &containerHeaders{
Status: http.StatusOK,
ContentLength: 0, // Empty blob (like empty config in ORAS artifacts)
ContentDigest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
})
// The key fix: Content-Length should be set even for 0-byte blobs
assert.Equal(t, "0", recorder.Header().Get("Content-Length"))
assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", recorder.Header().Get("Docker-Content-Digest"))
assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version"))
assert.Equal(t, `"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"`, recorder.Header().Get("ETag"))
assert.Equal(t, http.StatusOK, recorder.Code)
})
t.Run("Content-Length set for regular blob", func(t *testing.T) {
recorder := httptest.NewRecorder()
setResponseHeaders(recorder, &containerHeaders{
Status: http.StatusOK,
ContentLength: 1024,
ContentDigest: "sha256:abcd1234",
})
assert.Equal(t, "1024", recorder.Header().Get("Content-Length"))
assert.Equal(t, "sha256:abcd1234", recorder.Header().Get("Docker-Content-Digest"))
})
t.Run("All headers set correctly", func(t *testing.T) {
recorder := httptest.NewRecorder()
setResponseHeaders(recorder, &containerHeaders{
Status: http.StatusAccepted,
ContentLength: 512,
ContentDigest: "sha256:test123",
ContentType: "application/vnd.oci.image.manifest.v1+json",
Location: "/v2/test/repo/blobs/uploads/uuid123",
Range: "0-511",
UploadUUID: "uuid123",
})
assert.Equal(t, "512", recorder.Header().Get("Content-Length"))
assert.Equal(t, "sha256:test123", recorder.Header().Get("Docker-Content-Digest"))
assert.Equal(t, "application/vnd.oci.image.manifest.v1+json", recorder.Header().Get("Content-Type"))
assert.Equal(t, "/v2/test/repo/blobs/uploads/uuid123", recorder.Header().Get("Location"))
assert.Equal(t, "0-511", recorder.Header().Get("Range"))
assert.Equal(t, "uuid123", recorder.Header().Get("Docker-Upload-Uuid"))
assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version"))
assert.Equal(t, `"sha256:test123"`, recorder.Header().Get("ETag"))
assert.Equal(t, http.StatusAccepted, recorder.Code)
})
}

View file

@ -39,16 +39,9 @@ func LogAndProcessError(ctx *context.Context, status int, obj any, cb func(strin
}
}
// Serves the content of the package file
// ServePackageFile Serves the content of the package file
// If the url is set it will redirect the request, otherwise the content is copied to the response.
func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...*context.ServeHeaderOptions) {
if u != nil {
ctx.Redirect(u.String())
return
}
defer s.Close()
var opts *context.ServeHeaderOptions
if len(forceOpts) > 0 {
opts = forceOpts[0]
@ -59,5 +52,12 @@ func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf
}
}
if u != nil {
ctx.Redirect(u.String(), opts.RedirectStatusCode)
return
}
defer s.Close()
ctx.ServeContent(s, opts)
}

View file

@ -51,7 +51,7 @@ func NewExternalAttachment(ctx context.Context, attach *repo_model.Attachment) (
if attach.ExternalURL == "" {
return nil, fmt.Errorf("attachment %s should have a external url", attach.Name)
}
if !validation.IsValidExternalURL(attach.ExternalURL) {
if !validation.IsValidReleaseAssetURL(attach.ExternalURL) {
return nil, repo_model.ErrInvalidExternalURL{ExternalURL: attach.ExternalURL}
}

View file

@ -250,7 +250,7 @@ func (b *Base) PlainText(status int, text string) {
// Redirect redirects the request
func (b *Base) Redirect(location string, status ...int) {
code := http.StatusSeeOther
if len(status) == 1 {
if len(status) == 1 && status[0] > 0 {
code = status[0]
}

View file

@ -36,6 +36,7 @@ func TestRedirect(t *testing.T) {
cleanup()
has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy"
assert.Equal(t, c.keep, has, "url = %q", c.url)
assert.Equal(t, http.StatusSeeOther, resp.Code)
}
req, _ = http.NewRequest("GET", "/", nil)
@ -47,3 +48,24 @@ func TestRedirect(t *testing.T) {
assert.Equal(t, "/other", resp.Header().Get("HX-Redirect"))
assert.Equal(t, http.StatusNoContent, resp.Code)
}
func TestRedirectOptionalStatus(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")()
req, _ := http.NewRequest("GET", "/", nil)
cases := []struct {
expected int
actual int
}{
{expected: 303},
{http.StatusTemporaryRedirect, 307},
{http.StatusPermanentRedirect, 308},
}
for _, c := range cases {
resp := httptest.NewRecorder()
b, cleanup := NewBaseContext(resp, req)
b.Redirect("/", c.actual)
cleanup()
assert.Equal(t, c.expected, resp.Code)
}
}

View file

@ -42,11 +42,8 @@
<li>{{ctx.Locale.Tr "org.teams.can_create_org_repo"}}</li>
{{end}}
</ul>
{{if (eq .Team.AccessMode 2)}}
<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
{{ctx.Locale.Tr "org.teams.write_permission_desc"}}
{{else if (eq .Team.AccessMode 3)}}
<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
{{if (eq .Team.AccessMode 3)}}
{{ctx.Locale.Tr "org.teams.admin_permission_desc"}}
{{else}}
<table class="ui table">

View file

@ -98,7 +98,7 @@
{{range $release.Attachments}}
{{if .ExternalURL}}
<li>
<a class="tw-flex-1 flex-text-inline tw-font-bold" target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
<a class="tw-flex-1 flex-text-inline tw-font-bold" target="_blank" rel="nofollow" href="{{.DownloadURL}}">
{{svg "octicon-link-external" 16 "tw-mr-1"}}{{.Name}}
</a>
</li>

View file

@ -0,0 +1,116 @@
// @watch start
// templates/org/team/sidebar.tmpl
// @watch end
/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["assertPermissionsDetails", "assertRestrictedAccess", "assertOwnerPermissions"] }] */
import {expect, type Page} from '@playwright/test';
import {test} from './utils_e2e.ts';
type Permission = 'No access' | 'Write' | 'Read';
const UNIT_VALUES = [
'Code',
'Issues',
'Pull requests',
'Releases',
'Wiki',
'External Wiki',
'External issues',
'Projects',
'Packages',
'Actions',
] as const;
type Unit = typeof UNIT_VALUES[number];
const assertPermission = async (page: Page, name: Unit, permission: Permission) => {
await expect.soft(page.getByRole('row', {name}).getByRole('cell').nth(1)).toHaveText(permission);
};
const testTeamUrl = '/org/org17/teams/test_team';
const reviewTeamUrl = '/org/org17/teams/review_team';
const ownersUrl = '/org/org17/teams/owners';
const adminUrl = '/org/org17/teams/super-user';
const cases: Record<string, { read?: Unit[], write?: Unit[] }> = {
[testTeamUrl]: {write: ['Issues']},
[reviewTeamUrl]: {read: ['Code']},
};
const assertOwnerPermissions = async (page: Page, code: number = 200) => {
const response = await page.goto(ownersUrl);
expect(response?.status()).toBe(code);
await expect(page.getByText('Owners have full access to all repositories and have administrator access to the organization.')).toBeVisible();
};
const assertAdminPermissions = async (page: Page, code: number = 200) => {
const response = await page.goto(adminUrl);
expect(response?.status()).toBe(code);
await expect(page.getByText('This team grants Administrator access: members can read from, push to and add collaborators to team repositories.')).toBeVisible();
};
const assertRestrictedAccess = async (page: Page, ...urls: string[]) => {
for (const url of urls) {
expect((await page.goto(url))?.status(), 'should not see any details').toBe(404);
}
};
const assertPermissionsDetails = async (page: Page, url: (keyof typeof cases)) => {
const response = await page.goto(url);
expect(response?.status()).toBe(200);
const per = cases[url];
for (const unit of UNIT_VALUES) {
if (per.read?.includes(unit)) {
await assertPermission(page, unit, 'Read');
} else if (per.write?.includes(unit)) {
await assertPermission(page, unit, 'Write');
} else {
await assertPermission(page, unit, 'No access');
}
}
};
test.describe('Orga team overview', () => {
test.describe('admin', () => {
test.use({user: 'user1'});
test('should see all', async ({page}) => {
await assertPermissionsDetails(page, testTeamUrl);
await assertPermissionsDetails(page, reviewTeamUrl);
await assertOwnerPermissions(page);
await assertAdminPermissions(page);
});
});
test.describe('owner', () => {
test.use({user: 'user18'});
test('should see all', async ({page}) => {
await assertPermissionsDetails(page, testTeamUrl);
await assertPermissionsDetails(page, reviewTeamUrl);
await assertOwnerPermissions(page);
await assertAdminPermissions(page);
});
});
test.describe('reviewer team', () => {
test.use({user: 'user29'});
test('should only see permissions for `reviewer team` and restricted access to other resources', async ({page}) => {
await assertPermissionsDetails(page, reviewTeamUrl);
await assertRestrictedAccess(page, ownersUrl, testTeamUrl, adminUrl);
});
});
test.describe('test_team', () => {
test.use({user: 'user2'});
test('should only see permissions for test_team and restricted access to other resources', async ({page}) => {
await assertPermissionsDetails(page, testTeamUrl);
await assertRestrictedAccess(page, ownersUrl, reviewTeamUrl, adminUrl);
});
});
});

View file

@ -94,6 +94,8 @@ func createSessions(t testing.TB) {
"user1",
"user2",
"user12",
"user18",
"user29",
"user40",
}

View file

@ -56,7 +56,7 @@ func TestPackageContainer(t *testing.T) {
return values
}
images := []string{"test", "te/st"}
images := []string{"test", "te/st", "oras-artifact"}
tags := []string{"latest", "main"}
multiTag := "multi"
@ -177,6 +177,90 @@ func TestPackageContainer(t *testing.T) {
assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version"))
})
t.Run("ORAS Artifact Upload", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
image := "oras-artifact"
url := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, image)
// Empty config blob (common in ORAS artifacts)
emptyConfigDigest := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
emptyConfigContent := ""
// Upload empty config blob
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, emptyConfigDigest), bytes.NewReader([]byte(emptyConfigContent))).
AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusCreated)
assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, emptyConfigDigest), resp.Header().Get("Location"))
assert.Equal(t, emptyConfigDigest, resp.Header().Get("Docker-Content-Digest"))
// Verify empty blob exists and has correct Content-Length
req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, emptyConfigDigest)).
AddTokenAuth(userToken)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "0", resp.Header().Get("Content-Length")) // This was the main fix
assert.Equal(t, emptyConfigDigest, resp.Header().Get("Docker-Content-Digest"))
// Upload a small data blob (e.g., artifacthub metadata)
artifactData := `{"name":"test-artifact","version":"1.0.0"}`
artifactDigest := fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(artifactData)))
req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, artifactDigest), bytes.NewReader([]byte(artifactData))).
AddTokenAuth(userToken)
resp = MakeRequest(t, req, http.StatusCreated)
assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, artifactDigest), resp.Header().Get("Location"))
// Create OCI artifact manifest
artifactManifest := fmt.Sprintf(`{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.cncf.artifacthub.config.v1+yaml",
"config": {
"mediaType": "application/vnd.cncf.artifacthub.config.v1+yaml",
"digest": "%s",
"size": %d
},
"layers": [
{
"mediaType": "application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml",
"digest": "%s",
"size": %d
}
]
}`, emptyConfigDigest, len(emptyConfigContent), artifactDigest, len(artifactData))
artifactManifestDigest := fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(artifactManifest)))
// Upload artifact manifest
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/artifact-v1", url), bytes.NewReader([]byte(artifactManifest))).
AddTokenAuth(userToken).
SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json")
resp = MakeRequest(t, req, http.StatusCreated)
assert.Equal(t, fmt.Sprintf("/v2/%s/%s/manifests/artifact-v1", user.Name, image), resp.Header().Get("Location"))
assert.Equal(t, artifactManifestDigest, resp.Header().Get("Docker-Content-Digest"))
// Verify manifest can be retrieved
req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/artifact-v1", url)).
AddTokenAuth(userToken).
SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json")
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "application/vnd.oci.image.manifest.v1+json", resp.Header().Get("Content-Type"))
assert.Equal(t, artifactManifestDigest, resp.Header().Get("Docker-Content-Digest"))
// Verify package was created with correct metadata
pvs, err := packages_model.GetVersionsByPackageType(db.DefaultContext, user.ID, packages_model.TypeContainer)
require.NoError(t, err)
found := false
for _, pv := range pvs {
if pv.LowerVersion == "artifact-v1" {
found = true
break
}
}
assert.True(t, found, "ORAS artifact package should be created")
})
for _, image := range images {
t.Run(fmt.Sprintf("[Image:%s]", image), func(t *testing.T) {
url := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, image)
@ -430,14 +514,19 @@ func TestPackageContainer(t *testing.T) {
assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
})
t.Run("GetManifest", func(t *testing.T) {
t.Run("GetManifest unknown-tag", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/unknown-tag", url)).
AddTokenAuth(userToken)
MakeRequest(t, req, http.StatusNotFound)
})
req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)).
t.Run("GetManifest serv indirect", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer test.MockVariableValue(&setting.Packages.Storage.MinioConfig.ServeDirect, false)()
req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)).
AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusOK)
@ -446,6 +535,25 @@ func TestPackageContainer(t *testing.T) {
assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
assert.Equal(t, manifestContent, resp.Body.String())
})
t.Run("GetManifest serv direct", func(t *testing.T) {
if setting.Packages.Storage.Type != setting.MinioStorageType {
t.Skip("Test skipped for non-Minio-storage.")
return
}
defer tests.PrintCurrentTest(t)()
defer test.MockVariableValue(&setting.Packages.Storage.MinioConfig.ServeDirect, true)()
req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)).
AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusTemporaryRedirect)
assert.Empty(t, resp.Header().Get("Content-Length"))
assert.NotEmpty(t, resp.Header().Get("Location"))
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
assert.Empty(t, resp.Header().Get("Docker-Content-Digest"))
})
})
}
@ -580,36 +688,76 @@ func TestPackageContainer(t *testing.T) {
t.Run("GetTagList", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
cases := []struct {
var cases []struct {
URL string
ExpectedTags []string
ExpectedLink string
}{
{
URL: fmt.Sprintf("%s/tags/list", url),
ExpectedTags: []string{"latest", "main", "multi"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
},
{
URL: fmt.Sprintf("%s/tags/list?n=0", url),
ExpectedTags: []string{},
ExpectedLink: "",
},
{
URL: fmt.Sprintf("%s/tags/list?n=2", url),
ExpectedTags: []string{"latest", "main"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=2>; rel="next"`, user.Name, image),
},
{
URL: fmt.Sprintf("%s/tags/list?last=main", url),
ExpectedTags: []string{"multi"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
},
{
URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url),
ExpectedTags: []string{"main"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=1>; rel="next"`, user.Name, image),
},
}
if image == "oras-artifact" {
cases = []struct {
URL string
ExpectedTags []string
ExpectedLink string
}{
{
URL: fmt.Sprintf("%s/tags/list", url),
ExpectedTags: []string{"artifact-v1", "latest", "main", "multi"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
},
{
URL: fmt.Sprintf("%s/tags/list?n=0", url),
ExpectedTags: []string{},
ExpectedLink: "",
},
{
URL: fmt.Sprintf("%s/tags/list?n=2", url),
ExpectedTags: []string{"artifact-v1", "latest"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=latest&n=2>; rel="next"`, user.Name, image),
},
{
URL: fmt.Sprintf("%s/tags/list?last=main", url),
ExpectedTags: []string{"multi"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
},
{
URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url),
ExpectedTags: []string{"main"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=1>; rel="next"`, user.Name, image),
},
}
} else {
cases = []struct {
URL string
ExpectedTags []string
ExpectedLink string
}{
{
URL: fmt.Sprintf("%s/tags/list", url),
ExpectedTags: []string{"latest", "main", "multi"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
},
{
URL: fmt.Sprintf("%s/tags/list?n=0", url),
ExpectedTags: []string{},
ExpectedLink: "",
},
{
URL: fmt.Sprintf("%s/tags/list?n=2", url),
ExpectedTags: []string{"latest", "main"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=2>; rel="next"`, user.Name, image),
},
{
URL: fmt.Sprintf("%s/tags/list?last=main", url),
ExpectedTags: []string{"multi"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
},
{
URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url),
ExpectedTags: []string{"main"},
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=1>; rel="next"`, user.Name, image),
},
}
}
for _, c := range cases {
@ -636,7 +784,11 @@ func TestPackageContainer(t *testing.T) {
var apiPackages []*api.Package
DecodeJSON(t, resp, &apiPackages)
assert.Len(t, apiPackages, 4) // "latest", "main", "multi", "sha256:..."
if image == "oras-artifact" {
assert.Len(t, apiPackages, 5) // "artifact-v1", "latest", "main", "multi", "sha256:..."
} else {
assert.Len(t, apiPackages, 4) // "latest", "main", "multi", "sha256:..."
}
})
t.Run("Delete", func(t *testing.T) {

View file

@ -430,6 +430,30 @@ func TestAPIExternalAssetRelease(t *testing.T) {
assert.Equal(t, "external", attachment.Type)
}
func TestAPIAllowedAPIURLInRelease(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.LowerName)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
r := createNewReleaseUsingAPI(t, token, owner, repo, "release-tag", "", "Release Tag", "test")
internalURL := "https://localhost:3003/api/packages/owner/generic/test/1.0.0/test.txt"
req := NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset&external_url=%s", owner.Name, repo.Name, r.ID, url.QueryEscape(internalURL))).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
var attachment *api.Attachment
DecodeJSON(t, resp, &attachment)
assert.Equal(t, "test-asset", attachment.Name)
assert.EqualValues(t, 0, attachment.Size)
assert.Equal(t, internalURL, attachment.DownloadURL)
assert.Equal(t, "external", attachment.Type)
}
func TestAPIDuplicateAssetRelease(t *testing.T) {
defer tests.PrepareTestEnv(t)()

View file

@ -494,9 +494,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"version": "22.15.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
"integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@ -1117,9 +1117,9 @@
}
},
"node_modules/browserslist": {
"version": "4.24.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"version": "4.25.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
"integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
"funding": [
{
"type": "opencollective",
@ -1136,8 +1136,8 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.149",
"caniuse-lite": "^1.0.30001718",
"electron-to-chromium": "^1.5.160",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3"
},
@ -1249,9 +1249,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001718",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"version": "1.0.30001721",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
"integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==",
"funding": [
{
"type": "opencollective",
@ -2005,9 +2005,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.157",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz",
"integrity": "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==",
"version": "1.5.165",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",
"integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==",
"license": "ISC"
},
"node_modules/emoji-regex": {