@timeback/sdk 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +612 -0
- package/dist/client/adapters/react/SignInButton.d.ts +60 -0
- package/dist/client/adapters/react/SignInButton.d.ts.map +1 -0
- package/dist/client/adapters/react/index.d.ts +47 -0
- package/dist/client/adapters/react/index.d.ts.map +1 -0
- package/dist/client/adapters/react/index.js +478 -0
- package/dist/client/adapters/react/provider.d.ts +78 -0
- package/dist/client/adapters/react/provider.d.ts.map +1 -0
- package/dist/client/adapters/solid/SignInButton.d.ts +52 -0
- package/dist/client/adapters/solid/SignInButton.d.ts.map +1 -0
- package/dist/client/adapters/solid/SignInButton.tsx +321 -0
- package/dist/client/adapters/solid/context.d.ts +73 -0
- package/dist/client/adapters/solid/context.d.ts.map +1 -0
- package/dist/client/adapters/solid/context.tsx +91 -0
- package/dist/client/adapters/solid/index.d.ts +46 -0
- package/dist/client/adapters/solid/index.d.ts.map +1 -0
- package/dist/client/adapters/solid/index.ts +50 -0
- package/dist/client/adapters/svelte/SignInButton.svelte +234 -0
- package/dist/client/adapters/svelte/SignInButton.svelte.d.ts +24 -0
- package/dist/client/adapters/svelte/index.d.ts +37 -0
- package/dist/client/adapters/svelte/index.d.ts.map +1 -0
- package/dist/client/adapters/svelte/index.ts +42 -0
- package/dist/client/adapters/svelte/stores.d.ts +66 -0
- package/dist/client/adapters/svelte/stores.d.ts.map +1 -0
- package/dist/client/adapters/svelte/stores.ts +143 -0
- package/dist/client/adapters/vue/SignInButton.vue +260 -0
- package/dist/client/adapters/vue/SignInButton.vue.d.ts +53 -0
- package/dist/client/adapters/vue/index.d.ts +43 -0
- package/dist/client/adapters/vue/index.d.ts.map +1 -0
- package/dist/client/adapters/vue/index.ts +48 -0
- package/dist/client/adapters/vue/provider.d.ts +94 -0
- package/dist/client/adapters/vue/provider.d.ts.map +1 -0
- package/dist/client/adapters/vue/provider.ts +147 -0
- package/dist/client/index.d.ts +9 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/lib/activity/activity.class.d.ts +73 -0
- package/dist/client/lib/activity/activity.class.d.ts.map +1 -0
- package/dist/client/lib/activity/activity.d.ts +16 -0
- package/dist/client/lib/activity/activity.d.ts.map +1 -0
- package/dist/client/lib/activity/index.d.ts +6 -0
- package/dist/client/lib/activity/index.d.ts.map +1 -0
- package/dist/client/lib/utils.d.ts +20 -0
- package/dist/client/lib/utils.d.ts.map +1 -0
- package/dist/client/namespaces/activity.d.ts +41 -0
- package/dist/client/namespaces/activity.d.ts.map +1 -0
- package/dist/client/namespaces/auth.d.ts +33 -0
- package/dist/client/namespaces/auth.d.ts.map +1 -0
- package/dist/client/namespaces/index.d.ts +7 -0
- package/dist/client/namespaces/index.d.ts.map +1 -0
- package/dist/client/namespaces/user.d.ts +29 -0
- package/dist/client/namespaces/user.d.ts.map +1 -0
- package/dist/client/timeback-client.class.d.ts +37 -0
- package/dist/client/timeback-client.class.d.ts.map +1 -0
- package/dist/client/timeback-client.d.ts +29 -0
- package/dist/client/timeback-client.d.ts.map +1 -0
- package/dist/client.d.ts +30 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +198 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +0 -0
- package/dist/edge.d.ts +13 -0
- package/dist/edge.d.ts.map +1 -0
- package/dist/edge.js +1149 -0
- package/dist/identity.d.ts +14 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +1019 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84921 -0
- package/dist/server/adapters/express.d.ts +66 -0
- package/dist/server/adapters/express.d.ts.map +1 -0
- package/dist/server/adapters/express.js +67332 -0
- package/dist/server/adapters/native.d.ts +47 -0
- package/dist/server/adapters/native.d.ts.map +1 -0
- package/dist/server/adapters/native.js +190 -0
- package/dist/server/adapters/nextjs.d.ts +32 -0
- package/dist/server/adapters/nextjs.d.ts.map +1 -0
- package/dist/server/adapters/nextjs.js +202 -0
- package/dist/server/adapters/nuxt.d.ts +98 -0
- package/dist/server/adapters/nuxt.d.ts.map +1 -0
- package/dist/server/adapters/nuxt.js +67401 -0
- package/dist/server/adapters/solid-start.d.ts +63 -0
- package/dist/server/adapters/solid-start.d.ts.map +1 -0
- package/dist/server/adapters/solid-start.js +67300 -0
- package/dist/server/adapters/svelte-kit.d.ts +84 -0
- package/dist/server/adapters/svelte-kit.d.ts.map +1 -0
- package/dist/server/adapters/svelte-kit.js +243 -0
- package/dist/server/adapters/tanstack-start.d.ts +42 -0
- package/dist/server/adapters/tanstack-start.d.ts.map +1 -0
- package/dist/server/adapters/tanstack-start.js +67278 -0
- package/dist/server/adapters/types.d.ts +294 -0
- package/dist/server/adapters/types.d.ts.map +1 -0
- package/dist/server/adapters/utils.d.ts +76 -0
- package/dist/server/adapters/utils.d.ts.map +1 -0
- package/dist/server/handlers/activity.d.ts +28 -0
- package/dist/server/handlers/activity.d.ts.map +1 -0
- package/dist/server/handlers/identity-full.d.ts +28 -0
- package/dist/server/handlers/identity-full.d.ts.map +1 -0
- package/dist/server/handlers/identity-only.d.ts +22 -0
- package/dist/server/handlers/identity-only.d.ts.map +1 -0
- package/dist/server/handlers/index.d.ts +9 -0
- package/dist/server/handlers/index.d.ts.map +1 -0
- package/dist/server/handlers/user.d.ts +31 -0
- package/dist/server/handlers/user.d.ts.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/lib/build-activity-events.d.ts +39 -0
- package/dist/server/lib/build-activity-events.d.ts.map +1 -0
- package/dist/server/lib/build-user-profile.d.ts +62 -0
- package/dist/server/lib/build-user-profile.d.ts.map +1 -0
- package/dist/server/lib/index.d.ts +14 -0
- package/dist/server/lib/index.d.ts.map +1 -0
- package/dist/server/lib/logger.d.ts +21 -0
- package/dist/server/lib/logger.d.ts.map +1 -0
- package/dist/server/lib/oidc.d.ts +76 -0
- package/dist/server/lib/oidc.d.ts.map +1 -0
- package/dist/server/lib/resolve-activity-course.d.ts +22 -0
- package/dist/server/lib/resolve-activity-course.d.ts.map +1 -0
- package/dist/server/lib/resolve-timeback-id.d.ts +28 -0
- package/dist/server/lib/resolve-timeback-id.d.ts.map +1 -0
- package/dist/server/lib/resolve-timeback-user.d.ts +42 -0
- package/dist/server/lib/resolve-timeback-user.d.ts.map +1 -0
- package/dist/server/lib/utils.d.ts +54 -0
- package/dist/server/lib/utils.d.ts.map +1 -0
- package/dist/server/timeback-identity.d.ts +19 -0
- package/dist/server/timeback-identity.d.ts.map +1 -0
- package/dist/server/timeback.d.ts +68 -0
- package/dist/server/timeback.d.ts.map +1 -0
- package/dist/server/types.d.ts +421 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/shared/constants.d.ts +18 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/types.d.ts +159 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/package.json +119 -0
package/README.md
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
# Timeback SDK
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for integrating Timeback into your application. Provides server-side route handlers and client-side components for activity tracking and SSO authentication.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Quick Start](#quick-start)
|
|
9
|
+
- [Server Adapters](#server-adapters)
|
|
10
|
+
- [Next.js](#nextjs)
|
|
11
|
+
- [Nuxt](#nuxt)
|
|
12
|
+
- [SvelteKit](#sveltekit)
|
|
13
|
+
- [SolidStart](#solidstart)
|
|
14
|
+
- [TanStack Start](#tanstack-start)
|
|
15
|
+
- [Express](#express)
|
|
16
|
+
- [Client Adapters](#client-adapters)
|
|
17
|
+
- [React](#react)
|
|
18
|
+
- [Vue](#vue)
|
|
19
|
+
- [Svelte](#svelte)
|
|
20
|
+
- [Solid](#solid)
|
|
21
|
+
- [Identity Modes](#identity-modes)
|
|
22
|
+
- [Identity-Only Integration](#identity-only-integration)
|
|
23
|
+
- [Activity Tracking](#activity-tracking)
|
|
24
|
+
- [Advanced: Direct API Access](#advanced-direct-api-access)
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install timeback
|
|
30
|
+
# or
|
|
31
|
+
bun add timeback
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
1. Create a server instance with your credentials
|
|
37
|
+
2. Mount the route handlers for your framework
|
|
38
|
+
3. Wrap your app with the client provider
|
|
39
|
+
4. Use hooks/composables to track activities
|
|
40
|
+
|
|
41
|
+
## Server Adapters
|
|
42
|
+
|
|
43
|
+
All server adapters use the same core configuration:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// lib/timeback.ts
|
|
47
|
+
import { createTimeback } from '@timeback/sdk'
|
|
48
|
+
|
|
49
|
+
export const timeback = await createTimeback({
|
|
50
|
+
env: 'staging', // 'local' | 'staging' | 'production'
|
|
51
|
+
api: {
|
|
52
|
+
clientId: process.env.TIMEBACK_API_CLIENT_ID!,
|
|
53
|
+
clientSecret: process.env.TIMEBACK_API_CLIENT_SECRET!,
|
|
54
|
+
},
|
|
55
|
+
identity: {
|
|
56
|
+
mode: 'sso',
|
|
57
|
+
clientId: process.env.AWS_COGNITO_CLIENT_ID!,
|
|
58
|
+
clientSecret: process.env.AWS_COGNITO_CLIENT_SECRET!,
|
|
59
|
+
redirectUri: 'http://localhost:3000/api/auth/sso/callback/timeback',
|
|
60
|
+
onCallbackSuccess: async ({ user, state, redirect }) => {
|
|
61
|
+
// user.id is the timebackId (canonical stable identifier)
|
|
62
|
+
await setSession({ id: user.id, email: user.email })
|
|
63
|
+
return redirect(state?.returnTo ?? '/')
|
|
64
|
+
},
|
|
65
|
+
onCallbackError: ({ error, redirect }) => {
|
|
66
|
+
console.error('SSO Error:', error)
|
|
67
|
+
return redirect('/?error=sso_failed')
|
|
68
|
+
},
|
|
69
|
+
getUser: () => getSession(), // Return current user or undefined
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Next.js
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// app/api/timeback/[...timeback]/route.ts
|
|
78
|
+
import { toNextjsHandler } from '@timeback/sdk/nextjs'
|
|
79
|
+
|
|
80
|
+
import { timeback } from '@/lib/timeback'
|
|
81
|
+
|
|
82
|
+
export const { GET, POST } = toNextjsHandler(timeback)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Nuxt
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// server/middleware/timeback.ts
|
|
89
|
+
import { nuxtHandler } from '@timeback/sdk/nuxt'
|
|
90
|
+
|
|
91
|
+
import { timeback } from '../lib/timeback'
|
|
92
|
+
|
|
93
|
+
export default defineEventHandler(async event => {
|
|
94
|
+
const response = await nuxtHandler({
|
|
95
|
+
timeback,
|
|
96
|
+
event,
|
|
97
|
+
})
|
|
98
|
+
if (response) return response
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### SvelteKit
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// src/hooks.server.ts
|
|
106
|
+
import { building } from '$app/environment'
|
|
107
|
+
import { timeback } from '$lib/timeback'
|
|
108
|
+
import { svelteKitHandler } from '@timeback/sdk/svelte-kit'
|
|
109
|
+
|
|
110
|
+
import type { Handle } from '@sveltejs/kit'
|
|
111
|
+
|
|
112
|
+
export const handle: Handle = ({ event, resolve }) => {
|
|
113
|
+
return svelteKitHandler({
|
|
114
|
+
timeback,
|
|
115
|
+
event,
|
|
116
|
+
resolve,
|
|
117
|
+
building,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### SolidStart
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// src/middleware.ts
|
|
126
|
+
import { createMiddleware } from '@solidjs/start/middleware'
|
|
127
|
+
import { timeback } from '~/lib/timeback'
|
|
128
|
+
import { solidStartHandler } from '@timeback/sdk/solid-start'
|
|
129
|
+
|
|
130
|
+
export default createMiddleware({
|
|
131
|
+
onRequest: [
|
|
132
|
+
async event => {
|
|
133
|
+
const response = await solidStartHandler({
|
|
134
|
+
timeback,
|
|
135
|
+
event,
|
|
136
|
+
})
|
|
137
|
+
if (response) return response
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### TanStack Start
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// src/routes/api/timeback/$.ts
|
|
147
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
148
|
+
import { toTanStackStartHandler } from '@timeback/sdk/tanstack-start'
|
|
149
|
+
|
|
150
|
+
import { timeback } from '@/lib/timeback'
|
|
151
|
+
|
|
152
|
+
const handlers = toTanStackStartHandler(timeback)
|
|
153
|
+
|
|
154
|
+
export const Route = createFileRoute('/api/timeback/$')({
|
|
155
|
+
server: { handlers },
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Express
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// server.ts
|
|
163
|
+
import express from 'express'
|
|
164
|
+
import { toExpressMiddleware } from '@timeback/sdk/express'
|
|
165
|
+
|
|
166
|
+
import { timeback } from './lib/timeback'
|
|
167
|
+
|
|
168
|
+
const app = express()
|
|
169
|
+
app.use(express.json())
|
|
170
|
+
app.use('/api/timeback', toExpressMiddleware(timeback))
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Client Adapters
|
|
174
|
+
|
|
175
|
+
### React
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
// app/providers.tsx
|
|
179
|
+
'use client'
|
|
180
|
+
|
|
181
|
+
import { TimebackProvider } from '@timeback/sdk/react'
|
|
182
|
+
|
|
183
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
184
|
+
return <TimebackProvider>{children}</TimebackProvider>
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
// components/ActivityTracker.tsx
|
|
190
|
+
import { useEffect } from 'react'
|
|
191
|
+
import { SignInButton, useTimeback } from '@timeback/sdk/react'
|
|
192
|
+
|
|
193
|
+
function MyComponent() {
|
|
194
|
+
const timeback = useTimeback()
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (!timeback) return
|
|
198
|
+
|
|
199
|
+
const activity = timeback.activity
|
|
200
|
+
.new({ id: 'lesson-1', name: 'Intro', course: { subject: 'Math', grade: 3 } })
|
|
201
|
+
.start()
|
|
202
|
+
|
|
203
|
+
return () => {
|
|
204
|
+
activity.end()
|
|
205
|
+
}
|
|
206
|
+
}, [timeback])
|
|
207
|
+
|
|
208
|
+
return <SignInButton size="lg" />
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Vue
|
|
213
|
+
|
|
214
|
+
```vue
|
|
215
|
+
<!-- app.vue -->
|
|
216
|
+
<script setup>
|
|
217
|
+
import { TimebackProvider } from '@timeback/sdk/vue'
|
|
218
|
+
</script>
|
|
219
|
+
|
|
220
|
+
<template>
|
|
221
|
+
<TimebackProvider>
|
|
222
|
+
<NuxtPage />
|
|
223
|
+
</TimebackProvider>
|
|
224
|
+
</template>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```vue
|
|
228
|
+
<!-- components/ActivityTracker.vue -->
|
|
229
|
+
<script setup>
|
|
230
|
+
import { SignInButton, useTimeback } from '@timeback/sdk/vue'
|
|
231
|
+
import { onMounted, onUnmounted } from 'vue'
|
|
232
|
+
|
|
233
|
+
const timeback = useTimeback()
|
|
234
|
+
let activity
|
|
235
|
+
|
|
236
|
+
onMounted(() => {
|
|
237
|
+
if (timeback.value) {
|
|
238
|
+
activity = timeback.value.activity
|
|
239
|
+
.new({ id: 'lesson-1', name: 'Intro', course: { subject: 'Math', grade: 3 } })
|
|
240
|
+
.start()
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
onUnmounted(() => activity?.end())
|
|
245
|
+
</script>
|
|
246
|
+
|
|
247
|
+
<template>
|
|
248
|
+
<SignInButton size="lg" />
|
|
249
|
+
</template>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Svelte
|
|
253
|
+
|
|
254
|
+
```svelte
|
|
255
|
+
<!-- +layout.svelte -->
|
|
256
|
+
<script>
|
|
257
|
+
import { initTimeback } from '@timeback/sdk/svelte'
|
|
258
|
+
|
|
259
|
+
initTimeback()
|
|
260
|
+
|
|
261
|
+
let { children } = $props()
|
|
262
|
+
</script>
|
|
263
|
+
|
|
264
|
+
{@render children()}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```svelte
|
|
268
|
+
<!-- +page.svelte -->
|
|
269
|
+
<script>
|
|
270
|
+
import { onMount, onDestroy } from 'svelte'
|
|
271
|
+
import { SignInButton, timeback } from '@timeback/sdk/svelte'
|
|
272
|
+
|
|
273
|
+
let activity
|
|
274
|
+
|
|
275
|
+
onMount(() => {
|
|
276
|
+
if ($timeback) {
|
|
277
|
+
activity = $timeback.activity
|
|
278
|
+
.new({ id: 'lesson-1', name: 'Intro', course: { subject: 'Math', grade: 3 } })
|
|
279
|
+
.start()
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
onDestroy(() => activity?.end())
|
|
284
|
+
</script>
|
|
285
|
+
|
|
286
|
+
<SignInButton size="lg" />
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Solid
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
// app.tsx
|
|
293
|
+
import { TimebackProvider } from '@timeback/sdk/solid'
|
|
294
|
+
|
|
295
|
+
export default function App() {
|
|
296
|
+
return (
|
|
297
|
+
<TimebackProvider>
|
|
298
|
+
<Router root={props => <Suspense>{props.children}</Suspense>}>
|
|
299
|
+
<FileRoutes />
|
|
300
|
+
</Router>
|
|
301
|
+
</TimebackProvider>
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
// components/ActivityTracker.tsx
|
|
308
|
+
import { onCleanup, onMount } from 'solid-js'
|
|
309
|
+
import { SignInButton, useTimeback } from '@timeback/sdk/solid'
|
|
310
|
+
|
|
311
|
+
function MyComponent() {
|
|
312
|
+
const timeback = useTimeback()
|
|
313
|
+
let activity
|
|
314
|
+
|
|
315
|
+
onMount(() => {
|
|
316
|
+
if (!timeback) return
|
|
317
|
+
|
|
318
|
+
activity = timeback.activity
|
|
319
|
+
.new({ id: 'lesson-1', name: 'Intro', course: { subject: 'Math', grade: 3 } })
|
|
320
|
+
.start()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
onCleanup(() => activity?.end())
|
|
324
|
+
|
|
325
|
+
return <SignInButton size="lg" />
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Identity Modes
|
|
330
|
+
|
|
331
|
+
### SSO Mode
|
|
332
|
+
|
|
333
|
+
Uses Timeback as the identity provider via OIDC. When using `createTimeback()`, the SDK automatically resolves the Timeback user by email and returns an enriched `TimebackAuthUser` with `user.id` being the canonical `timebackId` (stable identifier).
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
identity: {
|
|
337
|
+
mode: 'sso',
|
|
338
|
+
clientId: process.env.AWS_COGNITO_CLIENT_ID!,
|
|
339
|
+
clientSecret: process.env.AWS_COGNITO_CLIENT_SECRET!,
|
|
340
|
+
redirectUri: 'http://localhost:3000/api/auth/sso/callback/timeback',
|
|
341
|
+
onCallbackSuccess: async ({ user, idp, state, redirect }) => {
|
|
342
|
+
// user.id is the timebackId (canonical stable identifier)
|
|
343
|
+
// user.email, user.name come from Timeback profile
|
|
344
|
+
// user.claims contains IdP data (sub, firstName, lastName, pictureUrl)
|
|
345
|
+
// idp.tokens and idp.userInfo contain raw OIDC data if needed
|
|
346
|
+
await setSession({ id: user.id, email: user.email })
|
|
347
|
+
return redirect(state?.returnTo ?? '/')
|
|
348
|
+
},
|
|
349
|
+
onCallbackError: ({ error, errorCode, redirect }) => {
|
|
350
|
+
// errorCode may be: missing_email, timeback_user_not_found,
|
|
351
|
+
// timeback_user_ambiguous, timeback_user_lookup_failed
|
|
352
|
+
console.error('SSO Error:', errorCode, error.message)
|
|
353
|
+
return redirect('/?error=sso_failed')
|
|
354
|
+
},
|
|
355
|
+
getUser: () => getCurrentSession(),
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
#### TimebackAuthUser Shape
|
|
360
|
+
|
|
361
|
+
The `user` in `onCallbackSuccess` is a `TimebackAuthUser` with this structure:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
interface TimebackAuthUser {
|
|
365
|
+
id: string // Timeback user ID
|
|
366
|
+
email: string
|
|
367
|
+
name?: string
|
|
368
|
+
school?: { id: string; name: string }
|
|
369
|
+
grade?: number
|
|
370
|
+
claims: {
|
|
371
|
+
sub: string // OIDC subject identifier
|
|
372
|
+
email: string
|
|
373
|
+
firstName?: string
|
|
374
|
+
lastName?: string
|
|
375
|
+
pictureUrl?: string
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Session Storage Guidance
|
|
381
|
+
|
|
382
|
+
For cookie-only sessions, store only the minimal payload to avoid cookie size limits:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// Recommended: store minimal session
|
|
386
|
+
await setSession({ id: user.id, email: user.email })
|
|
387
|
+
|
|
388
|
+
// Then in getUser, return what you stored:
|
|
389
|
+
getUser: req => {
|
|
390
|
+
const session = getSessionFromCookie(req)
|
|
391
|
+
return session ? { id: session.id, email: session.email } : undefined
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
For DB-backed sessions, you can store the full `TimebackAuthUser` if desired.
|
|
396
|
+
|
|
397
|
+
### Custom Mode
|
|
398
|
+
|
|
399
|
+
For apps with existing auth (Clerk, Auth0, Supabase, etc.):
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
identity: {
|
|
403
|
+
mode: 'custom',
|
|
404
|
+
getUser: async (req) => {
|
|
405
|
+
const session = await getSession(req)
|
|
406
|
+
if (!session) return undefined
|
|
407
|
+
// Return user with timebackId as the id
|
|
408
|
+
return { id: session.timebackId, email: session.email, name: session.name }
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Identity-Only Integration
|
|
414
|
+
|
|
415
|
+
If you only need Timeback SSO authentication without activity tracking or Timeback API integration, use `createTimebackIdentity()`. This is a lightweight alternative that:
|
|
416
|
+
|
|
417
|
+
- Does not require Timeback API credentials
|
|
418
|
+
- Does not require `timeback.config.ts`
|
|
419
|
+
- Only exposes identity routes (sign-in, callback, sign-out)
|
|
420
|
+
- Returns raw OIDC user info (no Timeback profile enrichment)
|
|
421
|
+
|
|
422
|
+
**Note:** Unlike `createTimeback()`, the identity-only callback returns raw OIDC user info (`sub`, `email`, `name`, etc.) without resolving a Timeback user. Use `createTimeback()` if you need the canonical `timebackId`.
|
|
423
|
+
|
|
424
|
+
### Cloudflare Workers / workerd compatibility
|
|
425
|
+
|
|
426
|
+
`createTimebackIdentity()` is runtime-agnostic, but the main `timeback` entrypoint also includes
|
|
427
|
+
Node-oriented functionality (notably config loading via `jiti`). Some edge runtimes
|
|
428
|
+
(Cloudflare Workers / workerd) do not support the Node modules that `jiti` depends on.
|
|
429
|
+
|
|
430
|
+
If you're deploying identity-only SSO on Workers/workerd, import from the worker-safe entrypoint:
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
import { createTimebackIdentity, toNativeHandler } from '@timeback/sdk/edge'
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Server Setup
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
// lib/timeback.ts
|
|
440
|
+
import { createTimebackIdentity } from '@timeback/sdk'
|
|
441
|
+
|
|
442
|
+
export const timeback = createTimebackIdentity({
|
|
443
|
+
env: 'production',
|
|
444
|
+
identity: {
|
|
445
|
+
mode: 'sso',
|
|
446
|
+
clientId: process.env.AWS_COGNITO_CLIENT_ID!,
|
|
447
|
+
clientSecret: process.env.AWS_COGNITO_CLIENT_SECRET!,
|
|
448
|
+
onCallbackSuccess: async ({ user, tokens, redirect }) => {
|
|
449
|
+
// user is raw OIDC userInfo (sub, email, name, picture, etc.)
|
|
450
|
+
// No Timeback profile enrichment in identity-only mode
|
|
451
|
+
await createSession({
|
|
452
|
+
sub: user.sub,
|
|
453
|
+
email: user.email,
|
|
454
|
+
name: user.name,
|
|
455
|
+
})
|
|
456
|
+
return redirect('/')
|
|
457
|
+
},
|
|
458
|
+
onCallbackError: ({ error, redirect }) => {
|
|
459
|
+
console.error('SSO Error:', error)
|
|
460
|
+
return redirect('/login?error=sso_failed')
|
|
461
|
+
},
|
|
462
|
+
getUser: req => getSessionFromRequest(req),
|
|
463
|
+
},
|
|
464
|
+
})
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Next.js
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
// app/api/timeback/[...timeback]/route.ts
|
|
471
|
+
import { toNextjsHandler } from '@timeback/sdk/nextjs'
|
|
472
|
+
|
|
473
|
+
import { timeback } from '@/lib/timeback'
|
|
474
|
+
|
|
475
|
+
export const { GET, POST } = toNextjsHandler(timeback)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Express
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// server.ts
|
|
482
|
+
import express from 'express'
|
|
483
|
+
import { toExpressMiddleware } from '@timeback/sdk/express'
|
|
484
|
+
|
|
485
|
+
import { timeback } from './lib/timeback'
|
|
486
|
+
|
|
487
|
+
const app = express()
|
|
488
|
+
app.use('/api/timeback', toExpressMiddleware(timeback))
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
All other framework adapters (Nuxt, SvelteKit, SolidStart, TanStack Start) work identically with identity-only instances.
|
|
492
|
+
|
|
493
|
+
## User Profile
|
|
494
|
+
|
|
495
|
+
Use `timeback.user.fetch()` to retrieve the enriched Timeback profile for the
|
|
496
|
+
current user (identity + school/grade + courses + goals + XP):
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
const profile = await timeback.user.fetch()
|
|
500
|
+
console.log(profile.school?.name)
|
|
501
|
+
console.log(profile.xp?.today, profile.xp?.all)
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
`profile.xp` has the shape `{ today, all }`, where:
|
|
505
|
+
|
|
506
|
+
- `today` is computed over the current **UTC day**.
|
|
507
|
+
- `all` is computed over a long-range analytics window (starting `2000-01-01`).
|
|
508
|
+
|
|
509
|
+
## Activity Tracking
|
|
510
|
+
|
|
511
|
+
Activities track time spent on learning content:
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
// Start an activity
|
|
515
|
+
const activity = timeback.activity
|
|
516
|
+
.new({
|
|
517
|
+
id: 'lesson-123',
|
|
518
|
+
name: 'Introduction to Fractions',
|
|
519
|
+
course: { subject: 'FastMath', grade: 3 },
|
|
520
|
+
})
|
|
521
|
+
.start()
|
|
522
|
+
|
|
523
|
+
// Pause/resume
|
|
524
|
+
activity.pause()
|
|
525
|
+
activity.resume()
|
|
526
|
+
|
|
527
|
+
// End with metrics
|
|
528
|
+
await activity.end({
|
|
529
|
+
totalQuestions: 10,
|
|
530
|
+
correctQuestions: 8,
|
|
531
|
+
xpEarned: 80,
|
|
532
|
+
masteredUnits: 1,
|
|
533
|
+
})
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The SDK automatically sends activity data to your server, which forwards it to the Timeback API.
|
|
537
|
+
|
|
538
|
+
**Note:** Activity ingestion requires **exactly one** Caliper sensor URL in `timeback.config.ts`:
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
export default {
|
|
542
|
+
name: 'My App',
|
|
543
|
+
courses: [
|
|
544
|
+
/* ... */
|
|
545
|
+
],
|
|
546
|
+
sensors: ['https://my-app.example.com/sensors/main'],
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
## Advanced: Direct API Access
|
|
551
|
+
|
|
552
|
+
For advanced use cases that need to call Timeback services (OneRoster, Edubridge, Caliper, QTI) beyond what the SDK handlers provide, use `timeback.api`:
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
// Access via the timeback instance (lazy-initialized on first access)
|
|
556
|
+
const { data: users } = await timeback.api.oneroster.users.list({
|
|
557
|
+
limit: 10,
|
|
558
|
+
where: { role: 'student' },
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
// Access any Timeback service
|
|
562
|
+
const { data: orgs } = await timeback.api.oneroster.orgs.list()
|
|
563
|
+
await timeback.api.caliper.emit(caliperEvent)
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Access Patterns
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
// lib/timeback.ts
|
|
570
|
+
import { createTimeback } from '@timeback/sdk'
|
|
571
|
+
|
|
572
|
+
export const timeback = await createTimeback({
|
|
573
|
+
env: 'staging',
|
|
574
|
+
api: {
|
|
575
|
+
clientId: process.env.TIMEBACK_API_CLIENT_ID!,
|
|
576
|
+
clientSecret: process.env.TIMEBACK_API_CLIENT_SECRET!,
|
|
577
|
+
},
|
|
578
|
+
identity: { mode: 'custom', getUser: () => getSession() },
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
// Access the API client via timeback.api
|
|
582
|
+
const { data: users } = await timeback.api.oneroster.users.list()
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
// Elsewhere in your app
|
|
587
|
+
import { timeback } from './lib/timeback'
|
|
588
|
+
|
|
589
|
+
// The client is available at timeback.api
|
|
590
|
+
const { data: orgs } = await timeback.api.oneroster.orgs.list()
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Environment Mapping
|
|
594
|
+
|
|
595
|
+
The SDK's `env` config controls runtime mode, but for outbound API calls:
|
|
596
|
+
|
|
597
|
+
| SDK `env` | API calls use |
|
|
598
|
+
| ------------ | ------------- |
|
|
599
|
+
| `local` | `staging` |
|
|
600
|
+
| `staging` | `staging` |
|
|
601
|
+
| `production` | `production` |
|
|
602
|
+
|
|
603
|
+
This means `env: 'local'` uses staging Timeback services, so you can develop locally against real (staging) data without additional configuration.
|
|
604
|
+
|
|
605
|
+
### When to Use
|
|
606
|
+
|
|
607
|
+
- Fetching data not exposed by SDK handlers (e.g., listing orgs, courses, enrollments)
|
|
608
|
+
- Emitting custom Caliper events
|
|
609
|
+
- Building admin dashboards or reporting tools
|
|
610
|
+
- Any direct Timeback API integration
|
|
611
|
+
|
|
612
|
+
**Note:** `timeback.api` is only available on the full SDK (`createTimeback()`), not on identity-only instances (`createTimebackIdentity()`).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign In with Timeback Button
|
|
3
|
+
*
|
|
4
|
+
* A pre-styled button for Timeback SSO authentication.
|
|
5
|
+
* Fully customizable via CSS variables.
|
|
6
|
+
*/
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
/**
|
|
9
|
+
* Props for SignInButton component.
|
|
10
|
+
*/
|
|
11
|
+
export interface SignInButtonProps {
|
|
12
|
+
/** Button text (default: "Sign in with Timeback") */
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
/** Additional CSS class names */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Inline styles */
|
|
17
|
+
style?: React.CSSProperties;
|
|
18
|
+
/** Disabled state */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Show loading spinner when clicked */
|
|
21
|
+
showLoading?: boolean;
|
|
22
|
+
/** Called before redirect (can prevent with e.preventDefault()) */
|
|
23
|
+
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
24
|
+
/** Button variant */
|
|
25
|
+
variant?: 'default' | 'outline' | 'minimal';
|
|
26
|
+
/** Button size */
|
|
27
|
+
size?: 'sm' | 'md' | 'lg';
|
|
28
|
+
/** Show Timeback logo */
|
|
29
|
+
showLogo?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sign In with Timeback button component.
|
|
33
|
+
*
|
|
34
|
+
* Triggers SSO authentication flow when clicked.
|
|
35
|
+
*
|
|
36
|
+
* @param props - Component props
|
|
37
|
+
* @returns Button element
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* // Basic usage
|
|
42
|
+
* <SignInButton />
|
|
43
|
+
*
|
|
44
|
+
* // Custom text
|
|
45
|
+
* <SignInButton>Continue with Timeback</SignInButton>
|
|
46
|
+
*
|
|
47
|
+
* // Outline variant
|
|
48
|
+
* <SignInButton variant="outline" />
|
|
49
|
+
*
|
|
50
|
+
* // Custom styling via CSS variables
|
|
51
|
+
* <SignInButton
|
|
52
|
+
* style={{
|
|
53
|
+
* '--tb-btn-bg': '#6366f1',
|
|
54
|
+
* '--tb-btn-bg-hover': '#4f46e5',
|
|
55
|
+
* } as React.CSSProperties}
|
|
56
|
+
* />
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function SignInButton(props: SignInButtonProps): React.ReactElement;
|
|
60
|
+
//# sourceMappingURL=SignInButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SignInButton.d.ts","sourceRoot":"","sources":["../../../../src/client/adapters/react/SignInButton.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,qDAAqD;IACrD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oBAAoB;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;IAC3B,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,wCAAwC;IACxC,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,mEAAmE;IACnE,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IAC1D,qBAAqB;IACrB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;IAC3C,kBAAkB;IAClB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;IACzB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAA;CAClB;AA8MD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,KAAK,CAAC,YAAY,CA4DzE"}
|