@timeback/sdk 0.1.8 → 0.1.9

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 (50) hide show
  1. package/README.md +6 -6
  2. package/dist/{chunk-j1xdrfqj.js → chunk-5171mkp2.js} +1 -1
  3. package/dist/{chunk-3886xy48.js → chunk-63afdp3y.js} +6 -6
  4. package/dist/chunk-8gg8n8v9.js +2 -0
  5. package/dist/{chunk-ewsp6v3b.js → chunk-agpf1x3g.js} +5 -5
  6. package/dist/chunk-hnf0tart.js +2 -0
  7. package/dist/{chunk-rgbpvxbv.js → chunk-x9gvef7q.js} +1 -1
  8. package/dist/client/adapters/react/hooks/types.d.ts +34 -0
  9. package/dist/client/adapters/react/hooks/types.d.ts.map +1 -1
  10. package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts +17 -5
  11. package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts.map +1 -1
  12. package/dist/client/adapters/react/index.d.ts +1 -1
  13. package/dist/client/adapters/react/index.d.ts.map +1 -1
  14. package/dist/client/adapters/react/index.js +2 -2
  15. package/dist/client/adapters/react/provider.d.ts.map +1 -1
  16. package/dist/client/adapters/solid/primitives/createTimebackVerification.d.ts +3 -1
  17. package/dist/client/adapters/solid/primitives/createTimebackVerification.d.ts.map +1 -1
  18. package/dist/client/adapters/solid/primitives/createTimebackVerification.ts +55 -15
  19. package/dist/client/adapters/solid/types.d.ts +23 -0
  20. package/dist/client/adapters/solid/types.d.ts.map +1 -1
  21. package/dist/client/adapters/solid/types.ts +25 -0
  22. package/dist/client/adapters/svelte/stores/verification.d.ts +1 -1
  23. package/dist/client/adapters/svelte/stores/verification.d.ts.map +1 -1
  24. package/dist/client/adapters/svelte/stores/verification.ts +94 -12
  25. package/dist/client/adapters/vue/composables/useTimebackVerification.d.ts +3 -1
  26. package/dist/client/adapters/vue/composables/useTimebackVerification.d.ts.map +1 -1
  27. package/dist/client/adapters/vue/composables/useTimebackVerification.ts +59 -18
  28. package/dist/client/adapters/vue/types.d.ts +23 -0
  29. package/dist/client/adapters/vue/types.d.ts.map +1 -1
  30. package/dist/client/adapters/vue/types.ts +25 -0
  31. package/dist/client/lib/utils.d.ts +15 -0
  32. package/dist/client/lib/utils.d.ts.map +1 -1
  33. package/dist/client.js +1 -1
  34. package/dist/edge.js +1 -1
  35. package/dist/identity.js +1 -1
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +6 -6
  38. package/dist/server/adapters/express.js +1 -1
  39. package/dist/server/adapters/native.js +1 -1
  40. package/dist/server/adapters/nextjs.js +1 -1
  41. package/dist/server/adapters/nuxt.js +1 -1
  42. package/dist/server/adapters/solid-start.js +1 -1
  43. package/dist/server/adapters/svelte-kit.js +1 -1
  44. package/dist/server/adapters/tanstack-start.js +1 -1
  45. package/dist/server/timeback.d.ts +2 -2
  46. package/dist/shared/constants.d.ts +13 -0
  47. package/dist/shared/constants.d.ts.map +1 -1
  48. package/package.json +5 -5
  49. package/dist/chunk-ahy54f2r.js +0 -2
  50. package/dist/chunk-qaa129bd.js +0 -2
@@ -9,7 +9,9 @@
9
9
 
10
10
  import { createEffect, createSignal, onCleanup } from 'solid-js'
11
11
 
12
+ import { DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAYS_MS } from '../../../../shared/constants'
12
13
  import { getVerifyCache, verifyOnce } from '../../../lib/user-cache'
14
+ import { getRetryDelay, sleep } from '../../../lib/utils'
13
15
  import { useTimeback } from '../context'
14
16
 
15
17
  import type { TimebackVerifyResult } from '../../../../shared/types'
@@ -37,8 +39,10 @@ function toState(result: TimebackVerifyResult): TimebackVerificationState {
37
39
  * Runs automatically once the Timeback client is available, and
38
40
  * provides a `refresh()` method to retry.
39
41
  *
42
+ * By default, retries failed verification attempts with exponential
43
+ * backoff to handle race conditions (e.g., user not yet created in backend).
44
+ *
40
45
  * @param options - Options
41
- * @param options.enabled - If false, does nothing and stays in loading state.
42
46
  * @returns Verification state and a refresh method
43
47
  *
44
48
  * @example
@@ -60,6 +64,8 @@ export function createTimebackVerification(
60
64
  options: CreateTimebackVerificationOptions = {},
61
65
  ): CreateTimebackVerificationResult {
62
66
  const enabled = options.enabled ?? true
67
+ const retryAttempts = options.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS
68
+ const retryDelays = options.retryDelays ?? DEFAULT_RETRY_DELAYS_MS
63
69
  const timeback = useTimeback()
64
70
 
65
71
  const [state, setState] = createSignal<TimebackVerificationState>({ status: 'loading' })
@@ -96,26 +102,60 @@ export function createTimebackVerification(
96
102
  setState({ status: 'loading' })
97
103
 
98
104
  void (async () => {
99
- try {
100
- if (!client) return
105
+ if (!client) return
101
106
 
102
- if (force) {
103
- lastHandledRefreshNonce = nonce
104
- }
107
+ if (force) {
108
+ lastHandledRefreshNonce = nonce
109
+ }
105
110
 
106
- const result = await verifyOnce(client, force)
111
+ let lastError: Error | undefined
107
112
 
113
+ for (let attempt = 0; attempt <= retryAttempts; attempt++) {
108
114
  if (cancelled) return
109
115
 
110
- setState(toState(result))
111
- } catch (err) {
112
- if (!cancelled) {
113
- setState({
114
- status: 'error',
115
- message:
116
- err instanceof Error ? err.message : 'Failed to verify Timeback user',
117
- })
116
+ /**
117
+ * Wait for a calculated delay before retrying the verification request.
118
+ *
119
+ * This occurs only on subsequent attempts (not the initial try),
120
+ * introducing a backoff based on the configured retry delays.
121
+ */
122
+ if (attempt > 0) {
123
+ const delay = getRetryDelay(retryDelays, attempt - 1)
124
+ await sleep(delay)
125
+ if (cancelled) return
118
126
  }
127
+
128
+ try {
129
+ const result = await verifyOnce(client, force || attempt > 0)
130
+
131
+ if (cancelled) return
132
+
133
+ setState(toState(result))
134
+
135
+ return
136
+ } catch (err) {
137
+ lastError =
138
+ err instanceof Error ? err : new Error('Failed to verify Timeback user')
139
+ /**
140
+ * An error occurred during this verification attempt.
141
+ * Proceeding to the next retry attempt if any remain.
142
+ *
143
+ * The last encountered error is saved for potential error state handling.
144
+ */
145
+ }
146
+ }
147
+
148
+ /**
149
+ * All verification attempts have failed after exhausting
150
+ * the configured retry policy. The verification state will
151
+ * transition to "error" if not cancelled, and the last encountered
152
+ * error message (if available) will be presented to the consumer.
153
+ */
154
+ if (!cancelled && lastError) {
155
+ setState({
156
+ status: 'error',
157
+ message: lastError.message,
158
+ })
119
159
  }
120
160
  })()
121
161
 
@@ -39,9 +39,32 @@ export interface CreateTimebackVerificationOptions {
39
39
  /**
40
40
  * If false, does nothing and stays in loading state.
41
41
  *
42
+ * Use this to delay verification until prerequisites are met
43
+ * (e.g., user exists in your database).
44
+ *
42
45
  * @default true
43
46
  */
44
47
  enabled?: boolean;
48
+ /**
49
+ * Number of retry attempts on failure.
50
+ *
51
+ * This helps handle race conditions where the user may not be
52
+ * fully set up in the backend when verification first runs.
53
+ *
54
+ * Set to 0 to disable retries.
55
+ *
56
+ * @default 3
57
+ */
58
+ retryAttempts?: number;
59
+ /**
60
+ * Delay in ms before each retry attempt.
61
+ *
62
+ * Can be a single number (same delay for all retries) or an array
63
+ * of delays for each attempt (e.g., [100, 300, 1000] for exponential backoff).
64
+ *
65
+ * @default [100, 300, 1000]
66
+ */
67
+ retryDelays?: number | readonly number[];
45
68
  }
46
69
  /**
47
70
  * Return value of createTimebackVerification.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/client/adapters/solid/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAE5D;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAClC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,GACxB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvC;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAC7B;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAC9C;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvC;;GAEG;AACH,MAAM,WAAW,iCAAiC;IACjD;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAChD,iCAAiC;IACjC,KAAK,EAAE,yBAAyB,CAAA;IAEhC,qDAAqD;IACrD,OAAO,EAAE,MAAM,IAAI,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC5C;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C,4BAA4B;IAC5B,KAAK,EAAE,oBAAoB,CAAA;IAE3B,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAA;IAEjB,oEAAoE;IACpE,YAAY,EAAE,MAAM,IAAI,CAAA;IAExB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,IAAI,CAAA;CACnB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/client/adapters/solid/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAE5D;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAClC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,GACxB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvC;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAC7B;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAC9C;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvC;;GAEG;AACH,MAAM,WAAW,iCAAiC;IACjD;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAChD,iCAAiC;IACjC,KAAK,EAAE,yBAAyB,CAAA;IAEhC,qDAAqD;IACrD,OAAO,EAAE,MAAM,IAAI,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC5C;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C,4BAA4B;IAC5B,KAAK,EAAE,oBAAoB,CAAA;IAE3B,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAA;IAEjB,oEAAoE;IACpE,YAAY,EAAE,MAAM,IAAI,CAAA;IAExB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,IAAI,CAAA;CACnB"}
@@ -31,9 +31,34 @@ export interface CreateTimebackVerificationOptions {
31
31
  /**
32
32
  * If false, does nothing and stays in loading state.
33
33
  *
34
+ * Use this to delay verification until prerequisites are met
35
+ * (e.g., user exists in your database).
36
+ *
34
37
  * @default true
35
38
  */
36
39
  enabled?: boolean
40
+
41
+ /**
42
+ * Number of retry attempts on failure.
43
+ *
44
+ * This helps handle race conditions where the user may not be
45
+ * fully set up in the backend when verification first runs.
46
+ *
47
+ * Set to 0 to disable retries.
48
+ *
49
+ * @default 3
50
+ */
51
+ retryAttempts?: number
52
+
53
+ /**
54
+ * Delay in ms before each retry attempt.
55
+ *
56
+ * Can be a single number (same delay for all retries) or an array
57
+ * of delays for each attempt (e.g., [100, 300, 1000] for exponential backoff).
58
+ *
59
+ * @default [100, 300, 1000]
60
+ */
61
+ retryDelays?: number | readonly number[]
37
62
  }
38
63
 
39
64
  /**
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Svelte store for user verification state.
5
5
  */
6
- import type { TimebackVerificationState } from '../types';
7
6
  import type { Readable } from 'svelte/store';
7
+ import type { TimebackVerificationState } from '../types';
8
8
  export declare const verificationStore: import("svelte/store").Writable<TimebackVerificationState>;
9
9
  /**
10
10
  * Store containing the Timeback verification state.
@@ -1 +1 @@
1
- {"version":3,"file":"verification.d.ts","sourceRoot":"","sources":["../../../../../src/client/adapters/svelte/stores/verification.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAc5C,eAAO,MAAM,iBAAiB,4DAA6D,CAAA;AA8D3F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,yBAAyB,CAAqB,CAAA;AAE1F;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAGlD"}
1
+ {"version":3,"file":"verification.d.ts","sourceRoot":"","sources":["../../../../../src/client/adapters/svelte/stores/verification.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAE5C,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAA;AAczD,eAAO,MAAM,iBAAiB,4DAA6D,CAAA;AA6I3F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,yBAAyB,CAAqB,CAAA;AAE1F;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAGlD"}
@@ -4,13 +4,16 @@
4
4
  * Svelte store for user verification state.
5
5
  */
6
6
 
7
+ import { writable } from 'svelte/store'
8
+
9
+ import { DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAYS_MS } from '../../../../shared/constants'
7
10
  import { getVerifyCache, verifyOnce } from '../../../lib/user-cache'
11
+ import { getRetryDelay, sleep } from '../../../lib/utils'
8
12
  import { clientStore, getClientInstance } from './client'
9
- import { writable } from 'svelte/store'
10
13
 
14
+ import type { Readable } from 'svelte/store'
11
15
  import type { TimebackVerifyResult } from '../../../../shared/types'
12
16
  import type { TimebackVerificationState } from '../types'
13
- import type { Readable } from 'svelte/store'
14
17
 
15
18
  /**
16
19
  * Convert a verify result into a store-friendly state machine.
@@ -27,6 +30,16 @@ function toVerificationState(result: TimebackVerifyResult): TimebackVerification
27
30
  export const verificationStore = writable<TimebackVerificationState>({ status: 'loading' })
28
31
  let verificationInitialized = false
29
32
 
33
+ /**
34
+ * Counter tracking the current verification run.
35
+ *
36
+ * Each call to `runVerification` increments this counter and captures
37
+ * its value. Before updating state, the run checks if it's still current.
38
+ * This prevents stale results from overwriting fresh ones when multiple
39
+ * verification attempts overlap (e.g., initial retry + user refresh).
40
+ */
41
+ let currentRunId = 0
42
+
30
43
  /**
31
44
  * Check if we're running in the browser.
32
45
  *
@@ -37,7 +50,11 @@ function isBrowser(): boolean {
37
50
  }
38
51
 
39
52
  /**
40
- * Run the verification check.
53
+ * Run the verification check with retry logic.
54
+ *
55
+ * Uses a run ID to prevent race conditions when multiple verification
56
+ * attempts overlap. Each run captures the current ID and only updates
57
+ * state if it's still the active run.
41
58
  *
42
59
  * @param force - If true, bypass cache and force a fresh verification
43
60
  */
@@ -45,23 +62,79 @@ async function runVerification(force: boolean): Promise<void> {
45
62
  const client = getClientInstance()
46
63
  if (!client) return
47
64
 
65
+ /**
66
+ * Capture this run's ID. Any subsequent call to runVerification
67
+ * will increment currentRunId, making this run "cancelled".
68
+ */
69
+ const runId = ++currentRunId
70
+
71
+ /**
72
+ * Check if this run is still the active one.
73
+ *
74
+ * @returns False if a newer verification run has started
75
+ */
76
+ const isCurrentRun = () => runId === currentRunId
77
+
48
78
  if (!force) {
49
79
  const cached = getVerifyCache(client)
50
80
  if (cached) {
51
- verificationStore.set(toVerificationState(cached))
81
+ if (isCurrentRun()) {
82
+ verificationStore.set(toVerificationState(cached))
83
+ }
52
84
  return
53
85
  }
54
86
  }
55
87
 
56
- verificationStore.set({ status: 'loading' })
88
+ if (isCurrentRun()) {
89
+ verificationStore.set({ status: 'loading' })
90
+ }
91
+
92
+ let lastError: Error | undefined
93
+
94
+ for (let attempt = 0; attempt <= DEFAULT_RETRY_ATTEMPTS; attempt++) {
95
+ if (!isCurrentRun()) return
96
+
97
+ /**
98
+ * Wait for a calculated delay before retrying the verification request.
99
+ *
100
+ * This occurs only on subsequent attempts (not the initial try),
101
+ * introducing a backoff based on the configured retry delays.
102
+ */
103
+ if (attempt > 0) {
104
+ const delay = getRetryDelay(DEFAULT_RETRY_DELAYS_MS, attempt - 1)
105
+ await sleep(delay)
106
+ if (!isCurrentRun()) return
107
+ }
108
+
109
+ try {
110
+ const result = await verifyOnce(client, force || attempt > 0)
111
+
112
+ if (!isCurrentRun()) return
113
+
114
+ verificationStore.set(toVerificationState(result))
115
+
116
+ return
117
+ } catch (err) {
118
+ lastError = err instanceof Error ? err : new Error('Failed to verify Timeback user')
119
+ /**
120
+ * An error occurred during this verification attempt.
121
+ * Proceeding to the next retry attempt if any remain.
122
+ *
123
+ * The last encountered error is saved for potential error state handling.
124
+ */
125
+ }
126
+ }
57
127
 
58
- try {
59
- const result = await verifyOnce(client, force)
60
- verificationStore.set(toVerificationState(result))
61
- } catch (err) {
128
+ /**
129
+ * All verification attempts have failed after exhausting
130
+ * the configured retry policy. The verification state will
131
+ * transition to "error" if this run is still current, and the last
132
+ * encountered error message (if available) will be presented to the consumer.
133
+ */
134
+ if (isCurrentRun() && lastError) {
62
135
  verificationStore.set({
63
136
  status: 'error',
64
- message: err instanceof Error ? err.message : 'Failed to verify Timeback user',
137
+ message: lastError.message,
65
138
  })
66
139
  }
67
140
  }
@@ -73,7 +146,17 @@ function initVerification(): void {
73
146
  if (verificationInitialized) return
74
147
  verificationInitialized = true
75
148
 
76
- // Subscribe to client changes to auto-verify
149
+ /**
150
+ * Subscribes to changes in the Timeback client store.
151
+ *
152
+ * Any time the client instance becomes available or changes,
153
+ * automatically triggers a verification attempt to ensure the
154
+ * user's Timeback verification status is kept up-to-date.
155
+ *
156
+ * This enables responsive status updates when authentication
157
+ * state changes or when the client instance is re-initialized,
158
+ * providing a seamless verification experience in Svelte apps.
159
+ */
77
160
  clientStore.subscribe(client => {
78
161
  if (client && isBrowser()) {
79
162
  void runVerification(false)
@@ -81,7 +164,6 @@ function initVerification(): void {
81
164
  })
82
165
  }
83
166
 
84
- // Auto-init in browser
85
167
  if (isBrowser()) {
86
168
  initVerification()
87
169
  }
@@ -14,8 +14,10 @@ import type { TimebackVerificationState, UseTimebackVerificationOptions } from '
14
14
  * The composable runs automatically once the Timeback client is available, and
15
15
  * provides a `refresh()` method to retry.
16
16
  *
17
+ * By default, the composable retries failed verification attempts with exponential
18
+ * backoff to handle race conditions (e.g., user not yet created in backend).
19
+ *
17
20
  * @param options - Composable options
18
- * @param options.enabled - If false, the composable does nothing and stays in loading state.
19
21
  * @returns Verification state and a refresh method
20
22
  *
21
23
  * @example
@@ -1 +1 @@
1
- {"version":3,"file":"useTimebackVerification.d.ts","sourceRoot":"","sources":["../../../../../src/client/adapters/vue/composables/useTimebackVerification.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAE9B,OAAO,KAAK,EAAE,yBAAyB,EAAE,8BAA8B,EAAE,MAAM,UAAU,CAAA;AAczF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,uBAAuB,CACtC,OAAO,GAAE,8BAAmC,GAC1C;IACF,KAAK,EAAE,GAAG,CAAC,yBAAyB,CAAC,CAAA;IACrC,OAAO,EAAE,MAAM,IAAI,CAAA;CACnB,CA+DA"}
1
+ {"version":3,"file":"useTimebackVerification.d.ts","sourceRoot":"","sources":["../../../../../src/client/adapters/vue/composables/useTimebackVerification.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAE9B,OAAO,KAAK,EAAE,yBAAyB,EAAE,8BAA8B,EAAE,MAAM,UAAU,CAAA;AAczF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,8BAAmC,GAAG;IACtF,KAAK,EAAE,GAAG,CAAC,yBAAyB,CAAC,CAAA;IACrC,OAAO,EAAE,MAAM,IAAI,CAAA;CACnB,CAsGA"}
@@ -9,7 +9,9 @@
9
9
 
10
10
  import { ref, watch } from 'vue'
11
11
 
12
+ import { DEFAULT_RETRY_ATTEMPTS, DEFAULT_RETRY_DELAYS_MS } from '../../../../shared/constants'
12
13
  import { getVerifyCache, verifyOnce } from '../../../lib/user-cache'
14
+ import { getRetryDelay, sleep } from '../../../lib/utils'
13
15
  import { useTimeback } from '../provider'
14
16
 
15
17
  import type { Ref } from 'vue'
@@ -34,8 +36,10 @@ function toState(result: TimebackVerifyResult): TimebackVerificationState {
34
36
  * The composable runs automatically once the Timeback client is available, and
35
37
  * provides a `refresh()` method to retry.
36
38
  *
39
+ * By default, the composable retries failed verification attempts with exponential
40
+ * backoff to handle race conditions (e.g., user not yet created in backend).
41
+ *
37
42
  * @param options - Composable options
38
- * @param options.enabled - If false, the composable does nothing and stays in loading state.
39
43
  * @returns Verification state and a refresh method
40
44
  *
41
45
  * @example
@@ -57,16 +61,18 @@ function toState(result: TimebackVerifyResult): TimebackVerificationState {
57
61
  * </template>
58
62
  * ```
59
63
  */
60
- export function useTimebackVerification(
61
- options: UseTimebackVerificationOptions = {},
62
- ): {
64
+ export function useTimebackVerification(options: UseTimebackVerificationOptions = {}): {
63
65
  state: Ref<TimebackVerificationState>
64
66
  refresh: () => void
65
67
  } {
66
68
  const enabled = options.enabled ?? true
69
+ const retryAttempts = options.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS
70
+ const retryDelays = options.retryDelays ?? DEFAULT_RETRY_DELAYS_MS
67
71
  const timebackRef = useTimeback()
68
72
 
69
- const state = ref<TimebackVerificationState>({ status: 'loading' }) as Ref<TimebackVerificationState>
73
+ const state = ref<TimebackVerificationState>({
74
+ status: 'loading',
75
+ }) as Ref<TimebackVerificationState>
70
76
  const refreshNonce = ref(0)
71
77
 
72
78
  let lastHandledRefreshNonce = 0
@@ -100,24 +106,59 @@ export function useTimebackVerification(
100
106
 
101
107
  state.value = { status: 'loading' }
102
108
 
103
- try {
104
- if (!timeback) return
109
+ if (!timeback) return
105
110
 
106
- if (force) {
107
- lastHandledRefreshNonce = nonce
108
- }
111
+ if (force) {
112
+ lastHandledRefreshNonce = nonce
113
+ }
109
114
 
110
- const result = await verifyOnce(timeback, force)
115
+ let lastError: Error | undefined
111
116
 
117
+ for (let attempt = 0; attempt <= retryAttempts; attempt++) {
112
118
  if (cancelled) return
113
119
 
114
- state.value = toState(result)
115
- } catch (err) {
116
- if (!cancelled) {
117
- state.value = {
118
- status: 'error',
119
- message: err instanceof Error ? err.message : 'Failed to verify Timeback user',
120
- }
120
+ /**
121
+ * Wait for a calculated delay before retrying the verification request.
122
+ *
123
+ * This occurs only on subsequent attempts (not the initial try),
124
+ * introducing a backoff based on the configured retry delays.
125
+ */
126
+ if (attempt > 0) {
127
+ const delay = getRetryDelay(retryDelays, attempt - 1)
128
+ await sleep(delay)
129
+ if (cancelled) return
130
+ }
131
+
132
+ try {
133
+ const result = await verifyOnce(timeback, force || attempt > 0)
134
+
135
+ if (cancelled) return
136
+
137
+ state.value = toState(result)
138
+
139
+ return
140
+ } catch (err) {
141
+ lastError =
142
+ err instanceof Error ? err : new Error('Failed to verify Timeback user')
143
+ /**
144
+ * An error occurred during this verification attempt.
145
+ * Proceeding to the next retry attempt if any remain.
146
+ *
147
+ * The last encountered error is saved for potential error state handling.
148
+ */
149
+ }
150
+ }
151
+
152
+ /**
153
+ * All verification attempts have failed after exhausting
154
+ * the configured retry policy. The verification state will
155
+ * transition to "error" if not cancelled, and the last encountered
156
+ * error message (if available) will be presented to the consumer.
157
+ */
158
+ if (!cancelled && lastError) {
159
+ state.value = {
160
+ status: 'error',
161
+ message: lastError.message,
121
162
  }
122
163
  }
123
164
  },
@@ -39,9 +39,32 @@ export interface UseTimebackVerificationOptions {
39
39
  /**
40
40
  * If false, the composable does nothing and stays in loading state.
41
41
  *
42
+ * Use this to delay verification until prerequisites are met
43
+ * (e.g., user exists in your database).
44
+ *
42
45
  * @default true
43
46
  */
44
47
  enabled?: boolean;
48
+ /**
49
+ * Number of retry attempts on failure.
50
+ *
51
+ * This helps handle race conditions where the user may not be
52
+ * fully set up in the backend when verification first runs.
53
+ *
54
+ * Set to 0 to disable retries.
55
+ *
56
+ * @default 3
57
+ */
58
+ retryAttempts?: number;
59
+ /**
60
+ * Delay in ms before each retry attempt.
61
+ *
62
+ * Can be a single number (same delay for all retries) or an array
63
+ * of delays for each attempt (e.g., [100, 300, 1000] for exponential backoff).
64
+ *
65
+ * @default [100, 300, 1000]
66
+ */
67
+ retryDelays?: number | readonly number[];
45
68
  }
46
69
  /**
47
70
  * Return value of the useTimebackVerification composable.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/client/adapters/vue/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAE5D;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAClC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,GACxB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvC;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAC7B;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAC9C;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvC;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC7C,gDAAgD;IAChD,KAAK,EAAE,yBAAyB,CAAA;IAEhC,qDAAqD;IACrD,OAAO,EAAE,MAAM,IAAI,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACzC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,2CAA2C;IAC3C,KAAK,EAAE,oBAAoB,CAAA;IAE3B,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAA;IAEjB,oEAAoE;IACpE,YAAY,EAAE,MAAM,IAAI,CAAA;IAExB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,IAAI,CAAA;CACnB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/client/adapters/vue/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAE5D;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAClC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,GACxB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvC;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAC7B;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAC9C;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvC;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC7C,gDAAgD;IAChD,KAAK,EAAE,yBAAyB,CAAA;IAEhC,qDAAqD;IACrD,OAAO,EAAE,MAAM,IAAI,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACzC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,2CAA2C;IAC3C,KAAK,EAAE,oBAAoB,CAAA;IAE3B,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAA;IAEjB,oEAAoE;IACpE,YAAY,EAAE,MAAM,IAAI,CAAA;IAExB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,IAAI,CAAA;CACnB"}
@@ -31,9 +31,34 @@ export interface UseTimebackVerificationOptions {
31
31
  /**
32
32
  * If false, the composable does nothing and stays in loading state.
33
33
  *
34
+ * Use this to delay verification until prerequisites are met
35
+ * (e.g., user exists in your database).
36
+ *
34
37
  * @default true
35
38
  */
36
39
  enabled?: boolean
40
+
41
+ /**
42
+ * Number of retry attempts on failure.
43
+ *
44
+ * This helps handle race conditions where the user may not be
45
+ * fully set up in the backend when verification first runs.
46
+ *
47
+ * Set to 0 to disable retries.
48
+ *
49
+ * @default 3
50
+ */
51
+ retryAttempts?: number
52
+
53
+ /**
54
+ * Delay in ms before each retry attempt.
55
+ *
56
+ * Can be a single number (same delay for all retries) or an array
57
+ * of delays for each attempt (e.g., [100, 300, 1000] for exponential backoff).
58
+ *
59
+ * @default [100, 300, 1000]
60
+ */
61
+ retryDelays?: number | readonly number[]
37
62
  }
38
63
 
39
64
  /**
@@ -17,4 +17,19 @@ export declare function isBrowser(): boolean;
17
17
  * @returns The default base URL or undefined during SSR
18
18
  */
19
19
  export declare function getDefaultBaseURL(): string | undefined;
20
+ /**
21
+ * Sleep for a given number of milliseconds.
22
+ *
23
+ * @param ms - Duration to sleep in milliseconds
24
+ * @returns Promise that resolves after the delay
25
+ */
26
+ export declare function sleep(ms: number): Promise<void>;
27
+ /**
28
+ * Get retry delay for a given attempt from a delay configuration.
29
+ *
30
+ * @param delays - Single delay or array of delays per attempt
31
+ * @param attempt - Zero-based attempt index
32
+ * @returns Delay in milliseconds
33
+ */
34
+ export declare function getRetryDelay(delays: number | readonly number[], attempt: number): number;
20
35
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/client/lib/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAMtD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/client/lib/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAMtD;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI/C;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAMzF"}
package/dist/client.js CHANGED
@@ -1 +1 @@
1
- import{a as o,b as y,c as b}from"./chunk-ahy54f2r.js";function r(e){let i=e.headerName??"Authorization",k=e.prefix??"Bearer ";return{wrapFetch(s){return async(t,m)=>{let a=await e.getToken();if(!a)throw Error("Missing bearer token (are you authenticated yet?)");let c=new Headers(m?.headers??(t instanceof Request?t.headers:void 0));if(!c.has(i))c.set(i,`${k}${a}`);return s(t,{...m,headers:c})}}}}export{b as createClient,r as bearer,y as TimebackClient,o as Activity};
1
+ import{e as o,f as y,g as b}from"./chunk-8gg8n8v9.js";function r(e){let i=e.headerName??"Authorization",k=e.prefix??"Bearer ";return{wrapFetch(s){return async(t,m)=>{let a=await e.getToken();if(!a)throw Error("Missing bearer token (are you authenticated yet?)");let c=new Headers(m?.headers??(t instanceof Request?t.headers:void 0));if(!c.has(i))c.set(i,`${k}${a}`);return s(t,{...m,headers:c})}}}}export{b as createClient,r as bearer,y as TimebackClient,o as Activity};
package/dist/edge.js CHANGED
@@ -1 +1 @@
1
- import{a as L,b as G,c as D}from"./chunk-3886xy48.js";import"./chunk-whc53e0y.js";function V(y){let g=y.trim();if(g==="")return"/";let x=g.indexOf("?");if(x===-1)return g;let C=g.slice(0,x);if(C==="")return"/";return C}function $(y){let g=V(y);if(g!==""&&!g.startsWith("/"))g=`/${g}`;if(g==="/"||g==="")return"";if(g.endsWith("/"))return g.slice(0,-1);return g}function X(y){let g=y.method.toUpperCase(),x=V(y.pathname),C=y.callbackPath?V(y.callbackPath):void 0;if(C&&x===C)return g==="GET"?"identity.callback":null;let I=y.basePath,M=I!==void 0,J=M?$(I):void 0,Q=(f)=>{if(g==="GET"){if(f===G.IDENTITY.SIGNIN)return"identity.signIn";if(f===G.IDENTITY.CALLBACK)return"identity.callback";if(f===G.IDENTITY.SIGNOUT)return"identity.signOut";if(f===G.USER.ME)return"user.me";if(f===G.USER.VERIFY)return"user.verify"}if(g==="POST"){if(f===G.ACTIVITY)return"activity"}return null};if(M&&J!==void 0){if(J!==""&&x===J)return null;if(J!==""&&!x.startsWith(`${J}/`))return null;let f=J===""?x:x.slice(J.length),_=f.startsWith("/")?f:`/${f}`;return Q(_)}if(g==="GET"){if(x.endsWith(G.IDENTITY.SIGNIN))return"identity.signIn";if(x.endsWith(G.IDENTITY.CALLBACK))return"identity.callback";if(x.endsWith(G.IDENTITY.SIGNOUT))return"identity.signOut";if(x.endsWith(G.USER.ME))return"user.me";if(x.endsWith(G.USER.VERIFY))return"user.verify"}if(g==="POST"){if(x.endsWith(G.ACTIVITY))return"activity"}return null}function Y(y){return"handle"in y?y.handle:y}function Z(y){return"activity"in y}function W(y){return"user"in y}function K(y){return typeof y==="object"&&y!==null&&"timeback"in y}function F(y){let{timeback:g,callbackPath:x}=K(y)?y:{timeback:y,callbackPath:void 0},C=Y(g);return(I)=>{let J=new URL(I.url).pathname,Q=I.method,f=X({pathname:J,method:Q,callbackPath:x});if(f==="identity.signIn")return C.identity.signIn(I);if(f==="identity.callback")return C.identity.callback(I);if(f==="identity.signOut")return Promise.resolve(C.identity.signOut());if(f==="user.me"){if(!W(C))return Promise.resolve(L({error:"Not found"},404));return C.user.me(I)}if(f==="user.verify"){if(!W(C))return Promise.resolve(L({error:"Not found"},404));return C.user.verify(I)}if(f==="activity"){if(!Z(C))return Promise.resolve(L({error:"Not found"},404));return C.activity(I)}return Promise.resolve(L({error:"Not found"},404))}}export{F as toNativeHandler,D as createTimebackIdentity,G as ROUTES};
1
+ import{a as L,b as G,c as D}from"./chunk-63afdp3y.js";import"./chunk-whc53e0y.js";function V(y){let g=y.trim();if(g==="")return"/";let x=g.indexOf("?");if(x===-1)return g;let C=g.slice(0,x);if(C==="")return"/";return C}function $(y){let g=V(y);if(g!==""&&!g.startsWith("/"))g=`/${g}`;if(g==="/"||g==="")return"";if(g.endsWith("/"))return g.slice(0,-1);return g}function X(y){let g=y.method.toUpperCase(),x=V(y.pathname),C=y.callbackPath?V(y.callbackPath):void 0;if(C&&x===C)return g==="GET"?"identity.callback":null;let I=y.basePath,M=I!==void 0,J=M?$(I):void 0,Q=(f)=>{if(g==="GET"){if(f===G.IDENTITY.SIGNIN)return"identity.signIn";if(f===G.IDENTITY.CALLBACK)return"identity.callback";if(f===G.IDENTITY.SIGNOUT)return"identity.signOut";if(f===G.USER.ME)return"user.me";if(f===G.USER.VERIFY)return"user.verify"}if(g==="POST"){if(f===G.ACTIVITY)return"activity"}return null};if(M&&J!==void 0){if(J!==""&&x===J)return null;if(J!==""&&!x.startsWith(`${J}/`))return null;let f=J===""?x:x.slice(J.length),_=f.startsWith("/")?f:`/${f}`;return Q(_)}if(g==="GET"){if(x.endsWith(G.IDENTITY.SIGNIN))return"identity.signIn";if(x.endsWith(G.IDENTITY.CALLBACK))return"identity.callback";if(x.endsWith(G.IDENTITY.SIGNOUT))return"identity.signOut";if(x.endsWith(G.USER.ME))return"user.me";if(x.endsWith(G.USER.VERIFY))return"user.verify"}if(g==="POST"){if(x.endsWith(G.ACTIVITY))return"activity"}return null}function Y(y){return"handle"in y?y.handle:y}function Z(y){return"activity"in y}function W(y){return"user"in y}function K(y){return typeof y==="object"&&y!==null&&"timeback"in y}function F(y){let{timeback:g,callbackPath:x}=K(y)?y:{timeback:y,callbackPath:void 0},C=Y(g);return(I)=>{let J=new URL(I.url).pathname,Q=I.method,f=X({pathname:J,method:Q,callbackPath:x});if(f==="identity.signIn")return C.identity.signIn(I);if(f==="identity.callback")return C.identity.callback(I);if(f==="identity.signOut")return Promise.resolve(C.identity.signOut());if(f==="user.me"){if(!W(C))return Promise.resolve(L({error:"Not found"},404));return C.user.me(I)}if(f==="user.verify"){if(!W(C))return Promise.resolve(L({error:"Not found"},404));return C.user.verify(I)}if(f==="activity"){if(!Z(C))return Promise.resolve(L({error:"Not found"},404));return C.activity(I)}return Promise.resolve(L({error:"Not found"},404))}}export{F as toNativeHandler,D as createTimebackIdentity,G as ROUTES};
package/dist/identity.js CHANGED
@@ -1 +1 @@
1
- import{c as e}from"./chunk-3886xy48.js";import"./chunk-whc53e0y.js";export{e as createTimebackIdentity};
1
+ import{c as e}from"./chunk-63afdp3y.js";import"./chunk-whc53e0y.js";export{e as createTimebackIdentity};
package/dist/index.d.ts CHANGED
@@ -28,8 +28,8 @@
28
28
  * env: 'production',
29
29
  * identity: {
30
30
  * mode: 'sso',
31
- * clientId: process.env.AWS_COGNITO_CLIENT_ID!,
32
- * clientSecret: process.env.AWS_COGNITO_CLIENT_SECRET!,
31
+ * clientId: process.env.TIMEBACK_SSO_CLIENT_ID!,
32
+ * clientSecret: process.env.TIMEBACK_SSO_CLIENT_SECRET!,
33
33
  * onCallbackSuccess: ({ user, redirect }) => redirect('/'),
34
34
  * onCallbackError: ({ error, redirect }) => redirect('/?error=sso_failed'),
35
35
  * getUser: (req) => getSession(req),