@tinybirdco/sdk 0.0.37 → 0.0.38
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 +50 -1
- package/dist/api/api.d.ts +49 -0
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +23 -0
- package/dist/api/api.js.map +1 -1
- package/dist/api/api.test.js +32 -0
- package/dist/api/api.test.js.map +1 -1
- package/dist/api/tokens.d.ts +79 -0
- package/dist/api/tokens.d.ts.map +1 -0
- package/dist/api/tokens.js +80 -0
- package/dist/api/tokens.js.map +1 -0
- package/dist/api/tokens.test.d.ts +2 -0
- package/dist/api/tokens.test.d.ts.map +1 -0
- package/dist/api/tokens.test.js +209 -0
- package/dist/api/tokens.test.js.map +1 -0
- package/dist/client/base.d.ts +3 -0
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js +5 -0
- package/dist/client/base.js.map +1 -1
- package/dist/client/tokens.d.ts +42 -0
- package/dist/client/tokens.d.ts.map +1 -0
- package/dist/client/tokens.js +67 -0
- package/dist/client/tokens.js.map +1 -0
- package/dist/client/tokens.test.d.ts +2 -0
- package/dist/client/tokens.test.d.ts.map +1 -0
- package/dist/client/tokens.test.js +79 -0
- package/dist/client/tokens.test.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/schema/project.d.ts +21 -12
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/schema/project.js +55 -13
- package/dist/schema/project.js.map +1 -1
- package/dist/schema/project.test.js +11 -10
- package/dist/schema/project.test.js.map +1 -1
- package/package.json +1 -1
- package/src/api/api.test.ts +43 -0
- package/src/api/api.ts +79 -0
- package/src/api/tokens.test.ts +253 -0
- package/src/api/tokens.ts +169 -0
- package/src/client/base.ts +12 -0
- package/src/client/tokens.test.ts +103 -0
- package/src/client/tokens.ts +69 -0
- package/src/index.ts +15 -0
- package/src/schema/project.test.ts +11 -10
- package/src/schema/project.ts +95 -27
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createJWT,
|
|
4
|
+
TokenApiError,
|
|
5
|
+
type TokenApiConfig,
|
|
6
|
+
} from "./tokens.js";
|
|
7
|
+
|
|
8
|
+
// Mock fetch globally
|
|
9
|
+
const mockFetch = vi.fn();
|
|
10
|
+
global.fetch = mockFetch;
|
|
11
|
+
|
|
12
|
+
function expectFromParam(url: string) {
|
|
13
|
+
const parsed = new URL(url);
|
|
14
|
+
expect(parsed.searchParams.get("from")).toBe("ts-sdk");
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("Token API client", () => {
|
|
19
|
+
const config: TokenApiConfig = {
|
|
20
|
+
baseUrl: "https://api.tinybird.co",
|
|
21
|
+
token: "p.admin-token",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockFetch.mockReset();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("createJWT", () => {
|
|
29
|
+
it("creates a JWT token with scopes", async () => {
|
|
30
|
+
const mockResponse = { token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test" };
|
|
31
|
+
|
|
32
|
+
mockFetch.mockResolvedValueOnce({
|
|
33
|
+
ok: true,
|
|
34
|
+
json: () => Promise.resolve(mockResponse),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = await createJWT(config, {
|
|
38
|
+
name: "user_token",
|
|
39
|
+
expiresAt: 1700000000,
|
|
40
|
+
scopes: [
|
|
41
|
+
{
|
|
42
|
+
type: "PIPES:READ",
|
|
43
|
+
resource: "analytics_pipe",
|
|
44
|
+
fixed_params: { user_id: 123 },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(result.token).toBe(mockResponse.token);
|
|
50
|
+
|
|
51
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
52
|
+
const parsed = expectFromParam(url);
|
|
53
|
+
expect(parsed.pathname).toBe("/v0/tokens/");
|
|
54
|
+
expect(parsed.searchParams.get("expiration_time")).toBe("1700000000");
|
|
55
|
+
expect(init.method).toBe("POST");
|
|
56
|
+
const headers = new Headers(init.headers);
|
|
57
|
+
expect(headers.get("Authorization")).toBe("Bearer p.admin-token");
|
|
58
|
+
expect(headers.get("Content-Type")).toBe("application/json");
|
|
59
|
+
|
|
60
|
+
const body = JSON.parse(init.body);
|
|
61
|
+
expect(body.name).toBe("user_token");
|
|
62
|
+
expect(body.scopes).toHaveLength(1);
|
|
63
|
+
expect(body.scopes[0].type).toBe("PIPES:READ");
|
|
64
|
+
expect(body.scopes[0].resource).toBe("analytics_pipe");
|
|
65
|
+
expect(body.scopes[0].fixed_params).toEqual({ user_id: 123 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("converts Date to Unix timestamp", async () => {
|
|
69
|
+
mockFetch.mockResolvedValueOnce({
|
|
70
|
+
ok: true,
|
|
71
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const expirationDate = new Date("2024-01-01T00:00:00Z");
|
|
75
|
+
await createJWT(config, {
|
|
76
|
+
name: "test",
|
|
77
|
+
expiresAt: expirationDate,
|
|
78
|
+
scopes: [{ type: "PIPES:READ", resource: "pipe" }],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const [url] = mockFetch.mock.calls[0];
|
|
82
|
+
const parsed = new URL(url);
|
|
83
|
+
expect(parsed.searchParams.get("expiration_time")).toBe("1704067200");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("converts ISO string to Unix timestamp", async () => {
|
|
87
|
+
mockFetch.mockResolvedValueOnce({
|
|
88
|
+
ok: true,
|
|
89
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await createJWT(config, {
|
|
93
|
+
name: "test",
|
|
94
|
+
expiresAt: "2024-01-01T00:00:00Z",
|
|
95
|
+
scopes: [{ type: "PIPES:READ", resource: "pipe" }],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const [url] = mockFetch.mock.calls[0];
|
|
99
|
+
const parsed = new URL(url);
|
|
100
|
+
expect(parsed.searchParams.get("expiration_time")).toBe("1704067200");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("includes limits when provided", async () => {
|
|
104
|
+
mockFetch.mockResolvedValueOnce({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await createJWT(config, {
|
|
110
|
+
name: "rate_limited_token",
|
|
111
|
+
expiresAt: 1700000000,
|
|
112
|
+
scopes: [{ type: "PIPES:READ", resource: "pipe" }],
|
|
113
|
+
limits: { rps: 100 },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
117
|
+
const body = JSON.parse(init.body);
|
|
118
|
+
expect(body.limits).toEqual({ rps: 100 });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("uses custom fetch implementation when provided", async () => {
|
|
122
|
+
const customFetch = vi.fn().mockResolvedValueOnce({
|
|
123
|
+
ok: true,
|
|
124
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await createJWT(
|
|
128
|
+
{
|
|
129
|
+
...config,
|
|
130
|
+
fetch: customFetch as typeof fetch,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "custom_fetch_token",
|
|
134
|
+
expiresAt: 1700000000,
|
|
135
|
+
scopes: [{ type: "PIPES:READ", resource: "pipe" }],
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(customFetch).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("supports datasource scope with filter", async () => {
|
|
144
|
+
mockFetch.mockResolvedValueOnce({
|
|
145
|
+
ok: true,
|
|
146
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await createJWT(config, {
|
|
150
|
+
name: "filtered_token",
|
|
151
|
+
expiresAt: 1700000000,
|
|
152
|
+
scopes: [
|
|
153
|
+
{
|
|
154
|
+
type: "DATASOURCES:READ",
|
|
155
|
+
resource: "events",
|
|
156
|
+
filter: "org_id = 'acme'",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
162
|
+
const body = JSON.parse(init.body);
|
|
163
|
+
expect(body.scopes[0].filter).toBe("org_id = 'acme'");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("throws TokenApiError on 403 with helpful message", async () => {
|
|
167
|
+
mockFetch.mockResolvedValueOnce({
|
|
168
|
+
ok: false,
|
|
169
|
+
status: 403,
|
|
170
|
+
statusText: "Forbidden",
|
|
171
|
+
text: () => Promise.resolve("Insufficient permissions"),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await expect(
|
|
175
|
+
createJWT(config, {
|
|
176
|
+
name: "test",
|
|
177
|
+
expiresAt: 1700000000,
|
|
178
|
+
scopes: [],
|
|
179
|
+
})
|
|
180
|
+
).rejects.toThrow(TokenApiError);
|
|
181
|
+
|
|
182
|
+
mockFetch.mockResolvedValueOnce({
|
|
183
|
+
ok: false,
|
|
184
|
+
status: 403,
|
|
185
|
+
statusText: "Forbidden",
|
|
186
|
+
text: () => Promise.resolve("Insufficient permissions"),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await createJWT(config, {
|
|
191
|
+
name: "test",
|
|
192
|
+
expiresAt: 1700000000,
|
|
193
|
+
scopes: [],
|
|
194
|
+
});
|
|
195
|
+
} catch (error) {
|
|
196
|
+
expect((error as TokenApiError).status).toBe(403);
|
|
197
|
+
expect((error as TokenApiError).message).toContain("TOKENS or ADMIN scope");
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("throws TokenApiError on 400 with validation error", async () => {
|
|
202
|
+
mockFetch.mockResolvedValueOnce({
|
|
203
|
+
ok: false,
|
|
204
|
+
status: 400,
|
|
205
|
+
statusText: "Bad Request",
|
|
206
|
+
text: () => Promise.resolve('{"error": "Invalid scope type"}'),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await expect(
|
|
210
|
+
createJWT(config, {
|
|
211
|
+
name: "test",
|
|
212
|
+
expiresAt: 1700000000,
|
|
213
|
+
scopes: [],
|
|
214
|
+
})
|
|
215
|
+
).rejects.toThrow(TokenApiError);
|
|
216
|
+
|
|
217
|
+
mockFetch.mockResolvedValueOnce({
|
|
218
|
+
ok: false,
|
|
219
|
+
status: 400,
|
|
220
|
+
statusText: "Bad Request",
|
|
221
|
+
text: () => Promise.resolve('{"error": "Invalid scope type"}'),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
await createJWT(config, {
|
|
226
|
+
name: "test",
|
|
227
|
+
expiresAt: 1700000000,
|
|
228
|
+
scopes: [],
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
expect((error as TokenApiError).status).toBe(400);
|
|
232
|
+
expect((error as TokenApiError).message).toContain("Invalid scope type");
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("does not include limits if not provided", async () => {
|
|
237
|
+
mockFetch.mockResolvedValueOnce({
|
|
238
|
+
ok: true,
|
|
239
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await createJWT(config, {
|
|
243
|
+
name: "simple_token",
|
|
244
|
+
expiresAt: 1700000000,
|
|
245
|
+
scopes: [{ type: "PIPES:READ", resource: "pipe" }],
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
249
|
+
const body = JSON.parse(init.body);
|
|
250
|
+
expect(body.limits).toBeUndefined();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tinybird Token API client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createTinybirdApi,
|
|
7
|
+
TinybirdApiError,
|
|
8
|
+
} from "./api.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* API configuration for token operations.
|
|
12
|
+
* Requires a token with TOKENS or ADMIN scope.
|
|
13
|
+
*/
|
|
14
|
+
export interface TokenApiConfig {
|
|
15
|
+
/** Tinybird API base URL */
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
/** Workspace token with TOKENS or ADMIN scope */
|
|
18
|
+
token: string;
|
|
19
|
+
/** Custom fetch implementation (optional, defaults to global fetch) */
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
/** Default timeout in milliseconds (optional) */
|
|
22
|
+
timeout?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Error thrown by token API operations
|
|
27
|
+
*/
|
|
28
|
+
export class TokenApiError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
message: string,
|
|
31
|
+
public readonly status: number,
|
|
32
|
+
public readonly body?: unknown
|
|
33
|
+
) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "TokenApiError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Scope type for JWT tokens
|
|
41
|
+
*/
|
|
42
|
+
export type JWTScopeType =
|
|
43
|
+
| "PIPES:READ"
|
|
44
|
+
| "DATASOURCES:READ"
|
|
45
|
+
| "DATASOURCES:APPEND";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A scope definition for JWT tokens
|
|
49
|
+
*/
|
|
50
|
+
export interface JWTScope {
|
|
51
|
+
/** The type of access being granted */
|
|
52
|
+
type: JWTScopeType;
|
|
53
|
+
/** The resource name (pipe or datasource) */
|
|
54
|
+
resource: string;
|
|
55
|
+
/** Fixed parameters embedded in the JWT (for pipes) */
|
|
56
|
+
fixed_params?: Record<string, string | number | boolean>;
|
|
57
|
+
/** SQL filter expression (for datasources) */
|
|
58
|
+
filter?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Rate limiting configuration for JWT tokens
|
|
63
|
+
*/
|
|
64
|
+
export interface JWTLimits {
|
|
65
|
+
/** Requests per second limit */
|
|
66
|
+
rps?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Options for creating a JWT token
|
|
71
|
+
*/
|
|
72
|
+
export interface CreateJWTOptions {
|
|
73
|
+
/** Token name/identifier */
|
|
74
|
+
name: string;
|
|
75
|
+
/** Expiration time as Date, Unix timestamp (number), or ISO string */
|
|
76
|
+
expiresAt: Date | number | string;
|
|
77
|
+
/** Array of scopes defining access permissions */
|
|
78
|
+
scopes: JWTScope[];
|
|
79
|
+
/** Optional rate limiting configuration */
|
|
80
|
+
limits?: JWTLimits;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Result of creating a JWT token
|
|
85
|
+
*/
|
|
86
|
+
export interface CreateJWTResult {
|
|
87
|
+
/** The generated JWT token string */
|
|
88
|
+
token: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Request body for creating a JWT token
|
|
93
|
+
*/
|
|
94
|
+
interface CreateJWTRequestBody {
|
|
95
|
+
name: string;
|
|
96
|
+
scopes: JWTScope[];
|
|
97
|
+
limits?: JWTLimits;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Convert expiration input to Unix timestamp
|
|
102
|
+
*/
|
|
103
|
+
function toUnixTimestamp(expiresAt: Date | number | string): number {
|
|
104
|
+
if (typeof expiresAt === "number") {
|
|
105
|
+
return expiresAt;
|
|
106
|
+
}
|
|
107
|
+
if (expiresAt instanceof Date) {
|
|
108
|
+
return Math.floor(expiresAt.getTime() / 1000);
|
|
109
|
+
}
|
|
110
|
+
return Math.floor(new Date(expiresAt).getTime() / 1000);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create a JWT token
|
|
115
|
+
* POST /v0/tokens/?expiration_time={unix_timestamp}
|
|
116
|
+
*
|
|
117
|
+
* @param config - API configuration (requires TOKENS or ADMIN scope)
|
|
118
|
+
* @param options - JWT creation options
|
|
119
|
+
* @returns The created JWT token
|
|
120
|
+
*/
|
|
121
|
+
export async function createJWT(
|
|
122
|
+
config: TokenApiConfig,
|
|
123
|
+
options: CreateJWTOptions
|
|
124
|
+
): Promise<CreateJWTResult> {
|
|
125
|
+
const expirationTime = toUnixTimestamp(options.expiresAt);
|
|
126
|
+
|
|
127
|
+
const body: CreateJWTRequestBody = {
|
|
128
|
+
name: options.name,
|
|
129
|
+
scopes: options.scopes,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (options.limits) {
|
|
133
|
+
body.limits = options.limits;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const api = createTinybirdApi({
|
|
137
|
+
baseUrl: config.baseUrl,
|
|
138
|
+
token: config.token,
|
|
139
|
+
fetch: config.fetch,
|
|
140
|
+
timeout: config.timeout,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const result = await api.createToken(body, {
|
|
145
|
+
expirationTime,
|
|
146
|
+
});
|
|
147
|
+
return { token: result.token };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (!(error instanceof TinybirdApiError)) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const responseBody = error.responseBody ?? error.message;
|
|
154
|
+
let message: string;
|
|
155
|
+
|
|
156
|
+
if (error.statusCode === 403) {
|
|
157
|
+
message =
|
|
158
|
+
`Permission denied creating JWT token. ` +
|
|
159
|
+
`Make sure the token has TOKENS or ADMIN scope. ` +
|
|
160
|
+
`API response: ${responseBody}`;
|
|
161
|
+
} else if (error.statusCode === 400) {
|
|
162
|
+
message = `Invalid JWT token request: ${responseBody}`;
|
|
163
|
+
} else {
|
|
164
|
+
message = `Failed to create JWT token: ${error.statusCode}. API response: ${responseBody}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw new TokenApiError(message, error.statusCode, responseBody);
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/client/base.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
} from "./types.js";
|
|
16
16
|
import { TinybirdError } from "./types.js";
|
|
17
17
|
import { TinybirdApi, TinybirdApiError } from "../api/api.js";
|
|
18
|
+
import { TokensNamespace } from "./tokens.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Resolved token info from dev mode
|
|
@@ -65,6 +66,9 @@ export class TinybirdClient {
|
|
|
65
66
|
*/
|
|
66
67
|
readonly datasources: DatasourcesNamespace;
|
|
67
68
|
|
|
69
|
+
/** Token operations (JWT creation, etc.) */
|
|
70
|
+
public readonly tokens: TokensNamespace;
|
|
71
|
+
|
|
68
72
|
constructor(config: ClientConfig) {
|
|
69
73
|
// Validate required config
|
|
70
74
|
if (!config.baseUrl) {
|
|
@@ -86,6 +90,14 @@ export class TinybirdClient {
|
|
|
86
90
|
return this.appendDatasource(datasourceName, options);
|
|
87
91
|
},
|
|
88
92
|
};
|
|
93
|
+
|
|
94
|
+
// Initialize tokens namespace
|
|
95
|
+
this.tokens = new TokensNamespace(
|
|
96
|
+
() => this.getToken(),
|
|
97
|
+
this.config.baseUrl,
|
|
98
|
+
this.config.fetch,
|
|
99
|
+
this.config.timeout
|
|
100
|
+
);
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
/**
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { TinybirdClient } from "./base.js";
|
|
3
|
+
|
|
4
|
+
const mockFetch = vi.fn();
|
|
5
|
+
global.fetch = mockFetch;
|
|
6
|
+
|
|
7
|
+
describe("TokensNamespace", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockFetch.mockReset();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("uses client custom fetch for createJWT", async () => {
|
|
13
|
+
const customFetch = vi.fn().mockResolvedValueOnce({
|
|
14
|
+
ok: true,
|
|
15
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const client = new TinybirdClient({
|
|
19
|
+
baseUrl: "https://api.tinybird.co",
|
|
20
|
+
token: "p.admin-token",
|
|
21
|
+
fetch: customFetch as typeof fetch,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const result = await client.tokens.createJWT({
|
|
25
|
+
name: "user_token",
|
|
26
|
+
expiresAt: 1700000000,
|
|
27
|
+
scopes: [{ type: "PIPES:READ", resource: "analytics_pipe" }],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({ token: "jwt-token" });
|
|
31
|
+
expect(customFetch).toHaveBeenCalledTimes(1);
|
|
32
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
33
|
+
|
|
34
|
+
const [url, init] = customFetch.mock.calls[0] as [string, RequestInit];
|
|
35
|
+
const parsed = new URL(url);
|
|
36
|
+
expect(parsed.pathname).toBe("/v0/tokens/");
|
|
37
|
+
expect(parsed.searchParams.get("expiration_time")).toBe("1700000000");
|
|
38
|
+
expect(parsed.searchParams.get("from")).toBe("ts-sdk");
|
|
39
|
+
expect(init.method).toBe("POST");
|
|
40
|
+
const headers = new Headers(init.headers);
|
|
41
|
+
expect(headers.get("Authorization")).toBe("Bearer p.admin-token");
|
|
42
|
+
expect(headers.get("Content-Type")).toBe("application/json");
|
|
43
|
+
|
|
44
|
+
const body = JSON.parse(String(init.body)) as {
|
|
45
|
+
name: string;
|
|
46
|
+
scopes: Array<{ type: string; resource: string }>;
|
|
47
|
+
limits?: { rps?: number };
|
|
48
|
+
};
|
|
49
|
+
expect(body).toEqual({
|
|
50
|
+
name: "user_token",
|
|
51
|
+
scopes: [{ type: "PIPES:READ", resource: "analytics_pipe" }],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("sends full JWT payload when limits and scope fields are provided", async () => {
|
|
56
|
+
const customFetch = vi.fn().mockResolvedValueOnce({
|
|
57
|
+
ok: true,
|
|
58
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const client = new TinybirdClient({
|
|
62
|
+
baseUrl: "https://api.tinybird.co",
|
|
63
|
+
token: "p.admin-token",
|
|
64
|
+
fetch: customFetch as typeof fetch,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await client.tokens.createJWT({
|
|
68
|
+
name: "tenant_token",
|
|
69
|
+
expiresAt: 1700000000,
|
|
70
|
+
scopes: [
|
|
71
|
+
{
|
|
72
|
+
type: "PIPES:READ",
|
|
73
|
+
resource: "analytics_pipe",
|
|
74
|
+
fixed_params: { tenant_id: 123 },
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
limits: { rps: 10 },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const [, init] = customFetch.mock.calls[0] as [string, RequestInit];
|
|
81
|
+
const body = JSON.parse(String(init.body)) as {
|
|
82
|
+
name: string;
|
|
83
|
+
scopes: Array<{
|
|
84
|
+
type: string;
|
|
85
|
+
resource: string;
|
|
86
|
+
fixed_params?: Record<string, unknown>;
|
|
87
|
+
}>;
|
|
88
|
+
limits?: { rps?: number };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
expect(body).toEqual({
|
|
92
|
+
name: "tenant_token",
|
|
93
|
+
scopes: [
|
|
94
|
+
{
|
|
95
|
+
type: "PIPES:READ",
|
|
96
|
+
resource: "analytics_pipe",
|
|
97
|
+
fixed_params: { tenant_id: 123 },
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
limits: { rps: 10 },
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token operations namespace for TinybirdClient
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CreateJWTOptions, CreateJWTResult } from "../api/tokens.js";
|
|
6
|
+
import { createJWT as apiCreateJWT, TokenApiError } from "../api/tokens.js";
|
|
7
|
+
import { TinybirdError } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Token operations namespace for TinybirdClient
|
|
11
|
+
*/
|
|
12
|
+
export class TokensNamespace {
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly getToken: () => Promise<string>,
|
|
15
|
+
private readonly baseUrl: string,
|
|
16
|
+
private readonly fetchFn?: typeof globalThis.fetch,
|
|
17
|
+
private readonly timeout?: number
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a JWT token
|
|
22
|
+
*
|
|
23
|
+
* Creates a short-lived JWT token with specific scopes for secure,
|
|
24
|
+
* time-limited access to pipes and datasources.
|
|
25
|
+
*
|
|
26
|
+
* @param options - JWT creation options
|
|
27
|
+
* @returns The created JWT token
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const result = await client.tokens.createJWT({
|
|
32
|
+
* name: "user_123_token",
|
|
33
|
+
* expiresAt: new Date(Date.now() + 3600 * 1000), // 1 hour
|
|
34
|
+
* scopes: [
|
|
35
|
+
* {
|
|
36
|
+
* type: "PIPES:READ",
|
|
37
|
+
* resource: "user_analytics",
|
|
38
|
+
* fixed_params: { user_id: 123 },
|
|
39
|
+
* },
|
|
40
|
+
* ],
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* console.log(result.token); // "eyJ..."
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
async createJWT(options: CreateJWTOptions): Promise<CreateJWTResult> {
|
|
47
|
+
const token = await this.getToken();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
return await apiCreateJWT(
|
|
51
|
+
{
|
|
52
|
+
baseUrl: this.baseUrl,
|
|
53
|
+
token,
|
|
54
|
+
fetch: this.fetchFn,
|
|
55
|
+
timeout: this.timeout,
|
|
56
|
+
},
|
|
57
|
+
options
|
|
58
|
+
);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof TokenApiError) {
|
|
61
|
+
throw new TinybirdError(error.message, error.status, {
|
|
62
|
+
error: error.message,
|
|
63
|
+
status: error.status,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -232,6 +232,10 @@ export type {
|
|
|
232
232
|
TinybirdApiIngestOptions,
|
|
233
233
|
TinybirdApiAppendOptions,
|
|
234
234
|
TinybirdApiRequestInit,
|
|
235
|
+
TinybirdApiTokenScope,
|
|
236
|
+
TinybirdApiCreateTokenRequest,
|
|
237
|
+
TinybirdApiCreateTokenOptions,
|
|
238
|
+
TinybirdApiCreateTokenResult,
|
|
235
239
|
} from "./api/api.js";
|
|
236
240
|
|
|
237
241
|
// ============ Preview Environment ============
|
|
@@ -251,6 +255,17 @@ export {
|
|
|
251
255
|
} from "./api/dashboard.js";
|
|
252
256
|
export type { RegionInfo } from "./api/dashboard.js";
|
|
253
257
|
|
|
258
|
+
// ============ Token API ============
|
|
259
|
+
export { createJWT, TokenApiError } from "./api/tokens.js";
|
|
260
|
+
export type {
|
|
261
|
+
TokenApiConfig,
|
|
262
|
+
JWTScope,
|
|
263
|
+
JWTScopeType,
|
|
264
|
+
JWTLimits,
|
|
265
|
+
CreateJWTOptions,
|
|
266
|
+
CreateJWTResult,
|
|
267
|
+
} from "./api/tokens.js";
|
|
268
|
+
|
|
254
269
|
// ============ Config Types ============
|
|
255
270
|
// Import from config-types.ts to avoid bundling esbuild in client code
|
|
256
271
|
export type { TinybirdConfig, DevMode } from "./cli/config-types.js";
|