@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.
- package/README.md +6 -6
- package/dist/{chunk-j1xdrfqj.js → chunk-5171mkp2.js} +1 -1
- package/dist/{chunk-3886xy48.js → chunk-63afdp3y.js} +6 -6
- package/dist/chunk-8gg8n8v9.js +2 -0
- package/dist/{chunk-ewsp6v3b.js → chunk-agpf1x3g.js} +5 -5
- package/dist/chunk-hnf0tart.js +2 -0
- package/dist/{chunk-rgbpvxbv.js → chunk-x9gvef7q.js} +1 -1
- package/dist/client/adapters/react/hooks/types.d.ts +34 -0
- package/dist/client/adapters/react/hooks/types.d.ts.map +1 -1
- package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts +17 -5
- package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts.map +1 -1
- package/dist/client/adapters/react/index.d.ts +1 -1
- package/dist/client/adapters/react/index.d.ts.map +1 -1
- package/dist/client/adapters/react/index.js +2 -2
- package/dist/client/adapters/react/provider.d.ts.map +1 -1
- package/dist/client/adapters/solid/primitives/createTimebackVerification.d.ts +3 -1
- package/dist/client/adapters/solid/primitives/createTimebackVerification.d.ts.map +1 -1
- package/dist/client/adapters/solid/primitives/createTimebackVerification.ts +55 -15
- package/dist/client/adapters/solid/types.d.ts +23 -0
- package/dist/client/adapters/solid/types.d.ts.map +1 -1
- package/dist/client/adapters/solid/types.ts +25 -0
- package/dist/client/adapters/svelte/stores/verification.d.ts +1 -1
- package/dist/client/adapters/svelte/stores/verification.d.ts.map +1 -1
- package/dist/client/adapters/svelte/stores/verification.ts +94 -12
- package/dist/client/adapters/vue/composables/useTimebackVerification.d.ts +3 -1
- package/dist/client/adapters/vue/composables/useTimebackVerification.d.ts.map +1 -1
- package/dist/client/adapters/vue/composables/useTimebackVerification.ts +59 -18
- package/dist/client/adapters/vue/types.d.ts +23 -0
- package/dist/client/adapters/vue/types.d.ts.map +1 -1
- package/dist/client/adapters/vue/types.ts +25 -0
- package/dist/client/lib/utils.d.ts +15 -0
- package/dist/client/lib/utils.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/edge.js +1 -1
- package/dist/identity.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -6
- package/dist/server/adapters/express.js +1 -1
- package/dist/server/adapters/native.js +1 -1
- package/dist/server/adapters/nextjs.js +1 -1
- package/dist/server/adapters/nuxt.js +1 -1
- package/dist/server/adapters/solid-start.js +1 -1
- package/dist/server/adapters/svelte-kit.js +1 -1
- package/dist/server/adapters/tanstack-start.js +1 -1
- package/dist/server/timeback.d.ts +2 -2
- package/dist/shared/constants.d.ts +13 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-ahy54f2r.js +0 -2
- 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
|
-
|
|
100
|
-
if (!client) return
|
|
105
|
+
if (!client) return
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
if (force) {
|
|
108
|
+
lastHandledRefreshNonce = nonce
|
|
109
|
+
}
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
let lastError: Error | undefined
|
|
107
112
|
|
|
113
|
+
for (let attempt = 0; attempt <= retryAttempts; attempt++) {
|
|
108
114
|
if (cancelled) return
|
|
109
115
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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;
|
|
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
|
-
|
|
81
|
+
if (isCurrentRun()) {
|
|
82
|
+
verificationStore.set(toVerificationState(cached))
|
|
83
|
+
}
|
|
52
84
|
return
|
|
53
85
|
}
|
|
54
86
|
}
|
|
55
87
|
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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:
|
|
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
|
-
|
|
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;
|
|
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>({
|
|
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
|
-
|
|
104
|
-
if (!timeback) return
|
|
109
|
+
if (!timeback) return
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
if (force) {
|
|
112
|
+
lastHandledRefreshNonce = nonce
|
|
113
|
+
}
|
|
109
114
|
|
|
110
|
-
|
|
115
|
+
let lastError: Error | undefined
|
|
111
116
|
|
|
117
|
+
for (let attempt = 0; attempt <= retryAttempts; attempt++) {
|
|
112
118
|
if (cancelled) return
|
|
113
119
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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{
|
|
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-
|
|
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-
|
|
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.
|
|
32
|
-
* clientSecret: process.env.
|
|
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),
|