@sideboard-ai/core 0.1.201 → 0.1.203

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 (44) hide show
  1. package/dist/{abletime-KGPD3UUJ.js → abletime-JFGZBDHH.js} +4 -2
  2. package/dist/{abletime-345SE3B4.js → abletime-SISJEPJ3.js} +4 -2
  3. package/dist/abletime-oauth-7TD5Z2QV.js +265 -0
  4. package/dist/abletime-oauth-OSQYFB3E.js +40 -0
  5. package/dist/abletime-rest-QPJTDLKX.js +25 -0
  6. package/dist/abletime-rest-ZJGWYALM.js +24 -0
  7. package/dist/{agents-KFAGRM46.js → agents-3755WYQM.js} +4 -4
  8. package/dist/{agents-HB6N3XJL.js → agents-ZMWL43SP.js} +4 -4
  9. package/dist/{app-settings-V6IHESEQ.js → app-settings-LGHMNHSP.js} +1 -1
  10. package/dist/{app-settings-AXCURTZJ.js → app-settings-YEPF46IJ.js} +1 -1
  11. package/dist/{chunk-AHPIU2VP.js → chunk-2DVCCLII.js} +97 -64
  12. package/dist/chunk-3PYH2RLA.js +388 -0
  13. package/dist/{chunk-RV3ZKE4Y.js → chunk-5IXJTUST.js} +5 -5
  14. package/dist/chunk-6VEIKW5C.js +390 -0
  15. package/dist/chunk-BFEBFGCS.js +250 -0
  16. package/dist/{chunk-STPZQR5B.js → chunk-CMGRV36E.js} +1 -1
  17. package/dist/{chunk-PGQGP4FD.js → chunk-FZ6IBUKY.js} +36 -218
  18. package/dist/{chunk-7AHAABKN.js → chunk-FZYYEMET.js} +36 -228
  19. package/dist/{chunk-54RSAMBH.js → chunk-LCU6GBZP.js} +2 -2
  20. package/dist/chunk-MHPINQKY.js +260 -0
  21. package/dist/chunk-PJHFNFXA.js +243 -0
  22. package/dist/{chunk-CXFW5LIH.js → chunk-QFBBUFD4.js} +38 -3
  23. package/dist/{chunk-RPX6VDW7.js → chunk-QSEF2YVQ.js} +1 -1
  24. package/dist/{chunk-LJHC62LZ.js → chunk-TNDUTGDV.js} +2 -2
  25. package/dist/{chunk-EXZMNINM.js → chunk-TZNJKZ2A.js} +101 -68
  26. package/dist/{chunk-3TOGAUQT.js → chunk-X2TFUFIS.js} +5 -5
  27. package/dist/{chunk-ISV5IFH3.js → chunk-X743YN5Y.js} +38 -3
  28. package/dist/{coordinator-prompt-TY5FHAXC.js → coordinator-prompt-2B2JA2IE.js} +3 -3
  29. package/dist/{coordinator-prompt-BH4EHFOO.js → coordinator-prompt-U4PEZDAW.js} +3 -3
  30. package/dist/{global-workspace-NLLAPQJI.js → global-workspace-JPJ3FKKY.js} +3 -3
  31. package/dist/{global-workspace-5AZEEQRU.js → global-workspace-PU7ZS3LS.js} +3 -3
  32. package/dist/index.cjs +932 -145
  33. package/dist/index.d.cts +61 -9
  34. package/dist/index.d.ts +61 -9
  35. package/dist/index.js +63 -18
  36. package/dist/mcp/run-stdio.cjs +863 -117
  37. package/dist/mcp/run-stdio.js +22 -20
  38. package/dist/{orchestrator-SWWQQFFD.js → orchestrator-3A6KUTNI.js} +5 -5
  39. package/dist/{orchestrator-47JZUHRC.js → orchestrator-Q33M24ZJ.js} +6 -6
  40. package/dist/{workspaces-RP76JH3J.js → workspaces-IMG7ZRUQ.js} +3 -3
  41. package/dist/{workspaces-PEWYWVHZ.js → workspaces-U6P6DB44.js} +3 -3
  42. package/dist/{worktree-ZQEUKSNE.js → worktree-FL3UIUKQ.js} +2 -2
  43. package/dist/{worktree-72TZWUSH.js → worktree-JSCDJPBU.js} +2 -2
  44. package/package.json +1 -1
@@ -1,198 +1,16 @@
1
1
  import {
2
- httpFetch
3
- } from "./chunk-N7WZ6NIN.js";
2
+ textFromAbleTimeDoc
3
+ } from "./chunk-3PYH2RLA.js";
4
+ import {
5
+ assertAbleTimeCredential,
6
+ callAbleTimeTool,
7
+ normalizeAbleTimeHost,
8
+ rewriteAbleTimeError
9
+ } from "./chunk-BFEBFGCS.js";
4
10
  import {
5
- getAbleTimeAccessToken,
6
- getAbleTimeHost,
7
11
  isAbleTimeConnected,
8
12
  saveAbleTimeConnection
9
- } from "./chunk-CXFW5LIH.js";
10
-
11
- // src/integrations/abletime-mcp.ts
12
- var DEFAULT_ABLETIME_HOST = "https://track.abletime.com";
13
- var ABLETIME_MCP_PATH = "/api/public/v2/mcp";
14
- var ABLETIME_MCP_PM_PATH = "/api/public/v2/mcp/pm";
15
- var MCP_PROTOCOL_VERSION = "2025-03-26";
16
- var ABLETIME_PM_TOOL_NAMES = [
17
- "set_task_assignee",
18
- "set_task_priority",
19
- "set_task_stage",
20
- "set_task_dependency",
21
- "set_task_blocked",
22
- "set_task_locked",
23
- "set_task_schedule",
24
- "clear_task_schedule",
25
- "archive_task",
26
- "delete_task"
27
- ];
28
- function abletimeToolUsesPm(name) {
29
- return ABLETIME_PM_TOOL_NAMES.includes(name);
30
- }
31
- function normalizeAbleTimeHost(raw) {
32
- const trimmed = raw?.trim() || DEFAULT_ABLETIME_HOST;
33
- const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
34
- return withScheme.replace(/\/+$/, "");
35
- }
36
- function abletimeMcpUrl(host, opts) {
37
- const path = opts?.pm ? ABLETIME_MCP_PM_PATH : ABLETIME_MCP_PATH;
38
- return `${normalizeAbleTimeHost(host)}${path}`;
39
- }
40
- function rewriteAbleTimeError(message) {
41
- if (/INTEGRATION_AGENT_ACCESS_DISABLED|agent access/i.test(message)) {
42
- return `${message} \u2014 Enable Agent access (MCP) in AbleTime Settings \u2192 Integrations \u2192 API Keys.`;
43
- }
44
- if (/INTEGRATION_PAT_REQUIRED|organization api key/i.test(message)) {
45
- return `${message} \u2014 AbleTime MCP needs a personal access token (apt_\u2026), not an organization API key.`;
46
- }
47
- if (/INTEGRATION_PLAN_REQUIRED/i.test(message)) {
48
- return `${message} \u2014 AbleTime plan does not include API access.`;
49
- }
50
- if (/INTEGRATION_KEY_INVALID|INTEGRATION_KEY_MISSING|401|unauthorized|invalid token/i.test(message)) {
51
- return `${message} \u2014 Reconnect AbleTime from Account settings (Profile \u2192 API Access).`;
52
- }
53
- return message;
54
- }
55
- function asRecord(value) {
56
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
57
- }
58
- function parseSseJson(body) {
59
- const blocks = body.split(/\n\n+/);
60
- let last;
61
- for (const block of blocks) {
62
- const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter((line) => line && line !== "[DONE]").join("\n");
63
- if (!data) continue;
64
- try {
65
- last = JSON.parse(data);
66
- } catch {
67
- }
68
- }
69
- if (last !== void 0) return last;
70
- try {
71
- return JSON.parse(body);
72
- } catch {
73
- throw new Error(`AbleTime MCP returned a non-JSON response: ${body.slice(0, 200)}`);
74
- }
75
- }
76
- function unwrapToolResult(result) {
77
- const record = asRecord(result);
78
- if (!record) return result;
79
- const content = record.content;
80
- if (Array.isArray(content)) {
81
- const texts = content.map((part) => {
82
- const item = asRecord(part);
83
- if (!item) return "";
84
- return typeof item.text === "string" ? item.text : "";
85
- }).filter(Boolean);
86
- if (texts.length === 1) {
87
- const text = texts[0];
88
- try {
89
- return JSON.parse(text);
90
- } catch {
91
- return text;
92
- }
93
- }
94
- if (texts.length > 1) return texts.join("\n");
95
- }
96
- if (record.structuredContent != null) return record.structuredContent;
97
- if (record.isError === true) {
98
- const message = typeof record.message === "string" ? record.message : textsFromUnknown(result) || "AbleTime tool failed";
99
- throw new Error(message);
100
- }
101
- return result;
102
- }
103
- function textsFromUnknown(value) {
104
- const record = asRecord(value);
105
- if (!record) return "";
106
- if (typeof record.message === "string") return record.message;
107
- if (typeof record.error === "string") return record.error;
108
- return "";
109
- }
110
- async function abletimeMcpRequest(method, params, opts) {
111
- const token = (opts?.token ?? getAbleTimeAccessToken())?.trim();
112
- if (!token) {
113
- throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
114
- }
115
- const url = abletimeMcpUrl(
116
- opts?.host ?? (opts?.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost()),
117
- { pm: opts?.pm }
118
- );
119
- const res = await httpFetch(url, {
120
- method: "POST",
121
- headers: {
122
- "Content-Type": "application/json",
123
- Accept: "application/json, text/event-stream",
124
- Authorization: `Bearer ${token}`,
125
- "MCP-Protocol-Version": MCP_PROTOCOL_VERSION
126
- },
127
- body: JSON.stringify({
128
- jsonrpc: "2.0",
129
- id: 1,
130
- method,
131
- ...params === void 0 ? {} : { params }
132
- })
133
- });
134
- const body = await res.text().catch(() => "");
135
- if (!res.ok) {
136
- throw new Error(
137
- rewriteAbleTimeError(
138
- `AbleTime MCP error ${res.status}${body ? `: ${body.slice(0, 240)}` : ""}`
139
- )
140
- );
141
- }
142
- const contentType = res.headers.get("content-type") ?? "";
143
- const parsed = contentType.includes("text/event-stream") ? parseSseJson(body) : (() => {
144
- try {
145
- return JSON.parse(body);
146
- } catch {
147
- return parseSseJson(body);
148
- }
149
- })();
150
- const rpc = asRecord(parsed);
151
- if (rpc?.error) {
152
- const detail = typeof rpc.error.message === "string" && rpc.error.message.trim() ? rpc.error.message : `AbleTime MCP error ${rpc.error.code ?? ""}`.trim();
153
- throw new Error(rewriteAbleTimeError(detail));
154
- }
155
- if (rpc && "result" in rpc) return rpc.result;
156
- return parsed;
157
- }
158
- async function initializeIfNeeded(opts) {
159
- try {
160
- await abletimeMcpRequest(
161
- "initialize",
162
- {
163
- protocolVersion: MCP_PROTOCOL_VERSION,
164
- capabilities: {},
165
- clientInfo: { name: "sideboard", version: "1.0.0" }
166
- },
167
- opts
168
- );
169
- } catch (err) {
170
- const message = err instanceof Error ? err.message : String(err);
171
- if (/already initialized|not (required|supported)|method not found|-32601/i.test(message)) {
172
- return;
173
- }
174
- throw err;
175
- }
176
- }
177
- async function callAbleTimeTool(name, args = {}, opts) {
178
- if (!opts?.token && !isAbleTimeConnected()) {
179
- throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
180
- }
181
- const requestOpts = { ...opts, pm: opts?.pm ?? abletimeToolUsesPm(name) };
182
- const call = () => abletimeMcpRequest("tools/call", { name, arguments: args }, requestOpts).then((result) => {
183
- return unwrapToolResult(result);
184
- });
185
- try {
186
- return await call();
187
- } catch (err) {
188
- const message = err instanceof Error ? err.message : String(err);
189
- if (/initialize|session|not initialized|-32000/i.test(message)) {
190
- await initializeIfNeeded(requestOpts);
191
- return await call();
192
- }
193
- throw err instanceof Error ? new Error(rewriteAbleTimeError(err.message)) : err;
194
- }
195
- }
13
+ } from "./chunk-QFBBUFD4.js";
196
14
 
197
15
  // src/integrations/abletime.ts
198
16
  var CLOSED_STATES = /* @__PURE__ */ new Set([
@@ -203,7 +21,7 @@ var CLOSED_STATES = /* @__PURE__ */ new Set([
203
21
  "archived",
204
22
  "closed"
205
23
  ]);
206
- function asRecord2(value) {
24
+ function asRecord(value) {
207
25
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
208
26
  }
209
27
  function asString(value) {
@@ -220,14 +38,14 @@ function firstString(record, keys) {
220
38
  function firstRecord(record, keys) {
221
39
  if (!record) return null;
222
40
  for (const key of keys) {
223
- const value = asRecord2(record[key]);
41
+ const value = asRecord(record[key]);
224
42
  if (value) return value;
225
43
  }
226
44
  return null;
227
45
  }
228
46
  function asList(value) {
229
47
  if (Array.isArray(value)) return value;
230
- const record = asRecord2(value);
48
+ const record = asRecord(value);
231
49
  if (!record) return [];
232
50
  for (const key of ["data", "tasks", "projects", "items", "results", "nodes"]) {
233
51
  if (Array.isArray(record[key])) return record[key];
@@ -239,7 +57,7 @@ function labelsOf(record) {
239
57
  if (!Array.isArray(raw)) return [];
240
58
  return raw.map((item) => {
241
59
  if (typeof item === "string") return item.trim();
242
- const rec = asRecord2(item);
60
+ const rec = asRecord(item);
243
61
  return rec ? firstString(rec, ["name", "title", "label"]) : "";
244
62
  }).filter(Boolean);
245
63
  }
@@ -247,13 +65,13 @@ function commentsOf(record) {
247
65
  const raw = record.comments ?? record.notes ?? record.discussion;
248
66
  const out = [];
249
67
  for (const item of asList(raw)) {
250
- const rec = asRecord2(item);
68
+ const rec = asRecord(item);
251
69
  if (!rec) continue;
252
- const body = firstString(rec, ["body", "text", "comment", "content", "message"]);
70
+ const body = firstString(rec, ["body", "text", "comment", "message", "contentSanitized"]) || textFromAbleTimeDoc(rec.content);
253
71
  if (!body) continue;
254
- const user = firstString(asRecord2(rec.user) ?? asRecord2(rec.author), ["name", "display_name"]) || firstString(rec, ["user_name", "author"]);
72
+ const user = firstString(asRecord(rec.user) ?? asRecord(rec.author), ["name", "display_name", "username"]) || firstString(rec, ["user_name", "author", "username"]);
255
73
  const comment = { body };
256
- const id = firstString(rec, ["id", "comment_id"]);
74
+ const id = firstString(rec, ["id", "comment_id", "commentId"]);
257
75
  if (id) comment.id = id;
258
76
  const url = firstString(rec, ["url", "permalink"]);
259
77
  if (url) comment.url = url;
@@ -277,11 +95,11 @@ function taskUrl(task, host) {
277
95
  return `${base}/tasks/${slug}`;
278
96
  }
279
97
  function mapAbleTimeTask(raw, host) {
280
- const record = asRecord2(raw);
98
+ const record = asRecord(raw);
281
99
  if (!record) return null;
282
100
  const nested = firstRecord(record, ["task", "data"]) ?? record;
283
- const id = firstString(nested, ["id", "task_id", "taskId"]);
284
- const identifier = firstString(nested, ["reference", "ref", "identifier", "key", "code", "number"]) || id;
101
+ const id = firstString(nested, ["id", "task_id", "taskId", "timeflowTaskId"]);
102
+ const identifier = firstString(nested, ["reference", "ref", "identifier", "key", "code", "number", "taskRef"]) || id;
285
103
  const title = firstString(nested, ["title", "name"]);
286
104
  if (!id && !identifier && !title) return null;
287
105
  const resolvedId = id || identifier;
@@ -292,21 +110,22 @@ function mapAbleTimeTask(raw, host) {
292
110
  identifier: resolvedIdentifier,
293
111
  title: title || resolvedIdentifier,
294
112
  url,
295
- description: firstString(nested, ["description", "body", "details"]) || void 0,
296
- state: firstString(nested, ["state", "board_state", "boardState", "status"]) || void 0,
297
- projectId: firstString(nested, ["project_id", "projectId"]) || firstString(asRecord2(nested.project), ["id"]) || void 0,
298
- categoryId: firstString(nested, ["category_id", "categoryId"]) || firstString(asRecord2(nested.category), ["id"]) || void 0,
113
+ description: firstString(nested, ["description", "body", "details", "descriptionSanitized"]) || textFromAbleTimeDoc(nested.description) || void 0,
114
+ state: firstString(nested, ["state", "board_state", "boardState", "status", "taskState"]) || void 0,
115
+ projectId: firstString(nested, ["project_id", "projectId"]) || firstString(asRecord(nested.project), ["id"]) || void 0,
116
+ categoryId: firstString(nested, ["category_id", "categoryId", "projectCategoryId"]) || firstString(asRecord(nested.category), ["id"]) || void 0,
299
117
  assignee: assigneeOf(nested),
300
118
  labels: labelsOf(nested),
301
119
  comments: commentsOf(nested),
302
- createdAt: firstString(nested, ["created_at", "createdAt", "created"]) || void 0,
120
+ createdAt: firstString(nested, ["created_at", "createdAt", "created", "dateCreated"]) || void 0,
303
121
  updatedAt: firstString(nested, [
304
122
  "updated_at",
305
123
  "updatedAt",
306
124
  "modified_at",
307
125
  "modifiedAt",
308
126
  "last_activity_at",
309
- "lastActivityAt"
127
+ "lastActivityAt",
128
+ "lastUpdate"
310
129
  ]) || void 0
311
130
  };
312
131
  }
@@ -337,16 +156,16 @@ function issueAttachmentForAbleTimeTask(task) {
337
156
  };
338
157
  }
339
158
  function mapProject(raw) {
340
- const record = asRecord2(raw);
159
+ const record = asRecord(raw);
341
160
  if (!record) return null;
342
- const id = firstString(record, ["id", "project_id"]);
343
- const name = firstString(record, ["name", "title"]);
161
+ const id = firstString(record, ["id", "project_id", "projectId"]);
162
+ const name = firstString(record, ["name", "title", "projectName"]);
344
163
  if (!id && !name) return null;
345
164
  const categories = asList(record.categories ?? record.category).map((item) => {
346
- const rec = asRecord2(item);
165
+ const rec = asRecord(item);
347
166
  if (!rec) return null;
348
- const categoryId = firstString(rec, ["id", "category_id"]);
349
- const categoryName = firstString(rec, ["name", "title"]);
167
+ const categoryId = firstString(rec, ["id", "category_id", "projectCategoryId", "categoryId"]);
168
+ const categoryName = firstString(rec, ["name", "title", "categoryName"]);
350
169
  if (!categoryId && !categoryName) return null;
351
170
  return { id: categoryId || categoryName, name: categoryName || categoryId };
352
171
  }).filter((item) => Boolean(item));
@@ -358,7 +177,7 @@ function isOpenTask(task) {
358
177
  }
359
178
  async function getAbleTimeOrientation(opts) {
360
179
  const raw = await callAbleTimeTool("orientation", {}, opts);
361
- const record = asRecord2(raw);
180
+ const record = asRecord(raw);
362
181
  const viewerRec = firstRecord(record, ["viewer", "user", "actor", "me", "person"]) ?? record;
363
182
  const viewer = {
364
183
  id: firstString(viewerRec, ["id", "user_id"]) || void 0,
@@ -437,7 +256,7 @@ async function commentAbleTimeTask(input, opts) {
437
256
  { id, task_id: id, task: id, body, comment: body, text: body },
438
257
  opts
439
258
  );
440
- const rec = asRecord2(raw);
259
+ const rec = asRecord(raw);
441
260
  return {
442
261
  id: rec ? firstString(rec, ["id", "comment_id"]) || void 0 : void 0,
443
262
  body: rec ? firstString(rec, ["body", "text", "comment"]) || body : body
@@ -627,8 +446,7 @@ async function listAbleTimeAssignedIssues(opts) {
627
446
  };
628
447
  }
629
448
  async function verifyAbleTimeConnection(input) {
630
- const token = input.token.trim();
631
- if (!token) throw new Error("AbleTime personal access token is required");
449
+ const token = assertAbleTimeCredential(input.token);
632
450
  const host = input.host?.trim() || void 0;
633
451
  const orientation = await getAbleTimeOrientation({ token, host });
634
452
  saveAbleTimeConnection({
@@ -640,16 +458,6 @@ async function verifyAbleTimeConnection(input) {
640
458
  }
641
459
 
642
460
  export {
643
- DEFAULT_ABLETIME_HOST,
644
- ABLETIME_MCP_PATH,
645
- ABLETIME_MCP_PM_PATH,
646
- ABLETIME_PM_TOOL_NAMES,
647
- abletimeToolUsesPm,
648
- normalizeAbleTimeHost,
649
- abletimeMcpUrl,
650
- rewriteAbleTimeError,
651
- abletimeMcpRequest,
652
- callAbleTimeTool,
653
461
  taskUrl,
654
462
  mapAbleTimeTask,
655
463
  toAbleTimeIssueInfo,
@@ -7,7 +7,7 @@ import {
7
7
  resolveRepoRoot,
8
8
  takenSlugsFromThread,
9
9
  teamSlugFromName
10
- } from "./chunk-RPX6VDW7.js";
10
+ } from "./chunk-QSEF2YVQ.js";
11
11
  import {
12
12
  ATTACHMENTS_DIR,
13
13
  attachmentsGitignoreBody
@@ -30,7 +30,7 @@ import {
30
30
  resolveThreadDefaults,
31
31
  resolveViewerProfile,
32
32
  resolveViewerProfileForRepo
33
- } from "./chunk-ISV5IFH3.js";
33
+ } from "./chunk-X743YN5Y.js";
34
34
  import {
35
35
  appDataDir,
36
36
  globalAgentCwd,
@@ -0,0 +1,260 @@
1
+ import {
2
+ DEFAULT_ABLETIME_HOST,
3
+ normalizeAbleTimeHost
4
+ } from "./chunk-BFEBFGCS.js";
5
+ import {
6
+ httpFetch
7
+ } from "./chunk-N7WZ6NIN.js";
8
+ import {
9
+ disconnectAbleTimeConnection,
10
+ getAbleTimeHost,
11
+ loadAppSettings,
12
+ saveAbleTimeConnection
13
+ } from "./chunk-QFBBUFD4.js";
14
+
15
+ // src/integrations/abletime-oauth.ts
16
+ import { createHash, randomBytes } from "crypto";
17
+ import { createServer } from "http";
18
+ var ABLETIME_OAUTH_PORT = 19849;
19
+ var ABLETIME_OAUTH_REDIRECT = `http://127.0.0.1:${ABLETIME_OAUTH_PORT}/callback`;
20
+ var BAKED_ABLETIME_OAUTH_CLIENT_ID = "https://www.sideboard.cloud/oauth/abletime-client.json";
21
+ var ABLETIME_OAUTH_RESOURCE = `${DEFAULT_ABLETIME_HOST}/api/public/v2/mcp`;
22
+ var REFRESH_SKEW_MS = 5 * 6e4;
23
+ function hasBakedAbleTimeOAuth() {
24
+ return Boolean(BAKED_ABLETIME_OAUTH_CLIENT_ID.trim());
25
+ }
26
+ function ableTimeOAuthClientId() {
27
+ return process.env.SIDEBOARD_ABLETIME_OAUTH_CLIENT_ID?.trim() || BAKED_ABLETIME_OAUTH_CLIENT_ID.trim();
28
+ }
29
+ function ableTimeOAuthResource(host) {
30
+ return `${normalizeAbleTimeHost(host)}/api/public/v2/mcp`;
31
+ }
32
+ function createAbleTimePkce() {
33
+ const verifier = randomBytes(32).toString("base64url");
34
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
35
+ return { verifier, challenge };
36
+ }
37
+ function ableTimeOAuthAuthorizeUrl(clientId, state, codeChallenge, host) {
38
+ const base = normalizeAbleTimeHost(host);
39
+ const params = new URLSearchParams({
40
+ response_type: "code",
41
+ client_id: clientId,
42
+ redirect_uri: ABLETIME_OAUTH_REDIRECT,
43
+ code_challenge: codeChallenge,
44
+ code_challenge_method: "S256",
45
+ state,
46
+ resource: ableTimeOAuthResource(host)
47
+ });
48
+ return `${base}/oauth/authorize?${params.toString()}`;
49
+ }
50
+ function htmlPage(title, body) {
51
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
52
+ <style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
53
+ h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
54
+ }
55
+ async function exchangeAbleTimeToken(body, host) {
56
+ const url = `${normalizeAbleTimeHost(host)}/api/oauth/token`;
57
+ const res = await httpFetch(url, {
58
+ method: "POST",
59
+ headers: {
60
+ Accept: "application/json",
61
+ "Content-Type": "application/x-www-form-urlencoded"
62
+ },
63
+ body
64
+ });
65
+ const data = await res.json().catch(() => ({}));
66
+ if (!res.ok || data.error || !data.access_token?.trim()) {
67
+ const detail = data.error_description || data.error || `HTTP ${res.status}`;
68
+ throw new Error(`AbleTime OAuth token exchange failed: ${detail}`);
69
+ }
70
+ return data;
71
+ }
72
+ function persistAbleTimeTokens(data, input) {
73
+ return saveAbleTimeConnection({
74
+ accessToken: data.access_token.trim(),
75
+ refreshToken: data.refresh_token?.trim(),
76
+ expiresIn: data.expires_in,
77
+ host: input?.host,
78
+ viewerName: input?.viewerName
79
+ });
80
+ }
81
+ var refreshInFlight = null;
82
+ async function refreshAbleTimeAccessToken(refreshToken) {
83
+ const host = getAbleTimeHost();
84
+ const body = new URLSearchParams({
85
+ grant_type: "refresh_token",
86
+ refresh_token: refreshToken,
87
+ client_id: ableTimeOAuthClientId(),
88
+ resource: ableTimeOAuthResource(host)
89
+ });
90
+ try {
91
+ const data = await exchangeAbleTimeToken(body, host);
92
+ persistAbleTimeTokens(data, { host });
93
+ return data.access_token.trim();
94
+ } catch (err) {
95
+ const message = err instanceof Error ? err.message : String(err);
96
+ if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
97
+ disconnectAbleTimeConnection();
98
+ throw new Error("AbleTime sign-in expired \u2014 connect again in Account settings");
99
+ }
100
+ throw err;
101
+ }
102
+ }
103
+ async function ensureAbleTimeAccessToken() {
104
+ const settings = loadAppSettings();
105
+ const access = settings.integrations.abletimeAccessToken?.trim() || "";
106
+ const refresh = settings.integrations.abletimeRefreshToken?.trim() || "";
107
+ const expiresAt = settings.integrations.abletimeTokenExpiresAt ?? 0;
108
+ if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS)) {
109
+ return access;
110
+ }
111
+ if (refresh) {
112
+ if (!refreshInFlight) {
113
+ refreshInFlight = refreshAbleTimeAccessToken(refresh).finally(() => {
114
+ refreshInFlight = null;
115
+ });
116
+ }
117
+ return refreshInFlight;
118
+ }
119
+ return access || null;
120
+ }
121
+ var ABLETIME_OAUTH_CANCELLED = "AbleTime sign-in cancelled";
122
+ var AbleTimeOAuthCancelledError = class extends Error {
123
+ constructor(message = ABLETIME_OAUTH_CANCELLED) {
124
+ super(message);
125
+ this.name = "AbleTimeOAuthCancelledError";
126
+ }
127
+ };
128
+ function isAbleTimeOAuthCancelled(err) {
129
+ if (err instanceof AbleTimeOAuthCancelledError) return true;
130
+ const message = err instanceof Error ? err.message : String(err);
131
+ return message.includes(ABLETIME_OAUTH_CANCELLED);
132
+ }
133
+ async function startAbleTimeOAuth(opts) {
134
+ if (opts?.signal?.aborted) {
135
+ throw new AbleTimeOAuthCancelledError();
136
+ }
137
+ const clientId = ableTimeOAuthClientId();
138
+ if (!clientId) {
139
+ throw new Error(
140
+ `AbleTime browser sign-in needs a Client ID Metadata Document URL. Host ${BAKED_ABLETIME_OAUTH_CLIENT_ID} (see site/oauth/abletime-client.json), or set SIDEBOARD_ABLETIME_OAUTH_CLIENT_ID.`
141
+ );
142
+ }
143
+ const host = opts?.host?.trim() || void 0;
144
+ const state = randomBytes(16).toString("hex");
145
+ const pkce = createAbleTimePkce();
146
+ const authorizeUrl = ableTimeOAuthAuthorizeUrl(clientId, state, pkce.challenge, host);
147
+ const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
148
+ const code = await new Promise((resolve, reject) => {
149
+ let settled = false;
150
+ const finish = (err, value) => {
151
+ if (settled) return;
152
+ settled = true;
153
+ cleanup();
154
+ if (err) reject(err);
155
+ else resolve(value);
156
+ };
157
+ const server = createServer((req, res) => {
158
+ try {
159
+ const url = new URL(req.url || "/", `http://127.0.0.1:${ABLETIME_OAUTH_PORT}`);
160
+ if (url.pathname !== "/callback") {
161
+ res.writeHead(404);
162
+ res.end("Not found");
163
+ return;
164
+ }
165
+ const err = url.searchParams.get("error");
166
+ const gotState = url.searchParams.get("state");
167
+ const gotCode = url.searchParams.get("code");
168
+ if (err) {
169
+ res.writeHead(400, { "Content-Type": "text/html" });
170
+ res.end(htmlPage("AbleTime", `<h1>Authorization cancelled</h1><p>${err}</p>`));
171
+ finish(new AbleTimeOAuthCancelledError(`AbleTime OAuth: ${err}`));
172
+ return;
173
+ }
174
+ if (gotState !== state || !gotCode) {
175
+ res.writeHead(400, { "Content-Type": "text/html" });
176
+ res.end(htmlPage("AbleTime", "<h1>Invalid callback</h1><p>State or code missing.</p>"));
177
+ finish(new Error("AbleTime OAuth callback was invalid"));
178
+ return;
179
+ }
180
+ res.writeHead(200, { "Content-Type": "text/html" });
181
+ res.end(
182
+ htmlPage(
183
+ "AbleTime connected",
184
+ "<h1>AbleTime connected</h1><p>You can close this tab and return to Sideboard.</p>"
185
+ )
186
+ );
187
+ finish(void 0, gotCode);
188
+ } catch (e) {
189
+ finish(e instanceof Error ? e : new Error(String(e)));
190
+ }
191
+ });
192
+ const timer = setTimeout(() => {
193
+ finish(new Error("AbleTime sign-in timed out \u2014 try again"));
194
+ }, timeoutMs);
195
+ const onAbort = () => finish(new AbleTimeOAuthCancelledError());
196
+ const cleanup = () => {
197
+ clearTimeout(timer);
198
+ opts?.signal?.removeEventListener("abort", onAbort);
199
+ server.close();
200
+ };
201
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
202
+ if (opts?.signal?.aborted) {
203
+ onAbort();
204
+ return;
205
+ }
206
+ server.on("error", (e) => {
207
+ finish(
208
+ e instanceof Error && e.code === "EADDRINUSE" ? new Error(
209
+ `Port ${ABLETIME_OAUTH_PORT} is in use. Close whatever is bound there, or paste an AbleTime token instead.`
210
+ ) : e instanceof Error ? e : new Error(String(e))
211
+ );
212
+ });
213
+ server.listen(ABLETIME_OAUTH_PORT, "127.0.0.1", () => {
214
+ void Promise.resolve(opts?.openUrl?.(authorizeUrl)).catch((e) => {
215
+ finish(e instanceof Error ? e : new Error(String(e)));
216
+ });
217
+ });
218
+ });
219
+ const body = new URLSearchParams({
220
+ grant_type: "authorization_code",
221
+ code,
222
+ redirect_uri: ABLETIME_OAUTH_REDIRECT,
223
+ client_id: clientId,
224
+ code_verifier: pkce.verifier,
225
+ resource: ableTimeOAuthResource(host)
226
+ });
227
+ const data = await exchangeAbleTimeToken(body, host);
228
+ persistAbleTimeTokens(data, { host });
229
+ const token = data.access_token.trim();
230
+ try {
231
+ const { getAbleTimeOrientation } = await import("./abletime-SISJEPJ3.js");
232
+ const orientation = await getAbleTimeOrientation({ token, host });
233
+ return saveAbleTimeConnection({
234
+ accessToken: token,
235
+ refreshToken: data.refresh_token?.trim(),
236
+ expiresIn: data.expires_in,
237
+ host,
238
+ viewerName: orientation.viewer.name
239
+ });
240
+ } catch {
241
+ return loadAppSettings();
242
+ }
243
+ }
244
+
245
+ export {
246
+ ABLETIME_OAUTH_PORT,
247
+ ABLETIME_OAUTH_REDIRECT,
248
+ BAKED_ABLETIME_OAUTH_CLIENT_ID,
249
+ ABLETIME_OAUTH_RESOURCE,
250
+ hasBakedAbleTimeOAuth,
251
+ ableTimeOAuthClientId,
252
+ ableTimeOAuthResource,
253
+ createAbleTimePkce,
254
+ ableTimeOAuthAuthorizeUrl,
255
+ ensureAbleTimeAccessToken,
256
+ ABLETIME_OAUTH_CANCELLED,
257
+ AbleTimeOAuthCancelledError,
258
+ isAbleTimeOAuthCancelled,
259
+ startAbleTimeOAuth
260
+ };