Skip to content

Commit 70fa6f9

Browse files
feat(wasm): add WASM build support with DD_ACCESS_TOKEN auth
Enable pup to compile to WebAssembly (GOOS=js GOARCH=wasm) for use in browser-like runtimes (Deno, Bun, Cloudflare Workers). The WASM build uses stateless authentication via DD_ACCESS_TOKEN since keychain storage and browser-based OAuth login are not available in WASM environments. - Add DD_ACCESS_TOKEN as highest-priority auth method (all platforms) - Extract OAuth storage logic into build-tagged files (oauth_storage.go) - Add //go:build !js to keychain, factory, and auth command files - Create WASM stubs for storage, keychain, factory, and auth commands - Add pup-wasm build target to GoReleaser with wasm_exec.js - Add WASM build verification step to CI workflow - Add DD_ACCESS_TOKEN tests for priority, fallthrough, and forceAPIKeys - Update README with WASM section and DD_ACCESS_TOKEN documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fe2156c commit 70fa6f9

21 files changed

Lines changed: 436 additions & 62 deletions

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ jobs:
233233
- name: Verify binary
234234
run: ./pup --version
235235

236+
- name: Build WASM binary
237+
env:
238+
GOOS: js
239+
GOARCH: wasm
240+
run: go build -o pup.wasm .
241+
236242
sast:
237243
name: Datadog Static Analysis
238244
runs-on: ubuntu-latest

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*.so
66
*.dylib
77
pup
8+
*.wasm
9+
wasm_exec.js
810
coverage.html
911

1012
# Test binary, built with `go test -c`

.goreleaser.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ before:
77
hooks:
88
# Ensure go.mod is tidy
99
- go mod tidy
10+
# Copy wasm_exec.js from Go toolchain for WASM builds
11+
- cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" wasm_exec.js
1012

1113
builds:
1214
# Darwin builds must have CGO_ENABLED=1 so the keyring library's
@@ -54,8 +56,28 @@ builds:
5456
- -X github.com/datadog-labs/pup/internal/version.GitCommit={{.Commit}}
5557
- -X github.com/datadog-labs/pup/internal/version.BuildDate={{.Date}}
5658

59+
# WASM build for browser/Deno/Bun/Workers runtimes.
60+
# Uses GOOS=js (Fetch API networking) with stateless auth (DD_ACCESS_TOKEN).
61+
- id: pup-wasm
62+
binary: pup
63+
main: .
64+
env:
65+
- CGO_ENABLED=0
66+
goos:
67+
- js
68+
goarch:
69+
- wasm
70+
ldflags:
71+
- -s -w
72+
- -X github.com/datadog-labs/pup/internal/version.Version={{.Version}}
73+
- -X github.com/datadog-labs/pup/internal/version.GitCommit={{.Commit}}
74+
- -X github.com/datadog-labs/pup/internal/version.BuildDate={{.Date}}
75+
5776
archives:
5877
- id: pup
78+
builds:
79+
- pup-darwin
80+
- pup-nix
5981
name_template: >-
6082
{{ .ProjectName }}_
6183
{{- .Version }}_
@@ -70,6 +92,15 @@ archives:
7092
- CHANGELOG.md
7193
- docs/*
7294

95+
- id: pup-wasm
96+
builds:
97+
- pup-wasm
98+
name_template: "{{ .ProjectName }}_{{ .Version }}_WASM"
99+
files:
100+
- wasm_exec.js
101+
- LICENSE
102+
- README.md
103+
73104
# Source archive
74105
source:
75106
enabled: true

README.md

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,25 @@ export DD_SITE="datadoghq.com" # Optional, defaults to datadoghq.com
234234
pup monitors list
235235
```
236236

237+
### Bearer Token Authentication (WASM / Headless)
238+
239+
For WASM builds or environments without keychain access, use a pre-obtained bearer token:
240+
241+
```bash
242+
export DD_ACCESS_TOKEN="your-oauth-access-token"
243+
export DD_SITE="datadoghq.com"
244+
245+
pup monitors list
246+
```
247+
248+
This is the only authentication method available in the WASM build. See the [WASM](#wasm) section below.
249+
237250
### Authentication Priority
238251

239252
Pup checks for authentication in this order:
240-
1. **OAuth2 tokens** (from `pup auth login`) - Used if valid tokens exist
241-
2. **API keys** (from `DD_API_KEY` and `DD_APP_KEY`) - Used if OAuth tokens not available
253+
1. **`DD_ACCESS_TOKEN`** - Stateless bearer token (highest priority)
254+
2. **OAuth2 tokens** (from `pup auth login`) - Used if valid tokens exist
255+
3. **API keys** (from `DD_API_KEY` and `DD_APP_KEY`) - Used if OAuth tokens not available
242256

243257
## Usage
244258

@@ -333,12 +347,55 @@ pup incidents get abc-123-def
333347

334348
## Environment Variables
335349

336-
- `DD_API_KEY`: Datadog API key (optional if using OAuth2)
337-
- `DD_APP_KEY`: Datadog Application key (optional if using OAuth2)
350+
- `DD_ACCESS_TOKEN`: Bearer token for stateless auth (highest priority, required for WASM)
351+
- `DD_API_KEY`: Datadog API key (optional if using OAuth2 or DD_ACCESS_TOKEN)
352+
- `DD_APP_KEY`: Datadog Application key (optional if using OAuth2 or DD_ACCESS_TOKEN)
338353
- `DD_SITE`: Datadog site (default: datadoghq.com)
339354
- `DD_AUTO_APPROVE`: Auto-approve destructive operations (true/false)
340355
- `DD_TOKEN_STORAGE`: Token storage backend (keychain or file, default: auto-detect)
341356

357+
## WASM
358+
359+
Pup compiles to WebAssembly (`GOOS=js GOARCH=wasm`) for use in browser-like runtimes such as Deno, Bun, and Cloudflare Workers.
360+
361+
### Building
362+
363+
```bash
364+
GOOS=js GOARCH=wasm go build -o pup.wasm .
365+
366+
# Copy the Go WASM support file
367+
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" .
368+
```
369+
370+
### Authentication
371+
372+
The WASM build uses **stateless authentication only** — keychain storage and browser-based OAuth login are not available. Set `DD_ACCESS_TOKEN` with a pre-obtained bearer token:
373+
374+
```bash
375+
DD_ACCESS_TOKEN="your-token" DD_SITE="datadoghq.com" deno run pup.wasm monitors list
376+
```
377+
378+
The `pup auth status` command works in WASM and reports which credentials are configured. The `login`, `logout`, and `refresh` subcommands return guidance to use `DD_ACCESS_TOKEN`.
379+
380+
### Limitations
381+
382+
- No local token storage (keychain/file) — use `DD_ACCESS_TOKEN`
383+
- No browser-based OAuth login flow
384+
- Networking relies on the host runtime's Fetch API
385+
386+
### Running with Deno
387+
388+
```javascript
389+
import "./wasm_exec.js";
390+
391+
const go = new Go();
392+
const wasm = await Deno.readFile("pup.wasm");
393+
const result = await WebAssembly.instantiate(wasm, go.importObject);
394+
go.run(result.instance);
395+
```
396+
397+
Pre-built WASM archives (including `wasm_exec.js`) are available in [GitHub Releases](https://github.com/datadog-labs/pup/releases).
398+
342399
## Development
343400

344401
```bash
@@ -348,6 +405,9 @@ go test ./...
348405
# Build
349406
go build -o pup .
350407

408+
# Build WASM
409+
GOOS=js GOARCH=wasm go build -o pup.wasm .
410+
351411
# Run without building
352412
go run main.go monitors list
353413
```

cmd/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/).
44
// Copyright 2024-present Datadog, Inc.
55

6+
//go:build !js
7+
68
package cmd
79

810
import (

cmd/auth_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/).
44
// Copyright 2024-present Datadog, Inc.
55

6+
//go:build !js
7+
68
package cmd
79

810
import (

cmd/auth_wasm.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2024-present Datadog, Inc.
5+
6+
//go:build js
7+
8+
package cmd
9+
10+
import (
11+
"fmt"
12+
13+
"github.com/spf13/cobra"
14+
)
15+
16+
var authCmd = &cobra.Command{
17+
Use: "auth",
18+
Short: "Authentication commands (limited in WASM)",
19+
Long: `In WASM builds, OAuth2 browser login is not available. Set DD_ACCESS_TOKEN or DD_API_KEY + DD_APP_KEY instead.`,
20+
}
21+
22+
var authLoginCmd = &cobra.Command{
23+
Use: "login",
24+
Short: "Login via OAuth2 (not available in WASM)",
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
return fmt.Errorf("OAuth2 browser login is not available in WASM builds; set DD_ACCESS_TOKEN instead")
27+
},
28+
}
29+
30+
var authStatusCmd = &cobra.Command{
31+
Use: "status",
32+
Short: "Check authentication status",
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
hasAccessToken := cfg.AccessToken != ""
35+
hasAPIKeys := cfg.APIKey != "" && cfg.AppKey != ""
36+
37+
if hasAccessToken {
38+
fmt.Println("Authenticated via DD_ACCESS_TOKEN")
39+
} else if hasAPIKeys {
40+
fmt.Println("Authenticated via API keys (DD_API_KEY + DD_APP_KEY)")
41+
} else {
42+
fmt.Println("Not authenticated")
43+
fmt.Println(" Set DD_ACCESS_TOKEN or DD_API_KEY + DD_APP_KEY environment variables")
44+
}
45+
fmt.Printf(" Site: %s\n", cfg.Site)
46+
return nil
47+
},
48+
}
49+
50+
var authLogoutCmd = &cobra.Command{
51+
Use: "logout",
52+
Short: "Logout (not available in WASM)",
53+
RunE: func(cmd *cobra.Command, args []string) error {
54+
return fmt.Errorf("token storage is not available in WASM builds; unset DD_ACCESS_TOKEN to revoke access")
55+
},
56+
}
57+
58+
var authRefreshCmd = &cobra.Command{
59+
Use: "refresh",
60+
Short: "Refresh access token (not available in WASM)",
61+
RunE: func(cmd *cobra.Command, args []string) error {
62+
return fmt.Errorf("token refresh is not available in WASM builds; set a new DD_ACCESS_TOKEN instead")
63+
},
64+
}
65+
66+
func init() {
67+
authCmd.AddCommand(authLoginCmd)
68+
authCmd.AddCommand(authStatusCmd)
69+
authCmd.AddCommand(authLogoutCmd)
70+
authCmd.AddCommand(authRefreshCmd)
71+
}

pkg/auth/storage/factory.go

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/).
44
// Copyright 2024-present Datadog, Inc.
55

6+
//go:build !js
7+
68
package storage
79

810
import (
@@ -25,20 +27,6 @@ var (
2527
mu sync.Mutex
2628
)
2729

28-
// StorageOptions configures storage backend selection
29-
type StorageOptions struct {
30-
// ForceBackend forces a specific storage backend
31-
ForceBackend BackendType
32-
33-
// StorageDir overrides the storage directory (file backend only)
34-
StorageDir string
35-
}
36-
37-
const (
38-
// StorageEnvVar is the environment variable to override storage backend
39-
StorageEnvVar = "DD_TOKEN_STORAGE"
40-
)
41-
4230
// GetStorage returns a storage instance, automatically detecting the best backend
4331
//
4432
// Selection priority:

pkg/auth/storage/factory_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/).
44
// Copyright 2024-present Datadog, Inc.
55

6+
//go:build !js
7+
68
package storage
79

810
import (

pkg/auth/storage/factory_wasm.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2024-present Datadog, Inc.
5+
6+
//go:build js
7+
8+
package storage
9+
10+
import "fmt"
11+
12+
// GetStorage returns an error on WASM directing users to DD_ACCESS_TOKEN.
13+
// Local token storage is not available in WASM runtimes.
14+
func GetStorage(_ *StorageOptions) (Storage, error) {
15+
return nil, fmt.Errorf(
16+
"token storage is not available in WASM builds; set DD_ACCESS_TOKEN instead",
17+
)
18+
}
19+
20+
// GetActiveBackend returns an empty backend type on WASM.
21+
func GetActiveBackend() BackendType {
22+
return ""
23+
}
24+
25+
// IsUsingSecureStorage always returns false on WASM.
26+
func IsUsingSecureStorage() bool {
27+
return false
28+
}
29+
30+
// GetStorageDescription returns a description indicating WASM mode.
31+
func GetStorageDescription() string {
32+
return "not available (WASM)"
33+
}
34+
35+
// ResetStorage is a no-op on WASM.
36+
func ResetStorage() {}

0 commit comments

Comments
 (0)