@timeback/caliper 0.2.0 → 0.2.1-beta.20260331190459

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/dist/index.d.ts CHANGED
@@ -1,741 +1,1521 @@
1
- import * as _timeback_internal_client_infra from '@timeback/internal-client-infra';
2
- import { ClientConfig, TransportOnlyConfig, CaliperPaths, RequestOptions, PaginatedResponse, ProviderClientConfig, BaseTransportConfig, BaseTransport, Paginator as Paginator$1, ListParams, ProviderRegistry, TimebackProvider, AuthCheckResult } from '@timeback/internal-client-infra';
3
- export { AuthCheckResult, EnvAuth, Environment, ExplicitAuth } from '@timeback/internal-client-infra';
1
+ import { CaliperEnvelope, ActivityCompletedEvent, AssessmentItemEvent, QuestionGradeEvent, TimeSpentEvent, CaliperEvent, SendEventsResult, ValidationResult, ListEventsResult, StoredEvent, JobStatus, WaitForCompletionOptions } from '@timeback/types/protocols/caliper';
2
+ export { ActivityCompletedEvent, ActivityMetricType, AssessmentItemEvent, CaliperEnvelope, CaliperEvent, CaliperProfile, JobStatus, QuestionGradeEvent, SendEventsResult, StoredEvent, TimeSpentEvent, TimeSpentMetric, TimeSpentMetricType, TimebackActivity, TimebackActivityContext, TimebackActivityMetric, TimebackActivityMetricsCollection, TimebackApp, TimebackCourse, TimebackEvent, TimebackSubject, TimebackTimeSpentMetricsCollection, TimebackUser, TimebackUserRole, ValidationResult } from '@timeback/types/protocols/caliper';
3
+ import { ActivityCompletedInput, QuestionAnsweredInput, QuestionGradedInput, QuestionSeenInput, TimeSpentInput, CaliperListEventsParams } from '@timeback/types/zod';
4
+ export { ActivityCompletedInput, CaliperListEventsParams as ListEventsParams, QuestionAnsweredInput, QuestionGradedInput, QuestionSeenInput, TimeSpentInput } from '@timeback/types/zod';
4
5
 
5
6
  /**
6
- * Timeback Shared Primitives
7
+ * Interface for obtaining OAuth2 access tokens.
7
8
  *
8
- * Core types used across multiple Timeback protocols.
9
- *
10
- * Organization:
11
- * - Shared types (this file): Used by multiple protocols (Caliper, OneRoster, etc.)
12
- * - Protocol-specific types: Live in their respective protocols/X/primitives.ts files
13
- *
14
- * What belongs here:
15
- * - TimebackSubject - Used in OneRoster courses AND Caliper events
16
- * - TimebackGrade - Used across OneRoster, Caliper, and QTI
17
- * - IMSErrorResponse - IMS Global standard used by multiple 1EdTech protocols
18
- *
19
- * What doesn't belong here:
20
- * - OneRoster-specific: protocols/oneroster/primitives.ts
21
- * - QTI-specific: protocols/qti/primitives.ts
9
+ * Implementations handle token caching and refresh automatically.
22
10
  */
23
-
24
- // ─────────────────────────────────────────────────────────────────────────────
25
- // TIMEBACK SHARED TYPES
26
- // ─────────────────────────────────────────────────────────────────────────────
11
+ interface TokenProvider {
12
+ /**
13
+ * Get a valid access token.
14
+ *
15
+ * Returns a cached token if still valid, otherwise fetches a new one.
16
+ *
17
+ * @returns A valid access token string
18
+ * @throws {Error} If token acquisition fails
19
+ */
20
+ getToken(): Promise<string>;
21
+ /**
22
+ * Invalidate the cached token.
23
+ *
24
+ * Forces the next getToken() call to fetch a fresh token.
25
+ * Should be called when a request fails with 401 Unauthorized.
26
+ *
27
+ * Optional - not all implementations may support invalidation.
28
+ */
29
+ invalidate?(): void;
30
+ }
27
31
 
28
32
  /**
29
- * Valid Timeback subject values.
30
- * Used in OneRoster courses and Caliper events.
31
- */
32
- type TimebackSubject =
33
- | 'Reading'
34
- | 'Language'
35
- | 'Vocabulary'
36
- | 'Social Studies'
37
- | 'Writing'
38
- | 'Science'
39
- | 'FastMath'
40
- | 'Math'
41
- | 'None'
42
- | 'Other'
33
+ * All supported platforms.
34
+ */
35
+ declare const PLATFORMS: readonly ["BEYOND_AI", "LEARNWITH_AI"];
43
36
 
44
37
  /**
45
- * Timeback Profile Types
38
+ * Type Definitions for `@timeback/internal-logger`
46
39
  *
47
- * First-class types for the Timeback Caliper profile, including
48
- * ActivityCompletedEvent and TimeSpentEvent.
40
+ * Central type definitions used across all logger components.
41
+ * These types define the contract between the logger, formatters, and consumers.
49
42
  */
50
-
51
-
52
-
53
- // ═══════════════════════════════════════════════════════════════════════════════
54
- // TIMEBACK ACTIVITY CONTEXT
55
- // ═══════════════════════════════════════════════════════════════════════════════
56
-
57
- // ═══════════════════════════════════════════════════════════════════════════════
58
- // TIMEBACK USER
59
- // ═══════════════════════════════════════════════════════════════════════════════
43
+ /**
44
+ * Log severity levels, ordered from least to most severe.
45
+ *
46
+ * - debug: Detailed diagnostic information for developers
47
+ * - info: General operational messages (app started, request received)
48
+ * - warn: Something unexpected but not breaking (deprecated API used)
49
+ * - error: Something failed (request failed, database connection lost)
50
+ */
51
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
52
+ /**
53
+ * Runtime environments that determine how logs are formatted.
54
+ *
55
+ * - terminal: Local development with colors and icons
56
+ * - ci: CI/CD pipelines with plain text (no ANSI codes)
57
+ * - production: JSON lines for log aggregation (DataDog, CloudWatch, etc.)
58
+ * - browser: Browser console with CSS styling
59
+ * - test: Test environment with no output
60
+ */
61
+ type Environment$2 = 'terminal' | 'ci' | 'production' | 'browser' | 'test';
62
+ /**
63
+ * Arbitrary key-value data attached to log entries.
64
+ *
65
+ * Context is merged into log output - in production it becomes top-level
66
+ * JSON fields, in terminal it's displayed as key=value pairs.
67
+ *
68
+ * @example
69
+ * log.info('User created', { userId: 123, email: 'foo@bar.com' })
70
+ */
71
+ type LogContext = Record<string, unknown>;
72
+ /**
73
+ * Configuration options for creating a logger instance.
74
+ */
75
+ interface LoggerOptions {
76
+ /**
77
+ * Logger scope/namespace for categorizing logs.
78
+ * Child loggers append to this with colons: "api" → "api:users"
79
+ */
80
+ scope?: string;
81
+ /**
82
+ * Minimum log level to output.
83
+ * Logs below this level are silently ignored.
84
+ * @default 'info' (or 'debug' if DEBUG env var is set)
85
+ */
86
+ minLevel?: LogLevel;
87
+ /**
88
+ * Override automatic environment detection.
89
+ * Useful for testing or forcing a specific format.
90
+ */
91
+ environment?: Environment$2;
92
+ /**
93
+ * Default context added to every log entry from this logger.
94
+ * Useful for request IDs, user IDs, etc.
95
+ */
96
+ defaultContext?: LogContext;
97
+ }
60
98
 
61
99
  /**
62
- * User role in the Timeback platform.
100
+ * Logger instance with environment-aware formatting.
101
+ *
102
+ * Instances are lightweight and can be created freely.
103
+ * Common pattern: one logger per module/component.
63
104
  */
64
- type TimebackUserRole = 'student' | 'teacher' | 'admin' | 'guide'
105
+ declare class Logger {
106
+ /** Namespace for this logger (e.g., "api", "api:users") */
107
+ private scope?;
108
+ /** Minimum level to output (logs below this are ignored) */
109
+ private minLevel;
110
+ /** The detected or configured environment */
111
+ private environment;
112
+ /** Function that formats and outputs log entries */
113
+ private formatter;
114
+ /** Context added to every log entry from this logger */
115
+ private defaultContext;
116
+ /**
117
+ * Create a new Logger instance.
118
+ *
119
+ * Usually you'd use createLogger() instead of new Logger().
120
+ */
121
+ constructor(options?: LoggerOptions);
122
+ /**
123
+ * Create a child logger with an additional scope segment.
124
+ *
125
+ * Child loggers inherit minLevel and defaultContext from parent.
126
+ * Scope is appended with a colon separator.
127
+ *
128
+ * @param scope - Additional scope segment to append
129
+ * @returns New Logger instance with extended scope
130
+ *
131
+ * @example
132
+ * const api = createLogger({ scope: 'api' })
133
+ * const users = api.child('users')
134
+ * users.info('Created') // scope: "api:users"
135
+ */
136
+ child(scope: string): Logger;
137
+ /**
138
+ * Create a logger with additional default context.
139
+ *
140
+ * The new context is merged with existing default context.
141
+ * Useful for adding request IDs, user IDs, etc.
142
+ *
143
+ * @param context - Additional context to include in all logs
144
+ * @returns New Logger instance with extended context
145
+ *
146
+ * @example
147
+ * const requestLog = log.withContext({ requestId: 'abc123' })
148
+ * requestLog.info('Processing') // requestId included automatically
149
+ */
150
+ withContext(context: LogContext): Logger;
151
+ /**
152
+ * Log a debug message.
153
+ *
154
+ * Use for detailed diagnostic information useful during development.
155
+ * These are typically filtered out in production.
156
+ *
157
+ * @param message - The log message
158
+ * @param context - Optional key-value context to include with the log
159
+ */
160
+ debug(message: string, context?: LogContext): void;
161
+ /**
162
+ * Log an info message.
163
+ *
164
+ * Use for general operational information.
165
+ * Examples: server started, request received, job completed.
166
+ *
167
+ * @param message - The log message
168
+ * @param context - Optional key-value context to include with the log
169
+ */
170
+ info(message: string, context?: LogContext): void;
171
+ /**
172
+ * Log a warning message.
173
+ *
174
+ * Use for unexpected but non-breaking issues.
175
+ * Examples: deprecated API used, retrying operation, approaching limit.
176
+ *
177
+ * @param message - The log message
178
+ * @param context - Optional key-value context to include with the log
179
+ */
180
+ warn(message: string, context?: LogContext): void;
181
+ /**
182
+ * Log an error message.
183
+ *
184
+ * Use for failures that need attention.
185
+ * Examples: request failed, database error, unhandled exception.
186
+ *
187
+ * @param message - The log message
188
+ * @param context - Optional key-value context to include with the log
189
+ */
190
+ error(message: string, context?: LogContext): void;
191
+ /**
192
+ * Internal method that builds the log entry and passes it to the formatter.
193
+ *
194
+ * All public log methods (debug, info, warn, error) delegate here.
195
+ *
196
+ * @param level - The log level
197
+ * @param message - The log message
198
+ * @param context - Optional key-value context to include with the log
199
+ */
200
+ private log;
201
+ }
65
202
 
66
203
  /**
67
- * Timeback user entity.
204
+ * Where Clause Types
68
205
  *
69
- * Represents a user in the Timeback platform. The `id` should ideally be
70
- * the OneRoster URL for the user when available.
206
+ * Type-safe object syntax for building filter expressions.
207
+ */
208
+ /**
209
+ * Primitive value types that can be used in filters.
210
+ */
211
+ type FilterValue = string | number | boolean | Date;
212
+ /**
213
+ * Operators for a single field.
71
214
  *
72
215
  * @example
73
216
  * ```typescript
74
- * const user: TimebackUser = {
75
- * id: 'https://api.alpha-1edtech.ai/ims/oneroster/rostering/v1p2/users/123',
76
- * type: 'TimebackUser',
77
- * email: 'student@example.edu',
78
- * name: 'Jane Doe',
79
- * role: 'student',
80
- * }
217
+ * { status: { ne: 'deleted' } }
218
+ * { score: { gt: 90 } }
219
+ * { email: { contains: '@school.edu' } }
220
+ * { role: { in: ['teacher', 'aide'] } }
81
221
  * ```
82
222
  */
83
- interface TimebackUser {
84
- /** User identifier (IRI format, preferably OneRoster URL) */
85
- id: string
86
- /** Must be 'TimebackUser' */
87
- type: 'TimebackUser'
88
- /** User email address */
89
- email: string
90
- /** User display name */
91
- name?: string
92
- /** User role */
93
- role?: TimebackUserRole
94
- /** Additional custom attributes */
95
- extensions?: Record<string, unknown>
96
- /** Index signature for Caliper compatibility */
97
- [key: string]: unknown
223
+ interface FieldOperators<T> {
224
+ /** Not equal */
225
+ ne?: T;
226
+ /** Greater than */
227
+ gt?: T;
228
+ /** Greater than or equal */
229
+ gte?: T;
230
+ /** Less than */
231
+ lt?: T;
232
+ /** Less than or equal */
233
+ lte?: T;
234
+ /** Contains substring (strings only) */
235
+ contains?: T extends string ? string : never;
236
+ /** Match any of the values */
237
+ in?: T[];
238
+ /** Match none of the values */
239
+ notIn?: T[];
98
240
  }
99
-
100
241
  /**
101
- * Application reference within activity context.
242
+ * A field condition can be:
243
+ * - A direct value (implies equality)
244
+ * - An object with operators
102
245
  */
103
- interface TimebackApp {
104
- /** Application identifier (IRI format) */
105
- id?: string
106
- /** Application name */
107
- name: string
108
- /** Additional custom attributes */
109
- extensions?: Record<string, unknown>
246
+ type FieldCondition<T> = T | FieldOperators<T>;
247
+ /**
248
+ * Map filter field types to field conditions.
249
+ *
250
+ * Each field in F becomes an optional filter condition.
251
+ *
252
+ * @typeParam F - Filter fields type (e.g., UserFilterFields)
253
+ */
254
+ type FilterFields<F> = {
255
+ [K in keyof F]?: FieldCondition<F[K] & FilterValue>;
256
+ };
257
+ /**
258
+ * OR condition for combining multiple field conditions.
259
+ */
260
+ interface OrCondition<F> {
261
+ OR: WhereClause<F>[];
110
262
  }
263
+ /**
264
+ * A where clause for filtering entities.
265
+ *
266
+ * The type parameter F should be a filter fields type that defines
267
+ * the available fields and their value types for filtering.
268
+ *
269
+ * Multiple fields at the same level are combined with AND.
270
+ * Use `OR` for explicit OR logic.
271
+ *
272
+ * @typeParam F - Filter fields type (e.g., UserFilterFields)
273
+ *
274
+ * @example
275
+ * ```typescript
276
+ * // Simple equality (implicit AND)
277
+ * { status: 'active', role: 'teacher' }
278
+ * // → status='active' AND role='teacher'
279
+ * ```
280
+ *
281
+ * @example
282
+ * ```typescript
283
+ * // With operators
284
+ * { status: { ne: 'deleted' }, score: { gte: 90 } }
285
+ * // → status!='deleted' AND score>=90
286
+ * ```
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * // OR condition
291
+ * { OR: [{ role: 'teacher' }, { role: 'aide' }] }
292
+ * // → role='teacher' OR role='aide'
293
+ * ```
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * // Match multiple values
298
+ * { role: { in: ['teacher', 'aide'] } }
299
+ * // → role='teacher' OR role='aide'
300
+ * ```
301
+ */
302
+ type WhereClause<F> = FilterFields<F> | OrCondition<F>;
111
303
 
112
304
  /**
113
- * Course reference within activity context.
305
+ * Shared Types
306
+ *
307
+ * Common types for API client infrastructure.
114
308
  */
115
- interface TimebackCourse {
116
- /** Course identifier (IRI format, preferably OneRoster URL) */
117
- id?: string
118
- /** Course name */
119
- name?: string
120
- /** Additional custom attributes */
121
- extensions?: Record<string, unknown>
122
- }
123
309
 
124
310
  /**
125
- * Activity reference within activity context.
311
+ * Fetch function signature for HTTP requests.
312
+ * Avoids Bun-specific extensions on `typeof fetch`.
313
+ */
314
+ type FetchFn$1 = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
315
+ /**
316
+ * Supported Timeback platform implementations.
317
+ */
318
+ type Platform$1 = (typeof PLATFORMS)[number];
319
+ /**
320
+ * Supported deployment environments.
321
+ */
322
+ type Environment$1 = 'staging' | 'production';
323
+ /**
324
+ * Auth credentials for environment mode.
325
+ * Token URL is derived automatically from environment.
126
326
  */
127
- interface TimebackActivity {
128
- /** Activity identifier (IRI format) */
129
- id?: string
130
- /** Activity name */
131
- name: string
132
- /** Additional custom attributes */
133
- extensions?: Record<string, unknown>
327
+ interface EnvAuth$1 {
328
+ clientId: string;
329
+ clientSecret: string;
330
+ }
331
+ /**
332
+ * Auth credentials for explicit mode.
333
+ * Includes authUrl for custom APIs.
334
+ * @deprecated Use separate authUrl and ProviderAuth fields instead
335
+ */
336
+ interface ExplicitAuth$1 {
337
+ clientId: string;
338
+ clientSecret: string;
339
+ authUrl: string;
340
+ }
341
+ /**
342
+ * Base configuration options shared by all modes.
343
+ */
344
+ interface BaseConfig$1 {
345
+ /** Request timeout in milliseconds */
346
+ timeout?: number;
347
+ /** Custom fetch implementation */
348
+ fetch?: FetchFn$1;
349
+ }
350
+ /**
351
+ * Environment-based configuration for Timeback APIs.
352
+ */
353
+ interface EnvConfig extends BaseConfig$1 {
354
+ /** Timeback platform implementation (defaults to 'BEYOND_AI') */
355
+ platform?: Platform$1;
356
+ /** Target environment - determines base URL and token URL */
357
+ env: Environment$1;
358
+ /** OAuth2 client credentials */
359
+ auth: EnvAuth$1;
360
+ }
361
+ /**
362
+ * Environment-based configuration with shared token provider.
363
+ */
364
+ interface TokenProviderEnvConfig extends BaseConfig$1 {
365
+ /** Timeback platform implementation (defaults to 'BEYOND_AI') */
366
+ platform?: Platform$1;
367
+ /** Target environment - determines base URL */
368
+ env: Environment$1;
369
+ /** Shared token provider (from @timeback/auth) */
370
+ tokenProvider: TokenProvider;
371
+ }
372
+ /**
373
+ * Explicit URL configuration for custom APIs.
374
+ * Supports both authenticated and public/no-auth services.
375
+ */
376
+ interface ExplicitConfig extends BaseConfig$1 {
377
+ /** API base URL */
378
+ baseUrl: string;
379
+ /**
380
+ * OAuth2 token URL. Omit for public/no-auth services.
381
+ * Can also be provided via auth.authUrl (legacy format).
382
+ */
383
+ authUrl?: string;
384
+ /**
385
+ * OAuth2 credentials. Required if authUrl is provided.
386
+ * Supports both ExplicitAuth (with authUrl) and ProviderAuth (without).
387
+ */
388
+ auth?: ExplicitAuth$1 | ProviderAuth;
389
+ /**
390
+ * Use a built-in path profile by name.
391
+ * Defaults to 'BEYOND_AI' if neither pathProfile nor paths is specified.
392
+ */
393
+ pathProfile?: Platform$1;
394
+ /** Custom path overrides (takes precedence over pathProfile) */
395
+ paths?: Partial<PlatformPaths>;
396
+ /** Not applicable to explicit config — use `EnvConfig` instead */
397
+ env?: never;
134
398
  }
135
-
136
399
  /**
137
- * Timeback activity context.
400
+ * Use pre-configured transport.
401
+ */
402
+ interface TransportConfig extends BaseConfig$1 {
403
+ /** Transport configuration */
404
+ transport: TransportLike;
405
+ }
406
+ /**
407
+ * HTTP request options.
408
+ */
409
+ interface RequestOptions$1 {
410
+ /** HTTP method */
411
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
412
+ /** Query parameters to append to the URL */
413
+ params?: Record<string, string | number | boolean | undefined>;
414
+ /** Request body (will be JSON-serialized) */
415
+ body?: unknown;
416
+ /** Additional headers to include */
417
+ headers?: Record<string, string>;
418
+ /**
419
+ * Unique identifier for this request.
420
+ * Used for log correlation and debugging.
421
+ * Auto-generated if not provided.
422
+ */
423
+ requestId?: string;
424
+ }
425
+ /**
426
+ * Duck-typed transport interface for testing and advanced use.
427
+ */
428
+ interface TransportLike {
429
+ /** Base URL of the API */
430
+ baseUrl: string;
431
+ /** Make an authenticated request */
432
+ request<T>(path: string, options?: RequestOptions$1): Promise<T>;
433
+ }
434
+ /**
435
+ * Configuration using a pre-configured transport.
436
+ *
437
+ * For advanced use cases like sharing a transport between clients
438
+ * or using a custom transport implementation.
138
439
  *
139
- * Represents the context where an event was recorded, including
140
- * subject, application, course, and activity information.
440
+ * @template T - Transport type (defaults to TransportLike, clients should specify their full transport type)
441
+ */
442
+ interface TransportOnlyConfig<T extends TransportLike = TransportLike> {
443
+ /** Existing transport instance */
444
+ transport: T;
445
+ }
446
+ /**
447
+ * Union of all client configuration types.
448
+ */
449
+ type ClientConfig = EnvConfig | TokenProviderEnvConfig | ExplicitConfig | TransportConfig | Partial<ExplicitConfig>;
450
+ /**
451
+ * Parameters for listing resources with pagination, sorting, and filtering.
452
+ *
453
+ * Common across all Timeback APIs (OneRoster, Edubridge, QTI, etc.).
454
+ *
455
+ * @typeParam T - Entity type for type-safe `where` clause (defaults to unknown)
141
456
  *
142
457
  * @example
143
458
  * ```typescript
144
- * const context: TimebackActivityContext = {
145
- * id: 'https://myapp.example.com/activities/123',
146
- * type: 'TimebackActivityContext',
147
- * subject: 'Math',
148
- * app: { name: 'My Learning App' },
149
- * course: { name: 'Algebra 101' },
150
- * activity: { name: 'Chapter 1 Quiz' },
151
- * }
459
+ * // Type-safe where
460
+ * client.users.list({
461
+ * where: { status: 'active', role: 'teacher' },
462
+ * sort: 'familyName',
463
+ * })
152
464
  * ```
153
465
  */
154
- interface TimebackActivityContext {
155
- /** Context identifier (IRI format) */
156
- id: string
157
- /** Must be 'TimebackActivityContext' */
158
- type: 'TimebackActivityContext'
159
- /** Subject area */
160
- subject: TimebackSubject
161
- /** Application where event was recorded */
162
- app: TimebackApp
163
- /** Course where event was recorded */
164
- course: TimebackCourse
165
- /** Activity where event was recorded */
166
- activity?: TimebackActivity
167
- /** Whether to process this event */
168
- process?: boolean
169
- /** Index signature for Caliper compatibility */
170
- [key: string]: unknown
466
+ interface ListParams<T = unknown> {
467
+ /**
468
+ * Maximum items per page.
469
+ * @default 100
470
+ */
471
+ limit?: number;
472
+ /**
473
+ * Number of items to skip (for pagination).
474
+ * @default 0
475
+ */
476
+ offset?: number;
477
+ /**
478
+ * Field name to sort results by.
479
+ *
480
+ * Type-safe: only valid field names for this resource are accepted.
481
+ *
482
+ * @example
483
+ * ```typescript
484
+ * sort: 'familyName'
485
+ * ```
486
+ */
487
+ sort?: keyof T & string;
488
+ /**
489
+ * Sort direction.
490
+ * @default "asc"
491
+ */
492
+ orderBy?: 'asc' | 'desc';
493
+ /**
494
+ * Type-safe filter using object syntax.
495
+ *
496
+ * Multiple fields are combined with AND. Use `OR` for OR logic.
497
+ *
498
+ * @example
499
+ * ```typescript
500
+ * // Simple equality
501
+ * where: { status: 'active' }
502
+ * ```
503
+ *
504
+ * @example
505
+ * ```typescript
506
+ * // Multiple fields (AND)
507
+ * where: { status: 'active', role: 'teacher' }
508
+ * ```
509
+ *
510
+ * @example
511
+ * ```typescript
512
+ * // With operators
513
+ * where: { score: { gte: 90 }, status: { ne: 'deleted' } }
514
+ * ```
515
+ *
516
+ * @example
517
+ * ```typescript
518
+ * // OR condition
519
+ * where: { role: { in: ['teacher', 'aide'] } }
520
+ * ```
521
+ */
522
+ where?: WhereClause<T>;
523
+ /**
524
+ * Fields to include in the response.
525
+ * Reduces payload size by requesting only needed fields.
526
+ * @example ['sourcedId', 'givenName', 'familyName']
527
+ */
528
+ fields?: string[];
529
+ /**
530
+ * Free-text search across multiple fields (proprietary extension).
531
+ * For users: searches givenName, familyName, email.
532
+ * @example "john@example.com"
533
+ */
534
+ search?: string;
535
+ /**
536
+ * Maximum total items to return across all pages.
537
+ *
538
+ * Unlike `limit` (which sets page size), `max` caps the total number
539
+ * of items yielded by the paginator. Pagination stops once this many
540
+ * items have been returned.
541
+ *
542
+ * @example
543
+ * ```typescript
544
+ * // Get at most 50 users total
545
+ * client.users.list({ max: 50 })
546
+ * ```
547
+ */
548
+ max?: number;
549
+ }
550
+ /**
551
+ * Response from a paginated API request.
552
+ */
553
+ interface PaginatedResponse<T> {
554
+ /** Array of items in this page */
555
+ data: T[];
556
+ /** Whether more pages are available */
557
+ hasMore: boolean;
558
+ /** Total count of items (if provided by server) */
559
+ total?: number;
171
560
  }
172
-
173
- // ═══════════════════════════════════════════════════════════════════════════════
174
- // ACTIVITY METRICS
175
- // ═══════════════════════════════════════════════════════════════════════════════
176
-
177
561
  /**
178
- * Types of activity metrics.
562
+ * Result of fetching a single page of resources.
563
+ *
564
+ * @typeParam T - The type of items in the page
565
+ */
566
+ interface PageResult<T> {
567
+ /** Array of items in this page */
568
+ data: T[];
569
+ /** Whether more pages are available */
570
+ hasMore: boolean;
571
+ /** Total count of items (if provided by server) */
572
+ total?: number;
573
+ /** Offset to use for fetching the next page */
574
+ nextOffset?: number;
575
+ }
576
+ /**
577
+ * Options for toArray() method.
578
+ */
579
+ interface ToArrayOptions {
580
+ /**
581
+ * Maximum number of items to collect.
582
+ *
583
+ * Throws an error if this limit is exceeded, preventing OOM on large datasets.
584
+ * Use `for await...of` to stream results instead.
585
+ *
586
+ * @default 10_000
587
+ * @example Set to Infinity to disable (use with caution!)
588
+ */
589
+ maxItems?: number;
590
+ }
591
+ /**
592
+ * Function that fetches a page of results.
593
+ * Provided by the transport layer.
594
+ */
595
+ type PageFetcher<T> = (path: string, options: {
596
+ params: Record<string, string | number | boolean | undefined>;
597
+ }) => Promise<PaginatedResponse<T>>;
598
+ /**
599
+ * Pagination style for API requests.
600
+ *
601
+ * - `'offset'`: Uses limit/offset parameters (default, e.g., OneRoster)
602
+ * - `'page'`: Uses limit/page parameters (1-indexed, e.g., QTI)
179
603
  */
180
- type ActivityMetricType =
181
- | 'xpEarned'
182
- | 'totalQuestions'
183
- | 'correctQuestions'
184
- | 'masteredUnits'
604
+ type PaginationStyle = 'offset' | 'page';
605
+ /**
606
+ * Options for creating a Paginator.
607
+ */
608
+ interface PaginatorOptions<T, F = unknown> {
609
+ /** Function to fetch a page of results */
610
+ fetcher: PageFetcher<T>;
611
+ /** API endpoint path */
612
+ path: string;
613
+ /** List parameters (filter, sort, limit, offset) */
614
+ params?: ListParams<F>;
615
+ /** Maximum total items to return across all pages (client-side cap) */
616
+ max?: number;
617
+ /** Response key containing the items array (e.g., "users") */
618
+ unwrapKey?: string;
619
+ /** Logger instance (defaults to client-common logger) */
620
+ logger?: Logger;
621
+ /** Optional transform function applied to each item before yielding */
622
+ transform?: (item: T) => T;
623
+ /**
624
+ * Pagination style to use for API requests.
625
+ *
626
+ * - `'offset'` (default): Sends `limit` and `offset` params
627
+ * - `'page'`: Sends `limit` and `page` params (1-indexed)
628
+ *
629
+ * @default 'offset'
630
+ */
631
+ paginationStyle?: PaginationStyle;
632
+ }
633
+ /**
634
+ * Result of an auth check operation.
635
+ */
636
+ interface AuthCheckResult$1 {
637
+ /** Whether auth succeeded */
638
+ ok: boolean;
639
+ /** Time taken to complete the check (ms) */
640
+ latencyMs: number;
641
+ /** Error message if failed */
642
+ error?: string;
643
+ /** Detailed check results */
644
+ checks: {
645
+ /** Token acquisition succeeded */
646
+ tokenAcquisition: boolean;
647
+ };
648
+ }
185
649
 
186
650
  /**
187
- * Individual activity metric.
651
+ * Config Types
188
652
  *
189
- * @example
190
- * ```typescript
191
- * const metric: TimebackActivityMetric = {
192
- * type: 'correctQuestions',
193
- * value: 8,
194
- * }
195
- * ```
653
+ * Types for TimebackProvider and provider resolution.
654
+ */
655
+
656
+ /**
657
+ * Caliper API path profile.
658
+ * Defines paths for Caliper operations. Use `null` for unsupported operations.
659
+ */
660
+ interface CaliperPaths {
661
+ /** Path for sending events (POST) */
662
+ send: string;
663
+ /** Path for validating events (POST), null if not supported */
664
+ validate: string | null;
665
+ /** Path for listing events (GET), null if not supported */
666
+ list: string | null;
667
+ /** Path template for getting single event (GET), use {id} placeholder */
668
+ get: string | null;
669
+ /** Path template for job status (GET), use {id} placeholder */
670
+ jobStatus: string | null;
671
+ }
672
+ /**
673
+ * Webhook API path profile.
674
+ * Defines paths for webhook management operations.
675
+ * Nullability is at the platform level (`webhooks: WebhookPaths | null` in PlatformPaths).
676
+ */
677
+ interface WebhookPaths {
678
+ /** Path for listing webhooks (GET) */
679
+ webhookList: string;
680
+ /** Path template for getting a single webhook (GET), use {id} placeholder */
681
+ webhookGet: string;
682
+ /** Path for creating a webhook (POST) */
683
+ webhookCreate: string;
684
+ /** Path template for updating a webhook (PUT), use {id} placeholder */
685
+ webhookUpdate: string;
686
+ /** Path template for deleting a webhook (DELETE), use {id} placeholder */
687
+ webhookDelete: string;
688
+ /** Path template for activating a webhook (PUT), use {id} placeholder */
689
+ webhookActivate: string;
690
+ /** Path template for deactivating a webhook (PUT), use {id} placeholder */
691
+ webhookDeactivate: string;
692
+ /** Path for listing all webhook filters (GET) */
693
+ webhookFilterList: string;
694
+ /** Path template for getting a single webhook filter (GET), use {id} placeholder */
695
+ webhookFilterGet: string;
696
+ /** Path for creating a webhook filter (POST) */
697
+ webhookFilterCreate: string;
698
+ /** Path template for updating a webhook filter (PUT), use {id} placeholder */
699
+ webhookFilterUpdate: string;
700
+ /** Path template for deleting a webhook filter (DELETE), use {id} placeholder */
701
+ webhookFilterDelete: string;
702
+ /** Path template for listing filters by webhook (GET), use {webhookId} placeholder */
703
+ webhookFiltersByWebhook: string;
704
+ }
705
+ /**
706
+ * Reporting API path profile.
707
+ * Defines paths for reporting MCP and REST operations.
708
+ * Nullability is at the platform level (`reporting: ReportingPaths | null` in PlatformPaths).
709
+ */
710
+ interface ReportingPaths {
711
+ /** Path for the reporting MCP JSON-RPC endpoint (POST) */
712
+ mcp: string;
713
+ /** Path template for executing a saved query (GET), use {id} placeholder */
714
+ savedQueryExecute: string;
715
+ /** Path template for checking reporting group membership (GET), use {email} placeholder */
716
+ adminGroupCheck: string;
717
+ /** Path template for adding a user to the reporting group (POST), use {email} placeholder */
718
+ adminGroupAdd: string;
719
+ /** Path template for removing a user from the reporting group (DELETE), use {email} placeholder */
720
+ adminGroupRemove: string;
721
+ }
722
+ /**
723
+ * OneRoster API path profile.
724
+ * Defines the base path prefix for all OneRoster resources.
725
+ */
726
+ interface OneRosterPaths {
727
+ /** Base path prefix for rostering resources (users, schools, classes, etc.) */
728
+ rostering: string;
729
+ /** Base path prefix for gradebook resources (lineItems, results, etc.) */
730
+ gradebook: string;
731
+ /** Base path prefix for resources API (digital learning resources) */
732
+ resources: string;
733
+ }
734
+ /**
735
+ * Edubridge API path profile.
736
+ * Defines path prefixes for Edubridge operations.
737
+ */
738
+ interface EdubridgePaths {
739
+ /** Base path prefix for all Edubridge resources */
740
+ base: string;
741
+ }
742
+ /**
743
+ * PowerPath API path profile.
744
+ * Defines path prefixes for PowerPath operations.
745
+ */
746
+ interface PowerPathPaths {
747
+ /** Base path prefix for all PowerPath resources */
748
+ base: string;
749
+ }
750
+ /**
751
+ * CASE API path profile.
752
+ * Defines path prefix for CASE (Competency and Academic Standards Exchange) operations.
753
+ */
754
+ interface CasePaths {
755
+ /** Base path prefix for all CASE resources */
756
+ base: string;
757
+ }
758
+ /**
759
+ * CLR API path profile.
760
+ * Defines path prefixes for CLR (Comprehensive Learner Record) operations.
761
+ */
762
+ interface ClrPaths {
763
+ /** Path for upserting CLR credentials (POST) */
764
+ credentials: string;
765
+ /** Path for API discovery (GET) */
766
+ discovery: string;
767
+ }
768
+ /**
769
+ * Platform path profiles for all services.
770
+ * Use `null` to indicate a service is not supported on the platform.
771
+ */
772
+ interface PlatformPaths {
773
+ caliper: CaliperPaths;
774
+ oneroster: OneRosterPaths;
775
+ webhooks: WebhookPaths | null;
776
+ reporting: ReportingPaths | null;
777
+ edubridge: EdubridgePaths | null;
778
+ powerpath: PowerPathPaths | null;
779
+ clr: ClrPaths | null;
780
+ case: CasePaths | null;
781
+ }
782
+ /**
783
+ * Services that have path configuration.
784
+ * Subset of ServiceName - excludes services without path profiles (e.g., 'qti').
785
+ */
786
+ type PathEnabledService = keyof PlatformPaths;
787
+ /**
788
+ * Supported Timeback platform implementations.
789
+ */
790
+ type Platform = (typeof PLATFORMS)[number];
791
+ /**
792
+ * Supported deployment environments.
793
+ */
794
+ type Environment = 'staging' | 'production';
795
+ /**
796
+ * Supported service names.
797
+ */
798
+ type ServiceName = 'oneroster' | 'caliper' | 'webhooks' | 'reporting' | 'edubridge' | 'qti' | 'powerpath' | 'clr' | 'case';
799
+ /**
800
+ * Resolved endpoint for a single service.
801
+ */
802
+ interface ResolvedEndpoint {
803
+ /** Base URL for the service API */
804
+ baseUrl: string;
805
+ /** OAuth2 token URL for this endpoint. Undefined for public/no-auth services. */
806
+ authUrl?: string;
807
+ }
808
+ /**
809
+ * Auth credentials for a provider.
810
+ */
811
+ interface ProviderAuth {
812
+ clientId: string;
813
+ clientSecret: string;
814
+ }
815
+ /**
816
+ * Configuration for environment-based provider.
817
+ * Uses known Timeback platform endpoints.
818
+ */
819
+ interface ProviderEnvConfig {
820
+ /** Timeback platform (defaults to 'BEYOND_AI') */
821
+ platform?: Platform;
822
+ /** Target environment */
823
+ env: Environment;
824
+ /** OAuth2 credentials */
825
+ auth: ProviderAuth;
826
+ /** Request timeout in milliseconds */
827
+ timeout?: number;
828
+ }
829
+ /**
830
+ * Configuration for explicit URL provider.
831
+ * Single base URL for all services.
832
+ */
833
+ interface ProviderExplicitConfig {
834
+ /** Base URL for all services */
835
+ baseUrl: string;
836
+ /** OAuth2 token URL. Omit for public/no-auth services. */
837
+ authUrl?: string;
838
+ /** OAuth2 credentials. Required if authUrl is provided. */
839
+ auth?: ProviderAuth;
840
+ /** Request timeout in milliseconds */
841
+ timeout?: number;
842
+ /**
843
+ * Use a built-in path profile by name.
844
+ * Defaults to 'BEYOND_AI' if neither pathProfile nor paths is specified.
845
+ */
846
+ pathProfile?: Platform;
847
+ /** Custom path overrides (takes precedence over pathProfile) */
848
+ paths?: Partial<PlatformPaths>;
849
+ }
850
+ /**
851
+ * Configuration for multi-service provider.
852
+ * Different URLs for different services.
853
+ */
854
+ interface ProviderServicesConfig {
855
+ /** Per-service base URLs */
856
+ services: Partial<Record<ServiceName, string>>;
857
+ /** OAuth2 token URL. Omit for public/no-auth services. */
858
+ authUrl?: string;
859
+ /** OAuth2 credentials. Required if authUrl is provided. */
860
+ auth?: ProviderAuth;
861
+ /** Request timeout in milliseconds */
862
+ timeout?: number;
863
+ /**
864
+ * Use a built-in path profile by name.
865
+ * Defaults to 'BEYOND_AI' if neither pathProfile nor paths is specified.
866
+ */
867
+ pathProfile?: Platform;
868
+ /** Custom path overrides (takes precedence over pathProfile) */
869
+ paths?: Partial<PlatformPaths>;
870
+ }
871
+ /**
872
+ * Union of all provider configuration types.
873
+ */
874
+ type TimebackProviderConfig = ProviderEnvConfig | ProviderExplicitConfig | ProviderServicesConfig;
875
+ /**
876
+ * Provider template - endpoints without auth.
877
+ * Used internally to define available platform+env combinations.
196
878
  */
197
- interface TimebackActivityMetric {
198
- /** Metric type */
199
- type: ActivityMetricType
200
- /** Metric value */
201
- value: number
202
- /** Additional custom attributes */
203
- extensions?: Record<string, unknown>
879
+ interface ProviderTemplate {
880
+ platform: Platform;
881
+ env: Environment;
204
882
  }
205
-
206
883
  /**
207
- * Collection of activity metrics.
884
+ * Registry of provider templates indexed by platform and environment.
885
+ *
886
+ * Use `satisfies ProviderRegistry` when defining a registry for type checking.
208
887
  *
209
888
  * @example
210
- * ```typescript
211
- * const metrics: TimebackActivityMetricsCollection = {
212
- * id: 'https://myapp.example.com/metrics/123',
213
- * type: 'TimebackActivityMetricsCollection',
214
- * attempt: 1,
215
- * items: [
216
- * { type: 'totalQuestions', value: 10 },
217
- * { type: 'correctQuestions', value: 8 },
218
- * { type: 'xpEarned', value: 150 },
219
- * ],
220
- * extensions: { pctCompleteApp: 67 },
221
- * }
222
- * ```
889
+ * const myRegistry = {
890
+ * defaultPlatform: 'MY_PLATFORM',
891
+ * templates: {
892
+ * MY_PLATFORM: {
893
+ * staging: { platform: 'BEYOND_AI', env: 'staging' },
894
+ * production: { platform: 'BEYOND_AI', env: 'production' },
895
+ * },
896
+ * },
897
+ * } satisfies ProviderRegistry
223
898
  */
224
- interface TimebackActivityMetricsCollection {
225
- /** Collection identifier (IRI format) */
226
- id: string
227
- /** Must be 'TimebackActivityMetricsCollection' */
228
- type: 'TimebackActivityMetricsCollection'
229
- /** Attempt number (1-based) */
230
- attempt?: number
231
- /** Array of metrics */
232
- items: TimebackActivityMetric[]
233
- /**
234
- * Additional custom attributes.
235
- *
236
- * Common fields:
237
- * - `pctCompleteApp`: App-defined course completion percentage (0–100)
238
- */
239
- extensions?: Record<string, unknown>
240
- /** Index signature for Caliper compatibility */
241
- [key: string]: unknown
899
+ interface ProviderRegistry {
900
+ /** Default platform when none specified */
901
+ defaultPlatform: string;
902
+ /** Available templates indexed by platform → env */
903
+ templates: Record<string, Record<string, ProviderTemplate>>;
242
904
  }
243
-
244
- // ═══════════════════════════════════════════════════════════════════════════════
245
- // TIME SPENT METRICS
246
- // ═══════════════════════════════════════════════════════════════════════════════
247
-
248
905
  /**
249
- * Types of time spent metrics.
906
+ * Client config that accepts a pre-built provider.
250
907
  */
251
- type TimeSpentMetricType = 'active' | 'inactive' | 'waste' | 'unknown' | 'anti-pattern'
908
+ interface ProviderClientConfig {
909
+ /** Pre-built provider */
910
+ provider: TimebackProvider;
911
+ }
252
912
 
253
913
  /**
254
- * Individual time spent metric.
914
+ * Timeback Provider
255
915
  *
256
- * @example
257
- * ```typescript
258
- * const metric: TimeSpentMetric = {
259
- * type: 'active',
260
- * value: 1800, // 30 minutes in seconds
261
- * startDate: '2024-01-15T10:00:00Z',
262
- * endDate: '2024-01-15T10:30:00Z',
263
- * }
264
- * ```
916
+ * Encapsulates platform connection configuration including endpoints and auth.
917
+ * Providers are complete "connection" objects that clients consume.
265
918
  */
266
- interface TimeSpentMetric {
267
- /** Metric type */
268
- type: TimeSpentMetricType
269
- /** Time spent in seconds (max 86400 = 24 hours) */
270
- value: number
271
- /** Sub-type for additional categorization */
272
- subType?: string
273
- /** Start of the time period */
274
- startDate?: string
275
- /** End of the time period */
276
- endDate?: string
277
- /** Additional custom attributes */
278
- extensions?: Record<string, unknown>
279
- }
280
919
 
281
920
  /**
282
- * Collection of time spent metrics.
921
+ * Timeback Provider - encapsulates a complete platform connection.
922
+ *
923
+ * A provider contains everything needed to connect to Timeback APIs:
924
+ * - Service endpoints (URLs)
925
+ * - Authentication credentials
926
+ * - Configuration options
927
+ *
928
+ * Providers can be created from:
929
+ * - Platform + environment (uses known Timeback endpoints)
930
+ * - Explicit base URL (single URL for all services)
931
+ * - Per-service URLs (different URLs for each service)
283
932
  *
284
933
  * @example
285
934
  * ```typescript
286
- * const metrics: TimebackTimeSpentMetricsCollection = {
287
- * id: 'https://myapp.example.com/time-metrics/123',
288
- * type: 'TimebackTimeSpentMetricsCollection',
289
- * items: [
290
- * { type: 'active', value: 1800 },
291
- * { type: 'inactive', value: 300 },
292
- * ],
293
- * }
935
+ * // Environment-based provider (Timeback hosted)
936
+ * const provider = new TimebackProvider({
937
+ * platform: 'BEYOND_AI',
938
+ * env: 'staging',
939
+ * auth: { clientId: '...', clientSecret: '...' },
940
+ * })
294
941
  * ```
295
- */
296
- interface TimebackTimeSpentMetricsCollection {
297
- /** Collection identifier (IRI format) */
298
- id: string
299
- /** Must be 'TimebackTimeSpentMetricsCollection' */
300
- type: 'TimebackTimeSpentMetricsCollection'
301
- /** Array of time spent metrics */
302
- items: TimeSpentMetric[]
303
- /** Additional custom attributes */
304
- extensions?: Record<string, unknown>
305
- /** Index signature for Caliper compatibility */
306
- [key: string]: unknown
307
- }
308
-
309
- // ═══════════════════════════════════════════════════════════════════════════════
310
- // TIMEBACK EVENTS
311
- // ═══════════════════════════════════════════════════════════════════════════════
312
-
313
- /**
314
- * Base properties common to all Timeback events.
315
- */
316
- interface TimebackEventBase {
317
- /** JSON-LD context */
318
- '@context': 'http://purl.imsglobal.org/ctx/caliper/v1p2'
319
- /** Unique identifier (URN UUID format) */
320
- id: string
321
- /** The user who performed the action */
322
- actor: TimebackUser
323
- /** The activity context */
324
- object: TimebackActivityContext
325
- /** ISO 8601 datetime when event occurred */
326
- eventTime: string
327
- /** Must be 'TimebackProfile' */
328
- profile: 'TimebackProfile'
329
- /** Application context (IRI or entity) */
330
- edApp?: string | Record<string, unknown>
331
- /** Target segment within object */
332
- target?: string | Record<string, unknown>
333
- /** Referring context */
334
- referrer?: string | Record<string, unknown>
335
- /** Organization/group context */
336
- group?: string | Record<string, unknown>
337
- /** User's membership/role */
338
- membership?: string | Record<string, unknown>
339
- /** Current user session */
340
- session?: string | Record<string, unknown>
341
- /** LTI session context */
342
- federatedSession?: string | Record<string, unknown>
343
- /** Additional custom attributes */
344
- extensions?: Record<string, unknown>
345
- }
346
-
347
- /**
348
- * Timeback Activity Completed Event.
349
- *
350
- * Records when a student completes an activity, along with performance metrics.
351
942
  *
352
943
  * @example
353
944
  * ```typescript
354
- * const event: ActivityCompletedEvent = {
355
- * '@context': 'http://purl.imsglobal.org/ctx/caliper/v1p2',
356
- * id: 'urn:uuid:c51570e4-f8ed-4c18-bb3a-dfe51b2cc594',
357
- * type: 'ActivityEvent',
358
- * action: 'Completed',
359
- * actor: {
360
- * id: 'https://api.example.com/users/123',
361
- * type: 'TimebackUser',
362
- * email: 'student@example.edu',
363
- * },
364
- * object: {
365
- * id: 'https://myapp.example.com/activities/456',
366
- * type: 'TimebackActivityContext',
367
- * subject: 'Math',
368
- * app: { name: 'My Learning App' },
369
- * },
370
- * eventTime: '2024-01-15T14:30:00Z',
371
- * profile: 'TimebackProfile',
372
- * generated: {
373
- * id: 'https://myapp.example.com/metrics/789',
374
- * type: 'TimebackActivityMetricsCollection',
375
- * items: [
376
- * { type: 'totalQuestions', value: 10 },
377
- * { type: 'correctQuestions', value: 8 },
378
- * ],
379
- * },
380
- * }
945
+ * // Explicit URL provider (self-hosted)
946
+ * const provider = new TimebackProvider({
947
+ * baseUrl: 'https://api.myschool.edu',
948
+ * authUrl: 'https://auth.myschool.edu/oauth/token',
949
+ * auth: { clientId: '...', clientSecret: '...' },
950
+ * })
381
951
  * ```
382
- */
383
- interface ActivityCompletedEvent extends TimebackEventBase {
384
- /** Must be 'ActivityEvent' */
385
- type: 'ActivityEvent'
386
- /** Must be 'Completed' */
387
- action: 'Completed'
388
- /** Activity metrics generated from this completion */
389
- generated: TimebackActivityMetricsCollection
390
- }
391
-
392
- /**
393
- * Timeback Time Spent Event.
394
- *
395
- * Records time spent on an activity, categorized by engagement type.
396
952
  *
397
953
  * @example
398
954
  * ```typescript
399
- * const event: TimeSpentEvent = {
400
- * '@context': 'http://purl.imsglobal.org/ctx/caliper/v1p2',
401
- * id: 'urn:uuid:d62681f5-g9fe-5d29-cc4b-efg62c3dd695',
402
- * type: 'TimeSpentEvent',
403
- * action: 'SpentTime',
404
- * actor: {
405
- * id: 'https://api.example.com/users/123',
406
- * type: 'TimebackUser',
407
- * email: 'student@example.edu',
408
- * },
409
- * object: {
410
- * id: 'https://myapp.example.com/activities/456',
411
- * type: 'TimebackActivityContext',
412
- * subject: 'Reading',
413
- * app: { name: 'My Learning App' },
414
- * },
415
- * eventTime: '2024-01-15T15:00:00Z',
416
- * profile: 'TimebackProfile',
417
- * generated: {
418
- * id: 'https://myapp.example.com/time-metrics/789',
419
- * type: 'TimebackTimeSpentMetricsCollection',
420
- * items: [
421
- * { type: 'active', value: 1800 },
422
- * { type: 'inactive', value: 300 },
423
- * ],
955
+ * // Per-service URLs
956
+ * const provider = new TimebackProvider({
957
+ * services: {
958
+ * oneroster: 'https://roster.myschool.edu',
959
+ * caliper: 'https://analytics.myschool.edu',
424
960
  * },
425
- * }
961
+ * authUrl: 'https://auth.myschool.edu/oauth/token',
962
+ * auth: { clientId: '...', clientSecret: '...' },
963
+ * })
426
964
  * ```
427
965
  */
428
- interface TimeSpentEvent extends TimebackEventBase {
429
- /** Must be 'TimeSpentEvent' */
430
- type: 'TimeSpentEvent'
431
- /** Must be 'SpentTime' */
432
- action: 'SpentTime'
433
- /** Time spent metrics generated from this session */
434
- generated: TimebackTimeSpentMetricsCollection
966
+ declare class TimebackProvider {
967
+ /** Platform identifier (if using known platform) */
968
+ readonly platform?: Platform;
969
+ /** Environment (if using known platform) */
970
+ readonly env?: Environment;
971
+ /** OAuth2 credentials. Undefined for public/no-auth services. */
972
+ readonly auth?: ProviderAuth;
973
+ /** Request timeout in milliseconds */
974
+ readonly timeout: number;
975
+ /** Resolved endpoints for each service */
976
+ /** @internal */
977
+ readonly _endpoints: Partial<Record<ServiceName, ResolvedEndpoint>>;
978
+ /** Token URL for authentication. Undefined for public/no-auth services. */
979
+ /** @internal */
980
+ readonly _authUrl?: string;
981
+ /** OAuth2 scope to request with access tokens. */
982
+ /** @internal */
983
+ readonly _tokenScope?: string;
984
+ /** API path profiles for this platform */
985
+ /** @internal */
986
+ readonly _pathProfiles: PlatformPaths;
987
+ /** Cached TokenManagers by authUrl (for token sharing) */
988
+ /** @internal */
989
+ readonly _tokenManagers: Map<string, TokenProvider>;
990
+ /**
991
+ * Create a new TimebackProvider.
992
+ *
993
+ * @param config - Provider configuration (env-based, explicit URL, or per-service)
994
+ * @throws {Error} If configuration is invalid or missing required fields
995
+ */
996
+ constructor(config: TimebackProviderConfig);
997
+ /**
998
+ * Get the resolved endpoint for a specific service.
999
+ *
1000
+ * @param service - Service name (oneroster, caliper, edubridge, qti, powerpath)
1001
+ * @returns Resolved endpoint with baseUrl and authUrl
1002
+ * @throws If the service is not configured in this provider
1003
+ */
1004
+ getEndpoint(service: ServiceName): ResolvedEndpoint;
1005
+ /**
1006
+ * Check if a service is available in this provider.
1007
+ *
1008
+ * @param service - Service name to check
1009
+ * @returns True if the service is configured
1010
+ */
1011
+ hasService(service: ServiceName): boolean;
1012
+ /**
1013
+ * Get all configured service names.
1014
+ *
1015
+ * @returns Array of service names available in this provider
1016
+ */
1017
+ getAvailableServices(): ServiceName[];
1018
+ /**
1019
+ * Get the token URL for this provider.
1020
+ * @returns The token URL for authentication
1021
+ */
1022
+ getTokenUrl(): string | undefined;
1023
+ /**
1024
+ * Get endpoint with paths for a service that has path configuration.
1025
+ *
1026
+ * @param service - Service name that has paths in PlatformPaths
1027
+ * @returns Resolved endpoint with baseUrl, authUrl, and paths
1028
+ * @throws If service is not configured or not supported on this platform
1029
+ */
1030
+ getEndpointWithPaths<S extends PathEnabledService>(service: S & ServiceName): ResolvedEndpoint & {
1031
+ paths: NonNullable<PlatformPaths[S]>;
1032
+ };
1033
+ /**
1034
+ * Get all path profiles for this provider (raw, may contain nulls).
1035
+ *
1036
+ * @returns Platform path profiles
1037
+ */
1038
+ getPaths(): PlatformPaths;
1039
+ /**
1040
+ * Get paths for a specific service.
1041
+ *
1042
+ * @param service - Service name
1043
+ * @returns Path configuration for the service
1044
+ * @throws If the service is not supported on this platform
1045
+ */
1046
+ getServicePaths<S extends PathEnabledService>(service: S): NonNullable<PlatformPaths[S]>;
1047
+ /**
1048
+ * Check if a service is supported on this platform.
1049
+ *
1050
+ * @param service - Service name
1051
+ * @returns true if the service has path configuration
1052
+ */
1053
+ hasServiceSupport(service: PathEnabledService): boolean;
1054
+ /**
1055
+ * Get a TokenProvider for a specific service.
1056
+ *
1057
+ * TokenProviders are cached by authUrl, so services sharing the same
1058
+ * token endpoint will share the same cached OAuth tokens.
1059
+ *
1060
+ * @param service - Service name (oneroster, caliper, edubridge, qti, powerpath)
1061
+ * @returns Cached TokenProvider for the service's token endpoint, or undefined for public/no-auth services
1062
+ * @throws If the service is not configured in this provider
1063
+ * @throws If auth is required but not configured
1064
+ */
1065
+ getTokenProvider(service: ServiceName): TokenProvider | undefined;
1066
+ /**
1067
+ * Verify that OAuth authentication is working.
1068
+ *
1069
+ * Attempts to acquire a token using the provider's credentials.
1070
+ * Returns a health check result with success/failure and latency info.
1071
+ *
1072
+ * @returns Auth check result
1073
+ * @throws {Error} If no auth is configured on this provider
1074
+ */
1075
+ checkAuth(): Promise<AuthCheckResult$1>;
1076
+ /**
1077
+ * Invalidate all cached OAuth tokens.
1078
+ *
1079
+ * Call this when closing the client or when tokens need to be refreshed.
1080
+ * New tokens will be acquired on the next API call.
1081
+ */
1082
+ invalidateTokens(): void;
435
1083
  }
436
1084
 
437
1085
  /**
438
- * Union of all Timeback event types.
439
- */
440
- type TimebackEvent = ActivityCompletedEvent | TimeSpentEvent
441
-
442
- /**
443
- * Caliper Event Types
1086
+ * Transport Types
444
1087
  *
445
- * Types for Caliper Analytics events and envelopes.
1088
+ * Types for HTTP transport layer.
446
1089
  */
447
1090
 
448
-
449
-
450
- /**
451
- * Supported Caliper profiles.
452
- */
453
- type CaliperProfile =
454
- | 'AnnotationProfile'
455
- | 'AssessmentProfile'
456
- | 'ToolUseProfile'
457
- | 'GeneralProfile'
458
- | 'FeedbackProfile'
459
- | 'MediaProfile'
460
- | 'SurveyProfile'
461
- | 'ResourceManagementProfile'
462
- | 'ForumProfile'
463
- | 'AssignableProfile'
464
- | 'GradingProfile'
465
- | 'ReadingProfile'
466
- | 'SessionProfile'
467
- | 'SearchProfile'
468
- | 'ToolLaunchProfile'
469
- | 'TimebackProfile'
470
-
471
1091
  /**
472
- * Base Caliper entity representation.
473
- *
474
- * Many Caliper fields can be provided either as an IRI string or as an
475
- * object-shaped entity, so the shared protocol model keeps this flexible.
1092
+ * Fetch function signature for HTTP requests.
1093
+ * Avoids Bun-specific extensions on `typeof fetch`.
476
1094
  */
477
- type CaliperEntity = string | { [key: string]: unknown }
478
-
1095
+ type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
479
1096
  /**
480
- * One actor-object shape for sending events.
481
- *
482
- * This is a structured actor object with an `id`, a `type`, and an
483
- * `extensions` bag containing `email`. It is one valid actor-object shape,
484
- * not the only representation allowed by `CaliperEvent.actor`.
485
- */
486
- interface CaliperActor {
487
- /** Unique identifier (IRI format) */
488
- id: string
489
- /** Entity type (e.g., 'Person', 'TimebackUser') */
490
- type: string
491
- /** Extensions carried on this actor-object shape */
492
- extensions: {
493
- /** Actor email on this actor-object shape */
494
- email: string
495
- /** Additional extension properties */
496
- [key: string]: unknown
497
- }
1097
+ * HTTP request options.
1098
+ */
1099
+ interface RequestOptions {
1100
+ /** HTTP method */
1101
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
1102
+ /** Query parameters to append to the URL */
1103
+ params?: Record<string, string | number | boolean | undefined>;
1104
+ /** Request body (will be JSON-serialized) */
1105
+ body?: unknown;
1106
+ /** Additional headers to include */
1107
+ headers?: Record<string, string>;
1108
+ /**
1109
+ * Unique identifier for this request.
1110
+ * Used for log correlation and debugging.
1111
+ * Auto-generated if not provided.
1112
+ */
1113
+ requestId?: string;
498
1114
  }
499
-
500
1115
  /**
501
- * Caliper Event.
502
- *
503
- * Represents a learning activity event conforming to IMS Caliper v1.2.
504
- */
505
- interface CaliperEvent {
506
- /** JSON-LD context */
507
- '@context'?: string
508
- /** Unique identifier (URN UUID format) */
509
- id: string
510
- /** Event type */
511
- type: string
512
- /** The agent who initiated the event (IRI string, CaliperActor, TimebackUser, or generic entity) */
513
- actor: CaliperEntity | CaliperActor | TimebackUser
514
- /** The action or predicate */
515
- action: string
516
- /** The object of the interaction */
517
- object: CaliperEntity
518
- /** ISO 8601 datetime when event occurred */
519
- eventTime: string
520
- /** Profile governing interpretation */
521
- profile: CaliperProfile
522
- /** Application context */
523
- edApp?: CaliperEntity
524
- /** Entity generated as result */
525
- generated?: CaliperEntity
526
- /** Target segment within object */
527
- target?: CaliperEntity
528
- /** Referring context */
529
- referrer?: CaliperEntity
530
- /** Organization/group context */
531
- group?: CaliperEntity
532
- /** User's membership/role */
533
- membership?: CaliperEntity
534
- /** Current user session */
535
- session?: CaliperEntity
536
- /** LTI session context */
537
- federatedSession?: CaliperEntity
538
- /** Additional custom attributes */
539
- extensions?: Record<string, unknown>
1116
+ * Auth credentials for environment mode.
1117
+ * Token URL is derived automatically from environment.
1118
+ */
1119
+ interface EnvAuth {
1120
+ clientId: string;
1121
+ clientSecret: string;
540
1122
  }
541
-
542
1123
  /**
543
- * Caliper Envelope.
544
- *
545
- * Container for transmitting Caliper events to the API.
546
- */
547
- interface CaliperEnvelope {
548
- /** Sensor identifier (IRI format) */
549
- sensor: string
550
- /** ISO 8601 datetime when data was sent */
551
- sendTime: string
552
- /** Caliper data version */
553
- dataVersion: 'http://purl.imsglobal.org/ctx/caliper/v1p2'
554
- /** Array of events or entities */
555
- data: CaliperEvent[]
556
- /** Allow additional properties for forward-compatibility and transformer passthrough. */
557
- [key: string]: unknown
1124
+ * Auth credentials for explicit mode.
1125
+ * Requires explicit authUrl for custom APIs.
1126
+ */
1127
+ interface ExplicitAuth {
1128
+ clientId: string;
1129
+ clientSecret: string;
1130
+ authUrl: string;
558
1131
  }
559
-
560
1132
  /**
561
- * Result of sending events.
1133
+ * Base configuration options shared by all modes.
562
1134
  */
563
- interface SendEventsResult {
564
- /** Job ID for tracking async processing (undefined when the platform does not return one) */
565
- jobId: string | undefined
1135
+ interface BaseConfig {
1136
+ /** Request timeout in milliseconds */
1137
+ timeout?: number;
1138
+ /** Custom fetch implementation */
1139
+ fetch?: FetchFn;
566
1140
  }
567
-
568
1141
  /**
569
- * Individual event result from job completion.
1142
+ * Internal resolved transport configuration.
570
1143
  */
571
- interface EventResult {
572
- /** Allocated internal ID */
573
- allocatedId: string
574
- /** External event ID */
575
- externalId: string
1144
+ interface ResolvedTransportConfig {
1145
+ /** Base URL of the API */
1146
+ baseUrl: string;
1147
+ /** Request timeout in milliseconds */
1148
+ timeout: number;
1149
+ /** Fetch implementation */
1150
+ fetch: FetchFn;
1151
+ /** Token provider for authentication. Undefined for public/no-auth services. */
1152
+ tokenProvider?: TokenProvider;
576
1153
  }
577
-
578
1154
  /**
579
- * Job status response.
1155
+ * Transport configuration with explicit auth.
580
1156
  */
581
- interface JobStatus {
582
- id: string
583
- state: 'waiting' | 'active' | 'completed' | 'failed'
584
- returnValue?: {
585
- status: 'success' | 'error'
586
- results: EventResult[]
587
- }
588
- processedOn?: string | null
1157
+ interface TransportConfigWithAuth extends BaseConfig {
1158
+ baseUrl: string;
1159
+ auth: ExplicitAuth;
1160
+ tokenProvider?: never;
589
1161
  }
590
-
591
1162
  /**
592
- * Stored Caliper event (from list/get).
593
- *
594
- * This represents an event as returned by the API, which differs from the
595
- * input CaliperEvent format. The API adds internal fields and transforms
596
- * the original event ID to `externalId`.
597
- *
598
- * @remarks
599
- * **API Docs Drift**: The official OpenAPI spec (caliper-api.yaml) does not
600
- * accurately document this response structure. This type was derived from
601
- * actual API responses. Key differences:
602
- * - `id` is a number (internal DB ID), not a string
603
- * - `externalId` contains the original URN UUID (use this for `get()` calls)
604
- * - Response is wrapped in `{ events: StoredEvent[] }` for list, `{ event: StoredEvent }` for get
605
- */
606
- interface StoredEvent {
607
- /** Internal numeric ID (allocated by the database) */
608
- id: number
609
- /** Original event ID (URN UUID format) - use this for get() calls */
610
- externalId: string
611
- /** Sensor that sent the event */
612
- sensor: string
613
- /** Event type (e.g., 'ActivityEvent', 'Event') */
614
- type: string
615
- /** Caliper profile (e.g., 'TimebackProfile') */
616
- profile?: string
617
- /** The action or predicate */
618
- action: string
619
- /** When the event occurred */
620
- eventTime: string
621
- /** When the event was sent */
622
- sendTime: string
623
- /** When the record was last updated */
624
- updated_at: string | null
625
- /** When the record was created */
626
- created_at: string
627
- /** When the record was deleted (soft delete) */
628
- deleted_at: string | null
629
- /** The agent who initiated the event */
630
- actor: CaliperEntity
631
- /** The object of the event */
632
- object: CaliperEntity
633
- /** Generated entity (e.g., result, score) */
634
- generated?: CaliperEntity | null
635
- /** Target entity */
636
- target?: CaliperEntity | null
637
- /** Referrer entity */
638
- referrer?: CaliperEntity | null
639
- /** EdApp entity */
640
- edApp?: CaliperEntity | null
641
- /** Group/organization entity */
642
- group?: CaliperEntity | null
643
- /** Membership entity */
644
- membership?: CaliperEntity | null
645
- /** Session entity */
646
- session?: CaliperEntity | null
647
- /** Federated session entity */
648
- federatedSession?: CaliperEntity | null
649
- /** Extension data */
650
- extensions?: Record<string, unknown> | null
651
- /** Client application ID */
652
- clientAppId?: string | null
1163
+ * Transport configuration with shared token provider.
1164
+ */
1165
+ interface TransportConfigWithTokenProvider extends BaseConfig {
1166
+ baseUrl: string;
1167
+ tokenProvider: TokenProvider;
1168
+ auth?: never;
653
1169
  }
654
-
655
1170
  /**
656
- * Result from listing events.
1171
+ * Transport configuration for public/no-auth services.
657
1172
  */
658
- interface ListEventsResult {
659
- events: StoredEvent[]
660
- pagination: PaginationMeta
1173
+ interface TransportConfigNoAuth extends BaseConfig {
1174
+ baseUrl: string;
1175
+ auth?: never;
1176
+ tokenProvider?: never;
661
1177
  }
662
-
663
- // ═══════════════════════════════════════════════════════════════════════════════
664
- // QUESTION EVENT TYPES (Standard Caliper)
665
- // ═══════════════════════════════════════════════════════════════════════════════
666
-
667
1178
  /**
668
- * AssessmentItemEvent (Question Seen / Question Answered).
669
- *
670
- * Represents when a student is presented with a question (Started)
671
- * or submits an answer (Completed).
1179
+ * Configuration for BaseTransport.
1180
+ */
1181
+ type BaseTransportConfig = TransportConfigWithAuth | TransportConfigWithTokenProvider | TransportConfigNoAuth;
1182
+ /**
1183
+ * Options for creating a BaseTransport.
672
1184
  */
673
- interface AssessmentItemEvent extends CaliperEvent {
674
- type: 'AssessmentItemEvent'
675
- action: 'Started' | 'Completed'
676
- profile: 'AssessmentProfile'
1185
+ interface BaseTransportOptions {
1186
+ /** Transport configuration */
1187
+ config: BaseTransportConfig;
1188
+ /** Logger instance for request/response logging */
1189
+ logger: Logger;
677
1190
  }
678
-
679
1191
  /**
680
- * GradeEvent for question-level grading.
681
- *
682
- * Represents when a question response is graded, with a Score entity
683
- * containing `scoreType: "QUESTION_RESULT"` in the `generated` field.
1192
+ * Result of an auth check operation.
684
1193
  */
685
- interface QuestionGradeEvent extends CaliperEvent {
686
- type: 'GradeEvent'
687
- action: 'Graded'
688
- profile: 'GradingProfile'
1194
+ interface AuthCheckResult {
1195
+ /** Whether auth succeeded */
1196
+ ok: boolean;
1197
+ /** Time taken to complete the check (ms) */
1198
+ latencyMs: number;
1199
+ /** Error message if failed */
1200
+ error?: string;
1201
+ /** Detailed check results */
1202
+ checks: {
1203
+ /** Token acquisition succeeded */
1204
+ tokenAcquisition: boolean;
1205
+ };
689
1206
  }
690
1207
 
691
1208
  /**
692
- * API Response Types
1209
+ * Base Transport Layer
693
1210
  *
694
- * Types for Caliper API responses.
1211
+ * HTTP transport with OAuth2 authentication, retries, and error handling.
1212
+ * Clients can extend this for protocol-specific features.
695
1213
  *
696
- * @remarks
697
- * **IMPORTANT - API Docs Drift**: The official API documentation (caliper/response.yaml
698
- * and caliper-api.yaml) does NOT accurately reflect actual API responses. The types
699
- * in this file were derived from testing actual API responses. See individual type
700
- * comments for specific discrepancies.
701
1214
  */
702
-
703
-
704
-
705
1215
  /**
706
- * Pagination metadata returned by list endpoints.
707
- */
708
- interface PaginationMeta {
709
- /** Total number of items across all pages */
710
- total: number
711
- /** Total number of pages */
712
- totalPages: number
713
- /** Current page number (1-indexed) */
714
- currentPage: number
715
- /** Number of items per page */
716
- limit: number
1216
+ * Base HTTP transport layer for API communication.
1217
+ *
1218
+ * Handles OAuth2 authentication, request/response lifecycle,
1219
+ * and automatic retries for transient failures.
1220
+ *
1221
+ * Clients can extend this class to add protocol-specific features
1222
+ * like custom error parsing or pagination.
1223
+ */
1224
+ declare class BaseTransport {
1225
+ protected readonly config: ResolvedTransportConfig;
1226
+ protected readonly log: Logger;
1227
+ /**
1228
+ * Create a new BaseTransport instance.
1229
+ *
1230
+ * @param options - Transport options with config and logger
1231
+ */
1232
+ constructor(options: BaseTransportOptions);
1233
+ /**
1234
+ * The base URL for API requests.
1235
+ * @returns The base URL
1236
+ */
1237
+ get baseUrl(): string;
1238
+ /**
1239
+ * Make an authenticated request to the API.
1240
+ *
1241
+ * Automatically retries on transient failures (429, 503).
1242
+ *
1243
+ * @template T - Expected response type
1244
+ * @param path - API endpoint path
1245
+ * @param options - Request options including method, params, body
1246
+ * @returns Parsed JSON response
1247
+ * @throws {ApiError} On API errors (4xx/5xx responses)
1248
+ */
1249
+ request<T>(path: string, options?: RequestOptions): Promise<T>;
1250
+ /**
1251
+ * Check whether a resource exists at the given path.
1252
+ *
1253
+ * Returns `true` for successful 2xx responses, `false` only for 404 Not Found,
1254
+ * and rethrows all other errors.
1255
+ *
1256
+ * @param path - API endpoint path
1257
+ * @param options - Request options including method, params, body
1258
+ * @returns Promise resolving to whether the resource exists
1259
+ */
1260
+ exists(path: string, options?: RequestOptions): Promise<boolean>;
1261
+ /**
1262
+ * Make a raw request, returning the Response object.
1263
+ *
1264
+ * ## Retry Behavior
1265
+ * Automatically retries on transient failures (429, 503) with exponential
1266
+ * backoff. Respects Retry-After header when present.
1267
+ *
1268
+ * ## Timeout Behavior
1269
+ * Uses an operation-level timeout that spans ALL retry attempts.
1270
+ * If configured timeout is 30s, the entire operation (including retries
1271
+ * and backoff delays) must complete within 30s total.
1272
+ *
1273
+ * ## Flow
1274
+ * 1. Build URL from path and query params
1275
+ * 2. Start operation timer
1276
+ * 3. Loop up to MAX_RETRIES times:
1277
+ * a. Check if we've exceeded the operation deadline
1278
+ * b. Get OAuth token (may be cached)
1279
+ * c. Make the HTTP request with per-request timeout
1280
+ * d. If 429/503 and not last attempt → calculate backoff and retry
1281
+ * e. If other error → throw appropriate ApiError subclass
1282
+ * f. If success → return response
1283
+ * 4. If all retries exhausted → throw "Max retries exceeded"
1284
+ *
1285
+ * @param path - API endpoint path (relative to baseUrl)
1286
+ * @param options - Request options (method, params, body, headers)
1287
+ * @returns Raw fetch Response for custom handling
1288
+ * @throws {ApiError} On timeout, non-retryable errors, or max retries exceeded
1289
+ */
1290
+ requestRaw(path: string, options?: RequestOptions): Promise<Response>;
1291
+ /**
1292
+ * Get a valid OAuth2 access token.
1293
+ * @returns Promise resolving to access token or undefined
1294
+ */
1295
+ protected getAccessToken(): Promise<string | undefined>;
1296
+ /**
1297
+ * Construct full URL with query parameters.
1298
+ *
1299
+ * @param path - The relative path or absolute URL
1300
+ * @param params - Query parameters
1301
+ * @returns Full URL string
1302
+ * @throws {Error} If path is an absolute URL (security protection)
1303
+ */
1304
+ protected buildUrl(path: string, params?: Record<string, string | number | boolean | undefined>): string;
1305
+ /**
1306
+ * Parse successful response or delegate to error handler.
1307
+ * @param response - The fetch Response
1308
+ * @returns Parsed response as type T
1309
+ */
1310
+ protected handleResponse<T>(response: Response): Promise<T>;
1311
+ /**
1312
+ * Parse JSON response with context-preserving error handling.
1313
+ *
1314
+ * Unlike raw `response.json()`, this method:
1315
+ * - Logs structured error context (URL, status, content-type, body preview)
1316
+ * - Throws `ApiError` with `parseError` and `body` in the response object
1317
+ * - Aids debugging when upstream services return malformed JSON
1318
+ *
1319
+ * @template T - Expected shape of the parsed response
1320
+ * @param response - The fetch Response to parse
1321
+ * @returns Parsed JSON as type T
1322
+ * @throws {ApiError} When JSON parsing fails, with status code and body preview
1323
+ */
1324
+ protected parseJsonResponse<T>(response: Response): Promise<T>;
1325
+ /**
1326
+ * Parse error response and throw appropriate ApiError subclass.
1327
+ *
1328
+ * Handles both JSON and non-JSON error responses gracefully.
1329
+ * Clients can override this to add protocol-specific error parsing.
1330
+ *
1331
+ * @param response - The error Response (status >= 400)
1332
+ * @param requestId - Request ID for log correlation
1333
+ * @throws {UnauthorizedError} For 401 responses (also invalidates token)
1334
+ * @throws {ForbiddenError} For 403 responses
1335
+ * @throws {NotFoundError} For 404 responses
1336
+ * @throws {ValidationError} For 422 responses
1337
+ * @throws {ApiError} For all other error status codes
1338
+ */
1339
+ protected handleErrorResponse(response: Response, requestId?: string): Promise<never>;
1340
+ /**
1341
+ * Extract error message from response body.
1342
+ *
1343
+ * Checks common error formats:
1344
+ * - `message` (most APIs)
1345
+ * - `error` (some APIs)
1346
+ * - `imsx_description` (IMS Global: OneRoster, Caliper, QTI)
1347
+ *
1348
+ * Override in client transports for API-specific error formats
1349
+ * not covered here (e.g., Edubridge's `errors[]` array format).
1350
+ *
1351
+ * @param body - The error response body
1352
+ * @param fallback - Fallback message if none found
1353
+ * @returns Extracted error message
1354
+ */
1355
+ protected extractErrorMessage(body: unknown, fallback: string): string;
1356
+ /**
1357
+ * Delay execution for retry backoff.
1358
+ *
1359
+ * @param ms - Number of milliseconds to delay
1360
+ * @returns Promise that resolves after delay
1361
+ */
1362
+ protected sleep(ms: number): Promise<void>;
1363
+ /**
1364
+ * Parse Retry-After header value.
1365
+ *
1366
+ * Handles both formats per RFC 7231:
1367
+ * - Numeric seconds: "120"
1368
+ * - HTTP-date: "Wed, 21 Oct 2025 07:28:00 GMT"
1369
+ *
1370
+ * @param retryAfter - Retry-After header value
1371
+ * @param attempt - Current attempt number (0-based)
1372
+ * @returns Delay in milliseconds
1373
+ */
1374
+ protected parseRetryAfter(retryAfter: string | null, attempt: number): number;
717
1375
  }
718
1376
 
719
1377
  /**
720
- * Options for waitForCompletion polling.
1378
+ * Pagination Utilities
1379
+ *
1380
+ * Helpers for iterating over paginated API responses.
721
1381
  */
722
- interface WaitForCompletionOptions {
723
- /** Maximum time to wait in milliseconds (default: 30000) */
724
- timeoutMs?: number
725
- /** Interval between status checks in milliseconds (default: 1000) */
726
- pollIntervalMs?: number
727
- }
728
1382
 
729
1383
  /**
730
- * Validation result from the validate endpoint.
1384
+ * Async iterator for paginated API responses.
1385
+ *
1386
+ * Automatically fetches subsequent pages as you iterate, making it easy
1387
+ * to process large datasets without manual pagination handling.
1388
+ *
1389
+ * @typeParam T - The type of items being paginated
1390
+ *
1391
+ * @example
1392
+ * ```typescript
1393
+ * // Iterate over all items
1394
+ * for await (const user of paginator) {
1395
+ * console.log(user.name)
1396
+ * }
1397
+ * ```
1398
+ *
1399
+ * @example
1400
+ * ```typescript
1401
+ * // Collect all items into an array
1402
+ * const allUsers = await paginator.toArray()
1403
+ * ```
1404
+ *
1405
+ * @example
1406
+ * ```typescript
1407
+ * // Get just the first page
1408
+ * const page = await paginator.firstPage()
1409
+ * console.log(`Got ${page.data.length} of ${page.total} users`)
1410
+ * ```
731
1411
  */
732
- interface ValidationResult {
733
- /** Whether validation succeeded */
734
- status: 'success' | 'error'
735
- /** Human-readable message */
736
- message?: string
737
- /** Validation errors (if any) */
738
- errors?: unknown
1412
+ declare class Paginator$1<T, F = unknown> implements AsyncIterable<T> {
1413
+ private readonly fetcher;
1414
+ private readonly path;
1415
+ private readonly params;
1416
+ private readonly max?;
1417
+ private readonly unwrapKey?;
1418
+ private readonly log;
1419
+ private readonly transform?;
1420
+ private readonly paginationStyle;
1421
+ /**
1422
+ * Create a new Paginator.
1423
+ *
1424
+ * @param options - Paginator configuration
1425
+ */
1426
+ constructor(options: PaginatorOptions<T, F>);
1427
+ /**
1428
+ * Builds query parameters for the paginated request.
1429
+ *
1430
+ * Converts the type-safe `where` clause to a filter string for the API,
1431
+ * and merges with other list parameters (sort, orderBy, fields, search).
1432
+ * Excludes client-side params like `max` that shouldn't be sent to the API.
1433
+ *
1434
+ * Uses the configured pagination style:
1435
+ * - `'offset'`: Sends `{ limit, offset }` params
1436
+ * - `'page'`: Sends `{ limit, page }` params (1-indexed)
1437
+ *
1438
+ * @param limit - Maximum items per page
1439
+ * @param offset - Number of items to skip (converted to page if using page style)
1440
+ * @returns Query parameters ready for the request
1441
+ */
1442
+ private buildRequestParams;
1443
+ /**
1444
+ * Extracts and validates items from response data.
1445
+ *
1446
+ * Handles two response formats:
1447
+ * - Direct array: `[item1, item2, ...]`
1448
+ * - Wrapped object: `{ users: [item1, item2, ...] }` (when unwrapKey is set)
1449
+ *
1450
+ * @param data - Raw response data from the API
1451
+ * @param pageNumber - Current page number (for error messages)
1452
+ * @returns Validated array of items
1453
+ * @throws {Error} If extracted data is not an array
1454
+ */
1455
+ private extractItems;
1456
+ /**
1457
+ * Validates that extracted data is an array.
1458
+ *
1459
+ * Protects against malformed API responses that could cause:
1460
+ * - Infinite loops (empty non-array values)
1461
+ * - Unexpected iteration (strings yield characters, not items)
1462
+ * - Runtime crashes (objects are not iterable)
1463
+ *
1464
+ * @param data - Data to validate (should be an array)
1465
+ * @param pageNumber - Current page number (for error messages)
1466
+ * @returns The data cast to T[] if valid
1467
+ * @throws {Error} If data is not an array (with helpful message including unwrapKey)
1468
+ */
1469
+ private validateItems;
1470
+ /**
1471
+ * Determines if more pages are available based on response metadata.
1472
+ *
1473
+ * Uses a three-tier fallback strategy:
1474
+ * 1. Link header (most reliable)
1475
+ * 2. X-Total-Count header
1476
+ * 3. Full page heuristic (assumes more if page is full and no total provided)
1477
+ *
1478
+ * Always returns false if the page is empty to prevent infinite loops
1479
+ * from buggy servers that return hasMore: true with no data.
1480
+ * @param response - Response with pagination metadata
1481
+ * @param itemCount - Number of items in current page
1482
+ * @param offset - Current offset
1483
+ * @param limit - Current limit
1484
+ * @returns True if more pages are available
1485
+ */
1486
+ private hasMorePages;
1487
+ /**
1488
+ * Async iterator implementation.
1489
+ *
1490
+ * Yields items one at a time, automatically fetching new pages as needed.
1491
+ * Stops when `max` items have been yielded (if specified).
1492
+ *
1493
+ * @yields Items of type T from paginated responses
1494
+ */
1495
+ [Symbol.asyncIterator](): AsyncIterator<T>;
1496
+ /**
1497
+ * Collect all items into an array.
1498
+ *
1499
+ * **Warning**: Use with caution on large datasets as this loads
1500
+ * all items into memory. Consider iterating with `for await...of`
1501
+ * for better memory efficiency.
1502
+ *
1503
+ * @param options - Optional configuration
1504
+ * @param options.maxItems - Maximum items to collect (default: 10,000).
1505
+ * Throws if limit is reached. Set to `Infinity` to disable.
1506
+ * @returns Promise resolving to an array of all items
1507
+ * @throws {Error} If maxItems limit is exceeded
1508
+ */
1509
+ toArray(options?: ToArrayOptions): Promise<T[]>;
1510
+ /**
1511
+ * Fetch only the first page of results.
1512
+ *
1513
+ * Useful when you need pagination metadata (total count, hasMore)
1514
+ * or want to implement custom pagination UI.
1515
+ *
1516
+ * @returns Promise resolving to the first page with metadata
1517
+ */
1518
+ firstPage(): Promise<PageResult<T>>;
739
1519
  }
740
1520
 
741
1521
  /**
@@ -800,175 +1580,6 @@ interface EventTransformer {
800
1580
  */
801
1581
  type CaliperClientInstance = InstanceType<typeof CaliperClient>;
802
1582
 
803
- type input<T> = T extends {
804
- _zod: {
805
- input: any;
806
- };
807
- } ? T["_zod"]["input"] : unknown;
808
-
809
- /**
810
- * Caliper Schemas
811
- *
812
- * Zod schemas for the IMS Caliper Analytics standard with Timeback Profile.
813
- */
814
-
815
-
816
-
817
- // ═══════════════════════════════════════════════════════════════════════════════
818
- // BUILDER INPUTS
819
- // ═══════════════════════════════════════════════════════════════════════════════
820
-
821
- declare const ActivityCompletedInput = z
822
- .object({
823
- actor: TimebackUser,
824
- object: TimebackActivityContext,
825
- metrics: z.array(TimebackActivityMetric).min(1, 'metrics must contain at least one metric'),
826
- eventTime: IsoDateTimeString.optional(),
827
- metricsId: z.string().optional(),
828
- id: z.string().optional(),
829
- /** Event-level extensions */
830
- extensions: z.record(z.string(), z.unknown()).optional(),
831
- /**
832
- * Application context (IRI or entity).
833
- *
834
- * Maps to `edApp` in the Caliper event. Identifies the educational
835
- * application where the event occurred.
836
- */
837
- edApp: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
838
- /**
839
- * Session context (IRI or entity).
840
- *
841
- * Maps to `session` in the Caliper event. Used for event-to-session
842
- * correlation (e.g., linking heartbeats with completions via runId).
843
- */
844
- session: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
845
- /**
846
- * Attempt number (1-based).
847
- *
848
- * Maps to `generated.attempt` in the Caliper event.
849
- */
850
- attempt: z.number().int().min(1).optional(),
851
- /**
852
- * Extensions for the generated metrics collection.
853
- *
854
- * Maps to `generated.extensions` in the Caliper event.
855
- * Use `generatedExtensions.pctCompleteApp` (0–100) for app-reported course progress.
856
- */
857
- generatedExtensions: z
858
- .object({
859
- /**
860
- * App-reported course progress (per enrollment), as a percentage from 0–100.
861
- *
862
- * This is emitted to Caliper as `generated.extensions.pctCompleteApp`.
863
- *
864
- * @remarks
865
- * This value is not clamped here (Caliper client keeps the raw shape).
866
- */
867
- pctCompleteApp: z.number().optional(),
868
- })
869
- .loose()
870
- .optional(),
871
- })
872
- .strict()
873
-
874
- // ═══════════════════════════════════════════════════════════════════════════════
875
- // TYPE EXPORTS (REQUEST INPUTS)
876
- // ═══════════════════════════════════════════════════════════════════════════════
877
-
878
- type ActivityCompletedInput = input<typeof ActivityCompletedInput>
879
-
880
- declare const TimeSpentInput = z
881
- .object({
882
- actor: TimebackUser,
883
- object: TimebackActivityContext,
884
- metrics: z.array(TimebackTimeMetric).min(1, 'metrics must contain at least one metric'),
885
- eventTime: IsoDateTimeString.optional(),
886
- metricsId: z.string().optional(),
887
- id: z.string().optional(),
888
- extensions: z.record(z.string(), z.unknown()).optional(),
889
- edApp: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
890
- session: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
891
- })
892
- .strict()
893
- type TimeSpentInput = input<typeof TimeSpentInput>
894
-
895
- declare const QuestionSeenInput = z
896
- .object({
897
- /** Actor (Person IRI or entity object with id, type, name, etc.) */
898
- actor: z.union([z.string(), CaliperActor, TimebackUser]),
899
- /** AssessmentItem being presented */
900
- object: AssessmentItemObject,
901
- /** Application identifier (IRI or entity) */
902
- edApp: CaliperEntity,
903
- /** Event identifier (auto-generated if omitted) */
904
- id: z.string().optional(),
905
- /** ISO 8601 datetime (defaults to now) */
906
- eventTime: IsoDateTimeString.optional(),
907
- /** Session context (IRI or entity) */
908
- session: CaliperEntity.optional(),
909
- /** Event-level extensions */
910
- extensions: z.record(z.string(), z.unknown()).optional(),
911
- })
912
- .strict()
913
- type QuestionSeenInput = input<typeof QuestionSeenInput>
914
-
915
- declare const QuestionAnsweredInput = z
916
- .object({
917
- /** Actor (Person IRI or entity object with id, type, name, etc.) */
918
- actor: z.union([z.string(), CaliperActor, TimebackUser]),
919
- /** AssessmentItem being answered */
920
- object: AssessmentItemObject,
921
- /** Application identifier (IRI or entity) */
922
- edApp: CaliperEntity,
923
- /** Response entity with optional attempt and timing */
924
- generated: ResponseGenerated.optional(),
925
- /** Event identifier (auto-generated if omitted) */
926
- id: z.string().optional(),
927
- /** ISO 8601 datetime (defaults to now) */
928
- eventTime: IsoDateTimeString.optional(),
929
- /** Session context (IRI or entity) */
930
- session: CaliperEntity.optional(),
931
- /** Event-level extensions */
932
- extensions: z.record(z.string(), z.unknown()).optional(),
933
- })
934
- .strict()
935
- type QuestionAnsweredInput = input<typeof QuestionAnsweredInput>
936
-
937
- declare const QuestionGradedInput = z
938
- .object({
939
- /** Actor (Person IRI or entity object with id, type, name, etc.) */
940
- actor: z.union([z.string(), CaliperActor, TimebackUser]),
941
- /** Attempt being graded (IRI) */
942
- object: z.string(),
943
- /** Score awarded for the question */
944
- generated: ScoreGenerated,
945
- /** Application identifier (IRI or entity) */
946
- edApp: CaliperEntity,
947
- /** Event identifier (auto-generated if omitted) */
948
- id: z.string().optional(),
949
- /** ISO 8601 datetime (defaults to now) */
950
- eventTime: IsoDateTimeString.optional(),
951
- /** Session context (IRI or entity) */
952
- session: CaliperEntity.optional(),
953
- /** Event-level extensions */
954
- extensions: z.record(z.string(), z.unknown()).optional(),
955
- })
956
- .strict()
957
- type QuestionGradedInput = input<typeof QuestionGradedInput>
958
-
959
- declare const CaliperListEventsParams = z
960
- .object({
961
- limit: z.number().int().positive().optional(),
962
- offset: z.number().int().min(0).optional(),
963
- sensor: NonEmptyString.optional(),
964
- startDate: IsoDateTimeString.optional(),
965
- endDate: IsoDateTimeString.optional(),
966
- actorId: NonEmptyString.optional(),
967
- actorEmail: z.email().optional(),
968
- })
969
- .strict()
970
- type CaliperListEventsParams = input<typeof CaliperListEventsParams>
971
-
972
1583
  /**
973
1584
  * Transport Layer
974
1585
  *
@@ -1604,11 +2215,11 @@ declare class JobsResource {
1604
2215
  declare const CaliperClient: {
1605
2216
  new (config?: CaliperClientConfig): {
1606
2217
  readonly transport: CaliperTransportLike;
1607
- readonly _provider?: _timeback_internal_client_infra.TimebackProvider | undefined;
2218
+ readonly _provider?: TimebackProvider | undefined;
1608
2219
  readonly events: EventsResource;
1609
2220
  readonly jobs: JobsResource;
1610
2221
  getTransport(): CaliperTransportLike;
1611
- checkAuth(): Promise<_timeback_internal_client_infra.AuthCheckResult>;
2222
+ checkAuth(): Promise<AuthCheckResult>;
1612
2223
  };
1613
2224
  };
1614
2225
 
@@ -1657,5 +2268,5 @@ declare const CALIPER_DATA_VERSION = "http://purl.imsglobal.org/ctx/caliper/v1p2
1657
2268
  */
1658
2269
  declare const QUESTION_RESULT_SCORE_TYPE: "QUESTION_RESULT";
1659
2270
 
1660
- export { ActivityCompletedInput, CALIPER_DATA_VERSION, CaliperClient, CaliperListEventsParams as ListEventsParams, QUESTION_RESULT_SCORE_TYPE, QuestionAnsweredInput, QuestionGradedInput, QuestionSeenInput, TimeSpentInput, Transport, createActivityEvent, createCaliperClient, createQuestionAnsweredEvent, createQuestionGradedEvent, createQuestionSeenEvent, createTimeSpentEvent };
1661
- export type { ActivityCompletedEvent, ActivityMetricType, AssessmentItemEvent, CaliperClientConfig, CaliperClientInstance, CaliperEnvelope, CaliperEvent, CaliperProfile, JobStatus, QuestionGradeEvent, SendEventsResult, StoredEvent, TimeSpentEvent, TimeSpentMetric, TimeSpentMetricType, TimebackActivity, TimebackActivityContext, TimebackActivityMetric, TimebackActivityMetricsCollection, TimebackApp, TimebackCourse, TimebackEvent, TimebackSubject, TimebackTimeSpentMetricsCollection, TimebackUser, TimebackUserRole, ValidationResult };
2271
+ export { CALIPER_DATA_VERSION, CaliperClient, QUESTION_RESULT_SCORE_TYPE, Transport, createActivityEvent, createCaliperClient, createQuestionAnsweredEvent, createQuestionGradedEvent, createQuestionSeenEvent, createTimeSpentEvent };
2272
+ export type { AuthCheckResult, CaliperClientConfig, CaliperClientInstance, EnvAuth, Environment, ExplicitAuth };