@hyperyai/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +391 -0
  3. package/dist/components/AuthButton.d.ts +51 -0
  4. package/dist/components/AuthButton.js +60 -0
  5. package/dist/components/AuthModal.d.ts +20 -0
  6. package/dist/components/AuthModal.js +62 -0
  7. package/dist/components/BuyButton.d.ts +47 -0
  8. package/dist/components/BuyButton.js +74 -0
  9. package/dist/components/ErrorBoundary.d.ts +13 -0
  10. package/dist/components/ErrorBoundary.js +24 -0
  11. package/dist/components/HyperyModals.d.ts +30 -0
  12. package/dist/components/HyperyModals.js +30 -0
  13. package/dist/components/InsufficientCreditsAlert.d.ts +11 -0
  14. package/dist/components/InsufficientCreditsAlert.js +12 -0
  15. package/dist/components/ModernAuthForm.d.ts +52 -0
  16. package/dist/components/ModernAuthForm.js +83 -0
  17. package/dist/components/RestrictionModal.d.ts +43 -0
  18. package/dist/components/RestrictionModal.js +167 -0
  19. package/dist/components/SignIn.d.ts +26 -0
  20. package/dist/components/SignIn.js +47 -0
  21. package/dist/components/SignInForm.d.ts +41 -0
  22. package/dist/components/SignInForm.js +86 -0
  23. package/dist/components/SignUp.d.ts +26 -0
  24. package/dist/components/SignUp.js +52 -0
  25. package/dist/components/SpendingLimitAlert.d.ts +12 -0
  26. package/dist/components/SpendingLimitAlert.js +14 -0
  27. package/dist/components/UserButton.d.ts +26 -0
  28. package/dist/components/UserButton.js +35 -0
  29. package/dist/components/UserProfile.d.ts +22 -0
  30. package/dist/components/UserProfile.js +26 -0
  31. package/dist/components/WorkspaceSwitcher.d.ts +29 -0
  32. package/dist/components/WorkspaceSwitcher.js +66 -0
  33. package/dist/components/control.d.ts +64 -0
  34. package/dist/components/control.js +89 -0
  35. package/dist/components/ui/dialog.d.ts +19 -0
  36. package/dist/components/ui/dialog.js +53 -0
  37. package/dist/hooks/index.d.ts +22 -0
  38. package/dist/hooks/index.js +23 -0
  39. package/dist/hooks/useBuyerWallet.d.ts +37 -0
  40. package/dist/hooks/useBuyerWallet.js +66 -0
  41. package/dist/hooks/useCheckout.d.ts +27 -0
  42. package/dist/hooks/useCheckout.js +246 -0
  43. package/dist/hooks/useError.d.ts +19 -0
  44. package/dist/hooks/useError.js +36 -0
  45. package/dist/hooks/useMarketplace.d.ts +50 -0
  46. package/dist/hooks/useMarketplace.js +69 -0
  47. package/dist/hooks/useMemberships.d.ts +71 -0
  48. package/dist/hooks/useMemberships.js +138 -0
  49. package/dist/hooks/useWallet.d.ts +62 -0
  50. package/dist/hooks/useWallet.js +123 -0
  51. package/dist/index.d.ts +55 -0
  52. package/dist/index.js +52 -0
  53. package/dist/lib/context.d.ts +50 -0
  54. package/dist/lib/context.js +409 -0
  55. package/dist/lib/oauth.d.ts +49 -0
  56. package/dist/lib/oauth.js +149 -0
  57. package/dist/lib/parse-error.d.ts +45 -0
  58. package/dist/lib/parse-error.js +185 -0
  59. package/dist/lib/parse-stream.d.ts +43 -0
  60. package/dist/lib/parse-stream.js +89 -0
  61. package/dist/lib/popup.d.ts +42 -0
  62. package/dist/lib/popup.js +80 -0
  63. package/dist/lib/storage.d.ts +43 -0
  64. package/dist/lib/storage.js +125 -0
  65. package/dist/lib/utils.d.ts +8 -0
  66. package/dist/lib/utils.js +11 -0
  67. package/dist/types/index.d.ts +191 -0
  68. package/dist/types/index.js +4 -0
  69. package/package.json +78 -0
  70. package/src/components/AuthButton.tsx +123 -0
  71. package/src/components/AuthModal.tsx +240 -0
  72. package/src/components/BuyButton.tsx +150 -0
  73. package/src/components/ErrorBoundary.tsx +97 -0
  74. package/src/components/HyperyModals.tsx +83 -0
  75. package/src/components/InsufficientCreditsAlert.tsx +73 -0
  76. package/src/components/ModernAuthForm.tsx +322 -0
  77. package/src/components/RestrictionModal.tsx +373 -0
  78. package/src/components/SignIn.tsx +84 -0
  79. package/src/components/SignInForm.tsx +256 -0
  80. package/src/components/SignUp.tsx +86 -0
  81. package/src/components/SpendingLimitAlert.tsx +91 -0
  82. package/src/components/UserButton.tsx +106 -0
  83. package/src/components/UserProfile.tsx +106 -0
  84. package/src/components/WorkspaceSwitcher.tsx +199 -0
  85. package/src/components/control.tsx +133 -0
  86. package/src/components/ui/dialog.tsx +151 -0
  87. package/src/hooks/index.ts +65 -0
  88. package/src/hooks/useBuyerWallet.ts +117 -0
  89. package/src/hooks/useCheckout.ts +312 -0
  90. package/src/hooks/useError.ts +60 -0
  91. package/src/hooks/useMarketplace.ts +129 -0
  92. package/src/hooks/useMemberships.ts +203 -0
  93. package/src/hooks/useWallet.ts +170 -0
  94. package/src/index.ts +147 -0
  95. package/src/lib/context.tsx +478 -0
  96. package/src/lib/oauth.ts +215 -0
  97. package/src/lib/parse-error.ts +224 -0
  98. package/src/lib/parse-stream.ts +121 -0
  99. package/src/lib/popup.ts +98 -0
  100. package/src/lib/storage.ts +140 -0
  101. package/src/lib/utils.ts +14 -0
  102. package/src/types/index.ts +216 -0
@@ -0,0 +1,224 @@
1
+ import { ErrorData, ParsedError } from '../types';
2
+
3
+ const SPENDING_LIMIT = 'SPENDING_LIMIT_EXCEEDED';
4
+ const INSUFFICIENT_CREDITS = 'INSUFFICIENT_CREDITS';
5
+ const PAYMENT_METHOD_REQUIRED = 'PAYMENT_METHOD_REQUIRED';
6
+ const PAYMENT_DECLINED = 'PAYMENT_DECLINED';
7
+ const UNAUTHENTICATED = 'UNAUTHENTICATED';
8
+ const PERMISSION_DENIED = 'PERMISSION_DENIED';
9
+ const INSUFFICIENT_SCOPE = 'INSUFFICIENT_SCOPE';
10
+ const RATE_LIMITED = 'RATE_LIMITED';
11
+
12
+ /**
13
+ * Extract the inner error object from any of the shapes the gateway / a caller
14
+ * may hand us:
15
+ * - the raw server envelope: `{ error: { code, message, type, ... } }`
16
+ * - an already-unwrapped error object: `{ code, message, type, ... }`
17
+ * Returns undefined if neither shape carries a `code`.
18
+ */
19
+ function unwrap(error: any): ErrorData | undefined {
20
+ if (error && typeof error === 'object') {
21
+ if (error.error && typeof error.error === 'object' && 'code' in error.error) {
22
+ return error.error as ErrorData;
23
+ }
24
+ if (typeof error.code === 'string') {
25
+ return error as ErrorData;
26
+ }
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ /** Pull an HTTP status off a Response-like / thrown-error object, if present. */
32
+ function statusOf(error: any): number | undefined {
33
+ const s = error?.status ?? error?.statusCode ?? error?.response?.status;
34
+ return typeof s === 'number' ? s : undefined;
35
+ }
36
+
37
+ /**
38
+ * Parse an error (a parsed JSON body, a thrown error, or a Response-like object)
39
+ * from the Hypery API into a normalized, classified shape. Tolerant of both the
40
+ * `{ error: { code } }` envelope and an already-unwrapped object, and falls back
41
+ * to the HTTP status when no recognized `code` is present.
42
+ */
43
+ export function parseError(error: any): ParsedError {
44
+ const errorData = unwrap(error);
45
+ const status = statusOf(error);
46
+
47
+ if (errorData) {
48
+ const code = errorData.code;
49
+ return {
50
+ code,
51
+ message: errorData.message,
52
+ type: errorData.type,
53
+ status,
54
+ isSpendingLimit: code === SPENDING_LIMIT,
55
+ isInsufficientCredits: code === INSUFFICIENT_CREDITS,
56
+ isPaymentMethodRequired: code === PAYMENT_METHOD_REQUIRED,
57
+ isPaymentDeclined: code === PAYMENT_DECLINED,
58
+ isAuth: code === UNAUTHENTICATED || errorData.type === 'authentication_error',
59
+ isPermissionDenied:
60
+ code === PERMISSION_DENIED ||
61
+ code === INSUFFICIENT_SCOPE ||
62
+ errorData.type === 'permission_error' ||
63
+ errorData.type === 'authorization_error',
64
+ isRateLimit: code === RATE_LIMITED || errorData.type === 'rate_limit_error',
65
+ data: errorData,
66
+ };
67
+ }
68
+
69
+ // No recognized code — classify from HTTP status so a payment/auth modal can
70
+ // still open for an endpoint that returns a non-canonical body.
71
+ if (status === 402) {
72
+ return statusFallback(INSUFFICIENT_CREDITS, 'Insufficient credits.', status, {
73
+ isInsufficientCredits: true,
74
+ });
75
+ }
76
+ if (status === 401) {
77
+ return statusFallback(UNAUTHENTICATED, 'Authentication required.', status, { isAuth: true });
78
+ }
79
+ if (status === 403) {
80
+ return statusFallback(PERMISSION_DENIED, 'You do not have permission to perform this action.', status, {
81
+ isPermissionDenied: true,
82
+ });
83
+ }
84
+ if (status === 429) {
85
+ return statusFallback(RATE_LIMITED, 'Rate limit exceeded. Please retry shortly.', status, {
86
+ isRateLimit: true,
87
+ });
88
+ }
89
+
90
+ // Handle generic errors
91
+ const message = error?.message || 'An unknown error occurred';
92
+ return {
93
+ code: 'UNKNOWN_ERROR',
94
+ message,
95
+ status,
96
+ isSpendingLimit: false,
97
+ isInsufficientCredits: false,
98
+ isPaymentMethodRequired: false,
99
+ isPaymentDeclined: false,
100
+ isAuth: false,
101
+ isPermissionDenied: false,
102
+ isRateLimit: false,
103
+ data: {
104
+ code: 'UNKNOWN_ERROR',
105
+ message,
106
+ },
107
+ };
108
+ }
109
+
110
+ function statusFallback(
111
+ code: string,
112
+ message: string,
113
+ status: number,
114
+ flags: Partial<
115
+ Pick<
116
+ ParsedError,
117
+ | 'isSpendingLimit'
118
+ | 'isInsufficientCredits'
119
+ | 'isPaymentMethodRequired'
120
+ | 'isPaymentDeclined'
121
+ | 'isAuth'
122
+ | 'isPermissionDenied'
123
+ | 'isRateLimit'
124
+ >
125
+ >,
126
+ ): ParsedError {
127
+ return {
128
+ code,
129
+ message,
130
+ status,
131
+ isSpendingLimit: false,
132
+ isInsufficientCredits: false,
133
+ isPaymentMethodRequired: false,
134
+ isPaymentDeclined: false,
135
+ isAuth: false,
136
+ isPermissionDenied: false,
137
+ isRateLimit: false,
138
+ ...flags,
139
+ data: { code, message },
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Check if an error is a spending limit error
145
+ */
146
+ export function isSpendingLimitError(error: any): boolean {
147
+ return parseError(error).isSpendingLimit;
148
+ }
149
+
150
+ /**
151
+ * Check if an error is an insufficient credits error
152
+ */
153
+ export function isInsufficientCreditsError(error: any): boolean {
154
+ return parseError(error).isInsufficientCredits;
155
+ }
156
+
157
+ /**
158
+ * Check if an error means the user must add a payment method (metered billing).
159
+ */
160
+ export function isPaymentMethodRequiredError(error: any): boolean {
161
+ return parseError(error).isPaymentMethodRequired;
162
+ }
163
+
164
+ /**
165
+ * Check if an error means the user's card was declined (metered billing).
166
+ */
167
+ export function isPaymentDeclinedError(error: any): boolean {
168
+ return parseError(error).isPaymentDeclined;
169
+ }
170
+
171
+ /**
172
+ * Check if an error is an authentication failure (re-auth / open the auth modal).
173
+ */
174
+ export function isAuthError(error: any): boolean {
175
+ return parseError(error).isAuth;
176
+ }
177
+
178
+ /**
179
+ * Check if an error is a scope/permission denial (403) — NOT an auth failure,
180
+ * so it should not trigger re-auth.
181
+ */
182
+ export function isPermissionDeniedError(error: any): boolean {
183
+ return parseError(error).isPermissionDenied;
184
+ }
185
+
186
+ /**
187
+ * Check if an error is a rate-limit denial (429 / RATE_LIMITED).
188
+ */
189
+ export function isRateLimitError(error: any): boolean {
190
+ return parseError(error).isRateLimit;
191
+ }
192
+
193
+ /**
194
+ * Any billing restriction the funds widget should surface a CTA for.
195
+ */
196
+ export function isBillingRestriction(error: any): boolean {
197
+ return (
198
+ isSpendingLimitError(error) ||
199
+ isInsufficientCreditsError(error) ||
200
+ isPaymentMethodRequiredError(error) ||
201
+ isPaymentDeclinedError(error)
202
+ );
203
+ }
204
+
205
+ /**
206
+ * Format time until reset
207
+ */
208
+ export function formatTimeUntilReset(resetsAt?: string): string {
209
+ if (!resetsAt) return '';
210
+
211
+ const reset = new Date(resetsAt);
212
+ const now = new Date();
213
+ const diff = reset.getTime() - now.getTime();
214
+
215
+ if (diff < 0) return 'soon';
216
+
217
+ const hours = Math.floor(diff / (1000 * 60 * 60));
218
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
219
+
220
+ if (hours > 0) {
221
+ return `in ${hours}h ${minutes}m`;
222
+ }
223
+ return `in ${minutes}m`;
224
+ }
@@ -0,0 +1,121 @@
1
+ import type { ParsedError } from "../types";
2
+ import { parseError } from "./parse-error";
3
+
4
+ /**
5
+ * Streaming (SSE) error classification for the gateway's chat/completions stream.
6
+ *
7
+ * A pre-stream denial arrives as a normal HTTP 402/429/403 with a JSON body —
8
+ * `parseError` already handles that. But a failure AFTER the stream has started
9
+ * (e.g. a billing/finalize error) is delivered as an SSE `event: error` frame,
10
+ * which a fetch-stream consumer must parse itself. These helpers surface that
11
+ * terminal error in the EXACT same classified shape as a non-streaming error,
12
+ * because the gateway now emits the canonical `{ error: { code, type, message } }`
13
+ * envelope inside the frame's `data:` (see lib/errors/api-error.ts).
14
+ */
15
+
16
+ export interface SSEEvent {
17
+ /** The `event:` name, if the frame set one (e.g. `error`). */
18
+ event?: string;
19
+ /** The concatenated `data:` payload. */
20
+ data: string;
21
+ }
22
+
23
+ /** Parse a raw SSE frame block (the text between blank-line delimiters). */
24
+ export function parseSSEFrame(block: string): SSEEvent | null {
25
+ let event: string | undefined;
26
+ const dataLines: string[] = [];
27
+ for (const line of block.split("\n")) {
28
+ if (line.startsWith("event:")) {
29
+ event = line.slice(6).trim();
30
+ } else if (line.startsWith("data:")) {
31
+ dataLines.push(line.slice(5).replace(/^ /, ""));
32
+ }
33
+ }
34
+ if (!event && dataLines.length === 0) return null;
35
+ return { event, data: dataLines.join("\n") };
36
+ }
37
+
38
+ /**
39
+ * If a frame is a terminal error — either `event: error`, or a `data:` payload
40
+ * carrying the canonical `{ error: { code } }` envelope — return the classified
41
+ * ParsedError (identical to `parseError()` for a non-streaming failure).
42
+ * Returns null for a normal data frame or the `[DONE]` sentinel.
43
+ */
44
+ export function parseSSEError(frame: SSEEvent | string): ParsedError | null {
45
+ const f = typeof frame === "string" ? parseSSEFrame(frame) : frame;
46
+ if (!f) return null;
47
+ if (f.data === "[DONE]") return null;
48
+
49
+ const isErrorEvent = f.event === "error";
50
+ let payload: unknown;
51
+ try {
52
+ payload = JSON.parse(f.data);
53
+ } catch {
54
+ // Non-JSON data on an error frame — still surface it as a classified error.
55
+ return isErrorEvent ? parseError({ message: f.data }) : null;
56
+ }
57
+
58
+ const hasEnvelope =
59
+ !!payload &&
60
+ typeof payload === "object" &&
61
+ typeof (payload as any).error === "object" &&
62
+ (payload as any).error !== null &&
63
+ "code" in (payload as any).error;
64
+
65
+ return isErrorEvent || hasEnvelope ? parseError(payload) : null;
66
+ }
67
+
68
+ export interface SSEStreamHandlers {
69
+ /** Called with each normal SSE `data:` payload (e.g. an OpenAI delta chunk). */
70
+ onData?: (data: string) => void;
71
+ /** Called ONCE with a classified error if the stream ends in an error frame. */
72
+ onError?: (error: ParsedError) => void;
73
+ /** Called when the stream completes (after `[DONE]` or the body closing). */
74
+ onDone?: () => void;
75
+ }
76
+
77
+ /**
78
+ * Consume a streaming Response body (`fetch(...).body`), splitting it into SSE
79
+ * frames. Normal data frames go to `onData`; a terminal `event: error` frame is
80
+ * classified and delivered to `onError` (NOT thrown), so a consumer using raw
81
+ * fetch-stream parsing gets the same `ParsedError` it would from a non-streaming
82
+ * call and can drive the same billing/auth modal. Resolves when the stream ends.
83
+ */
84
+ export async function consumeSSEStream(
85
+ body: ReadableStream<Uint8Array> | null | undefined,
86
+ handlers: SSEStreamHandlers,
87
+ ): Promise<void> {
88
+ if (!body) {
89
+ handlers.onDone?.();
90
+ return;
91
+ }
92
+ const reader = body.getReader();
93
+ const decoder = new TextDecoder();
94
+ let buffer = "";
95
+ try {
96
+ while (true) {
97
+ const { value, done } = await reader.read();
98
+ if (done) break;
99
+ buffer += decoder.decode(value, { stream: true });
100
+
101
+ let idx: number;
102
+ while ((idx = buffer.indexOf("\n\n")) !== -1) {
103
+ const block = buffer.slice(0, idx);
104
+ buffer = buffer.slice(idx + 2);
105
+ const frame = parseSSEFrame(block);
106
+ if (!frame) continue;
107
+
108
+ const err = parseSSEError(frame);
109
+ if (err) {
110
+ handlers.onError?.(err);
111
+ continue;
112
+ }
113
+ if (frame.data === "[DONE]") continue;
114
+ handlers.onData?.(frame.data);
115
+ }
116
+ }
117
+ } finally {
118
+ reader.releaseLock();
119
+ }
120
+ handlers.onDone?.();
121
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Popup helpers for the seamless (in-page) auth + charge flow.
3
+ *
4
+ * Both auth and Stripe card-entry can run in a popup so the whole
5
+ * press-buy → login → add-card → charge chain completes without leaving the
6
+ * developer's page (the PayPal / Firebase `signInWithPopup` model). Each helper
7
+ * resolves when the popup posts a completion message from the expected origin,
8
+ * or when the popup closes (treated as a cancel). If the browser blocks the
9
+ * popup, the helper returns `'blocked'` so the caller can fall back to redirect.
10
+ */
11
+
12
+ import type { InteractionMode, ResolvedMode } from '../types';
13
+
14
+ /**
15
+ * Resolve the effective mode. `auto` prefers popup on desktop but falls back to
16
+ * redirect on mobile / in-app webviews, where popups are unreliable (this mirrors
17
+ * Firebase's "popup on web, redirect on mobile" guidance).
18
+ */
19
+ export function resolveInteractionMode(mode: InteractionMode | undefined): ResolvedMode {
20
+ const m = mode || 'auto';
21
+ if (m === 'popup' || m === 'redirect') return m;
22
+ if (typeof navigator === 'undefined') return 'redirect';
23
+ const ua = navigator.userAgent || '';
24
+ const isMobile = /Android|iPhone|iPad|iPod|Mobile|Silk|Kindle|BlackBerry|Opera Mini/i.test(ua);
25
+ return isMobile ? 'redirect' : 'popup';
26
+ }
27
+
28
+ function centeredFeatures(w: number, h: number): string {
29
+ // Best-effort centering; harmless if the values are unavailable (SSR-guarded caller).
30
+ const dualLeft = typeof window !== 'undefined' ? (window.screenLeft ?? 0) : 0;
31
+ const dualTop = typeof window !== 'undefined' ? (window.screenTop ?? 0) : 0;
32
+ const width = typeof window !== 'undefined' ? window.innerWidth || w : w;
33
+ const height = typeof window !== 'undefined' ? window.innerHeight || h : h;
34
+ const left = dualLeft + Math.max(0, (width - w) / 2);
35
+ const top = dualTop + Math.max(0, (height - h) / 2);
36
+ return `width=${w},height=${h},left=${Math.round(left)},top=${Math.round(top)},resizable=yes,scrollbars=yes`;
37
+ }
38
+
39
+ interface PopupOptions {
40
+ url: string;
41
+ /** window.open target name; also read back in the callback to detect popup context. */
42
+ name: string;
43
+ /** Only completion messages from this origin are trusted. */
44
+ expectedOrigin: string;
45
+ /** Message discriminator to accept (e.g. 'hypery:auth', 'hypery:payment-method'). */
46
+ messageType: string;
47
+ width?: number;
48
+ height?: number;
49
+ }
50
+
51
+ export type PopupResult<T> = { blocked: true } | { blocked: false; cancelled: boolean; data?: T };
52
+
53
+ /**
54
+ * Open `url` in a popup and resolve when it posts `{ type: messageType, ... }`
55
+ * from `expectedOrigin`, or when it closes (cancelled). Returns `{ blocked: true }`
56
+ * if the browser blocked the popup.
57
+ */
58
+ export function openPopup<T = any>(opts: PopupOptions): Promise<PopupResult<T>> {
59
+ if (typeof window === 'undefined') {
60
+ return Promise.resolve({ blocked: false, cancelled: true });
61
+ }
62
+ const popup = window.open(
63
+ opts.url,
64
+ opts.name,
65
+ centeredFeatures(opts.width || 480, opts.height || 720),
66
+ );
67
+ if (!popup) {
68
+ return Promise.resolve({ blocked: true });
69
+ }
70
+
71
+ return new Promise<PopupResult<T>>((resolve) => {
72
+ let settled = false;
73
+ const finish = (result: PopupResult<T>) => {
74
+ if (settled) return;
75
+ settled = true;
76
+ window.removeEventListener('message', onMessage);
77
+ clearInterval(poll);
78
+ resolve(result);
79
+ };
80
+ const onMessage = (event: MessageEvent) => {
81
+ if (event.origin !== opts.expectedOrigin) return;
82
+ const data = event.data;
83
+ if (data && typeof data === 'object' && data.type === opts.messageType) {
84
+ try {
85
+ popup.close();
86
+ } catch {
87
+ /* cross-origin close may throw; ignore */
88
+ }
89
+ finish({ blocked: false, cancelled: false, data: data as T });
90
+ }
91
+ };
92
+ window.addEventListener('message', onMessage);
93
+ // Fallback: if the popup is closed without a message, treat it as a cancel.
94
+ const poll = setInterval(() => {
95
+ if (popup.closed) finish({ blocked: false, cancelled: true });
96
+ }, 500);
97
+ });
98
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Token storage utilities
3
+ */
4
+
5
+ import type { AuthTokens, User } from '../types';
6
+
7
+ const TOKEN_KEY = 'hypery_auth_tokens';
8
+ const USER_KEY = 'hypery_auth_user';
9
+
10
+ export class TokenStorage {
11
+ private storage: Storage | Map<string, string>;
12
+ private storageType: 'localStorage' | 'sessionStorage' | 'memory';
13
+
14
+ constructor(storageType: 'localStorage' | 'sessionStorage' | 'memory' = 'localStorage') {
15
+ this.storageType = storageType;
16
+
17
+ if (storageType === 'memory' || typeof window === 'undefined') {
18
+ // In-memory storage for SSR or memory mode
19
+ this.storage = new Map<string, string>();
20
+ } else {
21
+ this.storage = storageType === 'localStorage' ? localStorage : sessionStorage;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Save auth tokens
27
+ */
28
+ saveTokens(tokens: AuthTokens): void {
29
+ const data = JSON.stringify({
30
+ ...tokens,
31
+ savedAt: Date.now(),
32
+ });
33
+
34
+ if (this.storage instanceof Map) {
35
+ this.storage.set(TOKEN_KEY, data);
36
+ } else {
37
+ this.storage.setItem(TOKEN_KEY, data);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Get auth tokens
43
+ */
44
+ getTokens(): (AuthTokens & { savedAt: number }) | null {
45
+ let data: string | null = null;
46
+
47
+ if (this.storage instanceof Map) {
48
+ data = this.storage.get(TOKEN_KEY) || null;
49
+ } else {
50
+ data = this.storage.getItem(TOKEN_KEY);
51
+ }
52
+
53
+ if (!data) return null;
54
+
55
+ try {
56
+ return JSON.parse(data);
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Check if access token is expired
64
+ */
65
+ isTokenExpired(): boolean {
66
+ const tokens = this.getTokens();
67
+ if (!tokens) return true;
68
+
69
+ const expiresAt = tokens.savedAt + tokens.expiresIn * 1000;
70
+ const now = Date.now();
71
+
72
+ // Add 60s buffer to refresh before expiration
73
+ return now >= expiresAt - 60000;
74
+ }
75
+
76
+ /**
77
+ * Clear auth tokens
78
+ */
79
+ clearTokens(): void {
80
+ if (this.storage instanceof Map) {
81
+ this.storage.delete(TOKEN_KEY);
82
+ } else {
83
+ this.storage.removeItem(TOKEN_KEY);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Save user info
89
+ */
90
+ saveUser(user: User): void {
91
+ const data = JSON.stringify(user);
92
+
93
+ if (this.storage instanceof Map) {
94
+ this.storage.set(USER_KEY, data);
95
+ } else {
96
+ this.storage.setItem(USER_KEY, data);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get user info
102
+ */
103
+ getUser(): User | null {
104
+ let data: string | null = null;
105
+
106
+ if (this.storage instanceof Map) {
107
+ data = this.storage.get(USER_KEY) || null;
108
+ } else {
109
+ data = this.storage.getItem(USER_KEY);
110
+ }
111
+
112
+ if (!data) return null;
113
+
114
+ try {
115
+ return JSON.parse(data);
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Clear user info
123
+ */
124
+ clearUser(): void {
125
+ if (this.storage instanceof Map) {
126
+ this.storage.delete(USER_KEY);
127
+ } else {
128
+ this.storage.removeItem(USER_KEY);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Clear all auth data
134
+ */
135
+ clear(): void {
136
+ this.clearTokens();
137
+ this.clearUser();
138
+ }
139
+ }
140
+
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Utility functions for @hyperyai/sdk
3
+ */
4
+
5
+ import { type ClassValue, clsx } from "clsx"
6
+ import { twMerge } from "tailwind-merge"
7
+
8
+ /**
9
+ * Merge Tailwind CSS classes with proper precedence
10
+ */
11
+ export function cn(...inputs: ClassValue[]) {
12
+ return twMerge(clsx(inputs))
13
+ }
14
+