@timeback/sdk 0.2.1-beta.20260313200910 → 0.2.1-beta.20260314020510

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 CHANGED
@@ -1,42 +1,27 @@
1
- # Timeback SDK
1
+ # @timeback/sdk
2
2
 
3
3
  TypeScript SDK for integrating Timeback into your application. Provides server-side route handlers and client-side components for activity tracking and SSO authentication.
4
4
 
5
- ## Table of Contents
6
-
7
- - [Installation](#installation)
8
- - [Quick Start](#quick-start)
9
- - [Server Adapters](#server-adapters)
10
- - [Next.js](#nextjs)
11
- - [Nuxt](#nuxt)
12
- - [SvelteKit](#sveltekit)
13
- - [SolidStart](#solidstart)
14
- - [TanStack Start](#tanstack-start)
15
- - [Express](#express)
16
- - [Client Adapters](#client-adapters)
17
- - [React](#react)
18
- - [Vue](#vue)
19
- - [Svelte](#svelte)
20
- - [Solid](#solid)
21
- - [Identity Modes](#identity-modes)
22
- - [Identity-Only Integration](#identity-only-integration)
23
- - [Activity Tracking](#activity-tracking)
24
- - [Advanced: Direct API Access](#advanced-direct-api-access)
25
-
26
5
  ## Installation
27
6
 
28
7
  ```bash
29
- npm install timeback
30
- # or
31
- bun add timeback
8
+ bun add @timeback/sdk
9
+ # npm install @timeback/sdk
32
10
  ```
33
11
 
34
12
  ## Quick Start
35
13
 
36
- 1. Create a server instance with your credentials
37
- 2. Mount the route handlers for your framework
38
- 3. Wrap your app with the client provider
39
- 4. Use hooks/composables to track activities
14
+ ```typescript
15
+ import { createTimeback } from '@timeback/sdk'
16
+
17
+ const timeback = await createTimeback({
18
+ env: 'staging',
19
+ api: { clientId: '...', clientSecret: '...' },
20
+ identity: { mode: 'custom', getEmail: async req => getSession(req)?.email },
21
+ })
22
+ ```
23
+
24
+ Then: (1) create a server instance with your credentials, (2) mount the route handlers for your framework, (3) wrap your app with the client provider, (4) use hooks/composables to track activities.
40
25
 
41
26
  ## Server Adapters
42
27
 
@@ -0,0 +1,2 @@
1
+ import{d as Q,e as B,f as L,h as I,i as j,j as F,m as E,n as S,r as D}from"./chunk-t7pqt5q7.js";class J{params;id;runId;_startedAt;_timeEnabled;_timeOptions;_callbacks;_process;_windowStartMs;_activeSinceMs;_accumulatedActiveMs=0;_totalFlushedActiveMs=0;_isPaused=!1;_ended=!1;_ending=!1;_heartbeatTimer=null;_flushInFlight=null;_boundVisibilityHandler=null;_boundPageHideHandler=null;_hiddenTimeoutTimer=null;_timedOutWhileHidden=!1;constructor(q,X){this.params=q;this._callbacks=X;let z=Date.now();this.id=q.id,this.runId=q.runId??S(),this._process=q.process,this._startedAt=new Date(z),this._windowStartMs=z,this._timeEnabled=q.time!==!1;let $=q.time===!1?{}:q.time??{};if(this._timeOptions={flushIntervalMs:$.flushIntervalMs??B,visibilityAware:$.visibilityAware??!0,flushOnVisibilityHidden:$.flushOnVisibilityHidden??!0,flushOnPageHide:$.flushOnPageHide??!0,hiddenTimeoutMs:$.hiddenTimeoutMs??L,retryAttempts:$.retryAttempts??0,retryDelaysMs:$.retryDelaysMs??[...I]},this._timeEnabled)this._activeSinceMs=z,this._startHeartbeatTimer(),this._setupVisibilityHandlers();else this._activeSinceMs=null}_errorContext(q){return{type:q,activityId:this.params.id,runId:this.runId}}get startedAt(){return this._startedAt}get isPaused(){return this._isPaused}get isEnded(){return this._ended}get elapsedMs(){return this._getCurrentWindowActiveMs()}get totalActiveMs(){return this._totalFlushedActiveMs+this._getCurrentWindowActiveMs()}get activeTimeMs(){return this.totalActiveMs}_getCurrentWindowActiveMs(){let q=this._accumulatedActiveMs;if(this._activeSinceMs!==null)q+=Date.now()-this._activeSinceMs;return Math.max(0,q)}_startHeartbeatTimer(){if(this._heartbeatTimer!==null)return;this._heartbeatTimer=setInterval(()=>{this.flushTimeSpent()},this._timeOptions.flushIntervalMs)}_stopHeartbeatTimer(){if(this._heartbeatTimer!==null)clearInterval(this._heartbeatTimer),this._heartbeatTimer=null}_restartHeartbeatTimer(){this._stopHeartbeatTimer(),this._startHeartbeatTimer()}_setupVisibilityHandlers(){if(typeof document>"u")return;if(this._timeOptions.visibilityAware||this._timeOptions.flushOnVisibilityHidden)this._boundVisibilityHandler=this._handleVisibilityChange.bind(this),document.addEventListener("visibilitychange",this._boundVisibilityHandler);if(this._timeOptions.flushOnPageHide&&typeof window<"u")this._boundPageHideHandler=this._handlePageHide.bind(this),window.addEventListener("pagehide",this._boundPageHideHandler)}_teardownVisibilityHandlers(){if(this._clearHiddenTimeout(),this._boundVisibilityHandler)document.removeEventListener("visibilitychange",this._boundVisibilityHandler),this._boundVisibilityHandler=null;if(this._boundPageHideHandler&&typeof window<"u")window.removeEventListener("pagehide",this._boundPageHideHandler),this._boundPageHideHandler=null}_handleVisibilityChange(){if(this._ended)return;if(document.visibilityState==="hidden"){if(this._timeOptions.visibilityAware&&this._activeSinceMs!==null)this._accumulatedActiveMs+=Date.now()-this._activeSinceMs,this._activeSinceMs=null;if(this._timeOptions.flushOnVisibilityHidden)this.flushTimeSpent();this._startHiddenTimeout()}else{if(this._clearHiddenTimeout(),this._timedOutWhileHidden){this._timedOutWhileHidden=!1;let X=Date.now();if(this._windowStartMs=X,this._accumulatedActiveMs=0,!this._isPaused)this.params.onResume?.()}if(this._restartHeartbeatTimer(),this._timeOptions.visibilityAware&&!this._isPaused&&this._activeSinceMs===null)this._activeSinceMs=Date.now()}}_startHiddenTimeout(){let q=this._timeOptions.hiddenTimeoutMs;if(q===null||!Number.isFinite(q))return;this._clearHiddenTimeout(),this._hiddenTimeoutTimer=setTimeout(()=>{if(this._hiddenTimeoutTimer=null,this._stopHeartbeatTimer(),this._timedOutWhileHidden=!0,!this._isPaused)this.params.onPause?.()},q)}_clearHiddenTimeout(){if(this._hiddenTimeoutTimer!==null)clearTimeout(this._hiddenTimeoutTimer),this._hiddenTimeoutTimer=null}_handlePageHide(){if(this._ended)return;let q=Date.now(),X=this._accumulatedActiveMs;if(this._activeSinceMs!==null)X+=q-this._activeSinceMs;if(X<=0)return;this._flushSync(q,X)}async _flushSync(q,X){let z=this._buildHeartbeatPayload(q,X);if(this._totalFlushedActiveMs+=X,this._windowStartMs=q,this._accumulatedActiveMs=0,this._activeSinceMs!==null)this._activeSinceMs=q;try{await this._callbacks.sendHeartbeatOnPageHide(z),this.params.onFlush?.(X)}catch($){let K=$ instanceof Error?$:Error(String($));this.params.onError?.(K,this._errorContext("timeSpent"))}}_buildHeartbeatPayload(q,X){let z=q-this._windowStartMs-X;return{id:this.params.id,name:this.params.name,course:this.params.course,runId:this.runId,startedAt:new Date(this._windowStartMs).toISOString(),endedAt:new Date(q).toISOString(),elapsedMs:Math.max(0,X),pausedMs:Math.max(0,z)}}async flushTimeSpent(){if(!this._timeEnabled)return;if(this._isPaused&&!this._ended&&!this._ending)return;if(this._flushInFlight){await this._flushInFlight;return}let q=Date.now(),X=this._accumulatedActiveMs;if(this._activeSinceMs!==null)X+=q-this._activeSinceMs;if(q-this._windowStartMs<=0)return;let $=this._buildHeartbeatPayload(q,X);if(this._totalFlushedActiveMs+=X,this._windowStartMs=q,this._accumulatedActiveMs=0,this._activeSinceMs!==null)this._activeSinceMs=q;let{retryAttempts:K,retryDelaysMs:H}=this._timeOptions;this._flushInFlight=E(()=>this._callbacks.sendHeartbeat($),K,H).then(()=>{this.params.onFlush?.(X)}).catch((Y)=>{let O=Y instanceof Error?Y:Error(String(Y));this.params.onError?.(O,this._errorContext("timeSpent"))}).finally(()=>{this._flushInFlight=null}),await this._flushInFlight}pause(){if(this._isPaused||this._ended)return;if(this._activeSinceMs!==null)this._accumulatedActiveMs+=Date.now()-this._activeSinceMs,this._activeSinceMs=null;this.flushTimeSpent(),this._isPaused=!0,this._stopHeartbeatTimer(),this.params.onPause?.()}resume(){if(!this._isPaused||this._ended)return;if(this._isPaused=!1,this._timeEnabled){let q=Date.now();if(this._windowStartMs=q,this._accumulatedActiveMs=0,!this._timeOptions.visibilityAware||document.visibilityState==="visible")this._activeSinceMs=q;this._startHeartbeatTimer()}this.params.onResume?.()}async end(q={}){if(this._ended||this._ending)return;this._ending=!0,this._stopHeartbeatTimer(),this._teardownVisibilityHandlers();let X=this._activeSinceMs!==null,z=this._activeSinceMs;if(this._activeSinceMs!==null)this._accumulatedActiveMs+=Date.now()-this._activeSinceMs,this._activeSinceMs=null;try{if(await this.flushTimeSpent(),q.totalQuestions!==void 0||q.correctQuestions!==void 0||q.xpEarned!==void 0||q.masteredUnits!==void 0||q.pctComplete!==void 0)await this._sendCompletion(q);this._ended=!0,this._ending=!1}catch($){if(this._ending=!1,X)this._activeSinceMs=z;if(this._timeEnabled)this._startHeartbeatTimer(),this._setupVisibilityHandlers();let K=$ instanceof Error?$:Error(String($));throw this.params.onError?.(K,this._errorContext("completion")),$}}async _sendCompletion(q){if(q.xpEarned===void 0)throw Error("Invalid activity completion: xpEarned is required. The SDK cannot auto-calculate XP because total elapsed time may span multiple browser sessions.");let X=new Date,z=q.totalQuestions!==void 0,$=q.correctQuestions!==void 0;if(z!==$)throw Error("Invalid activity metrics: totalQuestions and correctQuestions must be provided together.");if(z&&$&&q.correctQuestions>q.totalQuestions)throw Error("Invalid activity metrics: correctQuestions cannot exceed totalQuestions.");let K={xpEarned:q.xpEarned,...z?{totalQuestions:q.totalQuestions}:{},...$?{correctQuestions:q.correctQuestions}:{},...q.masteredUnits===void 0?{}:{masteredUnits:q.masteredUnits}},H={id:this.params.id,name:this.params.name,course:this.params.course,...this._process===void 0?{}:{process:this._process},runId:this.runId,endedAt:X.toISOString(),metrics:K,...q.pctComplete===void 0?{}:{pctComplete:q.pctComplete}};await this._callbacks.sendSubmit(H)}}function R(){if(!(typeof globalThis>"u"?void 0:globalThis.fetch))return;return(X,z)=>globalThis.fetch(X,z)}function U(q){let{baseURL:X,fetch:z,canUseBeacon:$,credentials:K}=q,H=`${X}${Q.ACTIVITY.HEARTBEAT}`,Y=`${X}${Q.ACTIVITY.SUBMIT}`;function O(N,Z){if(!$)return!1;if(typeof navigator>"u")return!1;if(typeof navigator.sendBeacon!=="function")return!1;try{let G=new Blob([Z],{type:"application/json"});return navigator.sendBeacon(N,G)}catch{return!1}}function _(N,Z,G=!1){return z(N,{method:"POST",headers:{"Content-Type":"application/json"},body:Z,credentials:K,keepalive:G})}return{async sendHeartbeat(N){let Z=JSON.stringify(N),G=await _(H,Z);if(!G.ok){let W=await G.json().catch(()=>({error:{message:"Unknown error"}})),C=typeof W.error==="object"?W.error?.message:W.error;throw Error(C??"Failed to send heartbeat")}},async sendHeartbeatOnPageHide(N){let Z=JSON.stringify(N);if(O(H,Z))return;if(!(await _(H,Z,!0)).ok)throw Error("Failed to send heartbeat on page exit")},async sendSubmit(N){let Z=JSON.stringify(N),G=await _(Y,Z);if(!G.ok){let W=await G.json().catch(()=>({error:{message:"Unknown error"}})),C=typeof W.error==="object"?W.error?.message:W.error;throw Error(C??"Failed to submit activity")}}}}class V{transport;_current=null;constructor(q){this.transport=U(q)}get current(){if(this._current?.isEnded)this._current=null;return this._current}start(q){if(this._current&&!this._current.isEnded)throw Error(`An activity is already active (id: "${this._current.id}"). End it before starting a new one.`);return this._current=new J(q,this.transport),this._current}}class x{getBaseURL;constructor(q){this.getBaseURL=q}signIn(){if(!j())throw Error("signIn() requires a browser environment");window.location.href=`${this.getBaseURL()}${Q.IDENTITY.SIGNIN}`}}class P{getBaseURL;fetchImpl;constructor(q,X){this.getBaseURL=q;this.fetchImpl=X}async fetch(){if(!j())throw Error("user.fetch() requires a browser environment");let q=await this.fetchImpl(`${this.getBaseURL()}${Q.USER.ME}`,{method:"GET",credentials:"include"});if(!q.ok){let X=await q.json().catch(()=>({error:"Unknown error"}));throw Error(X.error??"Failed to fetch user profile")}return q.json()}async verify(){if(!j())throw Error("user.verify() requires a browser environment");let q=await this.fetchImpl(`${this.getBaseURL()}${Q.USER.VERIFY}`,{method:"GET",credentials:"include"});if(!q.ok){let z=await q.json().catch(()=>({error:"Unknown error"}));throw Error(z.error??"Failed to verify Timeback user")}let X=await q.json();if(X.verified&&X.timebackId)return{verified:!0,timebackId:X.timebackId};return{verified:!1}}}class A{activity;auth;lessons;user;_baseURL;_fetch;constructor(q={}){this._baseURL=q.baseURL;let X=q.fetch??R();if(!X)throw Error("TimebackClient requires a fetch implementation. Provide `fetch` in the constructor config for non-browser runtimes.");let z=q.plugins,$=Array.isArray(z)?z:z?[z]:[];this._fetch=$.reduce((K,H)=>H.wrapFetch(K),X),this.activity=new V({baseURL:this.baseURL,fetch:this._fetch,canUseBeacon:$.length===0,credentials:q.credentials??"include"}),this.auth=new x(()=>this.baseURL),this.lessons=new D({getBaseURL:()=>this.baseURL,fetchImpl:this._fetch,activity:this.activity,defaultCourse:q.defaultCourse}),this.user=new P(()=>this.baseURL,this._fetch)}get baseURL(){if(!this._baseURL){let q=F();if(!q)throw Error("Timeback client requires a browser environment for default baseURL. Provide an explicit baseURL for server-side usage.");this._baseURL=q}return this._baseURL}}function Kq(q={}){let X=q.baseURL??F();return new A({baseURL:X,fetch:q.fetch,plugins:q.plugins,credentials:q.credentials,defaultCourse:q.defaultCourse})}
2
+ export{J as a,A as b,Kq as c};
@@ -0,0 +1,2 @@
1
+ var Y="/api/timeback",V={ACTIVITY:{BASE:"/activity",HEARTBEAT:"/activity/heartbeat",SUBMIT:"/activity/submit"},ACTIVITY_HEARTBEAT:"/activity/heartbeat",ACTIVITY_SUBMIT:"/activity/submit",LESSONS:{LIST:"/lessons/list",START:"/lessons/start",NEXT:"/lessons/next",SUBMIT:"/lessons/submit",COMPLETE:"/lessons/complete",ATTEMPTS:"/lessons/attempts",ATTEMPT_DETAILS:"/lessons/attempt-details"},IDENTITY:{SIGNIN:"/identity/signin",SIGNOUT:"/identity/signout",CALLBACK:"/identity/callback"},USER:{ME:"/user/me",VERIFY:"/user/verify"}},_=15000,A=600000,C=3,g=[100,300,1000];function W(){return typeof window<"u"}function D(){if(!W())return;return`${window.location.origin}${Y}`}function H(j){return new Promise((G)=>{setTimeout(G,j)})}function J(j,G){if(typeof j==="number")return j;return j[Math.min(G,j.length-1)]??1000}async function I(j,G,K){let M;for(let N=0;N<=G;N++)try{return await j()}catch(X){if(M=X,N<G)await H(J(K,N))}throw M instanceof Error?M:Error(String(M))}function Q(){if(typeof crypto<"u"&&crypto.randomUUID)return crypto.randomUUID();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(j)=>{let G=Math.random()*16|0;return(j==="x"?G:G&3|8).toString(16)})}async function P(j){return await j.json().catch(()=>({error:"Unknown error"}))}function z(j){return"questions"in j&&Array.isArray(j.questions)}function Z(j){if(!j||typeof j!=="object")return{id:"unknown",content:{rawXml:""}};let G=j;return{...G,id:typeof G.id==="string"?G.id:"unknown",title:typeof G.title==="string"?G.title:void 0,difficulty:typeof G.difficulty==="string"?G.difficulty:void 0,content:G.content&&typeof G.content==="object"?G.content??{rawXml:""}:{rawXml:""}}}function f(j){return j.lessonType==="powerpath-100"?j.seenQuestions:j.questions}class ${_questionBuffer=[];_answeredIds=new Set;_deps;_completed;lessonId;lessonType;attempt;questionCount;seenQuestions;score;finalized;constructor(j,G){this._deps=j,this.lessonId=G.lessonId,this.lessonType=G.lessonType,this.attempt=G.attempt,this.score=G.score,this.questionCount=G.questionCount,this.seenQuestions=G.seenQuestions,this.finalized=G.finalized,this._completed=G.finalized}async next(){if(this._completed)return null;if(this.lessonType!=="powerpath-100"){if(this._questionBuffer.length===0){let K=await this._deps.fetchJson(V.LESSONS.NEXT,{method:"POST",body:{lessonId:this.lessonId,lessonType:this.lessonType}});if(!z(K))throw Error("Unexpected response shape from lessons.next — expected a batch result with questions array");this._questionBuffer.push(...K.questions.map(Z));for(let M of K.answeredIds)this._answeredIds.add(M);if(K.complete)this._completed=!0}let G=this._questionBuffer.find((K)=>!this._answeredIds.has(K.id));if(!G)return null;return G}let j=await this._deps.fetchJson(V.LESSONS.NEXT,{method:"POST",body:{lessonId:this.lessonId,lessonType:this.lessonType}});if(j.complete===!0&&(!j.id||j.id==="unknown"))return this._completed=!0,null;return Z(j)}async submit(j){let G=await this._deps.fetchJson(V.LESSONS.SUBMIT,{method:"POST",body:{lessonId:this.lessonId,questionId:j.question,response:j.response,lessonType:this.lessonType}});if(this._answeredIds.add(j.question),this.score=G.score,G.complete)this._completed=!0;return G}async complete(){if(this.finalized)throw Error("Lesson session has already been completed");let j;try{j=await this._deps.fetchJson(V.LESSONS.COMPLETE,{method:"POST",body:{lessonId:this.lessonId,lessonType:this.lessonType}})}catch(N){try{await this._deps.activity.end()}catch{}throw N}let G=Math.max(0,Math.round(this._deps.activity.activeTimeMs/1000)),K=!0,M;try{await this._deps.activity.end()}catch(N){K=!1,M=N instanceof Error?N.message:"Failed to flush final time tracking"}return this._completed=!0,this.score=j.score,this.finalized=j.finalized,{...j,lessonType:this.lessonType,timeSpentSeconds:G,timeTrackingSent:K,error:M}}pause(){this._deps.activity.pause()}resume(){this._deps.activity.resume()}}class F{config;constructor(j){this.config=j}async fetchJson(j,G){let K=await this.config.fetchImpl(`${this.config.getBaseURL()}${j}`,{method:G.method,credentials:"include",headers:{"Content-Type":"application/json"},body:G.body?JSON.stringify(G.body):void 0});if(!K.ok){let M=await P(K),N=typeof M.error==="string"&&M.error||typeof M.error==="object"&&M.error!==null&&"message"in M.error&&typeof M.error.message==="string"&&M.error.message||"Request failed";throw Error(N)}return await K.json()}async list(j={}){if(!W())throw Error("lessons.list() requires a browser environment");return(await this.fetchJson(V.LESSONS.LIST,{method:"POST",body:j.course?{course:j.course}:{}})).lessons}async get(j){return(await this.list()).find((K)=>K.id===j)??null}async start(j){if(!W())throw Error("lessons.start() requires a browser environment");let G=await this.fetchJson(V.LESSONS.START,{method:"POST",body:{lessonId:j.lessonId,forceNew:j.forceNew??!1,lessonType:j.lessonType}}),K=j.course??this.config.defaultCourse;if(!K)throw Error("lessons.start() requires a `course` unless a client-level `defaultCourse` is configured.");let M=this.config.activity.start({id:j.lessonId,name:j.name??G.lessonType??j.lessonId,course:K});return new $({fetchJson:(N,X)=>this.fetchJson(N,X),activity:M},{...G,lessonType:G.lessonType??j.lessonType??"quiz"})}async attempts(j){return(await this.fetchJson(V.LESSONS.ATTEMPTS,{method:"POST",body:{lessonId:j.lessonId}})).attempts}async attemptDetails(j){return await this.fetchJson(V.LESSONS.ATTEMPT_DETAILS,{method:"POST",body:{lessonId:j.lessonId,attempt:j.attempt}})}}
2
+ export{V as d,_ as e,A as f,C as g,g as h,W as i,D as j,H as k,J as l,I as m,Q as n,Z as o,f as p,$ as q,F as r};
@@ -1,4 +1,4 @@
1
- import{a as h,b as o,c as l,d as r,e as QK,j as S,k as UK}from"../../../chunk-2kfad25a.js";import*as D from"react";import{jsx as a}from"react/jsx-runtime";var p=D.createContext(void 0),R;function c(){if(!R)R=new S;return R}function i({client:K,children:J}){let[M,q]=D.useState(void 0);D.useEffect(()=>{if(!K&&!M)q(c())},[K,M]);let Q=K??M;return a(p.Provider,{value:Q,children:J})}function w(){return D.useContext(p)}import*as _ from"react";var T=new WeakMap,F=new WeakMap,j=new WeakMap,N=new WeakMap;function k(K){return!!K&&Date.now()-K.atMs<1500}function m(K){return!!K&&Date.now()-K.atMs<5000}async function b(K,J){if(!J){let q=F.get(K);if(k(q))return q.result;let Q=T.get(K);if(Q)return await Q}let M=K.user.verify().then((q)=>{if(T.get(K)===M)F.set(K,{atMs:Date.now(),result:q});return q});T.set(K,M);try{return await M}finally{if(T.get(K)===M)T.delete(K)}}function u(K){let J=F.get(K);return k(J)?J.result:void 0}async function d(K,J){if(!J){let q=N.get(K);if(m(q))return q.profile;let Q=j.get(K);if(Q)return await Q}let M=K.user.fetch().then((q)=>{if(j.get(K)===M)N.set(K,{atMs:Date.now(),profile:q});return q});j.set(K,M);try{return await M}finally{if(j.get(K)===M)j.delete(K)}}function v(K){let J=N.get(K);return m(J)?J.profile:void 0}function f(K){return K.verified?{status:"verified",timebackId:K.timebackId}:{status:"unverified"}}function C(K={}){let J=w(),M=K.enabled??!0,q=K.retryAttempts??h,Q=K.retryDelays??o,[L,Z]=_.useState({status:"loading"}),[G,H]=_.useState(0),V=_.useRef(0),E=_.useCallback(()=>{H((U)=>U+1)},[]);return _.useEffect(()=>{if(!M)return;let U=!1,A=G>V.current;if(J&&!A){let $=u(J);if($)return Z(f($)),()=>{U=!0}}return Z({status:"loading"}),(async()=>{if(!J)return;if(A)V.current=G;let $;for(let Y=0;Y<=q;Y++){if(U)return;if(Y>0){let X=r(Q,Y-1);if(await l(X),U)return}try{let X=await b(J,A||Y>0);if(U)return;Z(f(X));return}catch(X){$=X instanceof Error?X:Error("Failed to verify Timeback user")}}if(!U&&$)Z({status:"error",message:$.message})})(),()=>{U=!0}},[M,G,q,Q,J]),{state:L,refresh:E}}import*as z from"react";function t(K={}){let{enabled:J=!0,auto:M=!1}=K,q=w(),{state:Q}=C({enabled:J}),[L,Z]=z.useState({status:"idle"}),[G,H]=z.useState(0),[V,E]=z.useState(0),U=z.useRef(0),A=z.useRef(0),$=J&&Q.status==="verified"&&!!q,Y=z.useCallback(()=>{if(!$)return;H((O)=>O+1)},[$]),X=z.useCallback(()=>{if(!$)return;E((O)=>O+1)},[$]),P=z.useRef(!1);return z.useEffect(()=>{if(!J){Z({status:"idle"});return}if(Q.status!=="verified"){Z((W)=>W.status==="idle"?W:{status:"idle"}),P.current=!1;return}if(M&&!P.current&&G===0){P.current=!0,H(1);return}if(G===0&&V===0)return;let O=!1,n=G>U.current,y=V>A.current,g=y;if(!n&&!y){if(q){let W=v(q);if(W)return Z({status:"loaded",profile:W}),()=>{O=!0}}return}if(q&&!g){let W=v(q);if(W)return Z({status:"loaded",profile:W}),U.current=G,()=>{O=!0}}return Z({status:"loading"}),(async()=>{try{if(!q)return;U.current=G,A.current=V;let W=await d(q,g);if(O)return;Z({status:"loaded",profile:W})}catch(W){if(!O)Z({status:"error",message:W instanceof Error?W.message:"Failed to fetch profile"})}})(),()=>{O=!0}},[J,Q.status,G,V,q,M]),{state:L,canFetch:$,fetchProfile:Y,refresh:X}}import*as I from"react";import{jsx as B,jsxs as x}from"react/jsx-runtime";function e({className:K}){return x("svg",{className:K,width:"20",height:"18",viewBox:"0 0 199 180",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",children:[x("g",{clipPath:"url(#tb-logo-clip)",children:[B("path",{d:"M121.432 179.456C110.607 177.871 101.153 173.91 92.4882 168.085C91.1274 167.17 89.4598 165.404 89.4375 164.008C89.2123 149.891 89.3002 135.77 89.3002 119.86C93.1365 123.255 95.8538 125.706 98.6224 128.099C103.151 132.012 107.437 136.28 112.314 139.703C126.466 149.635 145.037 147.25 156.045 134.448C166.919 121.803 167.226 101.374 155.695 89.6399C145.727 79.4958 134.693 70.3995 123.251 60.0438C123.251 77.8871 123.251 94.5637 123.251 112.15C121.428 110.999 120.279 110.479 119.383 109.676C110.329 101.573 101.241 93.5044 92.3634 85.2109C90.8519 83.7988 89.4363 81.3758 89.4209 79.4084C89.2249 54.4218 89.2905 29.4332 89.3061 4.44502C89.3066 3.64954 89.3061 2.22136 89.3061 1.47755C90.8061 1.47755 92.694 1.47857 94.1427 1.47755C118.131 1.46071 142.12 1.52088 166.108 1.39821C169.235 1.38222 171.33 2.18661 173.251 4.75826C180.607 14.6032 188.189 24.279 196.266 34.7705C179.79 34.7705 164.169 34.7705 148.548 34.7705C147.871 34.7705 147.366 34.7705 146.866 34.7705C147.866 35.7705 150.35 38.3018 151.61 39.4825C162.643 49.8249 174.663 59.3807 184.245 70.947C202.474 92.9508 203.539 118.134 191.204 143.046C178.598 168.508 156.61 180.302 128.298 180.295C126.154 180.295 124.011 179.778 121.432 179.456Z",fill:"currentColor"}),B("circle",{cx:"40",cy:"133",r:"39",fill:"currentColor"}),B("circle",{cx:"39",cy:"39",r:"39",fill:"currentColor"})]}),B("defs",{children:B("clipPath",{id:"tb-logo-clip",children:B("rect",{width:"199",height:"180",fill:"currentColor"})})})]})}function KK({className:K}){return x("svg",{className:K,width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",children:[B("circle",{cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"2",strokeOpacity:"0.25"}),B("path",{d:"M12 2C6.48 2 2 6.48 2 12",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round"})]})}var qK=`
1
+ import{a as QK,b as S,c as UK}from"../../../chunk-3vemnwy2.js";import{g as h,h as o,k as l,l as r}from"../../../chunk-t7pqt5q7.js";import*as D from"react";import{jsx as a}from"react/jsx-runtime";var p=D.createContext(void 0),R;function c(){if(!R)R=new S;return R}function i({client:K,children:J}){let[M,q]=D.useState(void 0);D.useEffect(()=>{if(!K&&!M)q(c())},[K,M]);let Q=K??M;return a(p.Provider,{value:Q,children:J})}function w(){return D.useContext(p)}import*as _ from"react";var T=new WeakMap,F=new WeakMap,j=new WeakMap,N=new WeakMap;function k(K){return!!K&&Date.now()-K.atMs<1500}function m(K){return!!K&&Date.now()-K.atMs<5000}async function b(K,J){if(!J){let q=F.get(K);if(k(q))return q.result;let Q=T.get(K);if(Q)return await Q}let M=K.user.verify().then((q)=>{if(T.get(K)===M)F.set(K,{atMs:Date.now(),result:q});return q});T.set(K,M);try{return await M}finally{if(T.get(K)===M)T.delete(K)}}function u(K){let J=F.get(K);return k(J)?J.result:void 0}async function d(K,J){if(!J){let q=N.get(K);if(m(q))return q.profile;let Q=j.get(K);if(Q)return await Q}let M=K.user.fetch().then((q)=>{if(j.get(K)===M)N.set(K,{atMs:Date.now(),profile:q});return q});j.set(K,M);try{return await M}finally{if(j.get(K)===M)j.delete(K)}}function v(K){let J=N.get(K);return m(J)?J.profile:void 0}function f(K){return K.verified?{status:"verified",timebackId:K.timebackId}:{status:"unverified"}}function C(K={}){let J=w(),M=K.enabled??!0,q=K.retryAttempts??h,Q=K.retryDelays??o,[L,Z]=_.useState({status:"loading"}),[G,H]=_.useState(0),V=_.useRef(0),E=_.useCallback(()=>{H((U)=>U+1)},[]);return _.useEffect(()=>{if(!M)return;let U=!1,A=G>V.current;if(J&&!A){let $=u(J);if($)return Z(f($)),()=>{U=!0}}return Z({status:"loading"}),(async()=>{if(!J)return;if(A)V.current=G;let $;for(let Y=0;Y<=q;Y++){if(U)return;if(Y>0){let X=r(Q,Y-1);if(await l(X),U)return}try{let X=await b(J,A||Y>0);if(U)return;Z(f(X));return}catch(X){$=X instanceof Error?X:Error("Failed to verify Timeback user")}}if(!U&&$)Z({status:"error",message:$.message})})(),()=>{U=!0}},[M,G,q,Q,J]),{state:L,refresh:E}}import*as z from"react";function t(K={}){let{enabled:J=!0,auto:M=!1}=K,q=w(),{state:Q}=C({enabled:J}),[L,Z]=z.useState({status:"idle"}),[G,H]=z.useState(0),[V,E]=z.useState(0),U=z.useRef(0),A=z.useRef(0),$=J&&Q.status==="verified"&&!!q,Y=z.useCallback(()=>{if(!$)return;H((O)=>O+1)},[$]),X=z.useCallback(()=>{if(!$)return;E((O)=>O+1)},[$]),P=z.useRef(!1);return z.useEffect(()=>{if(!J){Z({status:"idle"});return}if(Q.status!=="verified"){Z((W)=>W.status==="idle"?W:{status:"idle"}),P.current=!1;return}if(M&&!P.current&&G===0){P.current=!0,H(1);return}if(G===0&&V===0)return;let O=!1,n=G>U.current,y=V>A.current,g=y;if(!n&&!y){if(q){let W=v(q);if(W)return Z({status:"loaded",profile:W}),()=>{O=!0}}return}if(q&&!g){let W=v(q);if(W)return Z({status:"loaded",profile:W}),U.current=G,()=>{O=!0}}return Z({status:"loading"}),(async()=>{try{if(!q)return;U.current=G,A.current=V;let W=await d(q,g);if(O)return;Z({status:"loaded",profile:W})}catch(W){if(!O)Z({status:"error",message:W instanceof Error?W.message:"Failed to fetch profile"})}})(),()=>{O=!0}},[J,Q.status,G,V,q,M]),{state:L,canFetch:$,fetchProfile:Y,refresh:X}}import*as I from"react";import{jsx as B,jsxs as x}from"react/jsx-runtime";function e({className:K}){return x("svg",{className:K,width:"20",height:"18",viewBox:"0 0 199 180",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",children:[x("g",{clipPath:"url(#tb-logo-clip)",children:[B("path",{d:"M121.432 179.456C110.607 177.871 101.153 173.91 92.4882 168.085C91.1274 167.17 89.4598 165.404 89.4375 164.008C89.2123 149.891 89.3002 135.77 89.3002 119.86C93.1365 123.255 95.8538 125.706 98.6224 128.099C103.151 132.012 107.437 136.28 112.314 139.703C126.466 149.635 145.037 147.25 156.045 134.448C166.919 121.803 167.226 101.374 155.695 89.6399C145.727 79.4958 134.693 70.3995 123.251 60.0438C123.251 77.8871 123.251 94.5637 123.251 112.15C121.428 110.999 120.279 110.479 119.383 109.676C110.329 101.573 101.241 93.5044 92.3634 85.2109C90.8519 83.7988 89.4363 81.3758 89.4209 79.4084C89.2249 54.4218 89.2905 29.4332 89.3061 4.44502C89.3066 3.64954 89.3061 2.22136 89.3061 1.47755C90.8061 1.47755 92.694 1.47857 94.1427 1.47755C118.131 1.46071 142.12 1.52088 166.108 1.39821C169.235 1.38222 171.33 2.18661 173.251 4.75826C180.607 14.6032 188.189 24.279 196.266 34.7705C179.79 34.7705 164.169 34.7705 148.548 34.7705C147.871 34.7705 147.366 34.7705 146.866 34.7705C147.866 35.7705 150.35 38.3018 151.61 39.4825C162.643 49.8249 174.663 59.3807 184.245 70.947C202.474 92.9508 203.539 118.134 191.204 143.046C178.598 168.508 156.61 180.302 128.298 180.295C126.154 180.295 124.011 179.778 121.432 179.456Z",fill:"currentColor"}),B("circle",{cx:"40",cy:"133",r:"39",fill:"currentColor"}),B("circle",{cx:"39",cy:"39",r:"39",fill:"currentColor"})]}),B("defs",{children:B("clipPath",{id:"tb-logo-clip",children:B("rect",{width:"199",height:"180",fill:"currentColor"})})})]})}function KK({className:K}){return x("svg",{className:K,width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",children:[B("circle",{cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"2",strokeOpacity:"0.25"}),B("path",{d:"M12 2C6.48 2 2 6.48 2 12",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round"})]})}var qK=`
2
2
  .timeback-signin-btn {
3
3
  /* Colors */
4
4
  --tb-btn-bg: #0f172a;
package/dist/client.js CHANGED
@@ -1 +1 @@
1
- import{e as m,f as n,g as l,h as a,i as L,j as y,k as x}from"./chunk-2kfad25a.js";function o(e){let i=e.headerName??"Authorization",b=e.prefix??"Bearer ";return{wrapFetch(p){return async(t,r)=>{let c=await e.getToken();if(!c)throw Error("Missing bearer token (are you authenticated yet?)");let s=new Headers(r?.headers??(t instanceof Request?t.headers:void 0));if(!s.has(i))s.set(i,`${b}${c}`);return p(t,{...r,headers:s})}}}}export{n as normalizeLessonQuestion,l as getLessonAttemptQuestions,x as createClient,o as bearer,y as TimebackClient,L as Lessons,a as LessonSession,m as Activity};
1
+ import{a as m,b as y,c as x}from"./chunk-3vemnwy2.js";import{o as n,p as l,q as a,r as L}from"./chunk-t7pqt5q7.js";function o(e){let i=e.headerName??"Authorization",b=e.prefix??"Bearer ";return{wrapFetch(p){return async(t,r)=>{let c=await e.getToken();if(!c)throw Error("Missing bearer token (are you authenticated yet?)");let s=new Headers(r?.headers??(t instanceof Request?t.headers:void 0));if(!s.has(i))s.set(i,`${b}${c}`);return p(t,{...r,headers:s})}}}}export{n as normalizeLessonQuestion,l as getLessonAttemptQuestions,x as createClient,o as bearer,y as TimebackClient,L as Lessons,a as LessonSession,m as Activity};
@@ -0,0 +1,62 @@
1
+ import type { LessonAttemptQuestion, LessonQuestion } from '../shared/types';
2
+ import type { ParsedQuizQuestion, ParsedReviewQuestion } from './types';
3
+ /**
4
+ * QTI Utilities
5
+ *
6
+ * Re-exports pure QTI XML parsing utilities from `@timeback/core/qti` and
7
+ * adds SDK-specific helpers that map lesson types to parsed QTI output.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // Pure parsing (no SDK types needed)
12
+ * import { parseQtiXml } from '@timeback/sdk/qti'
13
+ *
14
+ * // SDK helpers (for Managed Lessons flows)
15
+ * import { toQuizQuestion, toReviewQuestions } from '@timeback/sdk/qti'
16
+ * ```
17
+ */
18
+ export { extractChoices, extractCorrectResponse, extractFeedback, extractInteractionAttributes, extractModalFeedback, extractPrompt, extractResponseDeclaration, parseQtiXml, } from '@timeback/core/qti';
19
+ export type { ParsedQtiItem, QtiChoice, QtiInlineFeedback, QtiInteractionAttributes, QtiModalFeedback, QtiResponseDeclaration, } from '@timeback/core/qti';
20
+ export type { ParsedQuizQuestion, ParsedReviewQuestion } from './types';
21
+ /**
22
+ * Convert a lesson question from `session.next()` into a parsed quiz question.
23
+ *
24
+ * Normalizes the SDK question shape, parses the embedded QTI XML, and falls
25
+ * back to placeholder choices if parsing yields none.
26
+ *
27
+ * @param question - A `LessonQuestion` from the SDK lesson runtime
28
+ * @returns Parsed quiz question ready for UI rendering
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * import { toQuizQuestion } from '@timeback/sdk/qti'
33
+ *
34
+ * const next = await session.next()
35
+ * if (next) {
36
+ * const question = toQuizQuestion(next)
37
+ * // question.prompt, question.choices, question.id, question.difficulty
38
+ * }
39
+ * ```
40
+ */
41
+ export declare function toQuizQuestion(question: LessonQuestion): ParsedQuizQuestion;
42
+ /**
43
+ * Convert lesson attempt questions into parsed review questions.
44
+ *
45
+ * Parses each question's QTI XML and maps SDK response/outcome data to
46
+ * determine the user's answer and correctness.
47
+ *
48
+ * @param rawQuestions - Array of `LessonAttemptQuestion` from attempt details
49
+ * @returns Parsed review questions with answer and correctness data
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * import { toReviewQuestions } from '@timeback/sdk/qti'
54
+ * import { getLessonAttemptQuestions } from '@timeback/sdk/client'
55
+ *
56
+ * const details = await timeback.lessons.attemptDetails({ lessonId, attempt })
57
+ * const questions = toReviewQuestions(getLessonAttemptQuestions(details))
58
+ * // questions[0].userAnswer, questions[0].correctAnswer, questions[0].isCorrect
59
+ * ```
60
+ */
61
+ export declare function toReviewQuestions(rawQuestions: LessonAttemptQuestion[]): ParsedReviewQuestion[];
62
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/qti/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAC5E,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEvE;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EACN,cAAc,EACd,sBAAsB,EACtB,eAAe,EACf,4BAA4B,EAC5B,oBAAoB,EACpB,aAAa,EACb,0BAA0B,EAC1B,WAAW,GACX,MAAM,oBAAoB,CAAA;AAE3B,YAAY,EACX,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,wBAAwB,EACxB,gBAAgB,EAChB,sBAAsB,GACtB,MAAM,oBAAoB,CAAA;AAE3B,YAAY,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAavE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,kBAAkB,CAY3E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,qBAAqB,EAAE,GAAG,oBAAoB,EAAE,CA0B/F"}
@@ -0,0 +1 @@
1
+ import{o as V}from"../chunk-t7pqt5q7.js";import{extractChoices as W,extractCorrectResponse as K,extractPrompt as Y}from"@timeback/core/qti";import{extractChoices as S,extractCorrectResponse as D,extractFeedback as E,extractInteractionAttributes as I,extractModalFeedback as f,extractPrompt as R,extractResponseDeclaration as b,parseQtiXml as v}from"@timeback/core/qti";var M=[{identifier:"A",text:"Choice A"},{identifier:"B",text:"Choice B"},{identifier:"C",text:"Choice C"},{identifier:"D",text:"Choice D"}];function g(J){let j=V(J),F=j.content?.rawXml??"",B=Y(F),G=W(F);return{id:j.id||"unknown",prompt:B||j.title||"Question",choices:G.length>0?G:[...M],difficulty:j.difficulty||void 0}}function y(J){return J.map((j,F)=>{let B=j.content?.rawXml??"",G=Y(B),Z=W(B),$=K(B),N=null;if(j.response)N=Array.isArray(j.response)?j.response[0]??null:j.response;else if(j.responses?.RESPONSE){let U=j.responses.RESPONSE;N=Array.isArray(U)?U[0]??null:U}return{id:j.id??`question-${F+1}`,prompt:G||j.title||"Question",choices:Z,difficulty:j.difficulty,userAnswer:N,correctAnswer:$??j.result?.outcomes?.CORRECT_RESPONSE??null,isCorrect:j.correct===!0||j.result?.score===1}})}export{y as toReviewQuestions,g as toQuizQuestion,v as parseQtiXml,b as extractResponseDeclaration,R as extractPrompt,f as extractModalFeedback,I as extractInteractionAttributes,E as extractFeedback,D as extractCorrectResponse,S as extractChoices};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * QTI SDK Integration Types
3
+ *
4
+ * Type definitions for SDK-level QTI helpers.
5
+ */
6
+ import type { QtiChoice } from '@timeback/core/qti';
7
+ /** A quiz question parsed from QTI XML with SDK metadata. */
8
+ export interface ParsedQuizQuestion {
9
+ id: string;
10
+ prompt: string;
11
+ choices: QtiChoice[];
12
+ difficulty?: string;
13
+ }
14
+ /** A review question with user response and correctness data. */
15
+ export interface ParsedReviewQuestion extends ParsedQuizQuestion {
16
+ userAnswer: string | null;
17
+ correctAnswer: string | null;
18
+ isCorrect: boolean;
19
+ }
20
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/qti/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnD,6DAA6D;AAC7D,MAAM,WAAW,kBAAkB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,iEAAiE;AACjE,MAAM,WAAW,oBAAqB,SAAQ,kBAAkB;IAC/D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,OAAO,CAAA;CAClB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timeback/sdk",
3
- "version": "0.2.1-beta.20260313200910",
3
+ "version": "0.2.1-beta.20260314020510",
4
4
  "description": "Timeback SDK for frontend and backend integration",
5
5
  "type": "module",
6
6
  "exports": {
@@ -61,6 +61,10 @@
61
61
  "./nuxt": {
62
62
  "types": "./dist/server/adapters/nuxt.d.ts",
63
63
  "import": "./dist/server/adapters/nuxt.js"
64
+ },
65
+ "./qti": {
66
+ "types": "./dist/qti/index.d.ts",
67
+ "import": "./dist/qti/index.js"
64
68
  }
65
69
  },
66
70
  "main": "dist/index.js",
@@ -1,2 +0,0 @@
1
- var B="/api/timeback",N={ACTIVITY:{BASE:"/activity",HEARTBEAT:"/activity/heartbeat",SUBMIT:"/activity/submit"},ACTIVITY_HEARTBEAT:"/activity/heartbeat",ACTIVITY_SUBMIT:"/activity/submit",LESSONS:{LIST:"/lessons/list",START:"/lessons/start",NEXT:"/lessons/next",SUBMIT:"/lessons/submit",COMPLETE:"/lessons/complete",ATTEMPTS:"/lessons/attempts",ATTEMPT_DETAILS:"/lessons/attempt-details"},IDENTITY:{SIGNIN:"/identity/signin",SIGNOUT:"/identity/signout",CALLBACK:"/identity/callback"},USER:{ME:"/user/me",VERIFY:"/user/verify"}},I=15000,L=600000,v=3,E=[100,300,1000];function H(){return typeof window<"u"}function P(){if(!H())return;return`${window.location.origin}${B}`}function R(X){return new Promise(($)=>{setTimeout($,X)})}function h(X,$){if(typeof X==="number")return X;return X[Math.min($,X.length-1)]??1000}async function k(X,$,Z){let K;for(let G=0;G<=$;G++)try{return await X()}catch(W){if(K=W,G<$)await R(h(Z,G))}throw K instanceof Error?K:Error(String(K))}function S(){if(typeof crypto<"u"&&crypto.randomUUID)return crypto.randomUUID();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(X)=>{let $=Math.random()*16|0;return(X==="x"?$:$&3|8).toString(16)})}class Q{params;id;runId;_startedAt;_timeEnabled;_timeOptions;_callbacks;_process;_windowStartMs;_activeSinceMs;_accumulatedActiveMs=0;_totalFlushedActiveMs=0;_isPaused=!1;_ended=!1;_ending=!1;_heartbeatTimer=null;_flushInFlight=null;_boundVisibilityHandler=null;_boundPageHideHandler=null;_hiddenTimeoutTimer=null;_timedOutWhileHidden=!1;constructor(X,$){this.params=X;this._callbacks=$;let Z=Date.now();this.id=X.id,this.runId=X.runId??S(),this._process=X.process,this._startedAt=new Date(Z),this._windowStartMs=Z,this._timeEnabled=X.time!==!1;let K=X.time===!1?{}:X.time??{};if(this._timeOptions={flushIntervalMs:K.flushIntervalMs??I,visibilityAware:K.visibilityAware??!0,flushOnVisibilityHidden:K.flushOnVisibilityHidden??!0,flushOnPageHide:K.flushOnPageHide??!0,hiddenTimeoutMs:K.hiddenTimeoutMs??L,retryAttempts:K.retryAttempts??0,retryDelaysMs:K.retryDelaysMs??[...E]},this._timeEnabled)this._activeSinceMs=Z,this._startHeartbeatTimer(),this._setupVisibilityHandlers();else this._activeSinceMs=null}_errorContext(X){return{type:X,activityId:this.params.id,runId:this.runId}}get startedAt(){return this._startedAt}get isPaused(){return this._isPaused}get isEnded(){return this._ended}get elapsedMs(){return this._getCurrentWindowActiveMs()}get totalActiveMs(){return this._totalFlushedActiveMs+this._getCurrentWindowActiveMs()}get activeTimeMs(){return this.totalActiveMs}_getCurrentWindowActiveMs(){let X=this._accumulatedActiveMs;if(this._activeSinceMs!==null)X+=Date.now()-this._activeSinceMs;return Math.max(0,X)}_startHeartbeatTimer(){if(this._heartbeatTimer!==null)return;this._heartbeatTimer=setInterval(()=>{this.flushTimeSpent()},this._timeOptions.flushIntervalMs)}_stopHeartbeatTimer(){if(this._heartbeatTimer!==null)clearInterval(this._heartbeatTimer),this._heartbeatTimer=null}_restartHeartbeatTimer(){this._stopHeartbeatTimer(),this._startHeartbeatTimer()}_setupVisibilityHandlers(){if(typeof document>"u")return;if(this._timeOptions.visibilityAware||this._timeOptions.flushOnVisibilityHidden)this._boundVisibilityHandler=this._handleVisibilityChange.bind(this),document.addEventListener("visibilitychange",this._boundVisibilityHandler);if(this._timeOptions.flushOnPageHide&&typeof window<"u")this._boundPageHideHandler=this._handlePageHide.bind(this),window.addEventListener("pagehide",this._boundPageHideHandler)}_teardownVisibilityHandlers(){if(this._clearHiddenTimeout(),this._boundVisibilityHandler)document.removeEventListener("visibilitychange",this._boundVisibilityHandler),this._boundVisibilityHandler=null;if(this._boundPageHideHandler&&typeof window<"u")window.removeEventListener("pagehide",this._boundPageHideHandler),this._boundPageHideHandler=null}_handleVisibilityChange(){if(this._ended)return;if(document.visibilityState==="hidden"){if(this._timeOptions.visibilityAware&&this._activeSinceMs!==null)this._accumulatedActiveMs+=Date.now()-this._activeSinceMs,this._activeSinceMs=null;if(this._timeOptions.flushOnVisibilityHidden)this.flushTimeSpent();this._startHiddenTimeout()}else{if(this._clearHiddenTimeout(),this._timedOutWhileHidden){this._timedOutWhileHidden=!1;let $=Date.now();if(this._windowStartMs=$,this._accumulatedActiveMs=0,!this._isPaused)this.params.onResume?.()}if(this._restartHeartbeatTimer(),this._timeOptions.visibilityAware&&!this._isPaused&&this._activeSinceMs===null)this._activeSinceMs=Date.now()}}_startHiddenTimeout(){let X=this._timeOptions.hiddenTimeoutMs;if(X===null||!Number.isFinite(X))return;this._clearHiddenTimeout(),this._hiddenTimeoutTimer=setTimeout(()=>{if(this._hiddenTimeoutTimer=null,this._stopHeartbeatTimer(),this._timedOutWhileHidden=!0,!this._isPaused)this.params.onPause?.()},X)}_clearHiddenTimeout(){if(this._hiddenTimeoutTimer!==null)clearTimeout(this._hiddenTimeoutTimer),this._hiddenTimeoutTimer=null}_handlePageHide(){if(this._ended)return;let X=Date.now(),$=this._accumulatedActiveMs;if(this._activeSinceMs!==null)$+=X-this._activeSinceMs;if($<=0)return;this._flushSync(X,$)}async _flushSync(X,$){let Z=this._buildHeartbeatPayload(X,$);if(this._totalFlushedActiveMs+=$,this._windowStartMs=X,this._accumulatedActiveMs=0,this._activeSinceMs!==null)this._activeSinceMs=X;try{await this._callbacks.sendHeartbeatOnPageHide(Z),this.params.onFlush?.($)}catch(K){let G=K instanceof Error?K:Error(String(K));this.params.onError?.(G,this._errorContext("timeSpent"))}}_buildHeartbeatPayload(X,$){let Z=X-this._windowStartMs-$;return{id:this.params.id,name:this.params.name,course:this.params.course,runId:this.runId,startedAt:new Date(this._windowStartMs).toISOString(),endedAt:new Date(X).toISOString(),elapsedMs:Math.max(0,$),pausedMs:Math.max(0,Z)}}async flushTimeSpent(){if(!this._timeEnabled)return;if(this._isPaused&&!this._ended&&!this._ending)return;if(this._flushInFlight){await this._flushInFlight;return}let X=Date.now(),$=this._accumulatedActiveMs;if(this._activeSinceMs!==null)$+=X-this._activeSinceMs;if(X-this._windowStartMs<=0)return;let K=this._buildHeartbeatPayload(X,$);if(this._totalFlushedActiveMs+=$,this._windowStartMs=X,this._accumulatedActiveMs=0,this._activeSinceMs!==null)this._activeSinceMs=X;let{retryAttempts:G,retryDelaysMs:W}=this._timeOptions;this._flushInFlight=k(()=>this._callbacks.sendHeartbeat(K),G,W).then(()=>{this.params.onFlush?.($)}).catch((J)=>{let C=J instanceof Error?J:Error(String(J));this.params.onError?.(C,this._errorContext("timeSpent"))}).finally(()=>{this._flushInFlight=null}),await this._flushInFlight}pause(){if(this._isPaused||this._ended)return;if(this._activeSinceMs!==null)this._accumulatedActiveMs+=Date.now()-this._activeSinceMs,this._activeSinceMs=null;this.flushTimeSpent(),this._isPaused=!0,this._stopHeartbeatTimer(),this.params.onPause?.()}resume(){if(!this._isPaused||this._ended)return;if(this._isPaused=!1,this._timeEnabled){let X=Date.now();if(this._windowStartMs=X,this._accumulatedActiveMs=0,!this._timeOptions.visibilityAware||document.visibilityState==="visible")this._activeSinceMs=X;this._startHeartbeatTimer()}this.params.onResume?.()}async end(X={}){if(this._ended||this._ending)return;this._ending=!0,this._stopHeartbeatTimer(),this._teardownVisibilityHandlers();let $=this._activeSinceMs!==null,Z=this._activeSinceMs;if(this._activeSinceMs!==null)this._accumulatedActiveMs+=Date.now()-this._activeSinceMs,this._activeSinceMs=null;try{if(await this.flushTimeSpent(),X.totalQuestions!==void 0||X.correctQuestions!==void 0||X.xpEarned!==void 0||X.masteredUnits!==void 0||X.pctComplete!==void 0)await this._sendCompletion(X);this._ended=!0,this._ending=!1}catch(K){if(this._ending=!1,$)this._activeSinceMs=Z;if(this._timeEnabled)this._startHeartbeatTimer(),this._setupVisibilityHandlers();let G=K instanceof Error?K:Error(String(K));throw this.params.onError?.(G,this._errorContext("completion")),K}}async _sendCompletion(X){if(X.xpEarned===void 0)throw Error("Invalid activity completion: xpEarned is required. The SDK cannot auto-calculate XP because total elapsed time may span multiple browser sessions.");let $=new Date,Z=X.totalQuestions!==void 0,K=X.correctQuestions!==void 0;if(Z!==K)throw Error("Invalid activity metrics: totalQuestions and correctQuestions must be provided together.");if(Z&&K&&X.correctQuestions>X.totalQuestions)throw Error("Invalid activity metrics: correctQuestions cannot exceed totalQuestions.");let G={xpEarned:X.xpEarned,...Z?{totalQuestions:X.totalQuestions}:{},...K?{correctQuestions:X.correctQuestions}:{},...X.masteredUnits===void 0?{}:{masteredUnits:X.masteredUnits}},W={id:this.params.id,name:this.params.name,course:this.params.course,...this._process===void 0?{}:{process:this._process},runId:this.runId,endedAt:$.toISOString(),metrics:G,...X.pctComplete===void 0?{}:{pctComplete:X.pctComplete}};await this._callbacks.sendSubmit(W)}}async function g(X){return await X.json().catch(()=>({error:"Unknown error"}))}function f(X){return"questions"in X&&Array.isArray(X.questions)}function U(X){if(!X||typeof X!=="object")return{id:"unknown",content:{rawXml:""}};let $=X;return{...$,id:typeof $.id==="string"?$.id:"unknown",title:typeof $.title==="string"?$.title:void 0,difficulty:typeof $.difficulty==="string"?$.difficulty:void 0,content:$.content&&typeof $.content==="object"?$.content??{rawXml:""}:{rawXml:""}}}function r(X){return X.lessonType==="powerpath-100"?X.seenQuestions:X.questions}class M{_questionBuffer=[];_answeredIds=new Set;_deps;_completed;lessonId;lessonType;attempt;questionCount;seenQuestions;score;finalized;constructor(X,$){this._deps=X,this.lessonId=$.lessonId,this.lessonType=$.lessonType,this.attempt=$.attempt,this.score=$.score,this.questionCount=$.questionCount,this.seenQuestions=$.seenQuestions,this.finalized=$.finalized,this._completed=$.finalized}async next(){if(this._completed)return null;if(this.lessonType!=="powerpath-100"){if(this._questionBuffer.length===0){let Z=await this._deps.fetchJson(N.LESSONS.NEXT,{method:"POST",body:{lessonId:this.lessonId,lessonType:this.lessonType}});if(!f(Z))throw Error("Unexpected response shape from lessons.next — expected a batch result with questions array");this._questionBuffer.push(...Z.questions.map(U));for(let K of Z.answeredIds)this._answeredIds.add(K);if(Z.complete)this._completed=!0}let $=this._questionBuffer.find((Z)=>!this._answeredIds.has(Z.id));if(!$)return null;return $}let X=await this._deps.fetchJson(N.LESSONS.NEXT,{method:"POST",body:{lessonId:this.lessonId,lessonType:this.lessonType}});if(X.complete===!0&&(!X.id||X.id==="unknown"))return this._completed=!0,null;return U(X)}async submit(X){let $=await this._deps.fetchJson(N.LESSONS.SUBMIT,{method:"POST",body:{lessonId:this.lessonId,questionId:X.question,response:X.response,lessonType:this.lessonType}});if(this._answeredIds.add(X.question),this.score=$.score,$.complete)this._completed=!0;return $}async complete(){if(this.finalized)throw Error("Lesson session has already been completed");let X;try{X=await this._deps.fetchJson(N.LESSONS.COMPLETE,{method:"POST",body:{lessonId:this.lessonId,lessonType:this.lessonType}})}catch(G){try{await this._deps.activity.end()}catch{}throw G}let $=Math.max(0,Math.round(this._deps.activity.activeTimeMs/1000)),Z=!0,K;try{await this._deps.activity.end()}catch(G){Z=!1,K=G instanceof Error?G.message:"Failed to flush final time tracking"}return this._completed=!0,this.score=X.score,this.finalized=X.finalized,{...X,lessonType:this.lessonType,timeSpentSeconds:$,timeTrackingSent:Z,error:K}}pause(){this._deps.activity.pause()}resume(){this._deps.activity.resume()}}class x{config;constructor(X){this.config=X}async fetchJson(X,$){let Z=await this.config.fetchImpl(`${this.config.getBaseURL()}${X}`,{method:$.method,credentials:"include",headers:{"Content-Type":"application/json"},body:$.body?JSON.stringify($.body):void 0});if(!Z.ok){let K=await g(Z),G=typeof K.error==="string"&&K.error||typeof K.error==="object"&&K.error!==null&&"message"in K.error&&typeof K.error.message==="string"&&K.error.message||"Request failed";throw Error(G)}return await Z.json()}async list(X={}){if(!H())throw Error("lessons.list() requires a browser environment");return(await this.fetchJson(N.LESSONS.LIST,{method:"POST",body:X.course?{course:X.course}:{}})).lessons}async get(X){return(await this.list()).find((Z)=>Z.id===X)??null}async start(X){if(!H())throw Error("lessons.start() requires a browser environment");let $=await this.fetchJson(N.LESSONS.START,{method:"POST",body:{lessonId:X.lessonId,forceNew:X.forceNew??!1,lessonType:X.lessonType}}),Z=X.course??this.config.defaultCourse;if(!Z)throw Error("lessons.start() requires a `course` unless a client-level `defaultCourse` is configured.");let K=this.config.activity.start({id:X.lessonId,name:X.name??$.lessonType??X.lessonId,course:Z});return new M({fetchJson:(G,W)=>this.fetchJson(G,W),activity:K},{...$,lessonType:$.lessonType??X.lessonType??"quiz"})}async attempts(X){return(await this.fetchJson(N.LESSONS.ATTEMPTS,{method:"POST",body:{lessonId:X.lessonId}})).attempts}async attemptDetails(X){return await this.fetchJson(N.LESSONS.ATTEMPT_DETAILS,{method:"POST",body:{lessonId:X.lessonId,attempt:X.attempt}})}}function T(){if(!(typeof globalThis>"u"?void 0:globalThis.fetch))return;return($,Z)=>globalThis.fetch($,Z)}function w(X){let{baseURL:$,fetch:Z,canUseBeacon:K,credentials:G}=X,W=`${$}${N.ACTIVITY.HEARTBEAT}`,J=`${$}${N.ACTIVITY.SUBMIT}`;function C(z,Y){if(!K)return!1;if(typeof navigator>"u")return!1;if(typeof navigator.sendBeacon!=="function")return!1;try{let j=new Blob([Y],{type:"application/json"});return navigator.sendBeacon(z,j)}catch{return!1}}function D(z,Y,j=!1){return Z(z,{method:"POST",headers:{"Content-Type":"application/json"},body:Y,credentials:G,keepalive:j})}return{async sendHeartbeat(z){let Y=JSON.stringify(z),j=await D(W,Y);if(!j.ok){let V=await j.json().catch(()=>({error:{message:"Unknown error"}})),A=typeof V.error==="object"?V.error?.message:V.error;throw Error(A??"Failed to send heartbeat")}},async sendHeartbeatOnPageHide(z){let Y=JSON.stringify(z);if(C(W,Y))return;if(!(await D(W,Y,!0)).ok)throw Error("Failed to send heartbeat on page exit")},async sendSubmit(z){let Y=JSON.stringify(z),j=await D(J,Y);if(!j.ok){let V=await j.json().catch(()=>({error:{message:"Unknown error"}})),A=typeof V.error==="object"?V.error?.message:V.error;throw Error(A??"Failed to submit activity")}}}}class F{transport;_current=null;constructor(X){this.transport=w(X)}get current(){if(this._current?.isEnded)this._current=null;return this._current}start(X){if(this._current&&!this._current.isEnded)throw Error(`An activity is already active (id: "${this._current.id}"). End it before starting a new one.`);return this._current=new Q(X,this.transport),this._current}}class _{getBaseURL;constructor(X){this.getBaseURL=X}signIn(){if(!H())throw Error("signIn() requires a browser environment");window.location.href=`${this.getBaseURL()}${N.IDENTITY.SIGNIN}`}}class q{getBaseURL;fetchImpl;constructor(X,$){this.getBaseURL=X;this.fetchImpl=$}async fetch(){if(!H())throw Error("user.fetch() requires a browser environment");let X=await this.fetchImpl(`${this.getBaseURL()}${N.USER.ME}`,{method:"GET",credentials:"include"});if(!X.ok){let $=await X.json().catch(()=>({error:"Unknown error"}));throw Error($.error??"Failed to fetch user profile")}return X.json()}async verify(){if(!H())throw Error("user.verify() requires a browser environment");let X=await this.fetchImpl(`${this.getBaseURL()}${N.USER.VERIFY}`,{method:"GET",credentials:"include"});if(!X.ok){let Z=await X.json().catch(()=>({error:"Unknown error"}));throw Error(Z.error??"Failed to verify Timeback user")}let $=await X.json();if($.verified&&$.timebackId)return{verified:!0,timebackId:$.timebackId};return{verified:!1}}}class O{activity;auth;lessons;user;_baseURL;_fetch;constructor(X={}){this._baseURL=X.baseURL;let $=X.fetch??T();if(!$)throw Error("TimebackClient requires a fetch implementation. Provide `fetch` in the constructor config for non-browser runtimes.");let Z=X.plugins,K=Array.isArray(Z)?Z:Z?[Z]:[];this._fetch=K.reduce((G,W)=>W.wrapFetch(G),$),this.activity=new F({baseURL:this.baseURL,fetch:this._fetch,canUseBeacon:K.length===0,credentials:X.credentials??"include"}),this.auth=new _(()=>this.baseURL),this.lessons=new x({getBaseURL:()=>this.baseURL,fetchImpl:this._fetch,activity:this.activity,defaultCourse:X.defaultCourse}),this.user=new q(()=>this.baseURL,this._fetch)}get baseURL(){if(!this._baseURL){let X=P();if(!X)throw Error("Timeback client requires a browser environment for default baseURL. Provide an explicit baseURL for server-side usage.");this._baseURL=X}return this._baseURL}}function CX(X={}){let $=X.baseURL??P();return new O({baseURL:$,fetch:X.fetch,plugins:X.plugins,credentials:X.credentials,defaultCourse:X.defaultCourse})}
2
- export{v as a,E as b,R as c,h as d,Q as e,U as f,r as g,M as h,x as i,O as j,CX as k};