@tinybirdco/sdk 0.0.1

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 (258) hide show
  1. package/README.md +518 -0
  2. package/bin/tinybird.js +7 -0
  3. package/dist/api/branches.d.ts +98 -0
  4. package/dist/api/branches.d.ts.map +1 -0
  5. package/dist/api/branches.js +203 -0
  6. package/dist/api/branches.js.map +1 -0
  7. package/dist/api/branches.test.d.ts +2 -0
  8. package/dist/api/branches.test.d.ts.map +1 -0
  9. package/dist/api/branches.test.js +286 -0
  10. package/dist/api/branches.test.js.map +1 -0
  11. package/dist/api/build.d.ts +130 -0
  12. package/dist/api/build.d.ts.map +1 -0
  13. package/dist/api/build.js +143 -0
  14. package/dist/api/build.js.map +1 -0
  15. package/dist/api/build.test.d.ts +2 -0
  16. package/dist/api/build.test.d.ts.map +1 -0
  17. package/dist/api/build.test.js +138 -0
  18. package/dist/api/build.test.js.map +1 -0
  19. package/dist/api/deploy.d.ts +39 -0
  20. package/dist/api/deploy.d.ts.map +1 -0
  21. package/dist/api/deploy.js +135 -0
  22. package/dist/api/deploy.js.map +1 -0
  23. package/dist/api/deploy.test.d.ts +2 -0
  24. package/dist/api/deploy.test.d.ts.map +1 -0
  25. package/dist/api/deploy.test.js +118 -0
  26. package/dist/api/deploy.test.js.map +1 -0
  27. package/dist/api/workspaces.d.ts +46 -0
  28. package/dist/api/workspaces.d.ts.map +1 -0
  29. package/dist/api/workspaces.js +39 -0
  30. package/dist/api/workspaces.js.map +1 -0
  31. package/dist/api/workspaces.test.d.ts +2 -0
  32. package/dist/api/workspaces.test.d.ts.map +1 -0
  33. package/dist/api/workspaces.test.js +65 -0
  34. package/dist/api/workspaces.test.js.map +1 -0
  35. package/dist/cli/auth.d.ts +86 -0
  36. package/dist/cli/auth.d.ts.map +1 -0
  37. package/dist/cli/auth.js +284 -0
  38. package/dist/cli/auth.js.map +1 -0
  39. package/dist/cli/branch-store.d.ts +53 -0
  40. package/dist/cli/branch-store.d.ts.map +1 -0
  41. package/dist/cli/branch-store.js +91 -0
  42. package/dist/cli/branch-store.js.map +1 -0
  43. package/dist/cli/branch-store.test.d.ts +2 -0
  44. package/dist/cli/branch-store.test.d.ts.map +1 -0
  45. package/dist/cli/branch-store.test.js +115 -0
  46. package/dist/cli/branch-store.test.js.map +1 -0
  47. package/dist/cli/commands/branch.d.ts +82 -0
  48. package/dist/cli/commands/branch.d.ts.map +1 -0
  49. package/dist/cli/commands/branch.js +215 -0
  50. package/dist/cli/commands/branch.js.map +1 -0
  51. package/dist/cli/commands/build.d.ts +43 -0
  52. package/dist/cli/commands/build.d.ts.map +1 -0
  53. package/dist/cli/commands/build.js +138 -0
  54. package/dist/cli/commands/build.js.map +1 -0
  55. package/dist/cli/commands/dev.d.ts +78 -0
  56. package/dist/cli/commands/dev.d.ts.map +1 -0
  57. package/dist/cli/commands/dev.js +226 -0
  58. package/dist/cli/commands/dev.js.map +1 -0
  59. package/dist/cli/commands/init.d.ts +45 -0
  60. package/dist/cli/commands/init.d.ts.map +1 -0
  61. package/dist/cli/commands/init.js +277 -0
  62. package/dist/cli/commands/init.js.map +1 -0
  63. package/dist/cli/commands/init.test.d.ts +2 -0
  64. package/dist/cli/commands/init.test.d.ts.map +1 -0
  65. package/dist/cli/commands/init.test.js +158 -0
  66. package/dist/cli/commands/init.test.js.map +1 -0
  67. package/dist/cli/commands/login.d.ts +37 -0
  68. package/dist/cli/commands/login.d.ts.map +1 -0
  69. package/dist/cli/commands/login.js +64 -0
  70. package/dist/cli/commands/login.js.map +1 -0
  71. package/dist/cli/config.d.ts +114 -0
  72. package/dist/cli/config.d.ts.map +1 -0
  73. package/dist/cli/config.js +258 -0
  74. package/dist/cli/config.js.map +1 -0
  75. package/dist/cli/config.test.d.ts +2 -0
  76. package/dist/cli/config.test.d.ts.map +1 -0
  77. package/dist/cli/config.test.js +243 -0
  78. package/dist/cli/config.test.js.map +1 -0
  79. package/dist/cli/env.d.ts +29 -0
  80. package/dist/cli/env.d.ts.map +1 -0
  81. package/dist/cli/env.js +66 -0
  82. package/dist/cli/env.js.map +1 -0
  83. package/dist/cli/git.d.ts +29 -0
  84. package/dist/cli/git.d.ts.map +1 -0
  85. package/dist/cli/git.js +114 -0
  86. package/dist/cli/git.js.map +1 -0
  87. package/dist/cli/git.test.d.ts +2 -0
  88. package/dist/cli/git.test.d.ts.map +1 -0
  89. package/dist/cli/git.test.js +125 -0
  90. package/dist/cli/git.test.js.map +1 -0
  91. package/dist/cli/index.d.ts +7 -0
  92. package/dist/cli/index.d.ts.map +1 -0
  93. package/dist/cli/index.js +337 -0
  94. package/dist/cli/index.js.map +1 -0
  95. package/dist/cli/utils/schema-validation.d.ts +95 -0
  96. package/dist/cli/utils/schema-validation.d.ts.map +1 -0
  97. package/dist/cli/utils/schema-validation.js +175 -0
  98. package/dist/cli/utils/schema-validation.js.map +1 -0
  99. package/dist/cli/utils/schema-validation.test.d.ts +5 -0
  100. package/dist/cli/utils/schema-validation.test.d.ts.map +1 -0
  101. package/dist/cli/utils/schema-validation.test.js +173 -0
  102. package/dist/cli/utils/schema-validation.test.js.map +1 -0
  103. package/dist/client/base.d.ts +116 -0
  104. package/dist/client/base.d.ts.map +1 -0
  105. package/dist/client/base.js +328 -0
  106. package/dist/client/base.js.map +1 -0
  107. package/dist/client/types.d.ts +137 -0
  108. package/dist/client/types.d.ts.map +1 -0
  109. package/dist/client/types.js +43 -0
  110. package/dist/client/types.js.map +1 -0
  111. package/dist/generator/client.d.ts +44 -0
  112. package/dist/generator/client.d.ts.map +1 -0
  113. package/dist/generator/client.js +144 -0
  114. package/dist/generator/client.js.map +1 -0
  115. package/dist/generator/datasource.d.ts +57 -0
  116. package/dist/generator/datasource.d.ts.map +1 -0
  117. package/dist/generator/datasource.js +169 -0
  118. package/dist/generator/datasource.js.map +1 -0
  119. package/dist/generator/datasource.test.d.ts +2 -0
  120. package/dist/generator/datasource.test.d.ts.map +1 -0
  121. package/dist/generator/datasource.test.js +254 -0
  122. package/dist/generator/datasource.test.js.map +1 -0
  123. package/dist/generator/index.d.ts +131 -0
  124. package/dist/generator/index.d.ts.map +1 -0
  125. package/dist/generator/index.js +121 -0
  126. package/dist/generator/index.js.map +1 -0
  127. package/dist/generator/index.test.d.ts +2 -0
  128. package/dist/generator/index.test.d.ts.map +1 -0
  129. package/dist/generator/index.test.js +175 -0
  130. package/dist/generator/index.test.js.map +1 -0
  131. package/dist/generator/loader.d.ts +156 -0
  132. package/dist/generator/loader.d.ts.map +1 -0
  133. package/dist/generator/loader.js +295 -0
  134. package/dist/generator/loader.js.map +1 -0
  135. package/dist/generator/pipe.d.ts +72 -0
  136. package/dist/generator/pipe.d.ts.map +1 -0
  137. package/dist/generator/pipe.js +174 -0
  138. package/dist/generator/pipe.js.map +1 -0
  139. package/dist/generator/pipe.test.d.ts +2 -0
  140. package/dist/generator/pipe.test.d.ts.map +1 -0
  141. package/dist/generator/pipe.test.js +393 -0
  142. package/dist/generator/pipe.test.js.map +1 -0
  143. package/dist/index.d.ts +74 -0
  144. package/dist/index.d.ts.map +1 -0
  145. package/dist/index.js +73 -0
  146. package/dist/index.js.map +1 -0
  147. package/dist/infer/index.d.ts +202 -0
  148. package/dist/infer/index.d.ts.map +1 -0
  149. package/dist/infer/index.js +5 -0
  150. package/dist/infer/index.js.map +1 -0
  151. package/dist/schema/datasource.d.ts +135 -0
  152. package/dist/schema/datasource.d.ts.map +1 -0
  153. package/dist/schema/datasource.js +105 -0
  154. package/dist/schema/datasource.js.map +1 -0
  155. package/dist/schema/datasource.test.d.ts +2 -0
  156. package/dist/schema/datasource.test.d.ts.map +1 -0
  157. package/dist/schema/datasource.test.js +142 -0
  158. package/dist/schema/datasource.test.js.map +1 -0
  159. package/dist/schema/engines.d.ts +157 -0
  160. package/dist/schema/engines.d.ts.map +1 -0
  161. package/dist/schema/engines.js +155 -0
  162. package/dist/schema/engines.js.map +1 -0
  163. package/dist/schema/engines.test.d.ts +2 -0
  164. package/dist/schema/engines.test.d.ts.map +1 -0
  165. package/dist/schema/engines.test.js +221 -0
  166. package/dist/schema/engines.test.js.map +1 -0
  167. package/dist/schema/params.d.ts +106 -0
  168. package/dist/schema/params.d.ts.map +1 -0
  169. package/dist/schema/params.js +138 -0
  170. package/dist/schema/params.js.map +1 -0
  171. package/dist/schema/params.test.d.ts +2 -0
  172. package/dist/schema/params.test.d.ts.map +1 -0
  173. package/dist/schema/params.test.js +175 -0
  174. package/dist/schema/params.test.js.map +1 -0
  175. package/dist/schema/pipe.d.ts +436 -0
  176. package/dist/schema/pipe.d.ts.map +1 -0
  177. package/dist/schema/pipe.js +484 -0
  178. package/dist/schema/pipe.js.map +1 -0
  179. package/dist/schema/pipe.test.d.ts +2 -0
  180. package/dist/schema/pipe.test.d.ts.map +1 -0
  181. package/dist/schema/pipe.test.js +488 -0
  182. package/dist/schema/pipe.test.js.map +1 -0
  183. package/dist/schema/project.d.ts +202 -0
  184. package/dist/schema/project.d.ts.map +1 -0
  185. package/dist/schema/project.js +188 -0
  186. package/dist/schema/project.js.map +1 -0
  187. package/dist/schema/project.test.d.ts +2 -0
  188. package/dist/schema/project.test.d.ts.map +1 -0
  189. package/dist/schema/project.test.js +180 -0
  190. package/dist/schema/project.test.js.map +1 -0
  191. package/dist/schema/types.d.ts +140 -0
  192. package/dist/schema/types.d.ts.map +1 -0
  193. package/dist/schema/types.js +174 -0
  194. package/dist/schema/types.js.map +1 -0
  195. package/dist/schema/types.test.d.ts +2 -0
  196. package/dist/schema/types.test.d.ts.map +1 -0
  197. package/dist/schema/types.test.js +176 -0
  198. package/dist/schema/types.test.js.map +1 -0
  199. package/dist/test/handlers.d.ts +58 -0
  200. package/dist/test/handlers.d.ts.map +1 -0
  201. package/dist/test/handlers.js +62 -0
  202. package/dist/test/handlers.js.map +1 -0
  203. package/dist/test/setup.d.ts +5 -0
  204. package/dist/test/setup.d.ts.map +1 -0
  205. package/dist/test/setup.js +11 -0
  206. package/dist/test/setup.js.map +1 -0
  207. package/package.json +57 -0
  208. package/src/api/branches.test.ts +377 -0
  209. package/src/api/branches.ts +334 -0
  210. package/src/api/build.test.ts +216 -0
  211. package/src/api/build.ts +266 -0
  212. package/src/api/deploy.test.ts +193 -0
  213. package/src/api/deploy.ts +163 -0
  214. package/src/api/workspaces.test.ts +81 -0
  215. package/src/api/workspaces.ts +77 -0
  216. package/src/cli/auth.ts +358 -0
  217. package/src/cli/branch-store.test.ts +139 -0
  218. package/src/cli/branch-store.ts +137 -0
  219. package/src/cli/commands/branch.ts +306 -0
  220. package/src/cli/commands/build.ts +183 -0
  221. package/src/cli/commands/dev.ts +334 -0
  222. package/src/cli/commands/init.test.ts +249 -0
  223. package/src/cli/commands/init.ts +323 -0
  224. package/src/cli/commands/login.ts +98 -0
  225. package/src/cli/config.test.ts +359 -0
  226. package/src/cli/config.ts +335 -0
  227. package/src/cli/env.ts +86 -0
  228. package/src/cli/git.test.ts +147 -0
  229. package/src/cli/git.ts +125 -0
  230. package/src/cli/index.ts +382 -0
  231. package/src/cli/utils/schema-validation.test.ts +222 -0
  232. package/src/cli/utils/schema-validation.ts +272 -0
  233. package/src/client/base.ts +414 -0
  234. package/src/client/types.ts +165 -0
  235. package/src/generator/client.ts +194 -0
  236. package/src/generator/datasource.test.ts +297 -0
  237. package/src/generator/datasource.ts +217 -0
  238. package/src/generator/index.test.ts +209 -0
  239. package/src/generator/index.ts +203 -0
  240. package/src/generator/loader.ts +406 -0
  241. package/src/generator/pipe.test.ts +441 -0
  242. package/src/generator/pipe.ts +220 -0
  243. package/src/index.ts +191 -0
  244. package/src/infer/index.ts +247 -0
  245. package/src/schema/datasource.test.ts +187 -0
  246. package/src/schema/datasource.ts +195 -0
  247. package/src/schema/engines.test.ts +247 -0
  248. package/src/schema/engines.ts +271 -0
  249. package/src/schema/params.test.ts +208 -0
  250. package/src/schema/params.ts +249 -0
  251. package/src/schema/pipe.test.ts +588 -0
  252. package/src/schema/pipe.ts +832 -0
  253. package/src/schema/project.test.ts +236 -0
  254. package/src/schema/project.ts +394 -0
  255. package/src/schema/types.test.ts +212 -0
  256. package/src/schema/types.ts +366 -0
  257. package/src/test/handlers.ts +79 -0
  258. package/src/test/setup.ts +13 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tinybird Workspace API client
3
+ */
4
+
5
+ /**
6
+ * Workspace information from Tinybird API
7
+ */
8
+ export interface TinybirdWorkspace {
9
+ /** Workspace ID (UUID) */
10
+ id: string;
11
+ /** Workspace name */
12
+ name: string;
13
+ /** User ID of the workspace owner */
14
+ user_id: string;
15
+ /** Email of the workspace owner */
16
+ user_email: string;
17
+ /** Workspace scope */
18
+ scope: string;
19
+ /** Main branch (null for main workspace) */
20
+ main: string | null;
21
+ }
22
+
23
+ /**
24
+ * API configuration for workspace operations
25
+ */
26
+ export interface WorkspaceApiConfig {
27
+ /** Tinybird API base URL */
28
+ baseUrl: string;
29
+ /** Workspace token */
30
+ token: string;
31
+ }
32
+
33
+ /**
34
+ * Error thrown by workspace API operations
35
+ */
36
+ export class WorkspaceApiError extends Error {
37
+ constructor(
38
+ message: string,
39
+ public readonly status: number,
40
+ public readonly body?: unknown
41
+ ) {
42
+ super(message);
43
+ this.name = "WorkspaceApiError";
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Get workspace information
49
+ * GET /v1/workspace
50
+ *
51
+ * @param config - API configuration
52
+ * @returns Workspace information
53
+ */
54
+ export async function getWorkspace(
55
+ config: WorkspaceApiConfig
56
+ ): Promise<TinybirdWorkspace> {
57
+ const url = new URL("/v1/workspace", config.baseUrl);
58
+
59
+ const response = await fetch(url.toString(), {
60
+ method: "GET",
61
+ headers: {
62
+ Authorization: `Bearer ${config.token}`,
63
+ },
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const body = await response.text();
68
+ throw new WorkspaceApiError(
69
+ `Failed to get workspace: ${response.status} ${response.statusText}`,
70
+ response.status,
71
+ body
72
+ );
73
+ }
74
+
75
+ const data = (await response.json()) as TinybirdWorkspace;
76
+ return data;
77
+ }
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Browser-based authentication for Tinybird CLI
3
+ *
4
+ * Implements OAuth flow via local HTTP server callback
5
+ */
6
+
7
+ import * as http from "node:http";
8
+ import { spawn } from "node:child_process";
9
+ import { platform } from "node:os";
10
+ import { URL } from "node:url";
11
+
12
+ /**
13
+ * Port for the local OAuth callback server
14
+ */
15
+ export const AUTH_SERVER_PORT = 49160;
16
+
17
+ /**
18
+ * Default auth host (Tinybird cloud)
19
+ */
20
+ export const DEFAULT_AUTH_HOST = "https://cloud.tinybird.co";
21
+
22
+ /**
23
+ * Default API host (EU region)
24
+ */
25
+ export const DEFAULT_API_HOST = "https://api.tinybird.co";
26
+
27
+ /**
28
+ * Maximum time to wait for authentication (in seconds)
29
+ */
30
+ export const SERVER_MAX_WAIT_TIME = 180;
31
+
32
+ /**
33
+ * Get the auth host from environment or use default
34
+ */
35
+ export function getAuthHost(): string {
36
+ return process.env.TINYBIRD_AUTH_HOST ?? DEFAULT_AUTH_HOST;
37
+ }
38
+
39
+ /**
40
+ * Result of a login attempt
41
+ */
42
+ export interface AuthResult {
43
+ success: boolean;
44
+ token?: string;
45
+ baseUrl?: string;
46
+ workspaceName?: string;
47
+ userEmail?: string;
48
+ error?: string;
49
+ }
50
+
51
+ /**
52
+ * Options for the browser login flow
53
+ */
54
+ export interface LoginOptions {
55
+ /** Override the default auth host */
56
+ authHost?: string;
57
+ /** Override the API host (region) */
58
+ apiHost?: string;
59
+ }
60
+
61
+ /**
62
+ * Token response from Tinybird auth API
63
+ */
64
+ interface TokenResponse {
65
+ workspace_token: string;
66
+ user_token: string;
67
+ api_host: string;
68
+ workspace_name?: string;
69
+ user_email?: string;
70
+ }
71
+
72
+ /**
73
+ * Generate the HTML callback page served by the local server
74
+ *
75
+ * This page extracts the code from the query string and POSTs it back to the server
76
+ *
77
+ * NOTE: State parameter validation is disabled until Tinybird backend supports it.
78
+ * TODO: Re-enable state validation once /api/cli-login echoes back the state parameter.
79
+ */
80
+ function getCallbackHtml(authHost: string): string {
81
+ return `<!DOCTYPE html>
82
+ <html>
83
+ <head>
84
+ <style>
85
+ body {
86
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
87
+ background: #f5f5f5;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ height: 100vh;
92
+ margin: 0;
93
+ }
94
+ .container {
95
+ text-align: center;
96
+ padding: 2rem;
97
+ }
98
+ .spinner {
99
+ border: 3px solid #e0e0e0;
100
+ border-top: 3px solid #333;
101
+ border-radius: 50%;
102
+ width: 30px;
103
+ height: 30px;
104
+ animation: spin 1s linear infinite;
105
+ margin: 0 auto 1rem;
106
+ }
107
+ @keyframes spin {
108
+ 0% { transform: rotate(0deg); }
109
+ 100% { transform: rotate(360deg); }
110
+ }
111
+ </style>
112
+ </head>
113
+ <body>
114
+ <div class="container">
115
+ <div class="spinner"></div>
116
+ <p>Completing authentication...</p>
117
+ </div>
118
+ <script>
119
+ const searchParams = new URLSearchParams(window.location.search);
120
+ const code = searchParams.get('code');
121
+ const workspace = searchParams.get('workspace');
122
+ const region = searchParams.get('region');
123
+ const provider = searchParams.get('provider');
124
+ const host = "${authHost}";
125
+
126
+ if (!code) {
127
+ document.querySelector('.container').innerHTML = '<p>Missing authentication code. Please try again.</p>';
128
+ } else {
129
+ fetch('/?code=' + encodeURIComponent(code), { method: 'POST' })
130
+ .then(() => {
131
+ if (provider && region && workspace) {
132
+ window.location.href = host + "/" + provider + "/" + region + "/cli-login?workspace=" + workspace;
133
+ } else {
134
+ document.querySelector('.container').innerHTML = '<p>Authentication successful! You can close this tab.</p>';
135
+ }
136
+ })
137
+ .catch(() => {
138
+ document.querySelector('.container').innerHTML = '<p>Authentication failed. Please try again.</p>';
139
+ });
140
+ }
141
+ </script>
142
+ </body>
143
+ </html>`;
144
+ }
145
+
146
+ /**
147
+ * Start a local HTTP server to receive the OAuth callback
148
+ *
149
+ * @param onCode - Callback invoked when auth code is received
150
+ * @param authHost - Auth host for redirect URL in HTML
151
+ * @returns Promise that resolves to the server instance
152
+ *
153
+ * NOTE: State parameter validation is disabled until Tinybird backend supports it.
154
+ */
155
+ function startAuthServer(
156
+ onCode: (code: string) => void,
157
+ authHost: string
158
+ ): Promise<http.Server> {
159
+ return new Promise((resolve, reject) => {
160
+ const server = http.createServer((req, res) => {
161
+ const url = new URL(req.url ?? "/", `http://localhost:${AUTH_SERVER_PORT}`);
162
+
163
+ if (req.method === "GET") {
164
+ // Serve the callback HTML page
165
+ res.writeHead(200, { "Content-Type": "text/html" });
166
+ res.end(getCallbackHtml(authHost));
167
+ } else if (req.method === "POST") {
168
+ // Receive the auth code
169
+ const code = url.searchParams.get("code");
170
+
171
+ if (!code) {
172
+ res.writeHead(400);
173
+ res.end("Missing code parameter");
174
+ return;
175
+ }
176
+
177
+ onCode(code);
178
+ res.writeHead(200);
179
+ res.end();
180
+ } else {
181
+ res.writeHead(405);
182
+ res.end("Method not allowed");
183
+ }
184
+ });
185
+
186
+ server.on("error", (err) => {
187
+ reject(new Error(`Failed to start auth server: ${err.message}`));
188
+ });
189
+
190
+ // Bind to localhost only for security (prevents network access)
191
+ server.listen(AUTH_SERVER_PORT, "127.0.0.1", () => {
192
+ resolve(server);
193
+ });
194
+ });
195
+ }
196
+
197
+ /**
198
+ * Open a URL in the user's default browser
199
+ *
200
+ * Cross-platform support for macOS, Linux, and Windows
201
+ *
202
+ * @param url - URL to open
203
+ * @returns Promise that resolves to true if browser was opened successfully
204
+ */
205
+ export async function openBrowser(url: string): Promise<boolean> {
206
+ const os = platform();
207
+
208
+ let command: string;
209
+ let args: string[];
210
+
211
+ switch (os) {
212
+ case "darwin":
213
+ command = "open";
214
+ args = [url];
215
+ break;
216
+ case "win32":
217
+ command = "cmd";
218
+ args = ["/c", "start", "", url];
219
+ break;
220
+ default:
221
+ // Linux and others
222
+ command = "xdg-open";
223
+ args = [url];
224
+ break;
225
+ }
226
+
227
+ return new Promise((resolve) => {
228
+ try {
229
+ const child = spawn(command, args, {
230
+ detached: true,
231
+ stdio: "ignore",
232
+ });
233
+
234
+ child.unref();
235
+
236
+ child.on("error", () => {
237
+ resolve(false);
238
+ });
239
+
240
+ // Give it a moment to potentially fail
241
+ setTimeout(() => resolve(true), 500);
242
+ } catch {
243
+ resolve(false);
244
+ }
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Exchange an authorization code for tokens
250
+ *
251
+ * @param code - Authorization code from OAuth callback
252
+ * @param authHost - Auth host URL
253
+ * @returns Promise that resolves to token response
254
+ */
255
+ export async function exchangeCodeForTokens(
256
+ code: string,
257
+ authHost: string
258
+ ): Promise<TokenResponse> {
259
+ const url = new URL("/api/cli-login", authHost);
260
+ url.searchParams.set("code", code);
261
+
262
+ const response = await fetch(url.toString());
263
+
264
+ if (!response.ok) {
265
+ const body = await response.text();
266
+ throw new Error(
267
+ `Token exchange failed: ${response.status} ${response.statusText}\n${body}`
268
+ );
269
+ }
270
+
271
+ return (await response.json()) as TokenResponse;
272
+ }
273
+
274
+ /**
275
+ * Perform browser-based login flow
276
+ *
277
+ * 1. Starts a local HTTP server for OAuth callback
278
+ * 2. Opens the user's browser to the auth URL
279
+ * 3. Waits for the callback with auth code
280
+ * 4. Exchanges the code for tokens
281
+ *
282
+ * @param options - Login options
283
+ * @returns Promise that resolves to auth result
284
+ */
285
+ export async function browserLogin(
286
+ options: LoginOptions = {}
287
+ ): Promise<AuthResult> {
288
+ const authHost = options.authHost ?? getAuthHost();
289
+ const apiHost = options.apiHost ?? DEFAULT_API_HOST;
290
+
291
+ let server: http.Server | null = null;
292
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
293
+
294
+ try {
295
+ // Start the server first
296
+ const serverPromise = new Promise<{ server: http.Server; code: string }>((resolve, reject) => {
297
+ // Set up timeout
298
+ timeoutId = setTimeout(() => {
299
+ reject(new Error("Authentication timed out after 180 seconds"));
300
+ }, SERVER_MAX_WAIT_TIME * 1000);
301
+
302
+ startAuthServer(
303
+ (code) => {
304
+ if (timeoutId) clearTimeout(timeoutId);
305
+ if (server) {
306
+ resolve({ server, code });
307
+ }
308
+ },
309
+ authHost
310
+ )
311
+ .then((srv) => {
312
+ server = srv;
313
+ })
314
+ .catch(reject);
315
+ });
316
+
317
+ // Wait for server to start
318
+ await new Promise<void>((resolve) => setTimeout(resolve, 100));
319
+
320
+ // Build auth URL
321
+ const authUrl = new URL("/api/cli-login", authHost);
322
+ authUrl.searchParams.set("apiHost", apiHost);
323
+
324
+ console.log("Opening browser for authentication...");
325
+
326
+ // Open browser
327
+ await openBrowser(authUrl.toString());
328
+
329
+ console.log("\nIf the browser doesn't open, please visit:");
330
+ console.log(authUrl.toString());
331
+
332
+ // Wait for auth code
333
+ const { code } = await serverPromise;
334
+
335
+ // Exchange code for tokens
336
+ console.log("\nExchanging code for tokens...");
337
+ const tokens = await exchangeCodeForTokens(code, authHost);
338
+
339
+ return {
340
+ success: true,
341
+ token: tokens.workspace_token,
342
+ baseUrl: tokens.api_host,
343
+ workspaceName: tokens.workspace_name,
344
+ userEmail: tokens.user_email,
345
+ };
346
+ } catch (error) {
347
+ return {
348
+ success: false,
349
+ error: (error as Error).message,
350
+ };
351
+ } finally {
352
+ // Clean up
353
+ if (timeoutId) clearTimeout(timeoutId);
354
+ if (server) {
355
+ (server as http.Server).close();
356
+ }
357
+ }
358
+ }
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import {
6
+ loadBranchStore,
7
+ saveBranchStore,
8
+ getBranchToken,
9
+ setBranchToken,
10
+ removeBranch,
11
+ listCachedBranches,
12
+ type BranchStore,
13
+ } from "./branch-store.js";
14
+
15
+ describe("Branch store", () => {
16
+ let originalHome: string | undefined;
17
+ let testHomeDir: string;
18
+
19
+ beforeEach(() => {
20
+ // Create unique test directory for each test
21
+ testHomeDir = path.join(os.tmpdir(), ".tinybird-test-" + Date.now() + "-" + Math.random().toString(36).slice(2));
22
+ fs.mkdirSync(testHomeDir, { recursive: true });
23
+ // Mock HOME to use test directory
24
+ originalHome = process.env.HOME;
25
+ process.env.HOME = testHomeDir;
26
+ });
27
+
28
+ afterEach(() => {
29
+ // Restore original HOME
30
+ if (originalHome !== undefined) {
31
+ process.env.HOME = originalHome;
32
+ }
33
+ // Clean up test directory
34
+ try {
35
+ fs.rmSync(testHomeDir, { recursive: true });
36
+ } catch {
37
+ // Ignore cleanup errors
38
+ }
39
+ });
40
+
41
+ describe("loadBranchStore", () => {
42
+ it("returns empty store when file does not exist", () => {
43
+ const store = loadBranchStore();
44
+ expect(store).toEqual({ workspaces: {} });
45
+ });
46
+ });
47
+
48
+ describe("saveBranchStore and loadBranchStore", () => {
49
+ it("round-trips store data", () => {
50
+ const store: BranchStore = {
51
+ workspaces: {
52
+ ws_123: {
53
+ branches: {
54
+ "feature-a": {
55
+ id: "branch-id-1",
56
+ token: "p.token1",
57
+ createdAt: "2024-01-01T00:00:00Z",
58
+ },
59
+ },
60
+ },
61
+ },
62
+ };
63
+
64
+ saveBranchStore(store);
65
+ const loaded = loadBranchStore();
66
+ expect(loaded).toEqual(store);
67
+ });
68
+ });
69
+
70
+ describe("getBranchToken and setBranchToken", () => {
71
+ it("returns null for non-existent branch", () => {
72
+ const result = getBranchToken("ws_123", "non-existent");
73
+ expect(result).toBeNull();
74
+ });
75
+
76
+ it("sets and gets branch token", () => {
77
+ const info = {
78
+ id: "branch-id-2",
79
+ token: "p.token2",
80
+ createdAt: "2024-01-02T00:00:00Z",
81
+ };
82
+
83
+ setBranchToken("ws_456", "feature-b", info);
84
+ const result = getBranchToken("ws_456", "feature-b");
85
+
86
+ expect(result).toEqual(info);
87
+ });
88
+ });
89
+
90
+ describe("removeBranch", () => {
91
+ it("removes a cached branch", () => {
92
+ const info = {
93
+ id: "branch-id-3",
94
+ token: "p.token3",
95
+ createdAt: "2024-01-03T00:00:00Z",
96
+ };
97
+
98
+ setBranchToken("ws_789", "feature-c", info);
99
+ expect(getBranchToken("ws_789", "feature-c")).toEqual(info);
100
+
101
+ removeBranch("ws_789", "feature-c");
102
+ expect(getBranchToken("ws_789", "feature-c")).toBeNull();
103
+ });
104
+
105
+ it("does nothing for non-existent branch", () => {
106
+ // Should not throw
107
+ removeBranch("ws_nonexistent", "no-branch");
108
+ });
109
+ });
110
+
111
+ describe("listCachedBranches", () => {
112
+ it("returns empty object for workspace with no branches", () => {
113
+ const result = listCachedBranches("ws_empty");
114
+ expect(result).toEqual({});
115
+ });
116
+
117
+ it("returns all branches for a workspace", () => {
118
+ const info1 = {
119
+ id: "branch-id-4",
120
+ token: "p.token4",
121
+ createdAt: "2024-01-04T00:00:00Z",
122
+ };
123
+ const info2 = {
124
+ id: "branch-id-5",
125
+ token: "p.token5",
126
+ createdAt: "2024-01-05T00:00:00Z",
127
+ };
128
+
129
+ setBranchToken("ws_list", "feature-d", info1);
130
+ setBranchToken("ws_list", "feature-e", info2);
131
+
132
+ const result = listCachedBranches("ws_list");
133
+ expect(result).toEqual({
134
+ "feature-d": info1,
135
+ "feature-e": info2,
136
+ });
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Branch token storage in ~/.tinybird/branches.json
3
+ */
4
+
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import * as os from "os";
8
+
9
+ /**
10
+ * Information about a cached branch
11
+ */
12
+ export interface BranchInfo {
13
+ /** Branch ID from Tinybird */
14
+ id: string;
15
+ /** Branch token for API access */
16
+ token: string;
17
+ /** When the branch was created/cached */
18
+ createdAt: string;
19
+ }
20
+
21
+ /**
22
+ * Structure of the branches.json file
23
+ */
24
+ export interface BranchStore {
25
+ workspaces: Record<
26
+ string,
27
+ {
28
+ branches: Record<string, BranchInfo>;
29
+ }
30
+ >;
31
+ }
32
+
33
+ /**
34
+ * Get the path to the branches.json file
35
+ */
36
+ export function getBranchStorePath(): string {
37
+ return path.join(os.homedir(), ".tinybird", "branches.json");
38
+ }
39
+
40
+ /**
41
+ * Ensure the ~/.tinybird directory exists
42
+ */
43
+ function ensureTinybirdDir(): void {
44
+ const tinybirdDir = path.join(os.homedir(), ".tinybird");
45
+ if (!fs.existsSync(tinybirdDir)) {
46
+ try {
47
+ fs.mkdirSync(tinybirdDir, { recursive: true });
48
+ } catch (error) {
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ throw new Error(
51
+ `Failed to create Tinybird config directory at ${tinybirdDir}: ${message}. ` +
52
+ `Please ensure you have write permissions to your home directory.`
53
+ );
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Load the branch store from disk
60
+ * Returns an empty store if the file doesn't exist
61
+ */
62
+ export function loadBranchStore(): BranchStore {
63
+ const storePath = getBranchStorePath();
64
+
65
+ if (!fs.existsSync(storePath)) {
66
+ return { workspaces: {} };
67
+ }
68
+
69
+ try {
70
+ const content = fs.readFileSync(storePath, "utf-8");
71
+ return JSON.parse(content) as BranchStore;
72
+ } catch {
73
+ // If the file is corrupted, return empty store
74
+ return { workspaces: {} };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Save the branch store to disk
80
+ */
81
+ export function saveBranchStore(store: BranchStore): void {
82
+ ensureTinybirdDir();
83
+ const storePath = getBranchStorePath();
84
+ fs.writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
85
+ }
86
+
87
+ /**
88
+ * Get a cached branch token
89
+ * Returns null if not cached
90
+ */
91
+ export function getBranchToken(
92
+ workspaceId: string,
93
+ branchName: string
94
+ ): BranchInfo | null {
95
+ const store = loadBranchStore();
96
+ return store.workspaces[workspaceId]?.branches[branchName] ?? null;
97
+ }
98
+
99
+ /**
100
+ * Cache a branch token
101
+ */
102
+ export function setBranchToken(
103
+ workspaceId: string,
104
+ branchName: string,
105
+ info: BranchInfo
106
+ ): void {
107
+ const store = loadBranchStore();
108
+
109
+ if (!store.workspaces[workspaceId]) {
110
+ store.workspaces[workspaceId] = { branches: {} };
111
+ }
112
+
113
+ store.workspaces[workspaceId].branches[branchName] = info;
114
+ saveBranchStore(store);
115
+ }
116
+
117
+ /**
118
+ * Remove a cached branch
119
+ */
120
+ export function removeBranch(workspaceId: string, branchName: string): void {
121
+ const store = loadBranchStore();
122
+
123
+ if (store.workspaces[workspaceId]?.branches[branchName]) {
124
+ delete store.workspaces[workspaceId].branches[branchName];
125
+ saveBranchStore(store);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * List all cached branches for a workspace
131
+ */
132
+ export function listCachedBranches(
133
+ workspaceId: string
134
+ ): Record<string, BranchInfo> {
135
+ const store = loadBranchStore();
136
+ return store.workspaces[workspaceId]?.branches ?? {};
137
+ }