Skip to content
Draft
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
3 changes: 2 additions & 1 deletion packages/npm-wasm/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ function buildClient({ call, handle, hasInstall, close }) {
// dbcInstall takes the client handle (it needs the instance's registry
// config); dbcUninstall/dbcList are pure filesystem ops and intentionally
// do not — per-instance config does not apply to them.
api.install = (name, location) => parse("dbcInstall", [handle, name, normalizeLocation(location)]);
api.install = (name, location, options = {}) =>
parse("dbcInstall", [handle, name, normalizeLocation(location), Boolean(options.allowUnsigned)]);
api.uninstall = async (name, location) => {
await call("dbcUninstall", [name, normalizeLocation(location)]);
};
Expand Down
7 changes: 6 additions & 1 deletion packages/npm-wasm/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,18 @@ export interface Dbc {
/**
* Node-only (requires a filesystem).
*
* `install` verifies the driver's signature with Columnar's key; a driver
* that fails verification is removed and the call rejects. `allowUnsigned`
* skips verification (like the CLI's `--no-verify`); use it only for
* drivers from a trusted private registry.
*
* NOTE: `install`/`listInstalled` resolve the package via the **process-global**
* platform (see `DbcOptions.platform`) and have no per-call platform override.
* Concurrent in-process clients pinned to different platforms can therefore
* install or list the wrong platform's artifact; use the worker backend for
* multi-platform concurrency.
*/
install?(name: string, location: string): Promise<Manifest>;
install?(name: string, location: string, options?: { allowUnsigned?: boolean }): Promise<Manifest>;
uninstall?(name: string, location: string): Promise<void>;
listInstalled?(location: string): Promise<InstalledDriver[]>;
}
Expand Down
188 changes: 29 additions & 159 deletions wasm/api_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,18 @@
package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"sync"
"syscall/js"

"github.com/columnar-tech/dbc"
"github.com/columnar-tech/dbc/auth"
"github.com/columnar-tech/dbc/config"
"github.com/columnar-tech/dbc/internal/api"
)

type clientCredentialJSON struct {
RegistryURL string `json:"registryURL"`
AuthURI string `json:"authURI"`
Token string `json:"token"`
RefreshToken string `json:"refreshToken"`
ClientID string `json:"clientID"`
}

type clientConfigJSON struct {
BaseURL string `json:"baseURL"`
Credential *clientCredentialJSON `json:"credential"`
}

func init() {
// Route auth-internal HTTP (oauth/api-key refresh, license fetch use
// http.DefaultClient directly) through the JS fetch transport, since Go's
Expand All @@ -54,92 +38,30 @@ func init() {
dbc.DefaultClient = client
}

// clientFromConfig builds a dbc.Client from a JSON config string ({baseURL,
// credential}). Each loadDbc instance gets its own client (stored by handle),
// so instances stay isolated AND a client's credential pointer persists across
// calls — refreshed access tokens are retained instead of being rebuilt from
// the original (possibly stale) token. An empty string yields a bare client.
func clientFromConfig(cfgJSON string) (*dbc.Client, error) {
opts := []dbc.Option{dbc.WithHTTPClient(&http.Client{Transport: fetchRoundTripper{}})}
if cfgJSON == "" {
return dbc.NewClient(opts...)
}
var cc clientConfigJSON
if err := json.Unmarshal([]byte(cfgJSON), &cc); err != nil {
return nil, fmt.Errorf("invalid client config: %w", err)
}
if cc.BaseURL != "" {
opts = append(opts, dbc.WithBaseURL(cc.BaseURL))
}
if cc.Credential != nil {
regURL, err := url.Parse(cc.Credential.RegistryURL)
if err != nil {
return nil, fmt.Errorf("invalid credential registryURL: %w", err)
}
authURI, err := url.Parse(cc.Credential.AuthURI)
if err != nil {
return nil, fmt.Errorf("invalid credential authURI: %w", err)
}
cred := &auth.Credential{
Type: auth.TypeToken,
AuthURI: auth.Uri(*authURI),
RegistryURL: auth.Uri(*regURL),
Token: cc.Credential.Token,
RefreshToken: cc.Credential.RefreshToken,
ClientID: cc.Credential.ClientID,
}
host := regURL.Host
opts = append(opts, dbc.WithCredentialResolver(func(u *url.URL) (*auth.Credential, error) {
if u.Host == host {
return cred, nil
}
return nil, nil
}))
}
return dbc.NewClient(opts...)
}

var (
clientsMu sync.Mutex
clients = map[int]*dbc.Client{}
nextClient int
)

func registerClient(cfgJSON string) (int, error) {
c, err := clientFromConfig(cfgJSON)
if err != nil {
return 0, err
}
clientsMu.Lock()
defer clientsMu.Unlock()
nextClient++
clients[nextClient] = c
return nextClient, nil
}
// clients holds one dbc.Client per loadDbc instance, so instances stay
// isolated and each keeps its refreshed credential across calls.
var clients api.Clients

func clientByHandle(h int) (*dbc.Client, error) {
clientsMu.Lock()
defer clientsMu.Unlock()
if c, ok := clients[h]; ok {
return c, nil
// newFetchClient builds a client from loadDbc's JSON config ({baseURL,
// credential}); an empty string yields a default client.
func newFetchClient(cfgJSON string) (*dbc.Client, error) {
var cfg api.ClientConfig
if cfgJSON != "" {
if err := json.Unmarshal([]byte(cfgJSON), &cfg); err != nil {
return nil, fmt.Errorf("invalid client config: %w", err)
}
}
return nil, fmt.Errorf("invalid client handle %d", h)
}

func releaseClient(h int) {
clientsMu.Lock()
delete(clients, h)
clientsMu.Unlock()
return api.NewClient(cfg, &http.Client{Transport: fetchRoundTripper{}})
}

func jsNewClient(args []js.Value) func() (any, error) {
cfgJSON := args[0].String()
return func() (any, error) {
h, err := registerClient(cfgJSON)
c, err := newFetchClient(cfgJSON)
if err != nil {
return nil, err
}
return h, nil
return int(clients.Add(c)), nil
}
}

Expand All @@ -152,7 +74,7 @@ func registerCommon() {
}))
js.Global().Set("dbcNewClient", promisify(jsNewClient))
js.Global().Set("dbcCloseClient", js.FuncOf(func(_ js.Value, a []js.Value) any {
releaseClient(a[0].Int())
clients.Remove(uint64(a[0].Int()))
return nil
}))
js.Global().Set("dbcDebugPaths", promisify(jsDebugPaths))
Expand All @@ -176,94 +98,42 @@ func errString(e error) string {
return e.Error()
}

type driverDTO struct {
Path string `json:"path"`
Title string `json:"title"`
License string `json:"license"`
Description string `json:"description"`
}

type searchResultDTO struct {
Drivers []driverDTO `json:"drivers"`
Warning string `json:"warning,omitempty"`
}

func jsSearch(args []js.Value) func() (any, error) {
handle := args[0].Int()
handle := uint64(args[0].Int())
pattern := ""
if len(args) > 1 {
pattern = args[1].String()
}
return func() (any, error) {
c, err := clientByHandle(handle)
c, err := clients.Get(handle)
if err != nil {
return nil, err
}
drivers, searchErr := c.Search(context.Background(), pattern)
if searchErr != nil && len(drivers) == 0 {
return nil, searchErr
}
out := searchResultDTO{Drivers: make([]driverDTO, 0, len(drivers))}
for _, d := range drivers {
out.Drivers = append(out.Drivers, driverDTO{Path: d.Path, Title: d.Title, License: d.License, Description: d.Desc})
}
if searchErr != nil {
out.Warning = searchErr.Error()
res, err := api.Search(context.Background(), c, pattern)
if err != nil {
return nil, err
}
return toJSONValue(out)
return toJSONValue(res)
}
}

type resolveDTO struct {
Path string `json:"path"`
Platform string `json:"platform"`
Versions []string `json:"versions"`
Latest *struct {
Version string `json:"version"`
URL string `json:"url"`
} `json:"latest,omitempty"`
}

func jsResolve(args []js.Value) func() (any, error) {
handle := args[0].Int()
handle := uint64(args[0].Int())
name := args[1].String()
platform := ""
if len(args) > 2 && args[2].Type() == js.TypeString {
platform = args[2].String()
}
return func() (any, error) {
if platform == "" {
platform = config.PlatformTuple()
}
c, err := clientByHandle(handle)
c, err := clients.Get(handle)
if err != nil {
return nil, err
}
drivers, searchErr := c.Search(context.Background(), name)
if searchErr != nil && len(drivers) == 0 {
return nil, searchErr
}
for _, d := range drivers {
if d.Path != name {
continue
}
versions := []string{}
for _, v := range d.Versions(platform) {
versions = append(versions, v.String())
}
dto := resolveDTO{Path: d.Path, Platform: platform, Versions: versions}
if pkg, perr := d.GetPackage(nil, platform, false); perr == nil && pkg.Path != nil {
dto.Latest = &struct {
Version string `json:"version"`
URL string `json:"url"`
}{Version: pkg.Version.String(), URL: pkg.Path.String()}
}
return toJSONValue(dto)
}
if searchErr != nil {
return nil, fmt.Errorf("driver %q not found in reachable registries; some registries failed: %w", name, searchErr)
res, err := api.Resolve(context.Background(), c, name, platform)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("driver %q not found", name)
return toJSONValue(res)
}
}

Expand All @@ -273,7 +143,7 @@ func jsVerify(args []js.Value) func() (any, error) {
sig := make([]byte, args[1].Get("length").Int())
js.CopyBytesToGo(sig, args[1])
return func() (any, error) {
if err := dbc.SignedByColumnar(bytes.NewReader(lib), bytes.NewReader(sig)); err != nil {
if err := api.Verify(lib, sig); err != nil {
return nil, err
}
return true, nil
Expand Down
62 changes: 15 additions & 47 deletions wasm/ops_node_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"context"
"syscall/js"

"github.com/columnar-tech/dbc/config"
"github.com/columnar-tech/dbc/internal/api"
)

func registerNodeOps() {
Expand All @@ -29,73 +29,41 @@ func registerNodeOps() {
js.Global().Set("dbcUninstall", promisify(jsUninstall))
}

type manifestDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Source string `json:"source"`
DriverPath string `json:"driverPath"`
}

// The npm API takes a directory only; config levels are not exposed to JS.
func jsInstall(args []js.Value) func() (any, error) {
handle := args[0].Int()
handle := uint64(args[0].Int())
name := args[1].String()
location := args[2].String()
target := api.Target{Location: args[2].String()}
opts := api.InstallOptions{AllowUnsigned: len(args) > 3 && args[3].Truthy()}
return func() (any, error) {
c, err := clientByHandle(handle)
c, err := clients.Get(handle)
if err != nil {
return nil, err
}
m, err := c.Install(context.Background(), config.Config{Level: config.ConfigEnv, Location: location}, name)
m, err := api.Install(context.Background(), c, name, target, opts)
if err != nil {
return nil, err
}
version := ""
if m.DriverInfo.Version != nil {
version = m.DriverInfo.Version.String()
}
return toJSONValue(manifestDTO{
ID: m.DriverInfo.ID,
Name: m.DriverInfo.Name,
Version: version,
Source: m.DriverInfo.Source,
DriverPath: m.DriverInfo.Driver.Shared.Get(config.PlatformTuple()),
})
return toJSONValue(m)
}
}

type driverInfoDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
FilePath string `json:"filePath"`
}

func jsList(args []js.Value) func() (any, error) {
location := args[0].String()
target := api.Target{Location: args[0].String()}
return func() (any, error) {
drivers := config.FindDriverConfigsIn(location)
out := make([]driverInfoDTO, 0, len(drivers))
for _, d := range drivers {
version := ""
if d.Version != nil {
version = d.Version.String()
}
out = append(out, driverInfoDTO{ID: d.ID, Name: d.Name, Version: version, FilePath: d.FilePath})
drivers, err := api.ListInstalled(target)
if err != nil {
return nil, err
}
return toJSONValue(out)
return toJSONValue(drivers)
}
}

func jsUninstall(args []js.Value) func() (any, error) {
name := args[0].String()
location := args[1].String()
target := api.Target{Location: args[1].String()}
return func() (any, error) {
c, err := clientFromConfig("")
if err != nil {
return nil, err
}
if err := c.Uninstall(config.Config{Level: config.ConfigEnv, Location: location}, name); err != nil {
if err := api.Uninstall(name, target); err != nil {
return nil, err
}
return "ok", nil
Expand Down
Loading