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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "3.19.3"
".": "3.20.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 8
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-b969ce378479c79ee64c05127c0ed6c6ce2edbee017ecd037242fb618a5ebc9f.yml
openapi_spec_hash: a24aabaa5214effb679808b7f2be0ad4
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-1c6caa2891a7f3bdfc0caab143f285badc9145220c9b29cd5e4cf1a9b3ac11cf.yml
openapi_spec_hash: 28c4b734a5309067c39bb4c4b709b9ab
config_hash: a962ae71493deb11a1c903256fb25386
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## 3.20.0 (2026-04-11)

Full Changelog: [v3.19.3...v3.20.0](https://github.com/browserbase/stagehand-php/compare/v3.19.3...v3.20.0)

### Features

* [STG-1798] feat: support Browserbase verified sessions ([52c4636](https://github.com/browserbase/stagehand-php/commit/52c4636560e5dd556db9c061a3bb9c48ffa20e76))
* Bedrock auth passthrough ([40cb10e](https://github.com/browserbase/stagehand-php/commit/40cb10eb39c7f53b9c3fd668018c1b3569650e5c))
* Revert "[STG-1573] Add providerOptions for extensible model auth ([#1822](https://github.com/browserbase/stagehand-php/issues/1822))" ([1f99ac3](https://github.com/browserbase/stagehand-php/commit/1f99ac3957867970c6eaa968e8daaa83ae855b7b))


### Bug Fixes

* **client:** properly generate file params ([4a6d856](https://github.com/browserbase/stagehand-php/commit/4a6d85692b4bec4a518cf201999e99b2a7129263))

## 3.19.3 (2026-04-03)

Full Changelog: [v3.18.0...v3.19.3](https://github.com/browserbase/stagehand-php/compare/v3.18.0...v3.19.3)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ The REST API documentation can be found on [docs.stagehand.dev](https://docs.sta
<!-- x-release-please-start-version -->

```
composer require "browserbase/stagehand 3.19.3"
composer require "browserbase/stagehand 3.20.0"
```

<!-- x-release-please-end -->
Expand Down
4 changes: 4 additions & 0 deletions src/Core/Conversion.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public static function dump_unknown(mixed $value, DumpState $state): mixed
}

if (is_object($value)) {
if ($value instanceof FileParam) {
return $value;
}

if (is_a($value, class: ConverterSource::class)) {
return $value::converter()->dump($value, state: $state);
}
Expand Down
63 changes: 63 additions & 0 deletions src/Core/FileParam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Stagehand\Core;

/**
* Represents a file to upload in a multipart request.
*
* ```php
* // From a file on disk:
* $client->files->upload(file: FileParam::fromResource(fopen('data.csv', 'r')));
*
* // From a string:
* $client->files->upload(file: FileParam::fromString('csv data...', 'data.csv'));
* ```
*/
final class FileParam
{
public const DEFAULT_CONTENT_TYPE = 'application/octet-stream';

/**
* @param resource|string $data the file content as a resource or string
*/
private function __construct(
public readonly mixed $data,
public readonly string $filename,
public readonly string $contentType = self::DEFAULT_CONTENT_TYPE,
) {}

/**
* Create a FileParam from an open resource (e.g. from fopen()).
*
* @param resource $resource an open file resource
* @param string|null $filename Override the filename. Defaults to the resource URI basename.
* @param string $contentType override the content type
*/
public static function fromResource(mixed $resource, ?string $filename = null, string $contentType = self::DEFAULT_CONTENT_TYPE): self
{
if (!is_resource($resource)) {
throw new \InvalidArgumentException('Expected a resource, got '.get_debug_type($resource));
}

if (null === $filename) {
$meta = stream_get_meta_data($resource);
$filename = basename($meta['uri'] ?? 'upload');
}

return new self($resource, filename: $filename, contentType: $contentType);
}

/**
* Create a FileParam from a string.
*
* @param string $content the file content
* @param string $filename the filename for the Content-Disposition header
* @param string $contentType override the content type
*/
public static function fromString(string $content, string $filename, string $contentType = self::DEFAULT_CONTENT_TYPE): self
{
return new self($content, filename: $filename, contentType: $contentType);
}
}
60 changes: 47 additions & 13 deletions src/Core/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ public static function withSetBody(

if (preg_match('/^multipart\/form-data/', $contentType)) {
[$boundary, $gen] = self::encodeMultipartStreaming($body);
$encoded = implode('', iterator_to_array($gen));
$encoded = implode('', iterator_to_array($gen, preserve_keys: false));
$stream = $factory->createStream($encoded);

/** @var RequestInterface */
Expand Down Expand Up @@ -447,11 +447,18 @@ private static function writeMultipartContent(
): \Generator {
$contentLine = "Content-Type: %s\r\n\r\n";

if (is_resource($val)) {
yield sprintf($contentLine, $contentType ?? 'application/octet-stream');
while (!feof($val)) {
if ($read = fread($val, length: self::BUF_SIZE)) {
yield $read;
if ($val instanceof FileParam) {
$ct = $val->contentType ?? $contentType;

yield sprintf($contentLine, $ct);
$data = $val->data;
if (is_string($data)) {
yield $data;
} else { // resource
while (!feof($data)) {
if ($read = fread($data, length: self::BUF_SIZE)) {
yield $read;
}
}
}
} elseif (is_string($val) || is_numeric($val) || is_bool($val)) {
Expand Down Expand Up @@ -483,17 +490,48 @@ private static function writeMultipartChunk(
yield 'Content-Disposition: form-data';

if (!is_null($key)) {
$name = rawurlencode(self::strVal($key));
$name = str_replace(['"', "\r", "\n"], replace: '', subject: $key);

yield "; name=\"{$name}\"";
}

// File uploads require a filename in the Content-Disposition header,
// e.g. `Content-Disposition: form-data; name="file"; filename="data.csv"`
// Without this, many servers will reject the upload with a 400.
if ($val instanceof FileParam) {
$filename = str_replace(['"', "\r", "\n"], replace: '', subject: $val->filename);

yield "; filename=\"{$filename}\"";
}

yield "\r\n";
foreach (self::writeMultipartContent($val, closing: $closing) as $chunk) {
yield $chunk;
}
}

/**
* Expands list arrays into separate multipart parts, applying the configured array key format.
*
* @param list<callable> $closing
*
* @return \Generator<string>
*/
private static function writeMultipartField(
string $boundary,
?string $key,
mixed $val,
array &$closing
): \Generator {
if (is_array($val) && array_is_list($val)) {
foreach ($val as $item) {
yield from self::writeMultipartField(boundary: $boundary, key: $key, val: $item, closing: $closing);
}
} else {
yield from self::writeMultipartChunk(boundary: $boundary, key: $key, val: $val, closing: $closing);
}
}

/**
* @param bool|int|float|string|resource|\Traversable<mixed,>|array<string,mixed>|null $body
*
Expand All @@ -508,14 +546,10 @@ private static function encodeMultipartStreaming(mixed $body): array
try {
if (is_array($body) || is_object($body)) {
foreach ((array) $body as $key => $val) {
foreach (static::writeMultipartChunk(boundary: $boundary, key: $key, val: $val, closing: $closing) as $chunk) {
yield $chunk;
}
yield from static::writeMultipartField(boundary: $boundary, key: $key, val: $val, closing: $closing);
}
} else {
foreach (static::writeMultipartChunk(boundary: $boundary, key: null, val: $body, closing: $closing) as $chunk) {
yield $chunk;
}
yield from static::writeMultipartField(boundary: $boundary, key: null, val: $body, closing: $closing);
}

yield "--{$boundary}--\r\n";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Stagehand\Core\Contracts\BaseModel;
use Stagehand\Sessions\SessionStartParams\BrowserbaseSessionCreateParams\BrowserSettings\Context;
use Stagehand\Sessions\SessionStartParams\BrowserbaseSessionCreateParams\BrowserSettings\Fingerprint;
use Stagehand\Sessions\SessionStartParams\BrowserbaseSessionCreateParams\BrowserSettings\Os;
use Stagehand\Sessions\SessionStartParams\BrowserbaseSessionCreateParams\BrowserSettings\Viewport;

/**
Expand All @@ -19,12 +20,16 @@
* @phpstan-type BrowserSettingsShape = array{
* advancedStealth?: bool|null,
* blockAds?: bool|null,
* captchaImageSelector?: string|null,
* captchaInputSelector?: string|null,
* context?: null|Context|ContextShape,
* extensionID?: string|null,
* fingerprint?: null|Fingerprint|FingerprintShape,
* logSession?: bool|null,
* os?: null|Os|value-of<Os>,
* recordSession?: bool|null,
* solveCaptchas?: bool|null,
* verified?: bool|null,
* viewport?: null|Viewport|ViewportShape,
* }
*/
Expand All @@ -39,6 +44,12 @@ final class BrowserSettings implements BaseModel
#[Optional]
public ?bool $blockAds;

#[Optional]
public ?string $captchaImageSelector;

#[Optional]
public ?string $captchaInputSelector;

#[Optional]
public ?Context $context;

Expand All @@ -51,12 +62,19 @@ final class BrowserSettings implements BaseModel
#[Optional]
public ?bool $logSession;

/** @var value-of<Os>|null $os */
#[Optional(enum: Os::class)]
public ?string $os;

#[Optional]
public ?bool $recordSession;

#[Optional]
public ?bool $solveCaptchas;

#[Optional]
public ?bool $verified;

#[Optional]
public ?Viewport $viewport;

Expand All @@ -72,29 +90,38 @@ public function __construct()
*
* @param Context|ContextShape|null $context
* @param Fingerprint|FingerprintShape|null $fingerprint
* @param Os|value-of<Os>|null $os
* @param Viewport|ViewportShape|null $viewport
*/
public static function with(
?bool $advancedStealth = null,
?bool $blockAds = null,
?string $captchaImageSelector = null,
?string $captchaInputSelector = null,
Context|array|null $context = null,
?string $extensionID = null,
Fingerprint|array|null $fingerprint = null,
?bool $logSession = null,
Os|string|null $os = null,
?bool $recordSession = null,
?bool $solveCaptchas = null,
?bool $verified = null,
Viewport|array|null $viewport = null,
): self {
$self = new self;

null !== $advancedStealth && $self['advancedStealth'] = $advancedStealth;
null !== $blockAds && $self['blockAds'] = $blockAds;
null !== $captchaImageSelector && $self['captchaImageSelector'] = $captchaImageSelector;
null !== $captchaInputSelector && $self['captchaInputSelector'] = $captchaInputSelector;
null !== $context && $self['context'] = $context;
null !== $extensionID && $self['extensionID'] = $extensionID;
null !== $fingerprint && $self['fingerprint'] = $fingerprint;
null !== $logSession && $self['logSession'] = $logSession;
null !== $os && $self['os'] = $os;
null !== $recordSession && $self['recordSession'] = $recordSession;
null !== $solveCaptchas && $self['solveCaptchas'] = $solveCaptchas;
null !== $verified && $self['verified'] = $verified;
null !== $viewport && $self['viewport'] = $viewport;

return $self;
Expand All @@ -116,6 +143,22 @@ public function withBlockAds(bool $blockAds): self
return $self;
}

public function withCaptchaImageSelector(string $captchaImageSelector): self
{
$self = clone $this;
$self['captchaImageSelector'] = $captchaImageSelector;

return $self;
}

public function withCaptchaInputSelector(string $captchaInputSelector): self
{
$self = clone $this;
$self['captchaInputSelector'] = $captchaInputSelector;

return $self;
}

/**
* @param Context|ContextShape $context
*/
Expand Down Expand Up @@ -154,6 +197,17 @@ public function withLogSession(bool $logSession): self
return $self;
}

/**
* @param Os|value-of<Os> $os
*/
public function withOs(Os|string $os): self
{
$self = clone $this;
$self['os'] = $os;

return $self;
}

public function withRecordSession(bool $recordSession): self
{
$self = clone $this;
Expand All @@ -170,6 +224,14 @@ public function withSolveCaptchas(bool $solveCaptchas): self
return $self;
}

public function withVerified(bool $verified): self
{
$self = clone $this;
$self['verified'] = $verified;

return $self;
}

/**
* @param Viewport|ViewportShape $viewport
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Stagehand\Sessions\SessionStartParams\BrowserbaseSessionCreateParams\BrowserSettings;

enum Os: string
{
case WINDOWS = 'windows';

case MAC = 'mac';

case LINUX = 'linux';

case MOBILE = 'mobile';

case TABLET = 'tablet';
}
2 changes: 1 addition & 1 deletion src/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
namespace Stagehand;

// x-release-please-start-version
const VERSION = '3.19.3';
const VERSION = '3.20.0';
// x-release-please-end
Loading