@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.
- package/README.md +518 -0
- package/bin/tinybird.js +7 -0
- package/dist/api/branches.d.ts +98 -0
- package/dist/api/branches.d.ts.map +1 -0
- package/dist/api/branches.js +203 -0
- package/dist/api/branches.js.map +1 -0
- package/dist/api/branches.test.d.ts +2 -0
- package/dist/api/branches.test.d.ts.map +1 -0
- package/dist/api/branches.test.js +286 -0
- package/dist/api/branches.test.js.map +1 -0
- package/dist/api/build.d.ts +130 -0
- package/dist/api/build.d.ts.map +1 -0
- package/dist/api/build.js +143 -0
- package/dist/api/build.js.map +1 -0
- package/dist/api/build.test.d.ts +2 -0
- package/dist/api/build.test.d.ts.map +1 -0
- package/dist/api/build.test.js +138 -0
- package/dist/api/build.test.js.map +1 -0
- package/dist/api/deploy.d.ts +39 -0
- package/dist/api/deploy.d.ts.map +1 -0
- package/dist/api/deploy.js +135 -0
- package/dist/api/deploy.js.map +1 -0
- package/dist/api/deploy.test.d.ts +2 -0
- package/dist/api/deploy.test.d.ts.map +1 -0
- package/dist/api/deploy.test.js +118 -0
- package/dist/api/deploy.test.js.map +1 -0
- package/dist/api/workspaces.d.ts +46 -0
- package/dist/api/workspaces.d.ts.map +1 -0
- package/dist/api/workspaces.js +39 -0
- package/dist/api/workspaces.js.map +1 -0
- package/dist/api/workspaces.test.d.ts +2 -0
- package/dist/api/workspaces.test.d.ts.map +1 -0
- package/dist/api/workspaces.test.js +65 -0
- package/dist/api/workspaces.test.js.map +1 -0
- package/dist/cli/auth.d.ts +86 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +284 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/branch-store.d.ts +53 -0
- package/dist/cli/branch-store.d.ts.map +1 -0
- package/dist/cli/branch-store.js +91 -0
- package/dist/cli/branch-store.js.map +1 -0
- package/dist/cli/branch-store.test.d.ts +2 -0
- package/dist/cli/branch-store.test.d.ts.map +1 -0
- package/dist/cli/branch-store.test.js +115 -0
- package/dist/cli/branch-store.test.js.map +1 -0
- package/dist/cli/commands/branch.d.ts +82 -0
- package/dist/cli/commands/branch.d.ts.map +1 -0
- package/dist/cli/commands/branch.js +215 -0
- package/dist/cli/commands/branch.js.map +1 -0
- package/dist/cli/commands/build.d.ts +43 -0
- package/dist/cli/commands/build.d.ts.map +1 -0
- package/dist/cli/commands/build.js +138 -0
- package/dist/cli/commands/build.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +78 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +226 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/init.d.ts +45 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +277 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/init.test.d.ts +2 -0
- package/dist/cli/commands/init.test.d.ts.map +1 -0
- package/dist/cli/commands/init.test.js +158 -0
- package/dist/cli/commands/init.test.js.map +1 -0
- package/dist/cli/commands/login.d.ts +37 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +64 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/config.d.ts +114 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +258 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/config.test.d.ts +2 -0
- package/dist/cli/config.test.d.ts.map +1 -0
- package/dist/cli/config.test.js +243 -0
- package/dist/cli/config.test.js.map +1 -0
- package/dist/cli/env.d.ts +29 -0
- package/dist/cli/env.d.ts.map +1 -0
- package/dist/cli/env.js +66 -0
- package/dist/cli/env.js.map +1 -0
- package/dist/cli/git.d.ts +29 -0
- package/dist/cli/git.d.ts.map +1 -0
- package/dist/cli/git.js +114 -0
- package/dist/cli/git.js.map +1 -0
- package/dist/cli/git.test.d.ts +2 -0
- package/dist/cli/git.test.d.ts.map +1 -0
- package/dist/cli/git.test.js +125 -0
- package/dist/cli/git.test.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +337 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/schema-validation.d.ts +95 -0
- package/dist/cli/utils/schema-validation.d.ts.map +1 -0
- package/dist/cli/utils/schema-validation.js +175 -0
- package/dist/cli/utils/schema-validation.js.map +1 -0
- package/dist/cli/utils/schema-validation.test.d.ts +5 -0
- package/dist/cli/utils/schema-validation.test.d.ts.map +1 -0
- package/dist/cli/utils/schema-validation.test.js +173 -0
- package/dist/cli/utils/schema-validation.test.js.map +1 -0
- package/dist/client/base.d.ts +116 -0
- package/dist/client/base.d.ts.map +1 -0
- package/dist/client/base.js +328 -0
- package/dist/client/base.js.map +1 -0
- package/dist/client/types.d.ts +137 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +43 -0
- package/dist/client/types.js.map +1 -0
- package/dist/generator/client.d.ts +44 -0
- package/dist/generator/client.d.ts.map +1 -0
- package/dist/generator/client.js +144 -0
- package/dist/generator/client.js.map +1 -0
- package/dist/generator/datasource.d.ts +57 -0
- package/dist/generator/datasource.d.ts.map +1 -0
- package/dist/generator/datasource.js +169 -0
- package/dist/generator/datasource.js.map +1 -0
- package/dist/generator/datasource.test.d.ts +2 -0
- package/dist/generator/datasource.test.d.ts.map +1 -0
- package/dist/generator/datasource.test.js +254 -0
- package/dist/generator/datasource.test.js.map +1 -0
- package/dist/generator/index.d.ts +131 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +121 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/index.test.d.ts +2 -0
- package/dist/generator/index.test.d.ts.map +1 -0
- package/dist/generator/index.test.js +175 -0
- package/dist/generator/index.test.js.map +1 -0
- package/dist/generator/loader.d.ts +156 -0
- package/dist/generator/loader.d.ts.map +1 -0
- package/dist/generator/loader.js +295 -0
- package/dist/generator/loader.js.map +1 -0
- package/dist/generator/pipe.d.ts +72 -0
- package/dist/generator/pipe.d.ts.map +1 -0
- package/dist/generator/pipe.js +174 -0
- package/dist/generator/pipe.js.map +1 -0
- package/dist/generator/pipe.test.d.ts +2 -0
- package/dist/generator/pipe.test.d.ts.map +1 -0
- package/dist/generator/pipe.test.js +393 -0
- package/dist/generator/pipe.test.js.map +1 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/infer/index.d.ts +202 -0
- package/dist/infer/index.d.ts.map +1 -0
- package/dist/infer/index.js +5 -0
- package/dist/infer/index.js.map +1 -0
- package/dist/schema/datasource.d.ts +135 -0
- package/dist/schema/datasource.d.ts.map +1 -0
- package/dist/schema/datasource.js +105 -0
- package/dist/schema/datasource.js.map +1 -0
- package/dist/schema/datasource.test.d.ts +2 -0
- package/dist/schema/datasource.test.d.ts.map +1 -0
- package/dist/schema/datasource.test.js +142 -0
- package/dist/schema/datasource.test.js.map +1 -0
- package/dist/schema/engines.d.ts +157 -0
- package/dist/schema/engines.d.ts.map +1 -0
- package/dist/schema/engines.js +155 -0
- package/dist/schema/engines.js.map +1 -0
- package/dist/schema/engines.test.d.ts +2 -0
- package/dist/schema/engines.test.d.ts.map +1 -0
- package/dist/schema/engines.test.js +221 -0
- package/dist/schema/engines.test.js.map +1 -0
- package/dist/schema/params.d.ts +106 -0
- package/dist/schema/params.d.ts.map +1 -0
- package/dist/schema/params.js +138 -0
- package/dist/schema/params.js.map +1 -0
- package/dist/schema/params.test.d.ts +2 -0
- package/dist/schema/params.test.d.ts.map +1 -0
- package/dist/schema/params.test.js +175 -0
- package/dist/schema/params.test.js.map +1 -0
- package/dist/schema/pipe.d.ts +436 -0
- package/dist/schema/pipe.d.ts.map +1 -0
- package/dist/schema/pipe.js +484 -0
- package/dist/schema/pipe.js.map +1 -0
- package/dist/schema/pipe.test.d.ts +2 -0
- package/dist/schema/pipe.test.d.ts.map +1 -0
- package/dist/schema/pipe.test.js +488 -0
- package/dist/schema/pipe.test.js.map +1 -0
- package/dist/schema/project.d.ts +202 -0
- package/dist/schema/project.d.ts.map +1 -0
- package/dist/schema/project.js +188 -0
- package/dist/schema/project.js.map +1 -0
- package/dist/schema/project.test.d.ts +2 -0
- package/dist/schema/project.test.d.ts.map +1 -0
- package/dist/schema/project.test.js +180 -0
- package/dist/schema/project.test.js.map +1 -0
- package/dist/schema/types.d.ts +140 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +174 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/schema/types.test.d.ts +2 -0
- package/dist/schema/types.test.d.ts.map +1 -0
- package/dist/schema/types.test.js +176 -0
- package/dist/schema/types.test.js.map +1 -0
- package/dist/test/handlers.d.ts +58 -0
- package/dist/test/handlers.d.ts.map +1 -0
- package/dist/test/handlers.js +62 -0
- package/dist/test/handlers.js.map +1 -0
- package/dist/test/setup.d.ts +5 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +11 -0
- package/dist/test/setup.js.map +1 -0
- package/package.json +57 -0
- package/src/api/branches.test.ts +377 -0
- package/src/api/branches.ts +334 -0
- package/src/api/build.test.ts +216 -0
- package/src/api/build.ts +266 -0
- package/src/api/deploy.test.ts +193 -0
- package/src/api/deploy.ts +163 -0
- package/src/api/workspaces.test.ts +81 -0
- package/src/api/workspaces.ts +77 -0
- package/src/cli/auth.ts +358 -0
- package/src/cli/branch-store.test.ts +139 -0
- package/src/cli/branch-store.ts +137 -0
- package/src/cli/commands/branch.ts +306 -0
- package/src/cli/commands/build.ts +183 -0
- package/src/cli/commands/dev.ts +334 -0
- package/src/cli/commands/init.test.ts +249 -0
- package/src/cli/commands/init.ts +323 -0
- package/src/cli/commands/login.ts +98 -0
- package/src/cli/config.test.ts +359 -0
- package/src/cli/config.ts +335 -0
- package/src/cli/env.ts +86 -0
- package/src/cli/git.test.ts +147 -0
- package/src/cli/git.ts +125 -0
- package/src/cli/index.ts +382 -0
- package/src/cli/utils/schema-validation.test.ts +222 -0
- package/src/cli/utils/schema-validation.ts +272 -0
- package/src/client/base.ts +414 -0
- package/src/client/types.ts +165 -0
- package/src/generator/client.ts +194 -0
- package/src/generator/datasource.test.ts +297 -0
- package/src/generator/datasource.ts +217 -0
- package/src/generator/index.test.ts +209 -0
- package/src/generator/index.ts +203 -0
- package/src/generator/loader.ts +406 -0
- package/src/generator/pipe.test.ts +441 -0
- package/src/generator/pipe.ts +220 -0
- package/src/index.ts +191 -0
- package/src/infer/index.ts +247 -0
- package/src/schema/datasource.test.ts +187 -0
- package/src/schema/datasource.ts +195 -0
- package/src/schema/engines.test.ts +247 -0
- package/src/schema/engines.ts +271 -0
- package/src/schema/params.test.ts +208 -0
- package/src/schema/params.ts +249 -0
- package/src/schema/pipe.test.ts +588 -0
- package/src/schema/pipe.ts +832 -0
- package/src/schema/project.test.ts +236 -0
- package/src/schema/project.ts +394 -0
- package/src/schema/types.test.ts +212 -0
- package/src/schema/types.ts +366 -0
- package/src/test/handlers.ts +79 -0
- 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
|
+
}
|
package/src/cli/auth.ts
ADDED
|
@@ -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
|
+
}
|