Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Dependencies: update Go to 1.26, npm to 12.0.1, Playwright to 1.61.1, current Go terminal/system libraries, and cached Chrome cookie support to 3.0.2 with audited transitive overrides.
- Docker: add a local image with Node, Playwright Chromium, `/data` persistence, and CI smoke coverage.
- Add Sweden (`SE`) Foodora preset using `OP_SE`. (`#4`, thanks `@grenish`)
- Add Czech Republic (`CZ`) Foodora preset using `DJ_CZ`. (`#6`, thanks `@usimic`)
Expand Down
11 changes: 7 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# syntax=docker/dockerfile:1.7

ARG GO_VERSION=1.25
ARG PLAYWRIGHT_VERSION=1.58.2
ARG GO_VERSION=1.26
ARG NPM_VERSION=12.0.1
ARG PLAYWRIGHT_VERSION=1.61.1

FROM golang:${GO_VERSION}-bookworm AS build
WORKDIR /src
Expand All @@ -10,10 +11,12 @@ RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/ordercli ./cmd/ordercli

FROM node:24-bookworm-slim
FROM node:24.18.0-bookworm-slim
ARG NPM_VERSION
ARG PLAYWRIGHT_VERSION
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN apt-get update \
RUN npm install --global npm@${NPM_VERSION} \
&& apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& npx --yes playwright@${PLAYWRIGHT_VERSION} install --with-deps chromium \
&& rm -rf /var/lib/apt/lists/* /tmp/* \
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
module github.com/steipete/ordercli

go 1.25.0
go 1.26.0

require (
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.44.0
golang.org/x/term v0.45.0
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/sys v0.47.0 // indirect
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11 changes: 7 additions & 4 deletions internal/browserauth/browserauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func OAuthTokenPassword(ctx context.Context, req foodora.OAuthPasswordRequest, o
}
pw := opts.Playwright
if pw == "" {
pw = "playwright@1.58.2"
pw = "playwright@1.61.1"
}

td, err := os.MkdirTemp("", "ordercli-browserauth-*")
Expand Down Expand Up @@ -222,7 +222,8 @@ func runAuthScriptReal(ctx context.Context, td, scriptPath, outPath string, inpu
} else {
install.Stderr = io.Discard
}
install.Env = append(os.Environ(),
install.Env = append(
os.Environ(),
"npm_config_loglevel=error",
)
if err := install.Run(); err != nil {
Expand All @@ -241,7 +242,8 @@ func runAuthScriptReal(ctx context.Context, td, scriptPath, outPath string, inpu
} else {
installBrowsers.Stderr = io.Discard
}
installBrowsers.Env = append(os.Environ(),
installBrowsers.Env = append(
os.Environ(),
"npm_config_loglevel=error",
)
if err := installBrowsers.Run(); err != nil {
Expand All @@ -250,7 +252,8 @@ func runAuthScriptReal(ctx context.Context, td, scriptPath, outPath string, inpu

cmd := exec.CommandContext(cmdCtx, "node", scriptPath) //nolint:gosec
cmd.Dir = td
cmd.Env = append(os.Environ(),
cmd.Env = append(
os.Environ(),
"ORDERCLI_OUTPUT_PATH="+outPath,
"FOODCLI_OUTPUT_PATH="+outPath,
"FOODORACLI_OUTPUT_PATH="+outPath, // legacy
Expand Down
5 changes: 3 additions & 2 deletions internal/browserpage/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func ReadText(ctx context.Context, targetURL string, opts Options) (Result, erro
}
pw := strings.TrimSpace(opts.Playwright)
if pw == "" {
pw = "playwright@1.58.2"
pw = "playwright@1.61.1"
}

td, err := os.MkdirTemp("", "ordercli-browserpage-*")
Expand Down Expand Up @@ -127,7 +127,8 @@ func runFetchScript(ctx context.Context, td, scriptPath, outPath string, input [

cmd := exec.CommandContext(cmdCtx, "node", scriptPath) //nolint:gosec
cmd.Dir = td
cmd.Env = append(os.Environ(),
cmd.Env = append(
os.Environ(),
"ORDERCLI_OUTPUT_PATH="+outPath,
"npm_config_loglevel=error",
)
Expand Down
6 changes: 3 additions & 3 deletions internal/browserpage/fetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestReadText_UsesDefaultsAndDecodesResult(t *testing.T) {
if opts.Timeout != 2*time.Minute {
t.Fatalf("opts timeout=%s", opts.Timeout)
}
if playwright != "playwright@1.58.2" {
if playwright != "playwright@1.61.1" {
t.Fatalf("playwright=%q", playwright)
}
return []byte(`{"final_url":"https://example.com/final","title":"T","text":"Body"}`), nil
Expand Down Expand Up @@ -70,7 +70,7 @@ func TestReadText_InvalidJSON(t *testing.T) {
func TestRunFetchScript_NodeMissing(t *testing.T) {
t.Setenv("PATH", "")

_, err := runFetchScript(context.Background(), t.TempDir(), "script.mjs", "out.json", nil, Options{Timeout: time.Second}, "playwright@1.58.2")
_, err := runFetchScript(context.Background(), t.TempDir(), "script.mjs", "out.json", nil, Options{Timeout: time.Second}, "playwright@1.61.1")
if err == nil || !strings.Contains(err.Error(), "node not found") {
t.Fatalf("err=%v", err)
}
Expand All @@ -84,7 +84,7 @@ func TestRunFetchScript_NpmMissing(t *testing.T) {
}
t.Setenv("PATH", binDir)

_, err := runFetchScript(context.Background(), t.TempDir(), "script.mjs", "out.json", nil, Options{Timeout: time.Second}, "playwright@1.58.2")
_, err := runFetchScript(context.Background(), t.TempDir(), "script.mjs", "out.json", nil, Options{Timeout: time.Second}, "playwright@1.61.1")
if err == nil || !strings.Contains(err.Error(), "npm not found") {
t.Fatalf("err=%v", err)
}
Expand Down
83 changes: 78 additions & 5 deletions internal/chromecookies/chromecookies.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)

//go:embed load.mjs
var loadScript []byte

// Keep the install-time overrides until chrome-cookies-secure drops its vulnerable build chain.
const (
npmPackageJSON = `{"private":true,"type":"module","dependencies":{"chrome-cookies-secure":"3.0.2"},"overrides":{"tar":"7.5.20","@tootallnate/once":"2.0.1"}}`
npmStateFile = ".ordercli-dependencies"
)

type Options struct {
TargetURL string
ChromeProfile string
Expand Down Expand Up @@ -99,12 +106,11 @@ type scriptOutput struct {
var runScript = runScriptReal

func ensureNpmProject(ctx context.Context, dir string, logWriter io.Writer) error {
nodeModules := filepath.Join(dir, "node_modules", "chrome-cookies-secure", "package.json")
if _, err := os.Stat(nodeModules); err == nil {
if npmProjectCurrent(dir) {
return nil
}

pkg := []byte("{\"private\":true,\"type\":\"module\",\"dependencies\":{\"chrome-cookies-secure\":\"3.0.0\"}}\n")
pkg := []byte(npmPackageJSON + "\n")
if err := os.WriteFile(filepath.Join(dir, "package.json"), pkg, 0o600); err != nil {
return err
}
Expand All @@ -120,12 +126,78 @@ func ensureNpmProject(ctx context.Context, dir string, logWriter io.Writer) erro
} else {
install.Stderr = io.Discard
}
install.Env = append(os.Environ(),
install.Env = append(
os.Environ(),
"npm_config_loglevel=error",
)
if err := install.Run(); err != nil {
return fmt.Errorf("chromecookies: npm install chrome-cookies-secure: %w", err)
}
if err := verifyInstalledNpmDependencies(dir); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(dir, npmStateFile), []byte(npmPackageJSON+"\n"), 0o600); err != nil {
return fmt.Errorf("chromecookies: write dependency state: %w", err)
}
return nil
}

func npmProjectCurrent(dir string) bool {
pkg, err := os.ReadFile(filepath.Join(dir, "package.json"))
if err != nil || strings.TrimSpace(string(pkg)) != npmPackageJSON {
return false
}
state, err := os.ReadFile(filepath.Join(dir, npmStateFile))
if err != nil || strings.TrimSpace(string(state)) != npmPackageJSON {
return false
}
_, err = os.Stat(filepath.Join(dir, "node_modules", "chrome-cookies-secure", "package.json"))
return err == nil
}

func verifyInstalledNpmDependencies(dir string) error {
required := map[string]string{
"chrome-cookies-secure": "3.0.2",
"tar": "7.5.20",
"@tootallnate/once": "2.0.1",
}
seen := make(map[string]bool, len(required))
err := filepath.WalkDir(filepath.Join(dir, "node_modules"), func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() || entry.Name() != "package.json" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var pkg struct {
Name string `json:"name"`
Version string `json:"version"`
}
if err := json.Unmarshal(data, &pkg); err != nil {
return nil
}
want, ok := required[pkg.Name]
if !ok {
return nil
}
if pkg.Version != want {
return fmt.Errorf("chromecookies: installed %s version %s, want %s", pkg.Name, pkg.Version, want)
}
seen[pkg.Name] = true
return nil
})
if err != nil {
return fmt.Errorf("chromecookies: verify npm dependencies: %w", err)
}
for name := range required {
if !seen[name] {
return fmt.Errorf("chromecookies: installed dependency %s missing", name)
}
}
return nil
}

Expand All @@ -142,7 +214,8 @@ func runScriptReal(ctx context.Context, cacheDir, scriptPath, outPath string, in

cmd := exec.CommandContext(cmdCtx, "node", scriptPath) //nolint:gosec
cmd.Dir = cacheDir
cmd.Env = append(os.Environ(),
cmd.Env = append(
os.Environ(),
"ORDERCLI_OUTPUT_PATH="+outPath,
"FOODCLI_OUTPUT_PATH="+outPath,
"FOODORACLI_OUTPUT_PATH="+outPath, // legacy
Expand Down
Loading