doover-js 0.1.2 → 0.2.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,22 +4,27 @@ TypeScript client for Doover.
4
4
 
5
5
  ## Exports
6
6
 
7
+ ### Root (`doover-js`)
8
+
7
9
  - `DooverClient`
8
10
  - `DooverDataProvider`
9
11
  - `GatewayClient`
10
- - `AgentsApi`
11
- - `ChannelsApi`
12
- - `MessagesApi`
13
- - `AggregatesApi`
14
- - `AlarmsApi`
15
- - `ConnectionsApi`
16
- - `NotificationsApi`
17
- - `PermissionsApi`
18
- - `ProcessorsApi`
19
- - `TurnApi`
12
+ - `RestClient`
13
+ - `DooverAuth`, `CookieAuth`, `DooverTokenAuth`
14
+ - `AuthProfile`, `DooverAuthError`
15
+ - `buildAuth`
16
+ - `AgentsApi`, `ChannelsApi`, `MessagesApi`, `AggregatesApi`, `AlarmsApi`, `ConnectionsApi`, `NotificationsApi`, `PermissionsApi`, `ProcessorsApi`, `TurnApi`
17
+
18
+ ### Node subpath (`doover-js/node`)
19
+
20
+ - `ConfigManager` — file-backed profile store (Node-only, uses `fs`)
20
21
 
21
22
  ## Usage
22
23
 
24
+ ### Cookie-only browser usage (default)
25
+
26
+ When no auth inputs are provided, the client uses ambient cookies (`credentials: "include"`). This is the default browser behaviour and matches the original API.
27
+
23
28
  ```ts
24
29
  import { DooverClient } from "doover-js";
25
30
 
@@ -32,4 +37,93 @@ const client = new DooverClient({
32
37
  const channels = await client.viewer.getChannels({ agentId: "123" });
33
38
  ```
34
39
 
40
+ ### Explicit token usage
41
+
42
+ Pass a token directly to use bearer auth. The client will send `Authorization: Bearer <token>` on every HTTP request and use `credentials: "omit"`.
43
+
44
+ ```ts
45
+ import { DooverClient } from "doover-js";
46
+
47
+ const client = new DooverClient({
48
+ dataRestUrl: "https://example.com/api",
49
+ controlApiUrl: "https://example.com/control",
50
+ dataWssUrl: "wss://example.com/gateway",
51
+ token: "your-access-token",
52
+ refreshToken: "your-refresh-token",
53
+ authServerUrl: "https://auth.example.com",
54
+ authServerClientId: "your-client-id",
55
+ });
56
+
57
+ // Token refresh happens automatically when the token expires
58
+ const me = await client.rest.get("/users/me", undefined, client.rest.config.controlApiUrl);
59
+ ```
60
+
61
+ ### Profile / ConfigManager usage (Node)
62
+
63
+ Use the `ConfigManager` from the `doover-js/node` subpath to load profiles from `~/.doover/config`, matching pydoover's config format.
64
+
65
+ ```ts
66
+ import { DooverClient } from "doover-js";
67
+ import { ConfigManager } from "doover-js/node";
68
+
69
+ const configManager = new ConfigManager(); // reads ~/.doover/config
70
+ const client = new DooverClient({
71
+ dataRestUrl: "https://example.com/api",
72
+ controlApiUrl: "https://example.com/control",
73
+ dataWssUrl: "wss://example.com/gateway",
74
+ profile: "production",
75
+ configManager,
76
+ });
77
+ ```
78
+
79
+ You can also pass an `AuthProfile` instance directly:
80
+
81
+ ```ts
82
+ import { DooverClient, AuthProfile } from "doover-js";
83
+
84
+ const profile = new AuthProfile({
85
+ profile: "custom",
86
+ token: "my-token",
87
+ refreshToken: "my-refresh-token",
88
+ authServerUrl: "https://auth.example.com",
89
+ authServerClientId: "client-id",
90
+ });
91
+
92
+ const client = new DooverClient({
93
+ dataRestUrl: "https://example.com/api",
94
+ controlApiUrl: "https://example.com/control",
95
+ dataWssUrl: "wss://example.com/gateway",
96
+ profile,
97
+ });
98
+ ```
99
+
100
+ ### WebSocket auth behaviour
101
+
102
+ The auth layer automatically handles websocket authentication:
103
+
104
+ - **Cookie auth**: Uses the original websocket URL and relies on ambient cookies.
105
+ - **Token auth with `webSocketFactory`**: Passes `Authorization: Bearer <token>` via headers.
106
+ - **Token auth with standard `WebSocket`**: Appends `?token=<token>` to the websocket URL.
107
+
108
+ For Node.js websocket clients that support custom headers, provide a `webSocketFactory`:
109
+
110
+ ```ts
111
+ import WebSocket from "ws";
112
+ import { DooverClient } from "doover-js";
113
+
114
+ const client = new DooverClient({
115
+ dataRestUrl: "https://example.com/api",
116
+ controlApiUrl: "https://example.com/control",
117
+ dataWssUrl: "wss://example.com/gateway",
118
+ token: "your-token",
119
+ webSocketFactory: ({ url, headers }) => new WebSocket(url, { headers }),
120
+ });
121
+ ```
122
+
123
+ Reconnections automatically use the latest (potentially refreshed) token.
124
+
125
+ ## Architecture
126
+
127
+ `DooverClient` builds one shared `DooverAuth` instance and injects it into `RestClient`, `DooverDataProvider`, and `GatewayClient`. Token refreshes propagate everywhere automatically.
128
+
35
129
  `DooverDataProvider` preserves the older viewer-oriented interface. `DooverClient` exposes the broader API surface through subclients.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Browser-safe value object representing a single Doover auth profile.
3
+ *
4
+ * Property names use camelCase JS conventions. The static helpers `parse` and
5
+ * `dump` translate to/from the on-disk format shared with pydoover.
6
+ */
7
+ export interface AuthProfileData {
8
+ profile?: string;
9
+ token?: string | null;
10
+ tokenExpires?: string | null;
11
+ agentId?: string | null;
12
+ controlBaseUrl?: string | null;
13
+ dataBaseUrl?: string | null;
14
+ refreshToken?: string | null;
15
+ refreshTokenId?: string | null;
16
+ authServerUrl?: string | null;
17
+ authServerClientId?: string | null;
18
+ }
19
+ export declare class AuthProfile {
20
+ profile: string;
21
+ token: string | null;
22
+ tokenExpires: string | null;
23
+ agentId: string | null;
24
+ controlBaseUrl: string | null;
25
+ dataBaseUrl: string | null;
26
+ refreshToken: string | null;
27
+ refreshTokenId: string | null;
28
+ authServerUrl: string | null;
29
+ authServerClientId: string | null;
30
+ constructor(data: AuthProfileData & {
31
+ profile: string;
32
+ });
33
+ /**
34
+ * Parse a block of lines (excluding the `[profile=...]` header) into an
35
+ * `AuthProfile`. The profile name must be provided separately.
36
+ */
37
+ static parse(profileName: string, lines: string[]): AuthProfile;
38
+ /**
39
+ * Serialise to the on-disk reduced format (without the `[profile=...]`
40
+ * header line, which the caller is responsible for).
41
+ */
42
+ dump(): string;
43
+ /** Full block including the header line. */
44
+ dumpBlock(): string;
45
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthProfile = void 0;
4
+ /** On-disk key → JS property mapping (pydoover reduced-format Doover2). */
5
+ const DISK_KEY_MAP = {
6
+ TOKEN: "token",
7
+ TOKEN_EXPIRES: "tokenExpires",
8
+ AGENT_ID: "agentId",
9
+ BASE_URL: "controlBaseUrl",
10
+ BASE_DATA_URL: "dataBaseUrl",
11
+ REFRESH_TOKEN: "refreshToken",
12
+ REFRESH_TOKEN_ID: "refreshTokenId",
13
+ AUTH_SERVER_URL: "authServerUrl",
14
+ AUTH_SERVER_CLIENT_ID: "authServerClientId",
15
+ };
16
+ /** JS property → on-disk key mapping. */
17
+ const JS_KEY_MAP = Object.fromEntries(Object.entries(DISK_KEY_MAP).map(([disk, js]) => [js, disk]));
18
+ class AuthProfile {
19
+ constructor(data) {
20
+ this.profile = data.profile;
21
+ this.token = data.token ?? null;
22
+ this.tokenExpires = data.tokenExpires ?? null;
23
+ this.agentId = data.agentId ?? null;
24
+ this.controlBaseUrl = data.controlBaseUrl ?? null;
25
+ this.dataBaseUrl = data.dataBaseUrl ?? null;
26
+ this.refreshToken = data.refreshToken ?? null;
27
+ this.refreshTokenId = data.refreshTokenId ?? null;
28
+ this.authServerUrl = data.authServerUrl ?? null;
29
+ this.authServerClientId = data.authServerClientId ?? null;
30
+ }
31
+ /**
32
+ * Parse a block of lines (excluding the `[profile=...]` header) into an
33
+ * `AuthProfile`. The profile name must be provided separately.
34
+ */
35
+ static parse(profileName, lines) {
36
+ const data = { profile: profileName };
37
+ for (const line of lines) {
38
+ const trimmed = line.trim();
39
+ if (!trimmed || trimmed.startsWith("#"))
40
+ continue;
41
+ const eqIdx = trimmed.indexOf("=");
42
+ if (eqIdx === -1)
43
+ continue;
44
+ const key = trimmed.slice(0, eqIdx).trim();
45
+ const value = trimmed.slice(eqIdx + 1).trim();
46
+ const jsProp = DISK_KEY_MAP[key];
47
+ if (jsProp) {
48
+ data[jsProp] = value || null;
49
+ }
50
+ }
51
+ return new AuthProfile(data);
52
+ }
53
+ /**
54
+ * Serialise to the on-disk reduced format (without the `[profile=...]`
55
+ * header line, which the caller is responsible for).
56
+ */
57
+ dump() {
58
+ const lines = [];
59
+ for (const jsProp of Object.keys(JS_KEY_MAP)) {
60
+ const value = this[jsProp];
61
+ if (value !== null && value !== undefined) {
62
+ lines.push(`${JS_KEY_MAP[jsProp]}=${value}`);
63
+ }
64
+ }
65
+ return lines.join("\n");
66
+ }
67
+ /** Full block including the header line. */
68
+ dumpBlock() {
69
+ return `[profile=${this.profile}]\n${this.dump()}`;
70
+ }
71
+ }
72
+ exports.AuthProfile = AuthProfile;
@@ -0,0 +1,14 @@
1
+ import type { AuthProfile } from "./auth-profile";
2
+ /**
3
+ * Structural interface for profile stores accepted by auth classes.
4
+ * Implementations may be backed by files (ConfigManager in the Node subpath)
5
+ * or by in-memory stores for testing.
6
+ */
7
+ export interface AuthProfileStore {
8
+ currentProfile: string | null;
9
+ current: AuthProfile | null;
10
+ get(profileName: string): AuthProfile | undefined;
11
+ create(entry: AuthProfile): void;
12
+ delete(profileName: string): void;
13
+ write(): void;
14
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,37 @@
1
+ import type { AuthProfile } from "./auth-profile";
2
+ import type { AuthProfileStore } from "./auth-store";
3
+ import { DooverAuth } from "./doover-auth";
4
+ /**
5
+ * Raw auth inputs that can be provided in client configs.
6
+ *
7
+ * If `auth` is provided it is used as-is and raw inputs must not be combined
8
+ * with it. Otherwise these fields are used to build the appropriate auth
9
+ * instance automatically.
10
+ */
11
+ export interface AuthConfig {
12
+ auth?: DooverAuth;
13
+ profile?: string | AuthProfile;
14
+ configManager?: AuthProfileStore;
15
+ token?: string | null;
16
+ tokenExpires?: Date | number | null;
17
+ refreshToken?: string | null;
18
+ refreshTokenId?: string | null;
19
+ authServerUrl?: string | null;
20
+ authServerClientId?: string | null;
21
+ }
22
+ /** Fetch implementation to thread through to DooverTokenAuth. */
23
+ interface BuildAuthOptions extends AuthConfig {
24
+ fetchImpl?: typeof fetch;
25
+ }
26
+ /**
27
+ * Build a `DooverAuth` instance from the provided configuration.
28
+ *
29
+ * Rules:
30
+ * 1. If `auth` is provided it is returned as-is.
31
+ * 2. If any token-related field is present, build `DooverTokenAuth`.
32
+ * 3. If `profile` is an `AuthProfile`, use it to seed a `DooverTokenAuth`.
33
+ * 4. If `profile` is a string, look it up via `configManager`.
34
+ * 5. Otherwise fall back to `CookieAuth`.
35
+ */
36
+ export declare function buildAuth(options: BuildAuthOptions): DooverAuth;
37
+ export {};
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAuth = buildAuth;
4
+ const cookie_auth_1 = require("./cookie-auth");
5
+ const doover_token_auth_1 = require("./doover-token-auth");
6
+ const errors_1 = require("./errors");
7
+ const TOKEN_FIELDS = [
8
+ "token",
9
+ "tokenExpires",
10
+ "refreshToken",
11
+ "refreshTokenId",
12
+ "authServerUrl",
13
+ "authServerClientId",
14
+ ];
15
+ /**
16
+ * Build a `DooverAuth` instance from the provided configuration.
17
+ *
18
+ * Rules:
19
+ * 1. If `auth` is provided it is returned as-is.
20
+ * 2. If any token-related field is present, build `DooverTokenAuth`.
21
+ * 3. If `profile` is an `AuthProfile`, use it to seed a `DooverTokenAuth`.
22
+ * 4. If `profile` is a string, look it up via `configManager`.
23
+ * 5. Otherwise fall back to `CookieAuth`.
24
+ */
25
+ function buildAuth(options) {
26
+ if (options.auth) {
27
+ // Validate mutual exclusivity.
28
+ const hasRaw = TOKEN_FIELDS.some((k) => options[k] !== undefined);
29
+ if (hasRaw || options.profile !== undefined || options.configManager !== undefined) {
30
+ throw new errors_1.DooverAuthError("Cannot combine 'auth' with raw auth config fields (token, profile, configManager, etc.)");
31
+ }
32
+ return options.auth;
33
+ }
34
+ // Resolve the profile object.
35
+ let resolvedProfile = null;
36
+ if (typeof options.profile === "string") {
37
+ if (!options.configManager) {
38
+ throw new errors_1.DooverAuthError("A 'configManager' (AuthProfileStore) is required when 'profile' is a string. " +
39
+ "Use the doover-js/node subpath to import ConfigManager, or provide an AuthProfile directly.");
40
+ }
41
+ resolvedProfile = options.configManager.get(options.profile) ?? null;
42
+ if (!resolvedProfile) {
43
+ throw new errors_1.DooverAuthError(`Profile '${options.profile}' not found in the config manager`);
44
+ }
45
+ }
46
+ else if (options.profile instanceof Object && "profile" in options.profile) {
47
+ // AuthProfile instance
48
+ resolvedProfile = options.profile;
49
+ }
50
+ // Determine whether we need token auth.
51
+ const hasTokenInput = TOKEN_FIELDS.some((k) => options[k] !== undefined);
52
+ const profileHasToken = resolvedProfile?.token != null;
53
+ if (!hasTokenInput && !profileHasToken) {
54
+ // No token data at all — fall back to cookie auth.
55
+ const auth = new cookie_auth_1.CookieAuth();
56
+ if (resolvedProfile) {
57
+ auth.attachProfile(resolvedProfile, options.configManager);
58
+ }
59
+ return auth;
60
+ }
61
+ // Build DooverTokenAuth with explicit fields overriding profile values.
62
+ const auth = new doover_token_auth_1.DooverTokenAuth({
63
+ token: options.token ?? undefined,
64
+ tokenExpires: options.tokenExpires ?? undefined,
65
+ refreshToken: options.refreshToken ?? undefined,
66
+ refreshTokenId: options.refreshTokenId ?? undefined,
67
+ authServerUrl: options.authServerUrl ?? undefined,
68
+ authServerClientId: options.authServerClientId ?? undefined,
69
+ fetchImpl: options.fetchImpl,
70
+ });
71
+ if (resolvedProfile) {
72
+ auth.attachProfile(resolvedProfile, options.configManager);
73
+ }
74
+ return auth;
75
+ }
@@ -0,0 +1,17 @@
1
+ import { DooverAuth } from "./doover-auth";
2
+ /**
3
+ * Cookie-based auth — the default browser strategy.
4
+ *
5
+ * Relies on ambient cookies (`credentials: "include"`) and does not add any
6
+ * `Authorization` header. Token refresh is not applicable.
7
+ */
8
+ export declare class CookieAuth extends DooverAuth {
9
+ getHttpHeaders(): Promise<Record<string, string>>;
10
+ getFetchCredentials(): RequestCredentials;
11
+ prepareWebSocket(url: string, _canUseHeaders: boolean): Promise<{
12
+ url: string;
13
+ headers?: Record<string, string>;
14
+ }>;
15
+ setToken(_token: string | null, _tokenExpires?: Date | number | null): void;
16
+ ensureReady(): Promise<void>;
17
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CookieAuth = void 0;
4
+ const doover_auth_1 = require("./doover-auth");
5
+ /**
6
+ * Cookie-based auth — the default browser strategy.
7
+ *
8
+ * Relies on ambient cookies (`credentials: "include"`) and does not add any
9
+ * `Authorization` header. Token refresh is not applicable.
10
+ */
11
+ class CookieAuth extends doover_auth_1.DooverAuth {
12
+ async getHttpHeaders() {
13
+ return {};
14
+ }
15
+ getFetchCredentials() {
16
+ return "include";
17
+ }
18
+ async prepareWebSocket(url, _canUseHeaders) {
19
+ // Cookie auth relies on ambient cookies — no URL or header changes.
20
+ return { url };
21
+ }
22
+ setToken(_token, _tokenExpires) {
23
+ // No-op for cookie auth.
24
+ }
25
+ async ensureReady() {
26
+ // Nothing to prepare for cookie auth.
27
+ }
28
+ }
29
+ exports.CookieAuth = CookieAuth;
@@ -0,0 +1,41 @@
1
+ import type { AuthProfile } from "./auth-profile";
2
+ import type { AuthProfileStore } from "./auth-store";
3
+ /**
4
+ * Abstract base class shared by cookie and token auth strategies.
5
+ *
6
+ * Every client (`RestClient`, `GatewayClient`, `DooverDataProvider`) holds a
7
+ * reference to one `DooverAuth` instance so that refreshed tokens propagate
8
+ * everywhere automatically.
9
+ */
10
+ export declare abstract class DooverAuth {
11
+ protected profile: AuthProfile | null;
12
+ protected configManager: AuthProfileStore | null;
13
+ /** Headers to merge into every outgoing HTTP request. */
14
+ abstract getHttpHeaders(): Promise<Record<string, string>>;
15
+ /** The `credentials` value for `fetch()`. */
16
+ abstract getFetchCredentials(): RequestCredentials;
17
+ /**
18
+ * Prepare websocket connection parameters.
19
+ *
20
+ * @param url The base websocket URL.
21
+ * @param canUseHeaders `true` when a `webSocketFactory` that supports
22
+ * custom headers is available.
23
+ * @returns An object with the (possibly modified) URL and optional headers.
24
+ */
25
+ abstract prepareWebSocket(url: string, canUseHeaders: boolean): Promise<{
26
+ url: string;
27
+ headers?: Record<string, string>;
28
+ }>;
29
+ /**
30
+ * Imperatively set or clear the current access token.
31
+ * Subclasses that do not use tokens may no-op.
32
+ */
33
+ abstract setToken(token: string | null, tokenExpires?: Date | number | null): void;
34
+ /**
35
+ * Ensure the auth layer is ready (e.g. token refreshed if needed).
36
+ * Called before every HTTP request and before opening a websocket.
37
+ */
38
+ abstract ensureReady(): Promise<void>;
39
+ /** Attach a profile and optional config manager for refresh persistence. */
40
+ attachProfile(profile: AuthProfile, configManager?: AuthProfileStore): void;
41
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DooverAuth = void 0;
4
+ /**
5
+ * Abstract base class shared by cookie and token auth strategies.
6
+ *
7
+ * Every client (`RestClient`, `GatewayClient`, `DooverDataProvider`) holds a
8
+ * reference to one `DooverAuth` instance so that refreshed tokens propagate
9
+ * everywhere automatically.
10
+ */
11
+ class DooverAuth {
12
+ constructor() {
13
+ this.profile = null;
14
+ this.configManager = null;
15
+ }
16
+ /** Attach a profile and optional config manager for refresh persistence. */
17
+ attachProfile(profile, configManager) {
18
+ this.profile = profile;
19
+ if (configManager) {
20
+ this.configManager = configManager;
21
+ }
22
+ }
23
+ }
24
+ exports.DooverAuth = DooverAuth;
@@ -0,0 +1,35 @@
1
+ import { DooverAuth } from "./doover-auth";
2
+ import type { AuthProfile } from "./auth-profile";
3
+ import type { AuthProfileStore } from "./auth-store";
4
+ export declare class DooverTokenAuth extends DooverAuth {
5
+ private token;
6
+ private tokenExpires;
7
+ private refreshToken;
8
+ private refreshTokenId;
9
+ private authServerUrl;
10
+ private authServerClientId;
11
+ private fetchImpl;
12
+ private refreshInFlight;
13
+ constructor(options: {
14
+ token?: string | null;
15
+ tokenExpires?: Date | number | null;
16
+ refreshToken?: string | null;
17
+ refreshTokenId?: string | null;
18
+ authServerUrl?: string | null;
19
+ authServerClientId?: string | null;
20
+ fetchImpl?: typeof fetch;
21
+ });
22
+ getHttpHeaders(): Promise<Record<string, string>>;
23
+ getFetchCredentials(): RequestCredentials;
24
+ prepareWebSocket(url: string, canUseHeaders: boolean): Promise<{
25
+ url: string;
26
+ headers?: Record<string, string>;
27
+ }>;
28
+ setToken(token: string | null, tokenExpires?: Date | number | null): void;
29
+ ensureReady(): Promise<void>;
30
+ attachProfile(profile: AuthProfile, configManager?: AuthProfileStore): void;
31
+ private needsRefresh;
32
+ private refresh;
33
+ private persistToProfile;
34
+ private resolveExpiry;
35
+ }