@taruvi/sdk 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_IMPLEMENTATION_GUIDE.md +733 -0
- package/package.json +1 -1
- package/src/client.ts +32 -6
- package/src/index.ts +2 -1
- package/src/lib/Database/DatabaseClient.ts +16 -7
- package/src/lib/Storage/StorageClient.ts +24 -9
- package/src/lib/auth/AuthClient.ts +201 -48
- package/src/lib-internal/routes/DatabaseRoutes.ts +5 -1
- package/src/lib-internal/routes/StorageRoutes.ts +9 -1
- package/src/lib-internal/token/TokenClient.ts +249 -13
- package/src/lib-internal/routes/RouteBuilder.ts +0 -0
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -33,8 +33,11 @@ export class Client {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Extracts
|
|
37
|
-
* This handles
|
|
36
|
+
* Extracts authentication tokens from URL hash and stores them using TokenClient.
|
|
37
|
+
* This handles Web UI Flow callback URLs like:
|
|
38
|
+
* #session_token=xxx&access_token=yyy&refresh_token=zzz&expires_in=172800&token_type=Bearer
|
|
39
|
+
*
|
|
40
|
+
* After successful extraction, the URL hash is cleared to prevent token exposure.
|
|
38
41
|
*/
|
|
39
42
|
private extractTokensFromUrl(): void {
|
|
40
43
|
if (typeof window === "undefined" || typeof localStorage === "undefined") {
|
|
@@ -47,15 +50,38 @@ export class Client {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
const params = new URLSearchParams(hash.substring(1))
|
|
53
|
+
const sessionToken = params.get("session_token")
|
|
50
54
|
const accessToken = params.get("access_token")
|
|
51
55
|
const refreshToken = params.get("refresh_token")
|
|
56
|
+
const expiresIn = params.get("expires_in")
|
|
57
|
+
const tokenType = params.get("token_type")
|
|
52
58
|
|
|
53
|
-
if
|
|
54
|
-
|
|
59
|
+
// Only proceed if we have the required tokens
|
|
60
|
+
if (!accessToken || !refreshToken) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Store tokens using TokenClient
|
|
65
|
+
const tokens: any = {
|
|
66
|
+
accessToken,
|
|
67
|
+
refreshToken,
|
|
68
|
+
tokenType: tokenType || "Bearer"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (sessionToken) {
|
|
72
|
+
tokens.sessionToken = sessionToken
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (expiresIn) {
|
|
76
|
+
tokens.expiresIn = parseInt(expiresIn, 10)
|
|
55
77
|
}
|
|
56
78
|
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
this._tokenClient.setTokens(tokens)
|
|
80
|
+
|
|
81
|
+
// Clear hash from URL without reloading page
|
|
82
|
+
if (window.history && window.history.replaceState) {
|
|
83
|
+
const urlWithoutHash = window.location.pathname + window.location.search
|
|
84
|
+
window.history.replaceState(null, "", urlWithoutHash)
|
|
59
85
|
}
|
|
60
86
|
}
|
|
61
87
|
|
package/src/index.ts
CHANGED
|
@@ -12,9 +12,10 @@ export { Secrets } from "./lib/Secrets/SecretsClient.js"
|
|
|
12
12
|
|
|
13
13
|
// Export types
|
|
14
14
|
export type { TaruviConfig, StorageFilters, DatabaseFilters } from "./types.js"
|
|
15
|
+
export type { AuthTokens } from "./lib-internal/token/TokenClient.js"
|
|
15
16
|
export type { UserCreateRequest, UserCreateResponse as UserResponse, UserDataResponse } from "./lib/user/types.js"
|
|
16
17
|
export type { FunctionRequest, FunctionResponse, FunctionInvocation } from "./lib/Function/types.js"
|
|
17
18
|
export type { DatabaseRequest, DatabaseResponse } from "./lib/Database/types.js"
|
|
18
19
|
export type { StorageRequest, StorageUpdateRequest, StorageResponse } from "./lib/Storage/types.js"
|
|
19
|
-
export type {
|
|
20
|
+
export type { SettingsResponse } from "./lib/Settings/types.js"
|
|
20
21
|
export type { SecretRequest, SecretResponse } from "./lib/Secrets/types.js"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Client } from "../../client.js";
|
|
2
|
-
import { DatabaseRoutes } from "../../lib-internal/routes/DatabaseRoutes.js";
|
|
2
|
+
import { DatabaseRoutes, type DatabaseRouteKey } from "../../lib-internal/routes/DatabaseRoutes.js";
|
|
3
3
|
import { HttpMethod } from "../../lib-internal/http/types.js";
|
|
4
4
|
import type { TaruviConfig, DatabaseFilters } from "../../types.js";
|
|
5
5
|
import type { UrlParams } from "./types.js";
|
|
@@ -44,12 +44,21 @@ export class Database {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
private buildRoute(): string {
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
return (
|
|
48
|
+
DatabaseRoutes.baseUrl(this.config.appSlug) +
|
|
49
|
+
(Object.keys(this.urlParams) as DatabaseRouteKey[]).reduce((acc, key) => {
|
|
50
|
+
const value = this.urlParams[key]
|
|
51
|
+
const routeBuilder = DatabaseRoutes[key]
|
|
52
|
+
|
|
53
|
+
if (value && routeBuilder) {
|
|
54
|
+
acc += routeBuilder(value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return acc
|
|
58
|
+
}, "") +
|
|
59
|
+
"/" +
|
|
60
|
+
buildQueryString(this.filters)
|
|
61
|
+
)
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
async execute() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Client } from "../../client.js";
|
|
2
2
|
import type { BucketFileUpload, BucketUrlParams } from "./types.js";
|
|
3
|
-
import { StorageRoutes } from "../../lib-internal/routes/StorageRoutes.js";
|
|
3
|
+
import { StorageRoutes, type StorageRouteKey } from "../../lib-internal/routes/StorageRoutes.js";
|
|
4
4
|
import type { TaruviConfig, StorageFilters } from "../../types.js";
|
|
5
5
|
import { HttpMethod } from "../../lib-internal/http/types.js";
|
|
6
6
|
import { buildQueryString } from "../../utils/utils.js";
|
|
@@ -55,17 +55,32 @@ export class Storage {
|
|
|
55
55
|
}, HttpMethod.POST, formData)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
58
|
private buildRoute(): string {
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
return (
|
|
60
|
+
StorageRoutes.baseUrl(this.config.appSlug, this.urlParams.bucket!) +
|
|
61
|
+
(Object.keys(this.urlParams) as StorageRouteKey[]).reduce((acc, key) => {
|
|
62
|
+
const value = this.urlParams[key as keyof BucketUrlParams]
|
|
63
|
+
|
|
64
|
+
if (!value) return acc
|
|
65
|
+
|
|
66
|
+
if (key === 'path' && typeof value === 'string') {
|
|
67
|
+
acc += StorageRoutes.path(value)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ((key === 'upload' || key === 'delete') && typeof StorageRoutes[key] === 'function') {
|
|
71
|
+
acc += (StorageRoutes[key] as () => string)()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return acc
|
|
75
|
+
}, '') +
|
|
76
|
+
'/' +
|
|
77
|
+
buildQueryString(this.filters as Record<string, unknown>)
|
|
78
|
+
)
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async execute(): Promise<any> {
|
|
69
84
|
const url = this.buildRoute()
|
|
70
85
|
const operation = this.operation || HttpMethod.GET
|
|
71
86
|
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { Client } from "../../client.js";
|
|
2
|
-
import { User } from "../user/UserClient.js";
|
|
3
|
-
import { Settings } from "../Settings/SettingsClient.js";
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Auth Client - Handles user authentication using Web UI Flow
|
|
5
|
+
* Implements cross-domain authentication with redirect-based token delivery
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. User calls login() → Redirects to backend /accounts/login/
|
|
9
|
+
* 2. User authenticates on backend
|
|
10
|
+
* 3. Backend redirects back with tokens in URL hash
|
|
11
|
+
* 4. Client extracts and stores tokens automatically
|
|
12
|
+
*/
|
|
6
13
|
export class Auth {
|
|
7
14
|
private client: Client
|
|
8
15
|
|
|
@@ -10,55 +17,201 @@ export class Auth {
|
|
|
10
17
|
this.client = client
|
|
11
18
|
}
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Redirect to login page (Web UI Flow)
|
|
22
|
+
* @param callbackUrl - URL to redirect to after successful login (defaults to current page)
|
|
23
|
+
*/
|
|
24
|
+
login(callbackUrl?: string): void {
|
|
25
|
+
if (typeof window === "undefined") {
|
|
26
|
+
console.error("login() can only be called in browser environment")
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const config = this.client.getConfig()
|
|
31
|
+
const callback = callbackUrl || window.location.origin + window.location.pathname
|
|
32
|
+
|
|
33
|
+
// Redirect to /accounts/login/ with redirect_to parameter
|
|
34
|
+
const loginUrl = `${config.baseUrl}/accounts/login/?redirect_to=${encodeURIComponent(callback)}`
|
|
35
|
+
|
|
36
|
+
// Optional: Store state before redirecting
|
|
37
|
+
if (typeof sessionStorage !== "undefined") {
|
|
38
|
+
sessionStorage.setItem("auth_state", JSON.stringify({
|
|
39
|
+
returnTo: window.location.pathname,
|
|
40
|
+
timestamp: Date.now()
|
|
41
|
+
}))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
window.location.href = loginUrl
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Redirect to signup page (Web UI Flow)
|
|
49
|
+
* @param callbackUrl - URL to redirect to after successful signup (defaults to current page)
|
|
50
|
+
*/
|
|
51
|
+
signup(callbackUrl?: string): void {
|
|
52
|
+
if (typeof window === "undefined") {
|
|
53
|
+
console.error("signup() can only be called in browser environment")
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const config = this.client.getConfig()
|
|
58
|
+
const callback = callbackUrl || window.location.origin + window.location.pathname
|
|
59
|
+
|
|
60
|
+
// Redirect to /accounts/signup/ with redirect_to parameter
|
|
61
|
+
const signupUrl = `${config.baseUrl}/accounts/signup/?redirect_to=${encodeURIComponent(callback)}`
|
|
62
|
+
|
|
63
|
+
// Optional: Store state before redirecting
|
|
64
|
+
if (typeof sessionStorage !== "undefined") {
|
|
65
|
+
sessionStorage.setItem("auth_state", JSON.stringify({
|
|
66
|
+
returnTo: window.location.pathname,
|
|
67
|
+
timestamp: Date.now()
|
|
68
|
+
}))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
window.location.href = signupUrl
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Logout user and redirect to logout page
|
|
76
|
+
* @param callbackUrl - URL to redirect to after logout (defaults to home page)
|
|
77
|
+
*/
|
|
78
|
+
logout(callbackUrl?: string): void {
|
|
79
|
+
if (typeof window === "undefined") {
|
|
80
|
+
console.error("logout() can only be called in browser environment")
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clear tokens immediately
|
|
85
|
+
this.client.tokenClient.clearTokens()
|
|
86
|
+
|
|
87
|
+
const config = this.client.getConfig()
|
|
88
|
+
const callback = callbackUrl || window.location.origin
|
|
89
|
+
|
|
90
|
+
// Redirect to /accounts/logout/
|
|
91
|
+
const logoutUrl = `${config.baseUrl}/accounts/logout/?redirect_to=${encodeURIComponent(callback)}`
|
|
92
|
+
|
|
93
|
+
window.location.href = logoutUrl
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if user is authenticated
|
|
98
|
+
*/
|
|
99
|
+
isUserAuthenticated(): boolean {
|
|
100
|
+
return this.client.tokenClient.isAuthenticated()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the current access token
|
|
105
|
+
*/
|
|
106
|
+
getAccessToken(): string | null {
|
|
107
|
+
return this.client.tokenClient.getToken()
|
|
40
108
|
}
|
|
41
109
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Get the current refresh token
|
|
112
|
+
*/
|
|
113
|
+
getRefreshToken(): string | null {
|
|
114
|
+
return this.client.tokenClient.getRefreshToken()
|
|
45
115
|
}
|
|
46
116
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!frontEndUrl) frontEndUrl = this.client.getConfig().deskUrl
|
|
54
|
-
window.location.href = frontEndUrl + `?redirect=${currentUrl}`
|
|
117
|
+
/**
|
|
118
|
+
* Check if the access token is expired
|
|
119
|
+
*/
|
|
120
|
+
isTokenExpired(): boolean {
|
|
121
|
+
return this.client.tokenClient.isTokenExpired()
|
|
55
122
|
}
|
|
56
123
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Refresh the access token using the refresh token
|
|
126
|
+
* ⚠️ IMPORTANT: Taruvi uses refresh token rotation
|
|
127
|
+
* You will receive BOTH a new access token AND a new refresh token
|
|
128
|
+
*
|
|
129
|
+
* @returns Promise with new tokens or null if refresh failed
|
|
130
|
+
*/
|
|
131
|
+
async refreshAccessToken(): Promise<{ access: string; refresh: string; expires_in: number } | null> {
|
|
132
|
+
const refreshToken = this.client.tokenClient.getRefreshToken()
|
|
133
|
+
|
|
134
|
+
if (!refreshToken) {
|
|
135
|
+
console.error("No refresh token available")
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const config = this.client.getConfig()
|
|
141
|
+
const response = await fetch(`${config.baseUrl}/api/cloud/auth/jwt/token/refresh/`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
body: JSON.stringify({ refresh: refreshToken })
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new Error(`Token refresh failed: ${response.statusText}`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const data = await response.json()
|
|
152
|
+
|
|
153
|
+
// Update tokens in storage (both access and refresh due to rotation)
|
|
154
|
+
this.client.tokenClient.updateAccessToken(data.access, data.expires_in || 172800)
|
|
155
|
+
|
|
156
|
+
if (data.refresh) {
|
|
157
|
+
this.client.tokenClient.updateRefreshToken(data.refresh)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
access: data.access,
|
|
162
|
+
refresh: data.refresh || refreshToken,
|
|
163
|
+
expires_in: data.expires_in || 172800
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error("Failed to refresh access token:", error)
|
|
167
|
+
|
|
168
|
+
// Clear tokens and redirect to login
|
|
169
|
+
this.client.tokenClient.clearTokens()
|
|
170
|
+
|
|
171
|
+
if (typeof window !== "undefined") {
|
|
172
|
+
this.login()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get current user info from access token (JWT decode)
|
|
181
|
+
* Note: This only decodes the token, doesn't validate signature
|
|
182
|
+
*/
|
|
183
|
+
getCurrentUser(): any | null {
|
|
184
|
+
const accessToken = this.getAccessToken()
|
|
185
|
+
|
|
186
|
+
if (!accessToken) {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// Decode JWT (middle part is payload)
|
|
192
|
+
const parts = accessToken.split(".")
|
|
193
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
194
|
+
throw new Error("Invalid JWT format")
|
|
195
|
+
}
|
|
196
|
+
const payload = JSON.parse(atob(parts[1]))
|
|
197
|
+
return payload
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error("Failed to decode access token:", error)
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Legacy method: Redirect to login using desk URL
|
|
206
|
+
* @deprecated Use login() instead
|
|
207
|
+
*/
|
|
208
|
+
async redirectToLogin(): Promise<void> {
|
|
209
|
+
const config = this.client.getConfig()
|
|
210
|
+
const currentUrl = typeof window !== "undefined" ? window.location.href : ""
|
|
211
|
+
|
|
212
|
+
const deskUrl = config.deskUrl || config.baseUrl
|
|
213
|
+
if (typeof window !== "undefined") {
|
|
214
|
+
window.location.href = `${deskUrl}?redirect=${encodeURIComponent(currentUrl)}`
|
|
215
|
+
}
|
|
216
|
+
}
|
|
64
217
|
}
|
|
@@ -2,4 +2,8 @@ export const DatabaseRoutes = {
|
|
|
2
2
|
baseUrl: (appSlug: string) => `api/apps/${appSlug}`,
|
|
3
3
|
dataTables: (tableName: string): string => `/datatables/${tableName}/data`,
|
|
4
4
|
recordId: (recordId: string): string => `/${recordId}`
|
|
5
|
-
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type AllRouteKeys = keyof typeof DatabaseRoutes
|
|
8
|
+
export type DatabaseRouteKey = Exclude<AllRouteKeys, 'baseUrl'>
|
|
9
|
+
export type DatabaseUrlParams = Partial<Record<DatabaseRouteKey, string>>
|
|
@@ -4,4 +4,12 @@ export const StorageRoutes = {
|
|
|
4
4
|
upload: () => "/batch-upload",
|
|
5
5
|
delete: () => "/batch-delete"
|
|
6
6
|
// bucket: (appslug: string, bucketslug: string) => `${StorageRoutesClone.baseUrl(appslug)}/${bucketslug}`
|
|
7
|
-
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type StoragePathKey = 'path'
|
|
10
|
+
export type StorageFlagKey = 'upload' | 'delete'
|
|
11
|
+
export type StorageRouteKey = StoragePathKey | StorageFlagKey
|
|
12
|
+
|
|
13
|
+
export type BucketUrlParams = Partial<
|
|
14
|
+
Record<StorageRouteKey, string | true>
|
|
15
|
+
>
|