@timeback/sdk 0.1.9 → 0.1.10

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 (147) hide show
  1. package/dist/chunk-3mqpr9vx.js +2 -0
  2. package/dist/chunk-92nnwa7t.js +2 -0
  3. package/dist/{chunk-agpf1x3g.js → chunk-af3xwwsv.js} +1 -1
  4. package/dist/{chunk-07j8zre9.js → chunk-b8649tw4.js} +1 -1
  5. package/dist/{chunk-63afdp3y.js → chunk-bd09q1fw.js} +6 -6
  6. package/dist/chunk-js665z11.js +1 -0
  7. package/dist/chunk-nsr7a2dv.js +2 -0
  8. package/dist/chunk-sgcwg4j6.js +1 -0
  9. package/dist/{chunk-9se82640.js → chunk-txwjkpfz.js} +1 -1
  10. package/dist/client/adapters/react/hooks/types.d.ts +2 -29
  11. package/dist/client/adapters/react/hooks/types.d.ts.map +1 -1
  12. package/dist/client/adapters/react/hooks/useTimebackVerification.d.ts.map +1 -1
  13. package/dist/client/adapters/react/index.js +2 -2
  14. package/dist/client/adapters/solid/types.d.ts +2 -29
  15. package/dist/client/adapters/solid/types.d.ts.map +1 -1
  16. package/dist/client/adapters/solid/types.ts +2 -18
  17. package/dist/client/adapters/svelte/stores/client.d.ts.map +1 -1
  18. package/dist/client/adapters/svelte/stores/client.ts +2 -9
  19. package/dist/client/adapters/svelte/stores/profile.d.ts +1 -1
  20. package/dist/client/adapters/svelte/stores/profile.d.ts.map +1 -1
  21. package/dist/client/adapters/svelte/stores/profile.ts +4 -11
  22. package/dist/client/adapters/svelte/stores/verification.d.ts.map +1 -1
  23. package/dist/client/adapters/svelte/stores/verification.ts +1 -10
  24. package/dist/client/adapters/svelte/types.d.ts +1 -29
  25. package/dist/client/adapters/svelte/types.d.ts.map +1 -1
  26. package/dist/client/adapters/vue/provider.d.ts.map +1 -1
  27. package/dist/client/adapters/vue/provider.ts +4 -11
  28. package/dist/client/adapters/vue/types.d.ts +2 -29
  29. package/dist/client/adapters/vue/types.d.ts.map +1 -1
  30. package/dist/client/adapters/vue/types.ts +2 -18
  31. package/dist/client/auth/types.d.ts +1 -1
  32. package/dist/client/index.d.ts +1 -1
  33. package/dist/client/lib/activity/activity.class.d.ts +130 -22
  34. package/dist/client/lib/activity/activity.class.d.ts.map +1 -1
  35. package/dist/client/lib/activity/transport.d.ts +15 -0
  36. package/dist/client/lib/activity/transport.d.ts.map +1 -0
  37. package/dist/client/lib/activity/types.d.ts +53 -0
  38. package/dist/client/lib/activity/types.d.ts.map +1 -0
  39. package/dist/client/lib/utils.d.ts +18 -0
  40. package/dist/client/lib/utils.d.ts.map +1 -1
  41. package/dist/client/lib/utils.ts +109 -0
  42. package/dist/client/namespaces/activity.d.ts +45 -6
  43. package/dist/client/namespaces/activity.d.ts.map +1 -1
  44. package/dist/client/timeback-client.class.d.ts +7 -1
  45. package/dist/client/timeback-client.class.d.ts.map +1 -1
  46. package/dist/client.d.ts +1 -1
  47. package/dist/client.js +1 -1
  48. package/dist/edge.js +1 -1
  49. package/dist/identity.js +1 -1
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +22 -22
  53. package/dist/server/adapters/express.d.ts.map +1 -1
  54. package/dist/server/adapters/express.js +1 -1
  55. package/dist/server/adapters/native.d.ts.map +1 -1
  56. package/dist/server/adapters/native.js +1 -1
  57. package/dist/server/adapters/nextjs.js +1 -1
  58. package/dist/server/adapters/nuxt.d.ts.map +1 -1
  59. package/dist/server/adapters/nuxt.js +1 -1
  60. package/dist/server/adapters/solid-start.d.ts.map +1 -1
  61. package/dist/server/adapters/solid-start.js +1 -1
  62. package/dist/server/adapters/svelte-kit.d.ts.map +1 -1
  63. package/dist/server/adapters/svelte-kit.js +1 -1
  64. package/dist/server/adapters/tanstack-start.d.ts.map +1 -1
  65. package/dist/server/adapters/tanstack-start.js +1 -1
  66. package/dist/server/adapters/utils.d.ts +1 -1
  67. package/dist/server/adapters/utils.d.ts.map +1 -1
  68. package/dist/server/handlers/activity/caliper.d.ts +50 -14
  69. package/dist/server/handlers/activity/caliper.d.ts.map +1 -1
  70. package/dist/server/handlers/activity/heartbeat-handler.d.ts +15 -0
  71. package/dist/server/handlers/activity/heartbeat-handler.d.ts.map +1 -0
  72. package/dist/server/handlers/activity/index.d.ts +5 -3
  73. package/dist/server/handlers/activity/index.d.ts.map +1 -1
  74. package/dist/server/handlers/activity/progress.d.ts +2 -2
  75. package/dist/server/handlers/activity/progress.d.ts.map +1 -1
  76. package/dist/server/handlers/activity/schema.d.ts +40 -6
  77. package/dist/server/handlers/activity/schema.d.ts.map +1 -1
  78. package/dist/server/handlers/activity/submit-handler.d.ts +29 -0
  79. package/dist/server/handlers/activity/submit-handler.d.ts.map +1 -0
  80. package/dist/server/handlers/activity/submit.d.ts +44 -0
  81. package/dist/server/handlers/activity/submit.d.ts.map +1 -0
  82. package/dist/server/handlers/activity/types.d.ts +126 -5
  83. package/dist/server/handlers/activity/types.d.ts.map +1 -1
  84. package/dist/server/handlers/identity/handler.d.ts +23 -4
  85. package/dist/server/handlers/identity/handler.d.ts.map +1 -1
  86. package/dist/server/handlers/identity/index.d.ts +2 -2
  87. package/dist/server/handlers/identity/index.d.ts.map +1 -1
  88. package/dist/server/handlers/identity/oidc.d.ts.map +1 -1
  89. package/dist/server/handlers/identity/types.d.ts +0 -6
  90. package/dist/server/handlers/identity/types.d.ts.map +1 -1
  91. package/dist/server/handlers/index.d.ts +3 -3
  92. package/dist/server/handlers/index.d.ts.map +1 -1
  93. package/dist/server/handlers/user/handler.d.ts.map +1 -1
  94. package/dist/server/handlers/user/profile.d.ts.map +1 -1
  95. package/dist/server/handlers/user/types.d.ts +3 -0
  96. package/dist/server/handlers/user/types.d.ts.map +1 -1
  97. package/dist/server/handlers/user/verify.d.ts.map +1 -1
  98. package/dist/server/index.d.ts +1 -1
  99. package/dist/server/index.d.ts.map +1 -1
  100. package/dist/server/lib/hooks.d.ts +20 -0
  101. package/dist/server/lib/hooks.d.ts.map +1 -0
  102. package/dist/server/lib/index.d.ts +4 -2
  103. package/dist/server/lib/index.d.ts.map +1 -1
  104. package/dist/server/lib/logger.d.ts +36 -9
  105. package/dist/server/lib/logger.d.ts.map +1 -1
  106. package/dist/server/lib/resolve.d.ts +1 -1
  107. package/dist/server/lib/resolve.d.ts.map +1 -1
  108. package/dist/server/lib/utils.d.ts +19 -1
  109. package/dist/server/lib/utils.d.ts.map +1 -1
  110. package/dist/server/lib/validation.d.ts +55 -0
  111. package/dist/server/lib/validation.d.ts.map +1 -0
  112. package/dist/server/namespaces/activity/index.d.ts +8 -0
  113. package/dist/server/namespaces/activity/index.d.ts.map +1 -0
  114. package/dist/server/namespaces/activity/record.d.ts +49 -0
  115. package/dist/server/namespaces/activity/record.d.ts.map +1 -0
  116. package/dist/server/namespaces/activity/schema.d.ts +50 -0
  117. package/dist/server/namespaces/activity/schema.d.ts.map +1 -0
  118. package/dist/server/namespaces/user/get-profile.d.ts +32 -0
  119. package/dist/server/namespaces/user/get-profile.d.ts.map +1 -0
  120. package/dist/server/namespaces/user/index.d.ts +8 -0
  121. package/dist/server/namespaces/user/index.d.ts.map +1 -0
  122. package/dist/server/namespaces/user/verify.d.ts +28 -0
  123. package/dist/server/namespaces/user/verify.d.ts.map +1 -0
  124. package/dist/server/timeback.d.ts +1 -1
  125. package/dist/server/timeback.d.ts.map +1 -1
  126. package/dist/server/types.d.ts +394 -12
  127. package/dist/server/types.d.ts.map +1 -1
  128. package/dist/shared/constants.d.ts +7 -0
  129. package/dist/shared/constants.d.ts.map +1 -1
  130. package/dist/shared/constants.ts +51 -0
  131. package/dist/shared/index.d.ts +9 -0
  132. package/dist/shared/index.d.ts.map +1 -0
  133. package/dist/shared/schemas.d.ts +57 -0
  134. package/dist/shared/schemas.d.ts.map +1 -0
  135. package/dist/shared/types.d.ts +263 -10
  136. package/dist/shared/types.d.ts.map +1 -1
  137. package/dist/shared/types.ts +620 -0
  138. package/package.json +2 -2
  139. package/dist/chunk-5171mkp2.js +0 -2
  140. package/dist/chunk-8gg8n8v9.js +0 -2
  141. package/dist/chunk-hnf0tart.js +0 -2
  142. package/dist/chunk-qr0bbnsr.js +0 -1
  143. package/dist/chunk-x9gvef7q.js +0 -1
  144. package/dist/server/handlers/activity/handler.d.ts +0 -32
  145. package/dist/server/handlers/activity/handler.d.ts.map +0 -1
  146. package/dist/shared/xp-calculator.d.ts +0 -25
  147. package/dist/shared/xp-calculator.d.ts.map +0 -1
@@ -0,0 +1,620 @@
1
+ /**
2
+ * Shared Types
3
+ *
4
+ * Types shared between client and server.
5
+ */
6
+
7
+ import type { TimebackGrade, TimebackSubject } from '@timeback/types'
8
+
9
+ /**
10
+ * User identity returned from SSO.
11
+ */
12
+ export interface TimebackIdentity {
13
+ id: string
14
+ email: string
15
+ name?: string
16
+ }
17
+
18
+ /**
19
+ * Timeback user profile with enriched data from the Timeback API.
20
+ */
21
+ export interface TimebackProfile {
22
+ /** Timeback user ID */
23
+ id: string
24
+ /** User's email address */
25
+ email: string
26
+ /** User's display name */
27
+ name?: string
28
+
29
+ /** School information */
30
+ school?: {
31
+ id: string
32
+ name: string
33
+ }
34
+
35
+ /** Grade level */
36
+ grade?: number
37
+
38
+ /** XP earned on this app */
39
+ xp?: {
40
+ /** XP earned today (UTC day range) */
41
+ today: number
42
+ /** XP earned across all time (computed from analytics) */
43
+ all: number
44
+ }
45
+
46
+ /** Enrolled courses */
47
+ courses?: Array<{
48
+ id: string
49
+ code: string
50
+ name: string
51
+ }>
52
+
53
+ /** Goals and progress */
54
+ goals?: {
55
+ dailyXp?: number
56
+ dailyLessons?: number
57
+ dailyActiveMinutes?: number
58
+ dailyAccuracy?: number
59
+ dailyMasteredUnits?: number
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Recommended minimal user payload to persist in a session.
65
+ *
66
+ * **User-facing note:** this type is part of the SDK’s developer experience.
67
+ * It exists to give you a “safe default” session shape that works well for
68
+ * cookie-based sessions (small payload) while still carrying enough Timeback
69
+ * context to power common UI affordances (e.g. showing a school name or grade).
70
+ *
71
+ * **What it is:** a minimal, serializable subset of `TimebackProfile`.
72
+ * **What it isn’t:** a guarantee that you’ll always have the full Timeback
73
+ * profile (courses, goals, xp, etc.) in-session. If you need richer data, fetch
74
+ * it from the API (e.g. `timeback.user.fetch()`) and cache/store it according
75
+ * to your app’s needs.
76
+ *
77
+ * **Stability:** this is still early and we’re actively iterating on identity
78
+ * and session ergonomics. We may rename, restructure, or remove this type from
79
+ * the public surface as the SDK evolves.
80
+ */
81
+ export type TimebackSessionUser = Pick<
82
+ TimebackProfile,
83
+ 'id' | 'email' | 'name' | 'school' | 'grade'
84
+ >
85
+
86
+ /**
87
+ * Claims from the identity provider (IdP).
88
+ *
89
+ * Normalized subset of OIDC UserInfo claims.
90
+ */
91
+ export interface IdentityClaims {
92
+ /** Subject identifier (unique user ID from IdP) */
93
+ sub: string
94
+ /** User's email address */
95
+ email: string
96
+ /** User's first/given name */
97
+ firstName?: string
98
+ /** User's last/family name */
99
+ lastName?: string
100
+ /** User's profile picture URL */
101
+ pictureUrl?: string
102
+ }
103
+
104
+ /**
105
+ * Authenticated user with Timeback profile and IdP claims.
106
+ *
107
+ * This is the primary user object returned during SSO callback when using
108
+ * `createTimeback()`. The `id` field is the canonical `timebackId` (stable identifier).
109
+ */
110
+ export interface TimebackAuthUser extends TimebackProfile {
111
+ /** IdP claims (raw identity provider data) */
112
+ claims: IdentityClaims
113
+ }
114
+
115
+ /**
116
+ * Course selector by subject and grade (grade-based apps).
117
+ *
118
+ * Use this for traditional K-12 apps where courses are identified by subject + grade.
119
+ */
120
+ export interface SubjectGradeCourseRef {
121
+ subject: TimebackSubject
122
+ grade: TimebackGrade
123
+ }
124
+
125
+ /**
126
+ * Course selector by code (grade-less apps).
127
+ *
128
+ * Use this for apps without grade levels (e.g., CS platforms) where courses
129
+ * are identified by a unique course code.
130
+ */
131
+ export interface CourseCodeRef {
132
+ code: string
133
+ }
134
+
135
+ /**
136
+ * Course selector for activity tracking.
137
+ *
138
+ * This should correspond to a unique course entry in `timeback.config.json`.
139
+ *
140
+ * Two selector modes are supported:
141
+ * - **Grade-based**: `{ subject, grade }` — K-12 style
142
+ * - **Grade-less**: `{ code }` — CS/skill-based
143
+ *
144
+ * @example Grade-based
145
+ * ```typescript
146
+ * { subject: 'Math', grade: 3 }
147
+ * ```
148
+ *
149
+ * @example Grade-less
150
+ * ```typescript
151
+ * { code: 'CS-101' }
152
+ * ```
153
+ */
154
+ export type ActivityCourseRef = SubjectGradeCourseRef | CourseCodeRef
155
+
156
+ /**
157
+ * Type guard: Check if a course ref uses subject+grade identity.
158
+ *
159
+ * @param ref - Course reference to check
160
+ * @returns True if grade-based selector
161
+ */
162
+ export function isSubjectGradeCourseRef(ref: ActivityCourseRef): ref is SubjectGradeCourseRef {
163
+ return 'grade' in ref && ref.grade !== undefined
164
+ }
165
+
166
+ /**
167
+ * Time tracking configuration options.
168
+ *
169
+ * Controls how the SDK tracks and reports time-spent data via heartbeats.
170
+ */
171
+ export interface TimeTrackingOptions {
172
+ /**
173
+ * Interval in milliseconds between automatic heartbeat flushes.
174
+ * @default 15000 (15 seconds)
175
+ */
176
+ flushIntervalMs?: number
177
+ /**
178
+ * Whether to pause time tracking when the tab is not visible.
179
+ * @default true
180
+ */
181
+ visibilityAware?: boolean
182
+ /**
183
+ * Whether to flush accumulated time when the tab becomes hidden.
184
+ * @default true
185
+ */
186
+ flushOnVisibilityHidden?: boolean
187
+ /**
188
+ * Whether to attempt a best-effort flush on page unload (pagehide event).
189
+ *
190
+ * Implementation notes:
191
+ * - Prefer `navigator.sendBeacon()` when available and safe to use
192
+ * - Fall back to `fetch(..., { keepalive: true })` otherwise
193
+ *
194
+ * sendBeacon cannot set arbitrary headers (e.g. Authorization), so
195
+ * integrations that rely on bearer tokens typically use the keepalive
196
+ * fetch fallback.
197
+ * @default true
198
+ */
199
+ flushOnPageHide?: boolean
200
+ /**
201
+ * Timeout in milliseconds after which hidden time stops being tracked.
202
+ *
203
+ * When the tab is hidden for longer than this duration, heartbeats stop
204
+ * and the hidden time is not counted. When the user returns, tracking
205
+ * resumes fresh without counting the extended absence.
206
+ *
207
+ * Set to `null` or `Infinity` to disable (always track hidden time).
208
+ * @default 600000 (10 minutes)
209
+ */
210
+ hiddenTimeoutMs?: number | null
211
+ /**
212
+ * Number of retry attempts for failed heartbeat sends.
213
+ *
214
+ * Set to `0` (default) for no retries. Retries use exponential backoff
215
+ * with delays configured by `retryDelaysMs`.
216
+ *
217
+ * @default 0
218
+ */
219
+ retryAttempts?: number
220
+ /**
221
+ * Delay schedule (in milliseconds) between retry attempts.
222
+ *
223
+ * Each index corresponds to the delay before that retry attempt.
224
+ * If more attempts are made than there are entries, the last value is reused.
225
+ *
226
+ * @default [100, 300, 1000]
227
+ */
228
+ retryDelaysMs?: number[]
229
+ }
230
+
231
+ /**
232
+ * Activity start parameters.
233
+ *
234
+ * @remarks
235
+ * The `id` field is treated as a **slug** (not a URL). The SDK derives the
236
+ * canonical activity URL (`event.object.id`) from:
237
+ * - The configured sensor URL
238
+ * - The course selector (subject + grade or code)
239
+ * - This slug (URI-encoded)
240
+ *
241
+ * This allows upstream systems to process activities without requiring
242
+ * pre-synced OneRoster component resources.
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * // Grade-based course
247
+ * timeback.activity.start({
248
+ * id: 'fractions-with-like-denominators', // slug
249
+ * name: 'Fractions with Like Denominators', // human-readable
250
+ * course: { subject: 'Math', grade: 3 },
251
+ * })
252
+ * // => object.id: https://sensor.example.com/activities/Math/g3/fractions-with-like-denominators
253
+ *
254
+ * // Grade-less course
255
+ * timeback.activity.start({
256
+ * id: 'intro-to-loops',
257
+ * name: 'Introduction to Loops',
258
+ * course: { code: 'CS-101' },
259
+ * })
260
+ * // => object.id: https://sensor.example.com/activities/CS-101/intro-to-loops
261
+ * ```
262
+ */
263
+ export interface ActivityParams {
264
+ /**
265
+ * Activity slug (stable identifier for the learning object).
266
+ *
267
+ * This is used to construct the canonical activity URL sent to Caliper.
268
+ * Use a short, URL-safe slug like `"fractions-with-like-denominators"` or
269
+ * `"lesson-1"`. Special characters will be URI-encoded.
270
+ *
271
+ * @example 'fractions-with-like-denominators'
272
+ * @example 'ccss.math.content.3.nf.a.1'
273
+ * @example 'lesson-1'
274
+ */
275
+ id: string
276
+ /**
277
+ * Human-readable display name of the activity.
278
+ *
279
+ * This is sent as `object.activity.name` in Caliper events.
280
+ *
281
+ * @example 'Fractions with Like Denominators'
282
+ */
283
+ name: string
284
+ /** Course selector (must match a unique course in timeback.config.json) */
285
+ course: ActivityCourseRef
286
+ /**
287
+ * Optional run identifier for correlating events across sessions.
288
+ *
289
+ * When resuming an activity, provide the same `runId` from the previous session
290
+ * to correlate time-spent events with the eventual completion event.
291
+ *
292
+ * If not provided, the SDK generates a new UUID.
293
+ */
294
+ runId?: string
295
+ /**
296
+ * Time tracking configuration options.
297
+ *
298
+ * Heartbeats are enabled by default. Use this to customize flush intervals
299
+ * or disable visibility-aware tracking.
300
+ *
301
+ * Set to `false` to disable client-side time tracking entirely. When
302
+ * disabled, no heartbeats are sent, no visibility handlers are registered,
303
+ * and `end()` skips the final time flush. Use this when time is managed
304
+ * server-side (e.g. via `timeback.activity.record()`).
305
+ */
306
+ time?: TimeTrackingOptions | false
307
+
308
+ /**
309
+ * Called when a heartbeat or submission fails.
310
+ *
311
+ * Heartbeat errors are non-fatal — the SDK continues tracking time
312
+ * regardless. Submit errors (from `end()`) are also surfaced here
313
+ * before being re-thrown.
314
+ *
315
+ * @param error - The error that occurred
316
+ * @param context - Details about the failed operation
317
+ */
318
+ onError?: (error: Error, context: ActivityErrorContext) => void
319
+
320
+ /**
321
+ * Called when the activity is paused (via `pause()` or visibility timeout).
322
+ */
323
+ onPause?: () => void
324
+
325
+ /**
326
+ * Called when the activity resumes after being paused.
327
+ */
328
+ onResume?: () => void
329
+
330
+ /**
331
+ * Called after each successful heartbeat flush.
332
+ *
333
+ * @param elapsedMs - Active milliseconds reported in this flush
334
+ */
335
+ onFlush?: (elapsedMs: number) => void
336
+ }
337
+
338
+ /**
339
+ * Context passed to the `onError` callback.
340
+ */
341
+ export interface ActivityErrorContext {
342
+ /** Which operation failed — aligns with Caliper event types. */
343
+ type: 'timeSpent' | 'completion'
344
+ /** The activity slug passed to `activity.start()`. */
345
+ activityId: string
346
+ /** The `runId` for this activity instance. */
347
+ runId: string
348
+ }
349
+
350
+ /**
351
+ * Activity metrics (optional performance data).
352
+ */
353
+ export interface ActivityMetrics {
354
+ /** Total questions attempted */
355
+ totalQuestions?: number
356
+ /** Number of correct answers */
357
+ correctQuestions?: number
358
+ /** XP earned from this activity */
359
+ xpEarned?: number
360
+ /** Number of units mastered */
361
+ masteredUnits?: number
362
+ }
363
+
364
+ /**
365
+ * Question count metrics for activity completion.
366
+ *
367
+ * @example Basic usage
368
+ * ```typescript
369
+ * await activity.end({
370
+ * xpEarned: 100,
371
+ * totalQuestions: 10,
372
+ * correctQuestions: 8,
373
+ * })
374
+ * ```
375
+ *
376
+ * @example With time override
377
+ * ```typescript
378
+ * await activity.end({
379
+ * xpEarned: 100,
380
+ * totalQuestions: 10,
381
+ * correctQuestions: 8,
382
+ * time: { active: 42000, inactive: 3000 },
383
+ * })
384
+ * ```
385
+ */
386
+ type QuestionCountMetrics =
387
+ | {
388
+ /**
389
+ * Total questions attempted.
390
+ *
391
+ * If provided, `correctQuestions` is required too.
392
+ */
393
+ totalQuestions: number
394
+ /**
395
+ * Number of correct answers.
396
+ *
397
+ * Must be provided when `totalQuestions` is provided.
398
+ */
399
+ correctQuestions: number
400
+ }
401
+ | {
402
+ /** Omit question counts entirely. */
403
+ totalQuestions?: undefined
404
+ /** Omit question counts entirely. */
405
+ correctQuestions?: undefined
406
+ }
407
+
408
+ /** Optional time override for activity.end() */
409
+ interface ActivityEndTimeOverride {
410
+ /** Active time in milliseconds */
411
+ active: number
412
+ /** Inactive/paused time in milliseconds (defaults to 0) */
413
+ inactive?: number
414
+ }
415
+
416
+ /**
417
+ * Time-only end: final flush without completion event.
418
+ *
419
+ * Call `activity.end()` with no args or just a time override to flush
420
+ * accumulated time without sending an ActivityCompletedEvent.
421
+ */
422
+ type ActivityEndTimeOnly = {
423
+ totalQuestions?: undefined
424
+ correctQuestions?: undefined
425
+ xpEarned?: undefined
426
+ masteredUnits?: undefined
427
+ pctComplete?: undefined
428
+ time?: ActivityEndTimeOverride
429
+ }
430
+
431
+ /** Completion end: flush + ActivityCompletedEvent. */
432
+ type ActivityEndCompletion = QuestionCountMetrics & {
433
+ /** XP earned from this activity. */
434
+ xpEarned: number
435
+ /** Number of units mastered */
436
+ masteredUnits?: number
437
+ /**
438
+ * App-reported course progress (0–100). Values outside range are clamped.
439
+ */
440
+ pctComplete?: number
441
+ /**
442
+ * Optional time override. When provided, the SDK uses these values instead
443
+ * of the internal timer. Values are clamped to >= 0 and rounded to integers.
444
+ */
445
+ time?: ActivityEndTimeOverride
446
+ }
447
+
448
+ /**
449
+ * Data for ending an activity.
450
+ *
451
+ * Two modes:
452
+ * - **Time-only**: `activity.end()` or `activity.end({ time: {...} })` — final
453
+ * flush only, no completion event
454
+ * - **Completion**: `activity.end({ xpEarned, ... })` — final flush + completion event
455
+ */
456
+ export type ActivityEndData = ActivityEndTimeOnly | ActivityEndCompletion
457
+
458
+ /**
459
+ * Activity state sent to the server when ending.
460
+ *
461
+ * @see {@link ActivityParams} for documentation on `id` and `name` semantics.
462
+ */
463
+ export interface ActivityEndPayload {
464
+ /**
465
+ * Activity slug (stable identifier for the learning object).
466
+ *
467
+ * @see {@link ActivityParams.id}
468
+ */
469
+ id: string
470
+ /**
471
+ * Human-readable display name of the activity.
472
+ *
473
+ * @see {@link ActivityParams.name}
474
+ */
475
+ name: string
476
+ /** Course selector (must match a unique course in timeback.config.json) */
477
+ course: ActivityCourseRef
478
+ /** ISO 8601 timestamp when activity started */
479
+ startedAt: string
480
+ /** ISO 8601 timestamp when activity ended */
481
+ endedAt: string
482
+ /** Active time in milliseconds (excluding paused time) */
483
+ elapsedMs: number
484
+ /** Total paused time in milliseconds */
485
+ pausedMs: number
486
+ /** Activity metrics */
487
+ metrics: ActivityMetrics
488
+ /**
489
+ * App-reported course progress (per enrollment), as a percentage from 0–100.
490
+ *
491
+ * This is forwarded to Caliper events as `generated.extensions.pctCompleteApp`.
492
+ *
493
+ * @remarks
494
+ * - Scale: 0 to 100 (not 0 to 1)
495
+ * - Values outside 0–100 are clamped
496
+ */
497
+ pctComplete?: number
498
+ }
499
+
500
+ /**
501
+ * Activity heartbeat payload for time-spent tracking.
502
+ *
503
+ * Sent periodically to report accumulated active time for a time window.
504
+ * The server builds a `TimeSpentEvent` from this data.
505
+ */
506
+ export interface ActivityHeartbeatPayload {
507
+ /**
508
+ * Activity slug (stable identifier for the learning object).
509
+ */
510
+ id: string
511
+ /**
512
+ * Human-readable display name of the activity.
513
+ */
514
+ name: string
515
+ /** Course selector (must match a unique course in timeback.config.json) */
516
+ course: ActivityCourseRef
517
+ /**
518
+ * Run identifier for correlating heartbeats with the completion event.
519
+ */
520
+ runId: string
521
+ /**
522
+ * ISO 8601 timestamp when this time window started.
523
+ */
524
+ startedAt: string
525
+ /**
526
+ * ISO 8601 timestamp when this time window ended.
527
+ */
528
+ endedAt: string
529
+ /**
530
+ * Active time in this window in milliseconds.
531
+ */
532
+ elapsedMs: number
533
+ /**
534
+ * Paused time in this window in milliseconds.
535
+ */
536
+ pausedMs: number
537
+ }
538
+
539
+ /**
540
+ * Activity submit payload for completion events.
541
+ *
542
+ * Sent when the activity completes with metrics.
543
+ * The server builds an `ActivityCompletedEvent` from this data.
544
+ */
545
+ export interface ActivitySubmitPayload {
546
+ /**
547
+ * Activity slug (stable identifier for the learning object).
548
+ */
549
+ id: string
550
+ /**
551
+ * Human-readable display name of the activity.
552
+ */
553
+ name: string
554
+ /** Course selector (must match a unique course in timeback.config.json) */
555
+ course: ActivityCourseRef
556
+ /**
557
+ * Run identifier for correlating with previous heartbeats.
558
+ */
559
+ runId: string
560
+ /**
561
+ * ISO 8601 timestamp when the activity ended.
562
+ */
563
+ endedAt: string
564
+ /** Activity metrics */
565
+ metrics: ActivityMetrics
566
+ /**
567
+ * App-reported course progress (per enrollment), as a percentage from 0–100.
568
+ */
569
+ pctComplete?: number
570
+ }
571
+
572
+ /**
573
+ * Activity submission response.
574
+ */
575
+ export interface ActivityResponse {
576
+ success: boolean
577
+ error?: string
578
+ }
579
+
580
+ /**
581
+ * Result of verifying a user's Timeback status.
582
+ *
583
+ * Used by partner apps to check if a user exists in Timeback before
584
+ * granting access to Timeback-gated features (e.g., free tier for Timeback users).
585
+ */
586
+ export type TimebackVerifyResult =
587
+ | {
588
+ /** User exists in Timeback */
589
+ verified: true
590
+ /** Timeback user ID */
591
+ timebackId: string
592
+ }
593
+ | {
594
+ /** User does not exist in Timeback */
595
+ verified: false
596
+ }
597
+
598
+ /**
599
+ * Verification state for the current user.
600
+ *
601
+ * Used by framework adapters (React, Vue, Svelte, Solid) to expose
602
+ * verification status as a state machine for UI consumption.
603
+ */
604
+ export type TimebackVerificationState =
605
+ | { status: 'loading' }
606
+ | { status: 'verified'; timebackId: string }
607
+ | { status: 'unverified' }
608
+ | { status: 'error'; message: string }
609
+
610
+ /**
611
+ * Profile state for the current user.
612
+ *
613
+ * Used by framework adapters (React, Vue, Svelte, Solid) to expose
614
+ * profile fetching status as a state machine for UI consumption.
615
+ */
616
+ export type TimebackProfileState =
617
+ | { status: 'idle' }
618
+ | { status: 'loading' }
619
+ | { status: 'loaded'; profile: TimebackProfile }
620
+ | { status: 'error'; message: string }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timeback/sdk",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Timeback SDK for frontend and backend integration",
5
5
  "type": "module",
6
6
  "exports": {
@@ -84,7 +84,7 @@
84
84
  "zod": "^4.2.1"
85
85
  },
86
86
  "devDependencies": {
87
- "@timeback/caliper": "0.1.3",
87
+ "@timeback/caliper": "0.1.4",
88
88
  "@timeback/edubridge": "0.1.3",
89
89
  "@timeback/internal-cli-infra": "0.0.0",
90
90
  "@timeback/internal-client-infra": "0.0.0",
@@ -1,2 +0,0 @@
1
- import{C as F}from"./chunk-hnf0tart.js";function M(j){let g=j.trim();if(g==="")return"/";let A=g.indexOf("?");if(A===-1)return g;let L=g.slice(0,A);if(L==="")return"/";return L}function X(j){let g=M(j);if(g!==""&&!g.startsWith("/"))g=`/${g}`;if(g==="/"||g==="")return"";if(g.endsWith("/"))return g.slice(0,-1);return g}function Z(j){let g=j.method.toUpperCase(),A=M(j.pathname),L=j.callbackPath?M(j.callbackPath):void 0;if(L&&A===L)return g==="GET"?"identity.callback":null;let N=j.basePath,Q=N!==void 0,J=Q?X(N):void 0,V=(G)=>{if(g==="GET"){if(G===F.IDENTITY.SIGNIN)return"identity.signIn";if(G===F.IDENTITY.CALLBACK)return"identity.callback";if(G===F.IDENTITY.SIGNOUT)return"identity.signOut";if(G===F.USER.ME)return"user.me";if(G===F.USER.VERIFY)return"user.verify"}if(g==="POST"){if(G===F.ACTIVITY)return"activity"}return null};if(Q&&J!==void 0){if(J!==""&&A===J)return null;if(J!==""&&!A.startsWith(`${J}/`))return null;let G=J===""?A:A.slice(J.length),W=G.startsWith("/")?G:`/${G}`;return V(W)}if(g==="GET"){if(A.endsWith(F.IDENTITY.SIGNIN))return"identity.signIn";if(A.endsWith(F.IDENTITY.CALLBACK))return"identity.callback";if(A.endsWith(F.IDENTITY.SIGNOUT))return"identity.signOut";if(A.endsWith(F.USER.ME))return"user.me";if(A.endsWith(F.USER.VERIFY))return"user.verify"}if(g==="POST"){if(A.endsWith(F.ACTIVITY))return"activity"}return null}function _(j){return"handle"in j?j.handle:j}function $(j){return"activity"in j}function C(j){return"user"in j}
2
- export{M as x,Z as y,_ as z,$ as A,C as B};
@@ -1,2 +0,0 @@
1
- class H{params;sendActivity;_startedAt;_isPaused=!1;_pausedAt;_totalPausedMs=0;_ended=!1;constructor(q,z){this.params=q;this.sendActivity=z;this._startedAt=new Date}get startedAt(){return this._startedAt}get isPaused(){return this._isPaused}get elapsedMs(){if(this._ended)return 0;let q=new Date,z=q.getTime()-this._startedAt.getTime()-this._totalPausedMs;if(this._isPaused&&this._pausedAt)z-=q.getTime()-this._pausedAt.getTime();return Math.max(0,z)}_buildPayload(q){let z=new Date,J,Y;if(q.time)J=Math.max(0,Math.round(q.time.active)),Y=Math.max(0,Math.round(q.time.inactive??0));else Y=this._totalPausedMs+(this._isPaused&&this._pausedAt?z.getTime()-this._pausedAt.getTime():0),J=Math.max(0,z.getTime()-this._startedAt.getTime()-Y);let N=q.totalQuestions!==void 0,j=q.correctQuestions!==void 0;if(N!==j)throw Error("Invalid activity metrics: totalQuestions and correctQuestions must be provided together.");if(N&&j&&q.correctQuestions>q.totalQuestions)throw Error("Invalid activity metrics: correctQuestions cannot exceed totalQuestions.");let F={...N?{totalQuestions:q.totalQuestions}:{},...j?{correctQuestions:q.correctQuestions}:{},...q.xpEarned===void 0?{}:{xpEarned:q.xpEarned},...q.masteredUnits===void 0?{}:{masteredUnits:q.masteredUnits}};return{id:this.params.id,name:this.params.name,course:this.params.course,startedAt:this._startedAt.toISOString(),endedAt:z.toISOString(),elapsedMs:J,pausedMs:Y,metrics:F,...q.pctComplete===void 0?{}:{pctComplete:q.pctComplete}}}pause(){if(this._isPaused||this._ended)return;this._isPaused=!0,this._pausedAt=new Date}resume(){if(!this._isPaused||this._ended||!this._pausedAt)return;this._totalPausedMs+=new Date().getTime()-this._pausedAt.getTime(),this._isPaused=!1,this._pausedAt=void 0}async end(q){if(this._ended)return;if(this._ended=!0,this._isPaused&&this._pausedAt)this._totalPausedMs+=new Date().getTime()-this._pausedAt.getTime(),this._isPaused=!1,this._pausedAt=void 0;let z=this._buildPayload(q);await this.sendActivity(z)}}var Z="/api/timeback",X={ACTIVITY:"/activity",IDENTITY:{SIGNIN:"/identity/signin",SIGNOUT:"/identity/signout",CALLBACK:"/identity/callback"},USER:{ME:"/user/me",VERIFY:"/user/verify"}};var D=3,O=[100,300,1000];function _(){if(!(typeof globalThis>"u"?void 0:globalThis.fetch))return;return(z,J)=>globalThis.fetch(z,J)}function $(){return typeof window<"u"}function K(){if(!$())return;return`${window.location.origin}${Z}`}function B(q){return new Promise((z)=>{setTimeout(z,q)})}function C(q,z){if(typeof q==="number")return q;return q[Math.min(z,q.length-1)]??1000}class G{sendActivity;constructor(q){this.sendActivity=q}start(q){return new H(q,this.sendActivity)}}class Q{getBaseURL;constructor(q){this.getBaseURL=q}signIn(){if(!$())throw Error("signIn() requires a browser environment");window.location.href=`${this.getBaseURL()}${X.IDENTITY.SIGNIN}`}}class V{getBaseURL;fetchImpl;constructor(q,z){this.getBaseURL=q;this.fetchImpl=z}async fetch(){if(!$())throw Error("user.fetch() requires a browser environment");let q=await this.fetchImpl(`${this.getBaseURL()}${X.USER.ME}`,{method:"GET",credentials:"include"});if(!q.ok){let z=await q.json().catch(()=>({error:"Unknown error"}));throw Error(z.error??"Failed to fetch user profile")}return q.json()}async verify(){if(!$())throw Error("user.verify() requires a browser environment");let q=await this.fetchImpl(`${this.getBaseURL()}${X.USER.VERIFY}`,{method:"GET",credentials:"include"});if(!q.ok){let J=await q.json().catch(()=>({error:"Unknown error"}));throw Error(J.error??"Failed to verify Timeback user")}let z=await q.json();if(z.verified&&z.timebackId)return{verified:!0,timebackId:z.timebackId};return{verified:!1}}}class W{activity;auth;user;_baseURL;_fetch;constructor(q={}){this._baseURL=q.baseURL;let z=q.fetch??_();if(!z)throw Error("TimebackClient requires a fetch implementation. Provide `fetch` in the constructor config for non-browser runtimes.");let J=q.plugins,Y=Array.isArray(J)?J:J?[J]:[];this._fetch=Y.reduce((N,j)=>j.wrapFetch(N),z),this.activity=new G((N)=>this.sendActivity(N)),this.auth=new Q(()=>this.baseURL),this.user=new V(()=>this.baseURL,this._fetch)}get baseURL(){if(!this._baseURL){let q=K();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}async sendActivity(q){let z=await this._fetch(`${this.baseURL}${X.ACTIVITY}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(q),credentials:"include"});if(!z.ok){let J=await z.json().catch(()=>({error:"Unknown error"}));throw Error(J.error??"Failed to send activity")}}}function n(q={}){let z=q.baseURL??K();return new W({baseURL:z,fetch:q.fetch,plugins:q.plugins})}
2
- export{D as a,O as b,B as c,C as d,H as e,W as f,n as g};
@@ -1,2 +0,0 @@
1
- import{createHash as B}from"node:crypto";function F(q){return{course:q,component:`${q}-component`,resource:`${q}-resource`,componentResource:`${q}-cr`}}function G(q){return B("sha256").update(q).digest("hex")}function J(q){if(q==="local"||q==="staging")return"staging";return"production"}function K(q){if(q==="production"||q==="local"||q==="staging")return q;return"staging"}function L(q,w=200,z){let A=new Headers(z);return A.set("Content-Type","application/json"),new Response(JSON.stringify(q),{status:w,headers:A})}function M(q,w){let z=new Headers(w);return z.set("Location",q),new Response(null,{status:302,headers:z})}function N(q){let w=JSON.stringify(q);return btoa(w).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function O(q){let w=q.replace(/-/g,"+").replace(/_/g,"/"),z=atob(w);return JSON.parse(z)}var Q={ACTIVITY:"/activity",IDENTITY:{SIGNIN:"/identity/signin",SIGNOUT:"/identity/signout",CALLBACK:"/identity/callback"},USER:{ME:"/user/me",VERIFY:"/user/verify"}},S=0.999999;
2
- export{Q as C,S as D,F as E,G as F,J as G,K as H,L as I,M as J,N as K,O as L};