@redseat/api 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.
@@ -0,0 +1,32 @@
1
+ import { Method, AxiosRequestConfig } from 'axios';
2
+ import { IToken } from './auth';
3
+ import { IServer } from './interfaces';
4
+ export interface ClientOptions {
5
+ server: IServer;
6
+ getIdToken: () => Promise<string>;
7
+ refreshThreshold?: number;
8
+ }
9
+ export declare class RedseatClient {
10
+ private readonly axios;
11
+ private readonly server;
12
+ private readonly getIdToken;
13
+ private readonly refreshThreshold;
14
+ private baseUrl;
15
+ private localServerUrl?;
16
+ private tokenData?;
17
+ private tokenRefreshPromise?;
18
+ constructor(options: ClientOptions);
19
+ private getRegularServerUrl;
20
+ private detectLocalUrl;
21
+ private get serverId();
22
+ private isTokenExpiredOrExpiringSoon;
23
+ private refreshToken;
24
+ private ensureValidToken;
25
+ setToken(token: string | IToken): void;
26
+ get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<import("axios").AxiosResponse<T, any>>;
27
+ post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<import("axios").AxiosResponse<T, any>>;
28
+ put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<import("axios").AxiosResponse<T, any>>;
29
+ patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<import("axios").AxiosResponse<T, any>>;
30
+ delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<import("axios").AxiosResponse<T, any>>;
31
+ request<T = unknown>(method: Method, url: string, data?: unknown, config?: AxiosRequestConfig): Promise<import("axios").AxiosResponse<T, any>>;
32
+ }
package/dist/client.js ADDED
@@ -0,0 +1,157 @@
1
+ import axios from 'axios';
2
+ import { fetchServerToken } from './auth';
3
+ export class RedseatClient {
4
+ constructor(options) {
5
+ this.server = options.server;
6
+ this.getIdToken = options.getIdToken;
7
+ this.refreshThreshold = options.refreshThreshold ?? 5 * 60 * 1000; // 5 minutes default
8
+ // Initialize with regular URL, will be updated after local detection
9
+ this.baseUrl = this.getRegularServerUrl();
10
+ this.axios = axios.create({
11
+ baseURL: this.baseUrl,
12
+ });
13
+ // Detect local URL asynchronously and update axios instance
14
+ this.detectLocalUrl().then((url) => {
15
+ if (url !== this.baseUrl) {
16
+ this.baseUrl = url;
17
+ this.axios.defaults.baseURL = url;
18
+ }
19
+ }).catch(() => {
20
+ // If detection fails, use regular URL (already set)
21
+ });
22
+ // Request interceptor: check token expiration and refresh if needed
23
+ this.axios.interceptors.request.use(async (config) => {
24
+ await this.ensureValidToken();
25
+ if (this.tokenData) {
26
+ config.headers.Authorization = `Bearer ${this.tokenData.token}`;
27
+ }
28
+ return config;
29
+ }, (error) => Promise.reject(error));
30
+ // Response interceptor: handle 401 errors and retry with refreshed token
31
+ this.axios.interceptors.response.use((response) => response, async (error) => {
32
+ const originalRequest = error.config;
33
+ if (error.response?.status === 401 && !originalRequest._retry) {
34
+ originalRequest._retry = true;
35
+ try {
36
+ await this.refreshToken();
37
+ if (this.tokenData && originalRequest.headers) {
38
+ originalRequest.headers.Authorization = `Bearer ${this.tokenData.token}`;
39
+ }
40
+ return this.axios(originalRequest);
41
+ }
42
+ catch (refreshError) {
43
+ return Promise.reject(refreshError);
44
+ }
45
+ }
46
+ return Promise.reject(error);
47
+ });
48
+ }
49
+ getRegularServerUrl() {
50
+ let base = this.server.url;
51
+ if (this.server.port) {
52
+ base = `${base}:${this.server.port}`;
53
+ }
54
+ return `https://${base}`;
55
+ }
56
+ async detectLocalUrl() {
57
+ let localBase = `local.${this.server.url}`;
58
+ if (this.server.port) {
59
+ localBase = `${localBase}:${this.server.port}`;
60
+ }
61
+ const localUrl = `https://${localBase}`;
62
+ try {
63
+ console.log('trying local server url', localUrl);
64
+ const response = await axios.get(`${localUrl}/ping`, {
65
+ timeout: 200,
66
+ headers: { "Referrer-Policy": 'origin-when-cross-origin' }
67
+ });
68
+ if (response.status === 200) {
69
+ console.log('local server detected');
70
+ this.localServerUrl = localUrl;
71
+ return localUrl;
72
+ }
73
+ else {
74
+ console.warn('could not use server locally, status: ', response.status);
75
+ return this.getRegularServerUrl();
76
+ }
77
+ }
78
+ catch (e) {
79
+ console.log('could not detect server localhost', e);
80
+ return this.getRegularServerUrl();
81
+ }
82
+ }
83
+ get serverId() {
84
+ return this.server.id;
85
+ }
86
+ isTokenExpiredOrExpiringSoon() {
87
+ if (!this.tokenData) {
88
+ return true;
89
+ }
90
+ const now = Date.now();
91
+ const expirationTime = this.tokenData.expires - this.refreshThreshold;
92
+ return now >= expirationTime;
93
+ }
94
+ async refreshToken() {
95
+ // If a refresh is already in progress, return the existing promise
96
+ if (this.tokenRefreshPromise) {
97
+ return this.tokenRefreshPromise;
98
+ }
99
+ this.tokenRefreshPromise = (async () => {
100
+ try {
101
+ const idToken = await this.getIdToken();
102
+ // Use fetchServerToken which uses the global axios instance
103
+ // The token endpoint is on the frontend server, not the backend server
104
+ const newToken = await fetchServerToken(this.serverId, idToken);
105
+ this.tokenData = newToken;
106
+ return newToken;
107
+ }
108
+ finally {
109
+ // Clear the promise so future calls can trigger a new refresh
110
+ this.tokenRefreshPromise = undefined;
111
+ }
112
+ })();
113
+ return this.tokenRefreshPromise;
114
+ }
115
+ async ensureValidToken() {
116
+ if (this.isTokenExpiredOrExpiringSoon()) {
117
+ await this.refreshToken();
118
+ }
119
+ }
120
+ setToken(token) {
121
+ if (typeof token === 'string') {
122
+ // When setting token externally as string, we don't know the expiration
123
+ // So we'll treat it as needing refresh soon
124
+ this.tokenData = {
125
+ token,
126
+ expires: Date.now() + this.refreshThreshold
127
+ };
128
+ }
129
+ else {
130
+ // When setting full IToken object, use the provided expiration
131
+ this.tokenData = token;
132
+ }
133
+ }
134
+ async get(url, config) {
135
+ return this.axios.get(url, config);
136
+ }
137
+ async post(url, data, config) {
138
+ return this.axios.post(url, data, config);
139
+ }
140
+ async put(url, data, config) {
141
+ return this.axios.put(url, data, config);
142
+ }
143
+ async patch(url, data, config) {
144
+ return this.axios.patch(url, data, config);
145
+ }
146
+ async delete(url, config) {
147
+ return this.axios.delete(url, config);
148
+ }
149
+ async request(method, url, data, config) {
150
+ return this.axios.request({
151
+ method,
152
+ url,
153
+ data,
154
+ ...config
155
+ });
156
+ }
157
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Cross-platform crypto utilities
3
+ * Works on Node.js, Web browsers, and React Native (Expo)
4
+ */
5
+ /**
6
+ * Get the crypto.subtle API available in the current environment
7
+ */
8
+ export declare function getCryptoSubtle(): SubtleCrypto;
9
+ /**
10
+ * Get the crypto.getRandomValues API
11
+ */
12
+ export declare function getCryptoRandomValues(): (array: Uint8Array) => Uint8Array;
13
+ /**
14
+ * Convert base64 string to Uint8Array (cross-platform)
15
+ */
16
+ export declare function uint8ArrayFromBase64(base64: string): Uint8Array;
17
+ /**
18
+ * Convert base64url string to Uint8Array (cross-platform)
19
+ */
20
+ export declare function uint8ArrayFromBase64Url(base64Url: string): Uint8Array;
21
+ /**
22
+ * Convert ArrayBuffer to base64 string (cross-platform)
23
+ */
24
+ export declare function arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string;
25
+ /**
26
+ * Convert base64 to base64url
27
+ */
28
+ export declare function base64ToBase64Url(base64: string): string;
29
+ /**
30
+ * Convert base64url to base64 with proper padding
31
+ */
32
+ export declare function base64UrlToBase64(base64Url: string): string;
33
+ /**
34
+ * Pad string to the right
35
+ */
36
+ export declare function padRight(text: string, n: number, pad: string): string;
package/dist/crypto.js ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Cross-platform crypto utilities
3
+ * Works on Node.js, Web browsers, and React Native (Expo)
4
+ */
5
+ // Initialize atob/btoa fallbacks
6
+ let atob;
7
+ let btoa;
8
+ if (typeof globalThis !== 'undefined' && globalThis.atob && globalThis.btoa) {
9
+ atob = globalThis.atob;
10
+ btoa = globalThis.btoa;
11
+ }
12
+ else if (typeof window !== 'undefined' && window.atob && window.btoa) {
13
+ atob = window.atob;
14
+ btoa = window.btoa;
15
+ }
16
+ else {
17
+ // Fallback implementation (shouldn't be needed in modern environments)
18
+ atob = (str) => {
19
+ if (typeof Buffer !== 'undefined') {
20
+ return Buffer.from(str, 'base64').toString('binary');
21
+ }
22
+ throw new Error('atob is not available and no fallback could be found');
23
+ };
24
+ btoa = (str) => {
25
+ if (typeof Buffer !== 'undefined') {
26
+ return Buffer.from(str, 'binary').toString('base64');
27
+ }
28
+ throw new Error('btoa is not available and no fallback could be found');
29
+ };
30
+ }
31
+ /**
32
+ * Get the crypto.subtle API available in the current environment
33
+ */
34
+ export function getCryptoSubtle() {
35
+ // Try different ways to access crypto.subtle
36
+ if (typeof globalThis !== 'undefined' && globalThis.crypto?.subtle) {
37
+ return globalThis.crypto.subtle;
38
+ }
39
+ if (typeof window !== 'undefined' && window.crypto?.subtle) {
40
+ return window.crypto.subtle;
41
+ }
42
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
43
+ return crypto.subtle;
44
+ }
45
+ // Node.js fallback
46
+ if (typeof require !== 'undefined') {
47
+ try {
48
+ const nodeCrypto = require('crypto');
49
+ if (nodeCrypto.webcrypto?.subtle) {
50
+ return nodeCrypto.webcrypto.subtle;
51
+ }
52
+ }
53
+ catch (e) {
54
+ // require not available
55
+ }
56
+ }
57
+ throw new Error('crypto.subtle is not available in this environment');
58
+ }
59
+ /**
60
+ * Get the crypto.getRandomValues API
61
+ */
62
+ export function getCryptoRandomValues() {
63
+ if (typeof globalThis !== 'undefined' && globalThis.crypto?.getRandomValues) {
64
+ return globalThis.crypto.getRandomValues.bind(globalThis.crypto);
65
+ }
66
+ if (typeof window !== 'undefined' && window.crypto?.getRandomValues) {
67
+ return window.crypto.getRandomValues.bind(window.crypto);
68
+ }
69
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
70
+ return crypto.getRandomValues.bind(crypto);
71
+ }
72
+ // Node.js fallback
73
+ if (typeof require !== 'undefined') {
74
+ try {
75
+ const nodeCrypto = require('crypto');
76
+ if (nodeCrypto.webcrypto?.getRandomValues) {
77
+ return nodeCrypto.webcrypto.getRandomValues.bind(nodeCrypto.webcrypto);
78
+ }
79
+ if (nodeCrypto.randomFillSync) {
80
+ // Fallback for older Node.js
81
+ return (array) => {
82
+ nodeCrypto.randomFillSync(array);
83
+ return array;
84
+ };
85
+ }
86
+ }
87
+ catch (e) {
88
+ // require not available
89
+ }
90
+ }
91
+ throw new Error('crypto.getRandomValues is not available in this environment');
92
+ }
93
+ /**
94
+ * Convert base64 string to Uint8Array (cross-platform)
95
+ */
96
+ export function uint8ArrayFromBase64(base64) {
97
+ // Normalize base64 (handle base64url)
98
+ const normalized = base64.replace(/-/g, '+').replace(/_/g, '/');
99
+ // Node.js path
100
+ if (typeof Buffer !== 'undefined') {
101
+ return new Uint8Array(Buffer.from(normalized, 'base64'));
102
+ }
103
+ // Browser/RN path - manual conversion
104
+ const binary = atob(normalized);
105
+ const bytes = new Uint8Array(binary.length);
106
+ for (let i = 0; i < binary.length; i++) {
107
+ bytes[i] = binary.charCodeAt(i);
108
+ }
109
+ return bytes;
110
+ }
111
+ /**
112
+ * Convert base64url string to Uint8Array (cross-platform)
113
+ */
114
+ export function uint8ArrayFromBase64Url(base64Url) {
115
+ // Convert base64url to base64
116
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
117
+ // Add padding if needed
118
+ const padding = (4 - (base64.length % 4)) % 4;
119
+ const padded = base64 + '='.repeat(padding);
120
+ return uint8ArrayFromBase64(padded);
121
+ }
122
+ /**
123
+ * Convert ArrayBuffer to base64 string (cross-platform)
124
+ */
125
+ export function arrayBufferToBase64(buffer) {
126
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
127
+ // Node.js path
128
+ if (typeof Buffer !== 'undefined') {
129
+ return Buffer.from(bytes).toString('base64');
130
+ }
131
+ // Browser/RN path - manual conversion
132
+ let binary = '';
133
+ for (let i = 0; i < bytes.byteLength; i++) {
134
+ binary += String.fromCharCode(bytes[i]);
135
+ }
136
+ return btoa(binary);
137
+ }
138
+ /**
139
+ * Convert base64 to base64url
140
+ */
141
+ export function base64ToBase64Url(base64) {
142
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
143
+ }
144
+ /**
145
+ * Convert base64url to base64 with proper padding
146
+ */
147
+ export function base64UrlToBase64(base64Url) {
148
+ let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
149
+ // Add padding
150
+ const padding = (4 - (base64.length % 4)) % 4;
151
+ return base64 + '='.repeat(padding);
152
+ }
153
+ /**
154
+ * Pad string to the right
155
+ */
156
+ export function padRight(text, n, pad) {
157
+ let t = text;
158
+ if (n > text.length) {
159
+ for (let i = 0; i < n - text.length; i++) {
160
+ t += pad;
161
+ }
162
+ }
163
+ return t;
164
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Cross-platform encryption/decryption functions
3
+ * Supports the same formats as src/lib/crypt.ts
4
+ */
5
+ /**
6
+ * Derive a CryptoKey from a passphrase using PBKDF2
7
+ * @param passPhrase - The passphrase to derive the key from
8
+ * @param type - 'text' for text encryption, 'file' for file encryption
9
+ * @returns A CryptoKey suitable for AES-CBC encryption/decryption
10
+ */
11
+ export declare function deriveKey(passPhrase: string, type: 'text' | 'file'): Promise<CryptoKey>;
12
+ /**
13
+ * Generate a random 16-byte IV (Initialization Vector)
14
+ */
15
+ export declare function getRandomIV(): Uint8Array;
16
+ /**
17
+ * Encrypt a text string
18
+ * Returns format: `${base64ToBase64Url(IV)}.${base64ToBase64Url(encryptedData)}`
19
+ */
20
+ export declare function encryptText(key: CryptoKey, text: string): Promise<string>;
21
+ /**
22
+ * Decrypt a text string
23
+ * Expects format: `${base64ToBase64Url(IV)}.${base64ToBase64Url(encryptedData)}`
24
+ * Or can accept IV separately
25
+ */
26
+ export declare function decryptText(key: CryptoKey, encryptedText: string, iv?: ArrayBuffer | Uint8Array): Promise<string>;
27
+ /**
28
+ * Encrypt a buffer (ArrayBuffer or Uint8Array)
29
+ */
30
+ export declare function encryptBuffer(key: CryptoKey, iv: ArrayBuffer | Uint8Array, buffer: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>;
31
+ /**
32
+ * Decrypt a buffer (ArrayBuffer or Uint8Array)
33
+ */
34
+ export declare function decryptBuffer(key: CryptoKey, iv: ArrayBuffer | Uint8Array, buffer: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>;
35
+ /**
36
+ * Options for encrypting a file
37
+ */
38
+ export interface EncryptFileOptions {
39
+ filename: string;
40
+ buffer: ArrayBuffer | Uint8Array;
41
+ thumbMime: string;
42
+ thumb: ArrayBuffer | Uint8Array;
43
+ mime: string;
44
+ }
45
+ /**
46
+ * Result of encrypting a file
47
+ */
48
+ export interface EncryptedFile {
49
+ filename: string;
50
+ ivb64: string;
51
+ thumbsize: number;
52
+ blob: ArrayBuffer;
53
+ }
54
+ /**
55
+ * Encrypt a file with thumbnail and metadata
56
+ * Structure: [16 bytes IV][4 bytes thumb size][4 bytes info size][32 bytes thumb mime][256 bytes file mime][T bytes encrypted thumb][I bytes encrypted info][encrypted file data]
57
+ */
58
+ export declare function encryptFile(key: CryptoKey, options: EncryptFileOptions): Promise<EncryptedFile>;
59
+ /**
60
+ * Decrypt a file buffer
61
+ * Extracts and decrypts the file data from the encrypted file structure
62
+ */
63
+ export declare function decryptFile(key: CryptoKey, buffer: ArrayBuffer): Promise<ArrayBuffer>;
64
+ /**
65
+ * Decrypt a file thumbnail
66
+ * Extracts and decrypts the thumbnail from the encrypted file structure
67
+ */
68
+ export declare function decryptFileThumb(key: CryptoKey, buffer: ArrayBuffer): Promise<ArrayBuffer>;
69
+ /**
70
+ * Encrypt a filename
71
+ * Returns base64url encoded encrypted filename
72
+ */
73
+ export declare function encryptFilename(key: CryptoKey, filename: string, iv: ArrayBuffer | Uint8Array): Promise<string>;