@brantrusnak/openclaw-omadeus 1.0.2 → 1.0.4

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.
Files changed (41) hide show
  1. package/README.md +15 -61
  2. package/dist/_virtual/_rolldown/runtime.js +4 -0
  3. package/dist/api.js +5 -0
  4. package/dist/index.js +14 -0
  5. package/dist/runtime-api.js +15 -0
  6. package/dist/setup-entry.js +7 -0
  7. package/dist/src/allowed-reaction-emojis.js +21 -0
  8. package/dist/src/api/auth.api.js +118 -0
  9. package/dist/src/api/channel.api.js +23 -0
  10. package/dist/src/api/message.api.js +76 -0
  11. package/dist/src/api/nugget.api.js +127 -0
  12. package/dist/src/auth.js +30 -0
  13. package/dist/src/channel.js +626 -0
  14. package/dist/src/config.js +52 -0
  15. package/dist/src/defaults.js +5 -0
  16. package/dist/src/inbound-policy.js +205 -0
  17. package/dist/src/inbound.js +97 -0
  18. package/dist/src/member-resolve.js +53 -0
  19. package/dist/src/message-handler.js +262 -0
  20. package/dist/src/nugget-lookup.js +140 -0
  21. package/dist/src/onboarding.js +357 -0
  22. package/dist/src/outbound.js +17 -0
  23. package/dist/src/reply-dispatcher.js +46 -0
  24. package/dist/src/runtime.js +5 -0
  25. package/dist/src/setup-core.js +46 -0
  26. package/dist/src/setup-surface.js +2 -0
  27. package/dist/src/socket/dolphin.socket.js +18 -0
  28. package/dist/src/socket/jaguar.socket.js +22 -0
  29. package/dist/src/socket/socket.js +153 -0
  30. package/dist/src/store.js +13 -0
  31. package/dist/src/token.js +84 -0
  32. package/dist/src/types.js +15 -0
  33. package/dist/src/utils/http.util.js +43 -0
  34. package/dist/src/utils/jwt.util.js +15 -0
  35. package/package.json +10 -3
  36. package/src/api/auth.api.ts +27 -7
  37. package/src/channel.ts +127 -238
  38. package/src/member-resolve.ts +1 -1
  39. package/src/onboarding.ts +117 -163
  40. package/src/setup-core.ts +10 -1
  41. package/src/socket/socket.ts +24 -11
@@ -0,0 +1,153 @@
1
+ import { WebSocket } from "ws";
2
+ //#region src/socket/socket.ts
3
+ const RECONNECT_BASE_MS = 2e3;
4
+ const RECONNECT_MAX_MS = 6e4;
5
+ const HEARTBEAT_INTERVAL_MS = 3e4;
6
+ const HEARTBEAT_MISSED_MAX = 5;
7
+ const KEEP_ALIVE_CONTENT = "keep-alive";
8
+ const KEEP_ALIVE_ACTION = "answer";
9
+ function isServerKeepAlive(data) {
10
+ return data.content === KEEP_ALIVE_CONTENT;
11
+ }
12
+ function isClientKeepAlive(data) {
13
+ return data.data === KEEP_ALIVE_CONTENT;
14
+ }
15
+ function createOmadeusSocketClient(opts) {
16
+ const { maestroUrl, tokenManager, pathSuffix, logPrefix, onEvent, onConnect, onDisconnect, onError, log } = opts;
17
+ let ws = null;
18
+ let reconnectAttempt = 0;
19
+ let reconnectTimer = null;
20
+ let heartbeatTimer = null;
21
+ let heartbeatMissCount = 0;
22
+ let intentionalClose = false;
23
+ function buildWsUrl() {
24
+ const base = maestroUrl.replace(/^http/, "ws");
25
+ const token = tokenManager.getToken();
26
+ return `${base}/${pathSuffix}?token=${encodeURIComponent(token)}`;
27
+ }
28
+ function scheduleReconnect() {
29
+ if (intentionalClose) return;
30
+ const delayMs = Math.min(RECONNECT_BASE_MS * 2 ** reconnectAttempt, RECONNECT_MAX_MS);
31
+ reconnectAttempt++;
32
+ log?.info(`${logPrefix} reconnecting in ${delayMs}ms (attempt ${reconnectAttempt})`);
33
+ reconnectTimer = setTimeout(() => connect(), delayMs);
34
+ }
35
+ function stopHeartbeat() {
36
+ if (heartbeatTimer) {
37
+ clearInterval(heartbeatTimer);
38
+ heartbeatTimer = null;
39
+ }
40
+ }
41
+ function resetHeartbeat() {
42
+ heartbeatMissCount = 0;
43
+ }
44
+ function sendKeepAlive() {
45
+ if (ws?.readyState !== WebSocket.OPEN) return;
46
+ heartbeatMissCount += 1;
47
+ sendKeepAliveFrame();
48
+ if (heartbeatMissCount >= HEARTBEAT_MISSED_MAX) {
49
+ log?.warn(`${logPrefix} heartbeat unanswered ${heartbeatMissCount} times; reconnecting socket`);
50
+ ws.close();
51
+ }
52
+ }
53
+ function startHeartbeat() {
54
+ stopHeartbeat();
55
+ heartbeatTimer = setInterval(() => {
56
+ sendKeepAlive();
57
+ }, HEARTBEAT_INTERVAL_MS);
58
+ }
59
+ function sendKeepAliveFrame() {
60
+ if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({
61
+ data: KEEP_ALIVE_CONTENT,
62
+ action: KEEP_ALIVE_ACTION
63
+ }));
64
+ }
65
+ function connect() {
66
+ if (ws) {
67
+ ws.removeAllListeners();
68
+ ws.close();
69
+ ws = null;
70
+ }
71
+ intentionalClose = false;
72
+ stopHeartbeat();
73
+ resetHeartbeat();
74
+ if (tokenManager.needsRefresh()) {
75
+ tokenManager.refresh().then(() => connect()).catch((err) => {
76
+ onError?.(err instanceof Error ? err : new Error(String(err)));
77
+ scheduleReconnect();
78
+ });
79
+ return;
80
+ }
81
+ const url = buildWsUrl();
82
+ log?.info(`${logPrefix} connecting...`);
83
+ ws = new WebSocket(url);
84
+ ws.on("open", () => {
85
+ reconnectAttempt = 0;
86
+ log?.info(`${logPrefix} connected`);
87
+ onConnect?.();
88
+ resetHeartbeat();
89
+ sendKeepAlive();
90
+ startHeartbeat();
91
+ });
92
+ ws.on("message", (raw) => {
93
+ try {
94
+ const data = JSON.parse(String(raw));
95
+ const action = data.action;
96
+ if (isServerKeepAlive(data) && action === KEEP_ALIVE_ACTION) {
97
+ resetHeartbeat();
98
+ return;
99
+ }
100
+ if (isClientKeepAlive(data) && action === KEEP_ALIVE_ACTION) {
101
+ resetHeartbeat();
102
+ return;
103
+ }
104
+ if (isServerKeepAlive(data) && action === "heartbeat") {
105
+ resetHeartbeat();
106
+ sendKeepAliveFrame();
107
+ return;
108
+ }
109
+ resetHeartbeat();
110
+ onEvent?.(data);
111
+ } catch {
112
+ log?.warn(`${logPrefix} unparseable message: ${String(raw).slice(0, 200)}`);
113
+ }
114
+ });
115
+ ws.on("close", (code, reason) => {
116
+ const msg = `code=${code} reason=${String(reason)}`;
117
+ log?.info(`${logPrefix} disconnected: ${msg}`);
118
+ onDisconnect?.(msg);
119
+ ws = null;
120
+ stopHeartbeat();
121
+ resetHeartbeat();
122
+ scheduleReconnect();
123
+ });
124
+ ws.on("error", (err) => {
125
+ log?.error(`${logPrefix} error: ${err.message}`);
126
+ onError?.(err);
127
+ });
128
+ }
129
+ function disconnect() {
130
+ intentionalClose = true;
131
+ if (reconnectTimer) {
132
+ clearTimeout(reconnectTimer);
133
+ reconnectTimer = null;
134
+ }
135
+ stopHeartbeat();
136
+ resetHeartbeat();
137
+ if (ws) {
138
+ ws.removeAllListeners();
139
+ ws.close();
140
+ ws = null;
141
+ }
142
+ }
143
+ return {
144
+ connect,
145
+ disconnect,
146
+ isConnected: () => ws?.readyState === WebSocket.OPEN,
147
+ send: (data) => {
148
+ if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(data));
149
+ }
150
+ };
151
+ }
152
+ //#endregion
153
+ export { createOmadeusSocketClient };
@@ -0,0 +1,13 @@
1
+ //#region src/store.ts
2
+ let currentSession = null;
3
+ function setCasSession(session) {
4
+ currentSession = session;
5
+ }
6
+ function getCasSession() {
7
+ return currentSession;
8
+ }
9
+ function clearCasSession() {
10
+ currentSession = null;
11
+ }
12
+ //#endregion
13
+ export { clearCasSession, getCasSession, setCasSession };
@@ -0,0 +1,84 @@
1
+ import { decodeJwtPayload, tokenExpiresInMs } from "./utils/jwt.util.js";
2
+ import { authenticate } from "./auth.js";
3
+ //#region src/token.ts
4
+ const TOKEN_REFRESH_MARGIN_MS = 300 * 1e3;
5
+ const MAX_TIMEOUT_MS = 2147483647;
6
+ /** Whether the token should be refreshed now (within safety margin). */
7
+ function shouldRefreshToken(token) {
8
+ return tokenExpiresInMs(token) < TOKEN_REFRESH_MARGIN_MS;
9
+ }
10
+ function createTokenManager(params) {
11
+ const { casUrl, maestroUrl, email, password, organizationId, initialToken, onRefresh, onError } = params;
12
+ let currentToken = "";
13
+ let currentPayload = null;
14
+ if (initialToken) try {
15
+ const payload = decodeJwtPayload(initialToken);
16
+ currentToken = initialToken;
17
+ currentPayload = payload;
18
+ } catch (err) {
19
+ const error = err instanceof Error ? err : new Error(String(err));
20
+ onError?.(error);
21
+ }
22
+ let refreshTimer = null;
23
+ const refresh = async () => {
24
+ if (currentToken && !shouldRefreshToken(currentToken)) return;
25
+ const { dolphinToken, payload } = await authenticate({
26
+ casUrl,
27
+ maestroUrl,
28
+ email,
29
+ password,
30
+ organizationId
31
+ });
32
+ currentToken = dolphinToken;
33
+ currentPayload = payload;
34
+ onRefresh?.(dolphinToken);
35
+ };
36
+ const scheduleNextRefresh = () => {
37
+ if (refreshTimer) {
38
+ clearTimeout(refreshTimer);
39
+ refreshTimer = null;
40
+ }
41
+ if (!currentToken) return;
42
+ const desiredDelayMs = tokenExpiresInMs(currentToken) - TOKEN_REFRESH_MARGIN_MS;
43
+ refreshTimer = setTimeout(async () => {
44
+ try {
45
+ await refresh();
46
+ scheduleNextRefresh();
47
+ } catch (err) {
48
+ onError?.(err instanceof Error ? err : new Error(String(err)));
49
+ refreshTimer = setTimeout(() => void scheduleNextRefresh(), 3e4);
50
+ }
51
+ }, Math.min(Math.max(desiredDelayMs, 1e4), MAX_TIMEOUT_MS));
52
+ };
53
+ return {
54
+ getToken() {
55
+ return currentToken;
56
+ },
57
+ getPayload() {
58
+ if (!currentPayload) throw new Error("Omadeus: not authenticated");
59
+ return currentPayload;
60
+ },
61
+ async refresh() {
62
+ try {
63
+ await refresh();
64
+ } catch (err) {
65
+ onError?.(err instanceof Error ? err : new Error(String(err)));
66
+ throw err;
67
+ }
68
+ },
69
+ startAutoRefresh() {
70
+ scheduleNextRefresh();
71
+ },
72
+ stopAutoRefresh() {
73
+ if (refreshTimer) {
74
+ clearTimeout(refreshTimer);
75
+ refreshTimer = null;
76
+ }
77
+ },
78
+ needsRefresh() {
79
+ return !currentToken || shouldRefreshToken(currentToken);
80
+ }
81
+ };
82
+ }
83
+ //#endregion
84
+ export { createTokenManager };
@@ -0,0 +1,15 @@
1
+ //#region src/types.ts
2
+ /** Jaguar entity chats (`subscribableKind`) — DMs and channel rooms use other values. */
3
+ const OMADEUS_INBOUND_ENTITY_KINDS = [
4
+ "task",
5
+ "nugget",
6
+ "project",
7
+ "release",
8
+ "sprint",
9
+ "summary",
10
+ "client",
11
+ "folder"
12
+ ];
13
+ const OMADEUS_INBOUND_ENTITY_KIND_SET = new Set(OMADEUS_INBOUND_ENTITY_KINDS);
14
+ //#endregion
15
+ export { OMADEUS_INBOUND_ENTITY_KINDS, OMADEUS_INBOUND_ENTITY_KIND_SET };
@@ -0,0 +1,43 @@
1
+ import { randomUUID } from "node:crypto";
2
+ //#region src/utils/http.util.ts
3
+ function authHeaders(token) {
4
+ return {
5
+ Authorization: `Bearer ${token}`,
6
+ "Content-Type": "application/json"
7
+ };
8
+ }
9
+ async function apiFetch(opts, path, init) {
10
+ const token = opts.tokenManager.getToken();
11
+ if (!token) throw new Error("Omadeus: not authenticated");
12
+ const url = `${opts.maestroUrl}${path}`;
13
+ try {
14
+ return await fetch(url, {
15
+ ...init,
16
+ headers: {
17
+ ...authHeaders(token),
18
+ ...init?.headers
19
+ }
20
+ });
21
+ } catch (err) {
22
+ const message = err instanceof Error ? err.message : String(err);
23
+ throw new Error(`Omadeus API request to ${url} failed: ${message}`);
24
+ }
25
+ }
26
+ function withApiPrefix(prefix, path) {
27
+ if (!path) return prefix;
28
+ if (path.startsWith("/")) return `${prefix}${path}`;
29
+ return `${prefix}/${path}`;
30
+ }
31
+ const JAGUAR_PREFIX = "/jaguar/apiv1";
32
+ const DOLPHIN_PREFIX = "/dolphin/apiv1";
33
+ async function jaguarFetch(opts, path, init) {
34
+ return apiFetch(opts, withApiPrefix(JAGUAR_PREFIX, path), init);
35
+ }
36
+ async function dolphinFetch(opts, path, init) {
37
+ return apiFetch(opts, withApiPrefix(DOLPHIN_PREFIX, path), init);
38
+ }
39
+ function generateTemporaryId() {
40
+ return `_${randomUUID().replace(/-/g, "").slice(0, 10)}`;
41
+ }
42
+ //#endregion
43
+ export { dolphinFetch, generateTemporaryId, jaguarFetch };
@@ -0,0 +1,15 @@
1
+ //#region src/utils/jwt.util.ts
2
+ /** Decode the payload portion of a JWT without verifying the signature. */
3
+ function decodeJwtPayload(token) {
4
+ const parts = token.split(".");
5
+ if (parts.length !== 3) throw new Error("Invalid JWT: expected 3 parts");
6
+ const payload = Buffer.from(parts[1], "base64url").toString("utf-8");
7
+ return JSON.parse(payload);
8
+ }
9
+ /** Returns ms until the token expires (negative = already expired). */
10
+ function tokenExpiresInMs(token) {
11
+ const { exp } = decodeJwtPayload(token);
12
+ return exp * 1e3 - Date.now();
13
+ }
14
+ //#endregion
15
+ export { decodeJwtPayload, tokenExpiresInMs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brantrusnak/openclaw-omadeus",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "private": false,
5
5
  "description": "OpenClaw Omadeus project management channel plugin",
6
6
  "homepage": "https://github.com/brantrusnak/openclaw-omadeus-plugin#readme",
@@ -14,15 +14,17 @@
14
14
  "license": "ISC",
15
15
  "author": "Brant Rusnak",
16
16
  "type": "module",
17
- "main": "./index.ts",
17
+ "main": "./dist/index.js",
18
18
  "scripts": {
19
+ "build": "rolldown -c rolldown.config.mjs",
19
20
  "test": "vitest run",
20
- "prepack": "node ./scripts/verify-npm-files.mjs"
21
+ "prepack": "npm run build && node ./scripts/verify-npm-files.mjs"
21
22
  },
22
23
  "dependencies": {
23
24
  "ws": "^8.20.0"
24
25
  },
25
26
  "files": [
27
+ "dist",
26
28
  "index.ts",
27
29
  "setup-entry.ts",
28
30
  "api.ts",
@@ -33,6 +35,7 @@
33
35
  ],
34
36
  "devDependencies": {
35
37
  "openclaw": ">=2026.4.10",
38
+ "rolldown": "1.0.0-rc.17",
36
39
  "vitest": "^4.1.5"
37
40
  },
38
41
  "peerDependencies": {
@@ -50,7 +53,11 @@
50
53
  "extensions": [
51
54
  "./index.ts"
52
55
  ],
56
+ "runtimeExtensions": [
57
+ "./dist/index.js"
58
+ ],
53
59
  "setupEntry": "./setup-entry.ts",
60
+ "runtimeSetupEntry": "./dist/setup-entry.js",
54
61
  "channel": {
55
62
  "id": "omadeus",
56
63
  "label": "Omadeus",
@@ -9,6 +9,27 @@ import type {
9
9
  const CAS_APPLICATION_ID = 1;
10
10
  const CAS_SCOPES = "title,email,avatar,firstName,lastName,birth,phone,countryCode";
11
11
 
12
+ function formatFetchError(label: string, url: string, method: string, err: unknown): Error {
13
+ const base = err instanceof Error ? err.message : String(err);
14
+ const cause =
15
+ err instanceof Error && err.cause instanceof Error ? err.cause.message : undefined;
16
+ const detail = cause && cause !== base ? `${base} (${cause})` : base;
17
+ return new Error(`${label} (${method} ${url}) failed: ${detail}`);
18
+ }
19
+
20
+ async function omadeusFetch(
21
+ label: string,
22
+ url: string,
23
+ init: RequestInit,
24
+ ): Promise<Response> {
25
+ const method = init.method ?? "GET";
26
+ try {
27
+ return await fetch(url, init);
28
+ } catch (err) {
29
+ throw formatFetchError(label, url, method, err);
30
+ }
31
+ }
32
+
12
33
  export async function createCasToken(params: {
13
34
  casUrl: string;
14
35
  email: string;
@@ -17,11 +38,10 @@ export async function createCasToken(params: {
17
38
  const { casUrl, email, password } = params;
18
39
  const url = `${casUrl}/apiv1/tokens`;
19
40
  const jsonBody = JSON.stringify({ email, password });
20
- const res = await fetch(url, {
41
+ const res = await omadeusFetch("CAS token request", url, {
21
42
  method: "CREATE",
22
43
  headers: {
23
44
  "Content-Type": "application/json;charset=UTF-8",
24
- "Content-Length": String(jsonBody.length),
25
45
  },
26
46
  body: jsonBody,
27
47
  });
@@ -44,7 +64,7 @@ export async function getMe(params: {
44
64
  }): Promise<{ email: string }> {
45
65
  const { casUrl, casToken, refreshCookie } = params;
46
66
  const url = `${casUrl}/apiv1/members/me`;
47
- const res = await fetch(url, {
67
+ const res = await omadeusFetch("CAS get member", url, {
48
68
  method: "GET",
49
69
  headers: {
50
70
  Authorization: `Bearer ${casToken}`,
@@ -80,7 +100,7 @@ export async function createAuthorizationCode(params: {
80
100
  Authorization: `Bearer ${token}`,
81
101
  ...(casSession?.refreshCookie ? { Cookie: casSession.refreshCookie } : {}),
82
102
  };
83
- const res = await fetch(url, {
103
+ const res = await omadeusFetch("CAS authorization code request", url, {
84
104
  method: "CREATE",
85
105
  body,
86
106
  headers,
@@ -104,7 +124,7 @@ export async function obtainSessionToken(params: {
104
124
  }): Promise<string> {
105
125
  const { maestroUrl, authorizationCode, organizationId } = params;
106
126
  const url = `${maestroUrl}/dolphin/apiv1/oauth2/tokens`;
107
- const res = await fetch(url, {
127
+ const res = await omadeusFetch("Omadeus session token request", url, {
108
128
  method: "OBTAIN",
109
129
  headers: { "Content-Type": "application/json;charset=UTF-8" },
110
130
  body: JSON.stringify({ authorizationCode, organizationId }),
@@ -126,7 +146,7 @@ export async function listOrganizations(params: {
126
146
  }): Promise<OmadeusOrganization[]> {
127
147
  const { maestroUrl, email } = params;
128
148
  const url = `${maestroUrl}/dolphin/apiv1/organizations`;
129
- const res = await fetch(url, {
149
+ const res = await omadeusFetch("Omadeus list organizations", url, {
130
150
  method: "LIST",
131
151
  headers: { "Content-Type": "application/json;charset=UTF-8" },
132
152
  body: JSON.stringify({ email }),
@@ -145,7 +165,7 @@ export async function listOrganizationMembers(params: {
145
165
  }): Promise<OmadeusOrganizationMember[]> {
146
166
  const { maestroUrl, sessionToken, organizationId } = params;
147
167
  const url = `${maestroUrl}/dolphin/apiv1/organizations/${organizationId}/members`;
148
- const res = await fetch(url, {
168
+ const res = await omadeusFetch("Omadeus list organization members", url, {
149
169
  method: "LIST",
150
170
  headers: {
151
171
  Authorization: `Bearer ${sessionToken}`,