@eduskit/server-sdk 0.1.1
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 +158 -0
- package/dist/src/classroom/client.d.ts +61 -0
- package/dist/src/classroom/client.js +140 -0
- package/dist/src/classroom/types.d.ts +184 -0
- package/dist/src/classroom/types.js +1 -0
- package/dist/src/config.d.ts +18 -0
- package/dist/src/config.js +1 -0
- package/dist/src/eduskit.d.ts +12 -0
- package/dist/src/eduskit.js +49 -0
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +16 -0
- package/dist/src/http.d.ts +24 -0
- package/dist/src/http.js +96 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +5 -0
- package/dist/src/token.d.ts +22 -0
- package/dist/src/token.js +42 -0
- package/dist/src/whiteboard/client.d.ts +49 -0
- package/dist/src/whiteboard/client.js +122 -0
- package/dist/src/whiteboard/types.d.ts +140 -0
- package/dist/src/whiteboard/types.js +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @eduskit/server-sdk
|
|
2
|
+
|
|
3
|
+
Node.js / TypeScript B 端 Server SDK。`sdk.client` 调课堂 server-api,`sdk.whiteboardClient` 调白板 server-api。
|
|
4
|
+
|
|
5
|
+
概念与方法表见仓库 [docs/overview.md](../../docs/overview.md)、[docs/api.md](../../docs/api.md)。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @eduskit/server-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
要求 Node.js 18+(使用内置 `fetch`)。本仓库开发:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd packages/nodejs
|
|
17
|
+
npm install
|
|
18
|
+
npm test
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 初始化
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { Eduskit, EduskitError } from '@eduskit/server-sdk'
|
|
25
|
+
|
|
26
|
+
const sdk = new Eduskit({
|
|
27
|
+
timeoutMs: 10_000,
|
|
28
|
+
lang: 'zh-CN',
|
|
29
|
+
client: {
|
|
30
|
+
baseUrl: 'http://localhost:3112',
|
|
31
|
+
appId: process.env.EDU_APP_ID!,
|
|
32
|
+
appKey: process.env.EDU_APP_KEY!,
|
|
33
|
+
appSecret: process.env.EDU_APP_SECRET!,
|
|
34
|
+
},
|
|
35
|
+
whiteboardClient: {
|
|
36
|
+
baseUrl: 'http://localhost:3012',
|
|
37
|
+
appId: process.env.WB_APP_ID!,
|
|
38
|
+
appKey: process.env.WB_APP_KEY!,
|
|
39
|
+
appSecret: process.env.WB_APP_SECRET!,
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`client` 与 `whiteboardClient` 均可选。也可单独构造:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { ClassroomClient, WhiteboardClient } from '@eduskit/server-sdk'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 课堂 `sdk.client`
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const user = await sdk.client.users.register({
|
|
54
|
+
originId: 'stu_001',
|
|
55
|
+
nickname: '小明',
|
|
56
|
+
avatar: 'https://example.com/a.png',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const token = await sdk.client.auth.issueToken({
|
|
60
|
+
eduUserId: user.eduUserId,
|
|
61
|
+
originId: 'stu_001',
|
|
62
|
+
role: 'student',
|
|
63
|
+
expiresIn: 86400,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const classroom = await sdk.client.classrooms.create({
|
|
67
|
+
name: '一年级数学',
|
|
68
|
+
startsAt: '2026-08-17T10:00:00.000Z',
|
|
69
|
+
endsAt: '2026-08-17T11:00:00.000Z',
|
|
70
|
+
teacherEduUserId: user.eduUserId,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
await sdk.client.classrooms.start(classroom.classroomId)
|
|
74
|
+
await sdk.client.classrooms.members.add(classroom.classroomId, {
|
|
75
|
+
eduUserId: user.eduUserId,
|
|
76
|
+
role: 'student',
|
|
77
|
+
})
|
|
78
|
+
await sdk.client.classrooms.members.replaceStudents(classroom.classroomId, {
|
|
79
|
+
eduUserIds: [user.eduUserId],
|
|
80
|
+
})
|
|
81
|
+
await sdk.client.classrooms.permissions.set(classroom.classroomId, user.eduUserId, {
|
|
82
|
+
permission: 'camera',
|
|
83
|
+
effect: 'grant',
|
|
84
|
+
operatorEduUserId: user.eduUserId,
|
|
85
|
+
})
|
|
86
|
+
await sdk.client.classrooms.coursewares.bind(classroom.classroomId, {
|
|
87
|
+
coursewareIds: ['cw_xxx'],
|
|
88
|
+
})
|
|
89
|
+
await sdk.client.app.getUiConfig()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| 方法 | 说明 |
|
|
93
|
+
|------|------|
|
|
94
|
+
| `users.register(input)` | 注册/更新 C 端用户 |
|
|
95
|
+
| `auth.issueToken(input)` | 签发 C 端 accessToken |
|
|
96
|
+
| `classrooms.create(input)` | 创建课堂 |
|
|
97
|
+
| `classrooms.start(id)` / `end(id)` | 开课 / 结课 |
|
|
98
|
+
| `classrooms.members.add(id, input)` | 加人 |
|
|
99
|
+
| `classrooms.members.list(id)` | 成员列表 |
|
|
100
|
+
| `classrooms.members.replaceStudents(id, input)` | 全量替换学生 |
|
|
101
|
+
| `classrooms.permissions.get/set/clear(...)` | 代老师管权限 |
|
|
102
|
+
| `classrooms.coursewares.list/bind/unbind(...)` | 课件绑定 |
|
|
103
|
+
| `app.getUiConfig()` / `setUiConfig(input)` | App UI |
|
|
104
|
+
|
|
105
|
+
## 白板 `sdk.whiteboardClient`
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const credential = await sdk.whiteboardClient.auth.issueRoomToken({
|
|
109
|
+
roomId: 'room_1',
|
|
110
|
+
userId: user.eduUserId,
|
|
111
|
+
role: 'host',
|
|
112
|
+
expiresIn: 3600,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const session = await sdk.whiteboardClient.recordings.start('room_1', {
|
|
116
|
+
externalRef: 'lesson_001',
|
|
117
|
+
})
|
|
118
|
+
await sdk.whiteboardClient.recordings.stop('room_1', {
|
|
119
|
+
recordingId: session.recordingId,
|
|
120
|
+
})
|
|
121
|
+
await sdk.whiteboardClient.recordings.enqueueVideoExport(session.recordingId, {
|
|
122
|
+
profile: 'hd',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
await sdk.whiteboardClient.captures.create('room_1', { pageId: 'page_1' })
|
|
126
|
+
await sdk.whiteboardClient.files.convert({
|
|
127
|
+
sourceUrl: 'https://cdn.example.com/lesson.pptx',
|
|
128
|
+
fileName: 'lesson.pptx',
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| 方法 | 说明 |
|
|
133
|
+
|------|------|
|
|
134
|
+
| `auth.issueRoomToken(input)` | 签发 Room Token |
|
|
135
|
+
| `recordings.start/stop/list/get` | 录制会话 |
|
|
136
|
+
| `recordings.registerMediaAsset` / `deleteMediaAsset` | 媒体资产 |
|
|
137
|
+
| `recordings.enqueueVideoExport` / `getVideoExport` | 视频导出(stop 不自动出片) |
|
|
138
|
+
| `captures.create/list` | 页截图 |
|
|
139
|
+
| `files.convert` / `getConvertJob` | 文件转码 |
|
|
140
|
+
|
|
141
|
+
## 错误
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
try {
|
|
145
|
+
await sdk.client.auth.issueToken({ eduUserId: '' })
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof EduskitError) {
|
|
148
|
+
console.error(error.errorCode, error.status, error.traceId, error.source)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`sdk.client.lastTraceId` / `sdk.whiteboardClient.lastTraceId` 为最近一次调用的 trace。
|
|
154
|
+
|
|
155
|
+
## 示例
|
|
156
|
+
|
|
157
|
+
- [examples/classroom-quickstart.ts](examples/classroom-quickstart.ts)
|
|
158
|
+
- [examples/whiteboard-quickstart.ts](examples/whiteboard-quickstart.ts)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SideClientOptions } from '../config.js';
|
|
2
|
+
import { HttpTransport } from '../http.js';
|
|
3
|
+
import { type LocalTokenSigner } from '../token.js';
|
|
4
|
+
import type { AddMemberInput, AppUiConfig, BindCoursewaresInput, BindCoursewaresResult, Classroom, ClearPermissionInput, CoursewareList, CreateClassroomInput, EduUser, EduUserToken, IssueTokenInput, MemberList, Membership, Permissions, RegisterEduUserInput, ReplaceStudentsInput, ReplaceStudentsResult, SetAppUiConfigInput, SetPermissionInput, UnbindCoursewaresResult } from './types.js';
|
|
5
|
+
export declare class ClassroomClient {
|
|
6
|
+
readonly users: ClassroomUsersApi;
|
|
7
|
+
readonly auth: ClassroomAuthApi;
|
|
8
|
+
readonly classrooms: ClassroomClassroomsApi;
|
|
9
|
+
readonly app: ClassroomAppApi;
|
|
10
|
+
private readonly http;
|
|
11
|
+
constructor(options: SideClientOptions);
|
|
12
|
+
get lastTraceId(): string;
|
|
13
|
+
}
|
|
14
|
+
declare class ClassroomUsersApi {
|
|
15
|
+
private readonly http;
|
|
16
|
+
constructor(http: HttpTransport);
|
|
17
|
+
register(input: RegisterEduUserInput): Promise<EduUser>;
|
|
18
|
+
}
|
|
19
|
+
declare class ClassroomAuthApi {
|
|
20
|
+
private readonly signer;
|
|
21
|
+
constructor(signer: LocalTokenSigner);
|
|
22
|
+
issueToken(input: IssueTokenInput): Promise<EduUserToken>;
|
|
23
|
+
}
|
|
24
|
+
declare class ClassroomMembersApi {
|
|
25
|
+
private readonly http;
|
|
26
|
+
constructor(http: HttpTransport);
|
|
27
|
+
add(classroomId: string, input: AddMemberInput): Promise<Membership>;
|
|
28
|
+
list(classroomId: string): Promise<MemberList>;
|
|
29
|
+
replaceStudents(classroomId: string, input: ReplaceStudentsInput): Promise<ReplaceStudentsResult>;
|
|
30
|
+
}
|
|
31
|
+
declare class ClassroomPermissionsApi {
|
|
32
|
+
private readonly http;
|
|
33
|
+
constructor(http: HttpTransport);
|
|
34
|
+
get(classroomId: string, eduUserId: string): Promise<Permissions>;
|
|
35
|
+
set(classroomId: string, eduUserId: string, input: SetPermissionInput): Promise<Permissions>;
|
|
36
|
+
clear(classroomId: string, eduUserId: string, permission: string, input: ClearPermissionInput): Promise<Permissions>;
|
|
37
|
+
}
|
|
38
|
+
declare class ClassroomCoursewaresApi {
|
|
39
|
+
private readonly http;
|
|
40
|
+
constructor(http: HttpTransport);
|
|
41
|
+
list(classroomId: string): Promise<CoursewareList>;
|
|
42
|
+
bind(classroomId: string, input: BindCoursewaresInput): Promise<BindCoursewaresResult>;
|
|
43
|
+
unbind(classroomId: string, input: BindCoursewaresInput): Promise<UnbindCoursewaresResult>;
|
|
44
|
+
}
|
|
45
|
+
declare class ClassroomClassroomsApi {
|
|
46
|
+
private readonly http;
|
|
47
|
+
readonly members: ClassroomMembersApi;
|
|
48
|
+
readonly permissions: ClassroomPermissionsApi;
|
|
49
|
+
readonly coursewares: ClassroomCoursewaresApi;
|
|
50
|
+
constructor(http: HttpTransport);
|
|
51
|
+
create(input: CreateClassroomInput): Promise<Classroom>;
|
|
52
|
+
start(classroomId: string): Promise<Classroom>;
|
|
53
|
+
end(classroomId: string): Promise<Classroom>;
|
|
54
|
+
}
|
|
55
|
+
declare class ClassroomAppApi {
|
|
56
|
+
private readonly http;
|
|
57
|
+
constructor(http: HttpTransport);
|
|
58
|
+
getUiConfig(): Promise<AppUiConfig>;
|
|
59
|
+
setUiConfig(input: SetAppUiConfigInput): Promise<AppUiConfig>;
|
|
60
|
+
}
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { encodePath, HttpTransport } from '../http.js';
|
|
2
|
+
import { EDU_TOKEN_AUDIENCE, EDU_TOKEN_ISSUER, requireText, signHs256, } from '../token.js';
|
|
3
|
+
export class ClassroomClient {
|
|
4
|
+
users;
|
|
5
|
+
auth;
|
|
6
|
+
classrooms;
|
|
7
|
+
app;
|
|
8
|
+
http;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.http = new HttpTransport({ ...options, source: 'classroom' });
|
|
11
|
+
this.users = new ClassroomUsersApi(this.http);
|
|
12
|
+
this.auth = new ClassroomAuthApi({
|
|
13
|
+
appId: options.appId,
|
|
14
|
+
appSecret: options.appSecret,
|
|
15
|
+
source: 'classroom',
|
|
16
|
+
});
|
|
17
|
+
this.classrooms = new ClassroomClassroomsApi(this.http);
|
|
18
|
+
this.app = new ClassroomAppApi(this.http);
|
|
19
|
+
}
|
|
20
|
+
get lastTraceId() {
|
|
21
|
+
return this.http.lastTraceId;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
class ClassroomUsersApi {
|
|
25
|
+
http;
|
|
26
|
+
constructor(http) {
|
|
27
|
+
this.http = http;
|
|
28
|
+
}
|
|
29
|
+
register(input) {
|
|
30
|
+
return this.http.request('POST', '/v1/users', input);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
class ClassroomAuthApi {
|
|
34
|
+
signer;
|
|
35
|
+
constructor(signer) {
|
|
36
|
+
this.signer = signer;
|
|
37
|
+
}
|
|
38
|
+
async issueToken(input) {
|
|
39
|
+
const eduUserId = requireText(input.eduUserId, 'eduUserId', 'classroom');
|
|
40
|
+
const expiresIn = input.expiresIn ?? 86_400;
|
|
41
|
+
const signed = signHs256(this.signer, {
|
|
42
|
+
appId: this.signer.appId,
|
|
43
|
+
...(input.originId ? { originId: input.originId } : {}),
|
|
44
|
+
...(input.role ? { role: input.role } : {}),
|
|
45
|
+
}, {
|
|
46
|
+
issuer: EDU_TOKEN_ISSUER,
|
|
47
|
+
audience: EDU_TOKEN_AUDIENCE,
|
|
48
|
+
subject: eduUserId,
|
|
49
|
+
expiresIn,
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
accessToken: signed.token,
|
|
53
|
+
expiresIn,
|
|
54
|
+
expiresAt: new Date(signed.expiresAt * 1000).toISOString(),
|
|
55
|
+
tokenType: 'Bearer',
|
|
56
|
+
appId: this.signer.appId,
|
|
57
|
+
eduUserId,
|
|
58
|
+
originId: input.originId ?? '',
|
|
59
|
+
role: input.role ?? '',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
class ClassroomMembersApi {
|
|
64
|
+
http;
|
|
65
|
+
constructor(http) {
|
|
66
|
+
this.http = http;
|
|
67
|
+
}
|
|
68
|
+
add(classroomId, input) {
|
|
69
|
+
return this.http.request('POST', `/v1/classrooms/${encodePath(classroomId)}/members`, input);
|
|
70
|
+
}
|
|
71
|
+
list(classroomId) {
|
|
72
|
+
return this.http.request('GET', `/v1/classrooms/${encodePath(classroomId)}/members`);
|
|
73
|
+
}
|
|
74
|
+
replaceStudents(classroomId, input) {
|
|
75
|
+
return this.http.request('PUT', `/v1/classrooms/${encodePath(classroomId)}/members/students`, input);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
class ClassroomPermissionsApi {
|
|
79
|
+
http;
|
|
80
|
+
constructor(http) {
|
|
81
|
+
this.http = http;
|
|
82
|
+
}
|
|
83
|
+
get(classroomId, eduUserId) {
|
|
84
|
+
return this.http.request('GET', `/v1/classrooms/${encodePath(classroomId)}/members/${encodePath(eduUserId)}/permissions`);
|
|
85
|
+
}
|
|
86
|
+
set(classroomId, eduUserId, input) {
|
|
87
|
+
return this.http.request('POST', `/v1/classrooms/${encodePath(classroomId)}/members/${encodePath(eduUserId)}/permissions`, input);
|
|
88
|
+
}
|
|
89
|
+
clear(classroomId, eduUserId, permission, input) {
|
|
90
|
+
return this.http.request('DELETE', `/v1/classrooms/${encodePath(classroomId)}/members/${encodePath(eduUserId)}/permissions/${encodePath(permission)}`, input);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
class ClassroomCoursewaresApi {
|
|
94
|
+
http;
|
|
95
|
+
constructor(http) {
|
|
96
|
+
this.http = http;
|
|
97
|
+
}
|
|
98
|
+
list(classroomId) {
|
|
99
|
+
return this.http.request('GET', `/v1/classrooms/${encodePath(classroomId)}/coursewares`);
|
|
100
|
+
}
|
|
101
|
+
bind(classroomId, input) {
|
|
102
|
+
return this.http.request('POST', `/v1/classrooms/${encodePath(classroomId)}/coursewares`, input);
|
|
103
|
+
}
|
|
104
|
+
unbind(classroomId, input) {
|
|
105
|
+
return this.http.request('DELETE', `/v1/classrooms/${encodePath(classroomId)}/coursewares`, input);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
class ClassroomClassroomsApi {
|
|
109
|
+
http;
|
|
110
|
+
members;
|
|
111
|
+
permissions;
|
|
112
|
+
coursewares;
|
|
113
|
+
constructor(http) {
|
|
114
|
+
this.http = http;
|
|
115
|
+
this.members = new ClassroomMembersApi(http);
|
|
116
|
+
this.permissions = new ClassroomPermissionsApi(http);
|
|
117
|
+
this.coursewares = new ClassroomCoursewaresApi(http);
|
|
118
|
+
}
|
|
119
|
+
create(input) {
|
|
120
|
+
return this.http.request('POST', '/v1/classrooms', input);
|
|
121
|
+
}
|
|
122
|
+
start(classroomId) {
|
|
123
|
+
return this.http.request('POST', `/v1/classrooms/${encodePath(classroomId)}/start`);
|
|
124
|
+
}
|
|
125
|
+
end(classroomId) {
|
|
126
|
+
return this.http.request('POST', `/v1/classrooms/${encodePath(classroomId)}/end`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
class ClassroomAppApi {
|
|
130
|
+
http;
|
|
131
|
+
constructor(http) {
|
|
132
|
+
this.http = http;
|
|
133
|
+
}
|
|
134
|
+
getUiConfig() {
|
|
135
|
+
return this.http.request('GET', '/v1/app/ui-config');
|
|
136
|
+
}
|
|
137
|
+
setUiConfig(input) {
|
|
138
|
+
return this.http.request('PUT', '/v1/app/ui-config', input);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export type ClassroomMemberRole = 'teacher' | 'assistant' | 'student' | 'inspector';
|
|
2
|
+
export type ClassroomPermission = 'camera' | 'microphone' | 'whiteboard' | 'screen_share' | 'chat';
|
|
3
|
+
export type PermissionEffect = 'grant' | 'revoke';
|
|
4
|
+
export type RegisterEduUserInput = {
|
|
5
|
+
originId?: string;
|
|
6
|
+
nickname: string;
|
|
7
|
+
avatar: string;
|
|
8
|
+
};
|
|
9
|
+
export type EduUser = {
|
|
10
|
+
eduUserId: string;
|
|
11
|
+
appId: string;
|
|
12
|
+
originId?: string;
|
|
13
|
+
nickname: string;
|
|
14
|
+
avatar: string;
|
|
15
|
+
status: string;
|
|
16
|
+
};
|
|
17
|
+
export type IssueTokenInput = {
|
|
18
|
+
/** POST /v1/users 返回的平台 C 端用户 ID。 */
|
|
19
|
+
eduUserId: string;
|
|
20
|
+
originId?: string;
|
|
21
|
+
role?: ClassroomMemberRole;
|
|
22
|
+
expiresIn?: number;
|
|
23
|
+
};
|
|
24
|
+
export type EduUserToken = {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
expiresIn: number;
|
|
27
|
+
tokenType: string;
|
|
28
|
+
appId: string;
|
|
29
|
+
eduUserId: string;
|
|
30
|
+
originId: string;
|
|
31
|
+
role: string;
|
|
32
|
+
expiresAt: string;
|
|
33
|
+
};
|
|
34
|
+
export type CreateClassroomInput = {
|
|
35
|
+
name: string;
|
|
36
|
+
startsAt: string;
|
|
37
|
+
endsAt: string;
|
|
38
|
+
teacherEduUserId: string;
|
|
39
|
+
resolution?: '360p' | '480p' | '720p' | '1080p';
|
|
40
|
+
maxOnStage?: number;
|
|
41
|
+
defaultOnStage?: boolean;
|
|
42
|
+
videoOnly?: boolean;
|
|
43
|
+
recordMode?: 'none' | 'classroom' | 'mix';
|
|
44
|
+
allowTeacherControlDevice?: boolean;
|
|
45
|
+
avSubscribeMode?: 'teacher_only' | 'self_only' | 'on_stage_all';
|
|
46
|
+
uiConfig?: Record<string, unknown>;
|
|
47
|
+
overtimeMinutes?: number;
|
|
48
|
+
classroomType?: 'live' | 'realtime';
|
|
49
|
+
pushStreamUrl?: string;
|
|
50
|
+
autoStartOnSchedule?: boolean;
|
|
51
|
+
recordBackgroundUrl?: string;
|
|
52
|
+
joinPolicy?: 'invite_only' | 'open_app';
|
|
53
|
+
studentEduUserIds?: string[];
|
|
54
|
+
coursewareIds?: string[];
|
|
55
|
+
};
|
|
56
|
+
export type ClassroomTeacher = {
|
|
57
|
+
eduUserId: string;
|
|
58
|
+
nickname: string;
|
|
59
|
+
avatar: string;
|
|
60
|
+
status: string;
|
|
61
|
+
};
|
|
62
|
+
export type Classroom = {
|
|
63
|
+
classroomId: string;
|
|
64
|
+
appId: string;
|
|
65
|
+
name: string;
|
|
66
|
+
status: string;
|
|
67
|
+
createdBy?: string;
|
|
68
|
+
maxOnStage?: number;
|
|
69
|
+
defaultOnStage?: boolean;
|
|
70
|
+
startsAt: string;
|
|
71
|
+
endsAt: string;
|
|
72
|
+
actualStartedAt?: string;
|
|
73
|
+
endedAt?: string;
|
|
74
|
+
effectiveEndsAt?: string;
|
|
75
|
+
resolution?: string;
|
|
76
|
+
teacherEduUserId?: string;
|
|
77
|
+
teacher?: ClassroomTeacher | null;
|
|
78
|
+
recordMode?: string;
|
|
79
|
+
allowTeacherControlDevice?: boolean;
|
|
80
|
+
avSubscribeMode?: string;
|
|
81
|
+
overtimeMinutes?: number;
|
|
82
|
+
classroomType?: string;
|
|
83
|
+
pushStreamUrl?: string;
|
|
84
|
+
autoStartOnSchedule?: boolean;
|
|
85
|
+
recordBackgroundUrl?: string;
|
|
86
|
+
joinPolicy?: string;
|
|
87
|
+
videoOnly?: boolean;
|
|
88
|
+
schedulePhase?: string;
|
|
89
|
+
startOverdue?: boolean;
|
|
90
|
+
uiConfig?: Record<string, unknown>;
|
|
91
|
+
};
|
|
92
|
+
export type AddMemberInput = {
|
|
93
|
+
eduUserId: string;
|
|
94
|
+
role?: ClassroomMemberRole;
|
|
95
|
+
};
|
|
96
|
+
export type Membership = {
|
|
97
|
+
success?: boolean;
|
|
98
|
+
classroomId: string;
|
|
99
|
+
eduUserId: string;
|
|
100
|
+
role: string;
|
|
101
|
+
stageStatus?: string;
|
|
102
|
+
};
|
|
103
|
+
export type MembershipItem = {
|
|
104
|
+
eduUserId: string;
|
|
105
|
+
role: string;
|
|
106
|
+
joinedAt?: string;
|
|
107
|
+
stageStatus?: string;
|
|
108
|
+
};
|
|
109
|
+
export type MemberList = {
|
|
110
|
+
members: MembershipItem[];
|
|
111
|
+
};
|
|
112
|
+
export type ReplaceStudentsInput = {
|
|
113
|
+
eduUserIds: string[];
|
|
114
|
+
};
|
|
115
|
+
export type ReplaceStudentsResult = {
|
|
116
|
+
classroomId: string;
|
|
117
|
+
studentCount: number;
|
|
118
|
+
eduUserIds: string[];
|
|
119
|
+
};
|
|
120
|
+
export type PermissionEntry = {
|
|
121
|
+
permission: string;
|
|
122
|
+
allowed: boolean;
|
|
123
|
+
source?: string;
|
|
124
|
+
status?: string;
|
|
125
|
+
detail?: string;
|
|
126
|
+
};
|
|
127
|
+
export type Permissions = {
|
|
128
|
+
classroomId: string;
|
|
129
|
+
eduUserId: string;
|
|
130
|
+
role: string;
|
|
131
|
+
permissions: PermissionEntry[];
|
|
132
|
+
};
|
|
133
|
+
export type SetPermissionInput = {
|
|
134
|
+
permission: ClassroomPermission;
|
|
135
|
+
effect: PermissionEffect;
|
|
136
|
+
operatorEduUserId: string;
|
|
137
|
+
};
|
|
138
|
+
export type ClearPermissionInput = {
|
|
139
|
+
operatorEduUserId: string;
|
|
140
|
+
};
|
|
141
|
+
export type Courseware = {
|
|
142
|
+
coursewareId: string;
|
|
143
|
+
ownerEduUserId?: string;
|
|
144
|
+
appId?: string;
|
|
145
|
+
name?: string;
|
|
146
|
+
originalName?: string;
|
|
147
|
+
format?: string;
|
|
148
|
+
sizeBytes?: number;
|
|
149
|
+
status?: string;
|
|
150
|
+
progress?: number;
|
|
151
|
+
jobId?: string;
|
|
152
|
+
objectKey?: string;
|
|
153
|
+
sourceUrl?: string;
|
|
154
|
+
resultManifestUrl?: string;
|
|
155
|
+
coursewareBaseUrl?: string;
|
|
156
|
+
wbCoursewareId?: string;
|
|
157
|
+
errorCode?: string;
|
|
158
|
+
errorMessage?: string;
|
|
159
|
+
contentHash?: string;
|
|
160
|
+
createdAt?: string;
|
|
161
|
+
updatedAt?: string;
|
|
162
|
+
};
|
|
163
|
+
export type CoursewareList = {
|
|
164
|
+
items: Courseware[];
|
|
165
|
+
};
|
|
166
|
+
export type BindCoursewaresInput = {
|
|
167
|
+
coursewareIds: string[];
|
|
168
|
+
};
|
|
169
|
+
export type BindCoursewaresResult = {
|
|
170
|
+
boundCount?: number;
|
|
171
|
+
skippedCount?: number;
|
|
172
|
+
boundCoursewareIds?: string[];
|
|
173
|
+
skippedCoursewareIds?: string[];
|
|
174
|
+
};
|
|
175
|
+
export type UnbindCoursewaresResult = {
|
|
176
|
+
unboundCount?: number;
|
|
177
|
+
};
|
|
178
|
+
export type AppUiConfig = {
|
|
179
|
+
appId: string;
|
|
180
|
+
uiConfig: Record<string, unknown>;
|
|
181
|
+
};
|
|
182
|
+
export type SetAppUiConfigInput = {
|
|
183
|
+
uiConfig: Record<string, unknown>;
|
|
184
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FetchLike } from './http.js';
|
|
2
|
+
export type ClientCredentials = {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
/** 租户应用 ID;用于客户后端本地签发 SDK Token。 */
|
|
5
|
+
appId: string;
|
|
6
|
+
appKey: string;
|
|
7
|
+
appSecret: string;
|
|
8
|
+
};
|
|
9
|
+
export type SharedClientOptions = {
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
lang?: string;
|
|
12
|
+
fetch?: FetchLike;
|
|
13
|
+
};
|
|
14
|
+
export type EduskitOptions = SharedClientOptions & {
|
|
15
|
+
client?: ClientCredentials;
|
|
16
|
+
whiteboardClient?: ClientCredentials;
|
|
17
|
+
};
|
|
18
|
+
export type SideClientOptions = ClientCredentials & SharedClientOptions;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ClassroomClient } from './classroom/client.js';
|
|
2
|
+
import type { EduskitOptions, SideClientOptions } from './config.js';
|
|
3
|
+
import { WhiteboardClient } from './whiteboard/client.js';
|
|
4
|
+
export declare class Eduskit {
|
|
5
|
+
private readonly classroom?;
|
|
6
|
+
private readonly whiteboard?;
|
|
7
|
+
constructor(options: EduskitOptions);
|
|
8
|
+
get client(): ClassroomClient;
|
|
9
|
+
get whiteboardClient(): WhiteboardClient;
|
|
10
|
+
}
|
|
11
|
+
export declare function createClassroomClient(options: SideClientOptions): ClassroomClient;
|
|
12
|
+
export declare function createWhiteboardClient(options: SideClientOptions): WhiteboardClient;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ClassroomClient } from './classroom/client.js';
|
|
2
|
+
import { EduskitError } from './errors.js';
|
|
3
|
+
import { WhiteboardClient } from './whiteboard/client.js';
|
|
4
|
+
export class Eduskit {
|
|
5
|
+
classroom;
|
|
6
|
+
whiteboard;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
const shared = {
|
|
9
|
+
timeoutMs: options.timeoutMs,
|
|
10
|
+
lang: options.lang,
|
|
11
|
+
fetch: options.fetch,
|
|
12
|
+
};
|
|
13
|
+
if (options.client) {
|
|
14
|
+
this.classroom = new ClassroomClient({ ...shared, ...options.client });
|
|
15
|
+
}
|
|
16
|
+
if (options.whiteboardClient) {
|
|
17
|
+
this.whiteboard = new WhiteboardClient({
|
|
18
|
+
...shared,
|
|
19
|
+
...options.whiteboardClient,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
get client() {
|
|
24
|
+
if (!this.classroom) {
|
|
25
|
+
throw new EduskitError({
|
|
26
|
+
message: 'classroom client is not configured',
|
|
27
|
+
errorCode: 'SDK_CLIENT_NOT_CONFIGURED',
|
|
28
|
+
source: 'classroom',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return this.classroom;
|
|
32
|
+
}
|
|
33
|
+
get whiteboardClient() {
|
|
34
|
+
if (!this.whiteboard) {
|
|
35
|
+
throw new EduskitError({
|
|
36
|
+
message: 'whiteboard client is not configured',
|
|
37
|
+
errorCode: 'SDK_CLIENT_NOT_CONFIGURED',
|
|
38
|
+
source: 'whiteboard',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return this.whiteboard;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function createClassroomClient(options) {
|
|
45
|
+
return new ClassroomClient(options);
|
|
46
|
+
}
|
|
47
|
+
export function createWhiteboardClient(options) {
|
|
48
|
+
return new WhiteboardClient(options);
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type EduskitSource = 'classroom' | 'whiteboard';
|
|
2
|
+
export type EduskitErrorInit = {
|
|
3
|
+
message: string;
|
|
4
|
+
status?: number;
|
|
5
|
+
errorCode?: string;
|
|
6
|
+
traceId?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
source?: EduskitSource;
|
|
9
|
+
};
|
|
10
|
+
export declare class EduskitError extends Error {
|
|
11
|
+
readonly status?: number;
|
|
12
|
+
readonly errorCode?: string;
|
|
13
|
+
readonly traceId?: string;
|
|
14
|
+
readonly path?: string;
|
|
15
|
+
readonly source?: EduskitSource;
|
|
16
|
+
constructor(init: EduskitErrorInit);
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class EduskitError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
errorCode;
|
|
4
|
+
traceId;
|
|
5
|
+
path;
|
|
6
|
+
source;
|
|
7
|
+
constructor(init) {
|
|
8
|
+
super(init.message);
|
|
9
|
+
this.name = 'EduskitError';
|
|
10
|
+
this.status = init.status;
|
|
11
|
+
this.errorCode = init.errorCode;
|
|
12
|
+
this.traceId = init.traceId;
|
|
13
|
+
this.path = init.path;
|
|
14
|
+
this.source = init.source;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type EduskitSource } from './errors.js';
|
|
2
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
3
|
+
export type HttpConfig = {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
appKey: string;
|
|
6
|
+
appSecret: string;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
lang?: string;
|
|
9
|
+
fetch?: FetchLike;
|
|
10
|
+
source: EduskitSource;
|
|
11
|
+
};
|
|
12
|
+
export declare class HttpTransport {
|
|
13
|
+
lastTraceId: string;
|
|
14
|
+
private readonly baseUrl;
|
|
15
|
+
private readonly appKey;
|
|
16
|
+
private readonly appSecret;
|
|
17
|
+
private readonly timeoutMs;
|
|
18
|
+
private readonly lang;
|
|
19
|
+
private readonly fetchImpl;
|
|
20
|
+
private readonly source;
|
|
21
|
+
constructor(config: HttpConfig);
|
|
22
|
+
request<T>(method: string, path: string, body?: unknown): Promise<T>;
|
|
23
|
+
}
|
|
24
|
+
export declare function encodePath(value: string): string;
|
package/dist/src/http.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { EduskitError } from './errors.js';
|
|
2
|
+
export class HttpTransport {
|
|
3
|
+
lastTraceId = '';
|
|
4
|
+
baseUrl;
|
|
5
|
+
appKey;
|
|
6
|
+
appSecret;
|
|
7
|
+
timeoutMs;
|
|
8
|
+
lang;
|
|
9
|
+
fetchImpl;
|
|
10
|
+
source;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
13
|
+
this.appKey = config.appKey;
|
|
14
|
+
this.appSecret = config.appSecret;
|
|
15
|
+
this.timeoutMs = config.timeoutMs ?? 10_000;
|
|
16
|
+
this.lang = config.lang ?? 'zh-CN';
|
|
17
|
+
this.fetchImpl = config.fetch ?? fetch;
|
|
18
|
+
this.source = config.source;
|
|
19
|
+
}
|
|
20
|
+
async request(method, path, body) {
|
|
21
|
+
const url = `${this.baseUrl}${path}`;
|
|
22
|
+
const traceId = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
|
|
23
|
+
const headers = {
|
|
24
|
+
'content-type': 'application/json',
|
|
25
|
+
accept: 'application/json',
|
|
26
|
+
'x-app-key': this.appKey,
|
|
27
|
+
'x-app-secret': this.appSecret,
|
|
28
|
+
'x-lang': this.lang,
|
|
29
|
+
'x-trace-id': traceId,
|
|
30
|
+
};
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
33
|
+
let response;
|
|
34
|
+
try {
|
|
35
|
+
response = await this.fetchImpl(url, {
|
|
36
|
+
method,
|
|
37
|
+
headers,
|
|
38
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
45
|
+
throw new EduskitError({
|
|
46
|
+
message: `request timeout after ${this.timeoutMs}ms`,
|
|
47
|
+
errorCode: 'SDK_TIMEOUT',
|
|
48
|
+
path,
|
|
49
|
+
source: this.source,
|
|
50
|
+
traceId,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
throw new EduskitError({
|
|
54
|
+
message: error instanceof Error ? error.message : String(error),
|
|
55
|
+
errorCode: 'SDK_NETWORK_ERROR',
|
|
56
|
+
path,
|
|
57
|
+
source: this.source,
|
|
58
|
+
traceId,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
const headerTrace = response.headers.get('x-trace-id') ?? traceId;
|
|
63
|
+
const text = await response.text();
|
|
64
|
+
let payload = {};
|
|
65
|
+
if (text) {
|
|
66
|
+
try {
|
|
67
|
+
payload = JSON.parse(text);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new EduskitError({
|
|
71
|
+
message: text || `invalid json (HTTP ${response.status})`,
|
|
72
|
+
status: response.status,
|
|
73
|
+
errorCode: 'SDK_INVALID_JSON',
|
|
74
|
+
path,
|
|
75
|
+
source: this.source,
|
|
76
|
+
traceId: headerTrace,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.lastTraceId = String(payload.traceId || headerTrace);
|
|
81
|
+
if (!response.ok || (payload.code != null && payload.code !== 0)) {
|
|
82
|
+
throw new EduskitError({
|
|
83
|
+
message: payload.message || `HTTP ${response.status}`,
|
|
84
|
+
status: response.status,
|
|
85
|
+
errorCode: payload.errorCode || `HTTP_${response.status}`,
|
|
86
|
+
path: payload.path || path,
|
|
87
|
+
source: this.source,
|
|
88
|
+
traceId: this.lastTraceId,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return (payload.data ?? payload);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function encodePath(value) {
|
|
95
|
+
return encodeURIComponent(value);
|
|
96
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { Eduskit, createClassroomClient, createWhiteboardClient } from './eduskit.js';
|
|
2
|
+
export { ClassroomClient } from './classroom/client.js';
|
|
3
|
+
export { WhiteboardClient } from './whiteboard/client.js';
|
|
4
|
+
export { EduskitError } from './errors.js';
|
|
5
|
+
export { EDU_TOKEN_AUDIENCE, EDU_TOKEN_ISSUER, ROOM_TOKEN_AUDIENCE, ROOM_TOKEN_ISSUER, } from './token.js';
|
|
6
|
+
export type { EduskitSource, EduskitErrorInit } from './errors.js';
|
|
7
|
+
export type { ClientCredentials, EduskitOptions, SharedClientOptions, SideClientOptions, } from './config.js';
|
|
8
|
+
export type * from './classroom/types.js';
|
|
9
|
+
export type * from './whiteboard/types.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Eduskit, createClassroomClient, createWhiteboardClient } from './eduskit.js';
|
|
2
|
+
export { ClassroomClient } from './classroom/client.js';
|
|
3
|
+
export { WhiteboardClient } from './whiteboard/client.js';
|
|
4
|
+
export { EduskitError } from './errors.js';
|
|
5
|
+
export { EDU_TOKEN_AUDIENCE, EDU_TOKEN_ISSUER, ROOM_TOKEN_AUDIENCE, ROOM_TOKEN_ISSUER, } from './token.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { EduskitError, type EduskitSource } from './errors.js';
|
|
2
|
+
export declare const EDU_TOKEN_ISSUER = "eduskit-edu-auth";
|
|
3
|
+
export declare const EDU_TOKEN_AUDIENCE = "eduskit-edu";
|
|
4
|
+
export declare const ROOM_TOKEN_ISSUER = "eduskit";
|
|
5
|
+
export declare const ROOM_TOKEN_AUDIENCE = "eduskit-room";
|
|
6
|
+
export type LocalTokenSigner = {
|
|
7
|
+
appId: string;
|
|
8
|
+
appSecret: string;
|
|
9
|
+
source: EduskitSource;
|
|
10
|
+
};
|
|
11
|
+
export declare function signHs256(signer: LocalTokenSigner, claims: Record<string, unknown>, options: {
|
|
12
|
+
issuer: string;
|
|
13
|
+
audience: string;
|
|
14
|
+
subject: string;
|
|
15
|
+
expiresIn: number;
|
|
16
|
+
}): {
|
|
17
|
+
token: string;
|
|
18
|
+
issuedAt: number;
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
};
|
|
21
|
+
export declare function requireText(value: unknown, field: string, source: EduskitSource): string;
|
|
22
|
+
export declare function sdkTokenError(message: string, source: EduskitSource): EduskitError;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import { EduskitError } from './errors.js';
|
|
3
|
+
export const EDU_TOKEN_ISSUER = 'eduskit-edu-auth';
|
|
4
|
+
export const EDU_TOKEN_AUDIENCE = 'eduskit-edu';
|
|
5
|
+
export const ROOM_TOKEN_ISSUER = 'eduskit';
|
|
6
|
+
export const ROOM_TOKEN_AUDIENCE = 'eduskit-room';
|
|
7
|
+
export function signHs256(signer, claims, options) {
|
|
8
|
+
requireText(signer.appId, 'appId', signer.source);
|
|
9
|
+
requireText(signer.appSecret, 'appSecret', signer.source);
|
|
10
|
+
requireText(options.subject, 'subject', signer.source);
|
|
11
|
+
if (!Number.isInteger(options.expiresIn) || options.expiresIn < 60 || options.expiresIn > 604_800) {
|
|
12
|
+
throw sdkTokenError('expiresIn must be an integer between 60 and 604800', signer.source);
|
|
13
|
+
}
|
|
14
|
+
const issuedAt = Math.floor(Date.now() / 1000);
|
|
15
|
+
const expiresAt = issuedAt + options.expiresIn;
|
|
16
|
+
const header = encode({ alg: 'HS256', typ: 'JWT' });
|
|
17
|
+
const payload = encode({
|
|
18
|
+
...claims,
|
|
19
|
+
iss: options.issuer,
|
|
20
|
+
aud: options.audience,
|
|
21
|
+
sub: options.subject,
|
|
22
|
+
iat: issuedAt,
|
|
23
|
+
exp: expiresAt,
|
|
24
|
+
});
|
|
25
|
+
const content = `${header}.${payload}`;
|
|
26
|
+
const signature = createHmac('sha256', signer.appSecret)
|
|
27
|
+
.update(content)
|
|
28
|
+
.digest('base64url');
|
|
29
|
+
return { token: `${content}.${signature}`, issuedAt, expiresAt };
|
|
30
|
+
}
|
|
31
|
+
export function requireText(value, field, source) {
|
|
32
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
33
|
+
throw sdkTokenError(`${field} is required`, source);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
export function sdkTokenError(message, source) {
|
|
38
|
+
return new EduskitError({ message, errorCode: 'SDK_TOKEN_INPUT_INVALID', source });
|
|
39
|
+
}
|
|
40
|
+
function encode(value) {
|
|
41
|
+
return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');
|
|
42
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { SideClientOptions } from '../config.js';
|
|
2
|
+
import { HttpTransport } from '../http.js';
|
|
3
|
+
import { type LocalTokenSigner } from '../token.js';
|
|
4
|
+
import type { Capture, ConvertJob, CreateCaptureInput, CreateConvertJobInput, EnqueueVideoExportInput, IssueRoomTokenInput, MediaAsset, RecordingSession, RegisterMediaAssetInput, RoomToken, StartRecordingInput, StopRecordingInput, VideoExportJob, AttachRoomCoursewareInput, AttachedRoomCourseware } from './types.js';
|
|
5
|
+
export declare class WhiteboardClient {
|
|
6
|
+
readonly auth: WhiteboardAuthApi;
|
|
7
|
+
readonly recordings: WhiteboardRecordingsApi;
|
|
8
|
+
readonly captures: WhiteboardCapturesApi;
|
|
9
|
+
readonly files: WhiteboardFilesApi;
|
|
10
|
+
readonly rooms: WhiteboardRoomsApi;
|
|
11
|
+
private readonly http;
|
|
12
|
+
constructor(options: SideClientOptions);
|
|
13
|
+
get lastTraceId(): string;
|
|
14
|
+
}
|
|
15
|
+
declare class WhiteboardRoomsApi {
|
|
16
|
+
private readonly http;
|
|
17
|
+
constructor(http: HttpTransport);
|
|
18
|
+
attachCourseware(roomId: string, input: AttachRoomCoursewareInput): Promise<AttachedRoomCourseware>;
|
|
19
|
+
}
|
|
20
|
+
declare class WhiteboardAuthApi {
|
|
21
|
+
private readonly signer;
|
|
22
|
+
constructor(signer: LocalTokenSigner);
|
|
23
|
+
issueRoomToken(input: IssueRoomTokenInput): Promise<RoomToken>;
|
|
24
|
+
}
|
|
25
|
+
declare class WhiteboardRecordingsApi {
|
|
26
|
+
private readonly http;
|
|
27
|
+
constructor(http: HttpTransport);
|
|
28
|
+
start(roomId: string, input?: StartRecordingInput): Promise<RecordingSession>;
|
|
29
|
+
stop(roomId: string, input: StopRecordingInput): Promise<RecordingSession>;
|
|
30
|
+
list(roomId: string): Promise<RecordingSession[]>;
|
|
31
|
+
get(recordingId: string): Promise<RecordingSession>;
|
|
32
|
+
registerMediaAsset(recordingId: string, input: RegisterMediaAssetInput): Promise<MediaAsset>;
|
|
33
|
+
deleteMediaAsset(recordingId: string, assetId: string): Promise<MediaAsset>;
|
|
34
|
+
enqueueVideoExport(recordingId: string, input?: EnqueueVideoExportInput): Promise<VideoExportJob>;
|
|
35
|
+
getVideoExport(recordingId: string, jobId: string): Promise<VideoExportJob>;
|
|
36
|
+
}
|
|
37
|
+
declare class WhiteboardCapturesApi {
|
|
38
|
+
private readonly http;
|
|
39
|
+
constructor(http: HttpTransport);
|
|
40
|
+
create(roomId: string, input: CreateCaptureInput): Promise<Capture>;
|
|
41
|
+
list(roomId: string): Promise<Capture[]>;
|
|
42
|
+
}
|
|
43
|
+
declare class WhiteboardFilesApi {
|
|
44
|
+
private readonly http;
|
|
45
|
+
constructor(http: HttpTransport);
|
|
46
|
+
convert(input: CreateConvertJobInput): Promise<ConvertJob>;
|
|
47
|
+
getConvertJob(jobId: string): Promise<ConvertJob>;
|
|
48
|
+
}
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { encodePath, HttpTransport } from '../http.js';
|
|
2
|
+
import { ROOM_TOKEN_AUDIENCE, ROOM_TOKEN_ISSUER, requireText, sdkTokenError, signHs256, } from '../token.js';
|
|
3
|
+
export class WhiteboardClient {
|
|
4
|
+
auth;
|
|
5
|
+
recordings;
|
|
6
|
+
captures;
|
|
7
|
+
files;
|
|
8
|
+
rooms;
|
|
9
|
+
http;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.http = new HttpTransport({ ...options, source: 'whiteboard' });
|
|
12
|
+
this.auth = new WhiteboardAuthApi({
|
|
13
|
+
appId: options.appId,
|
|
14
|
+
appSecret: options.appSecret,
|
|
15
|
+
source: 'whiteboard',
|
|
16
|
+
});
|
|
17
|
+
this.recordings = new WhiteboardRecordingsApi(this.http);
|
|
18
|
+
this.captures = new WhiteboardCapturesApi(this.http);
|
|
19
|
+
this.files = new WhiteboardFilesApi(this.http);
|
|
20
|
+
this.rooms = new WhiteboardRoomsApi(this.http);
|
|
21
|
+
}
|
|
22
|
+
get lastTraceId() {
|
|
23
|
+
return this.http.lastTraceId;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class WhiteboardRoomsApi {
|
|
27
|
+
http;
|
|
28
|
+
constructor(http) {
|
|
29
|
+
this.http = http;
|
|
30
|
+
}
|
|
31
|
+
attachCourseware(roomId, input) {
|
|
32
|
+
return this.http.request('POST', `/v1/rooms/${encodePath(roomId)}/coursewares`, input);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
class WhiteboardAuthApi {
|
|
36
|
+
signer;
|
|
37
|
+
constructor(signer) {
|
|
38
|
+
this.signer = signer;
|
|
39
|
+
}
|
|
40
|
+
async issueRoomToken(input) {
|
|
41
|
+
const roomId = requireText(input.roomId, 'roomId', 'whiteboard');
|
|
42
|
+
const userId = requireText(input.userId, 'userId', 'whiteboard');
|
|
43
|
+
if (!['host', 'participant', 'observer'].includes(input.role)) {
|
|
44
|
+
throw sdkTokenError('role must be host, participant, or observer', 'whiteboard');
|
|
45
|
+
}
|
|
46
|
+
const expiresIn = input.expiresIn ?? 3600;
|
|
47
|
+
const signed = signHs256(this.signer, {
|
|
48
|
+
app_id: this.signer.appId,
|
|
49
|
+
room_id: roomId,
|
|
50
|
+
role: input.role,
|
|
51
|
+
source: 'server_sdk',
|
|
52
|
+
}, {
|
|
53
|
+
issuer: ROOM_TOKEN_ISSUER,
|
|
54
|
+
audience: ROOM_TOKEN_AUDIENCE,
|
|
55
|
+
subject: userId,
|
|
56
|
+
expiresIn,
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
token: signed.token,
|
|
60
|
+
appId: this.signer.appId,
|
|
61
|
+
roomId,
|
|
62
|
+
userId,
|
|
63
|
+
role: input.role,
|
|
64
|
+
expiresIn,
|
|
65
|
+
expiresAt: new Date(signed.expiresAt * 1000).toISOString(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
class WhiteboardRecordingsApi {
|
|
70
|
+
http;
|
|
71
|
+
constructor(http) {
|
|
72
|
+
this.http = http;
|
|
73
|
+
}
|
|
74
|
+
start(roomId, input = {}) {
|
|
75
|
+
return this.http.request('POST', `/v1/rooms/${encodePath(roomId)}/recording/start`, input);
|
|
76
|
+
}
|
|
77
|
+
stop(roomId, input) {
|
|
78
|
+
return this.http.request('POST', `/v1/rooms/${encodePath(roomId)}/recording/stop`, input);
|
|
79
|
+
}
|
|
80
|
+
list(roomId) {
|
|
81
|
+
return this.http.request('GET', `/v1/rooms/${encodePath(roomId)}/recordings`);
|
|
82
|
+
}
|
|
83
|
+
get(recordingId) {
|
|
84
|
+
return this.http.request('GET', `/v1/recordings/${encodePath(recordingId)}`);
|
|
85
|
+
}
|
|
86
|
+
registerMediaAsset(recordingId, input) {
|
|
87
|
+
return this.http.request('POST', `/v1/recordings/${encodePath(recordingId)}/media-assets`, input);
|
|
88
|
+
}
|
|
89
|
+
deleteMediaAsset(recordingId, assetId) {
|
|
90
|
+
return this.http.request('DELETE', `/v1/recordings/${encodePath(recordingId)}/media-assets/${encodePath(assetId)}`);
|
|
91
|
+
}
|
|
92
|
+
enqueueVideoExport(recordingId, input = {}) {
|
|
93
|
+
return this.http.request('POST', `/v1/recordings/${encodePath(recordingId)}/video-exports`, input);
|
|
94
|
+
}
|
|
95
|
+
getVideoExport(recordingId, jobId) {
|
|
96
|
+
return this.http.request('GET', `/v1/recordings/${encodePath(recordingId)}/video-exports/${encodePath(jobId)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
class WhiteboardCapturesApi {
|
|
100
|
+
http;
|
|
101
|
+
constructor(http) {
|
|
102
|
+
this.http = http;
|
|
103
|
+
}
|
|
104
|
+
create(roomId, input) {
|
|
105
|
+
return this.http.request('POST', `/v1/rooms/${encodePath(roomId)}/captures`, { roomId, ...input });
|
|
106
|
+
}
|
|
107
|
+
list(roomId) {
|
|
108
|
+
return this.http.request('GET', `/v1/rooms/${encodePath(roomId)}/captures`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
class WhiteboardFilesApi {
|
|
112
|
+
http;
|
|
113
|
+
constructor(http) {
|
|
114
|
+
this.http = http;
|
|
115
|
+
}
|
|
116
|
+
convert(input) {
|
|
117
|
+
return this.http.request('POST', '/v1/files/convert', input);
|
|
118
|
+
}
|
|
119
|
+
getConvertJob(jobId) {
|
|
120
|
+
return this.http.request('GET', `/v1/files/convert/${encodePath(jobId)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export type RoomRole = 'host' | 'participant' | 'observer';
|
|
2
|
+
export type IssueRoomTokenInput = {
|
|
3
|
+
roomId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
role: RoomRole;
|
|
6
|
+
expiresIn?: number;
|
|
7
|
+
};
|
|
8
|
+
export type RoomToken = {
|
|
9
|
+
token: string;
|
|
10
|
+
roomId: string;
|
|
11
|
+
userId: string;
|
|
12
|
+
role: string;
|
|
13
|
+
expiresIn: number;
|
|
14
|
+
expiresAt: string;
|
|
15
|
+
appId: string;
|
|
16
|
+
};
|
|
17
|
+
export type StartRecordingInput = {
|
|
18
|
+
externalRef?: string;
|
|
19
|
+
};
|
|
20
|
+
export type StopRecordingInput = {
|
|
21
|
+
recordingId: string;
|
|
22
|
+
};
|
|
23
|
+
export type MediaAsset = {
|
|
24
|
+
assetId: string;
|
|
25
|
+
recordingId: string;
|
|
26
|
+
kind: string;
|
|
27
|
+
url: string;
|
|
28
|
+
contentType?: string;
|
|
29
|
+
durationMs?: number;
|
|
30
|
+
metadata?: Record<string, unknown>;
|
|
31
|
+
metadataJson?: string;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
};
|
|
34
|
+
export type RecordingSession = {
|
|
35
|
+
recordingId: string;
|
|
36
|
+
appId: string;
|
|
37
|
+
roomId: string;
|
|
38
|
+
status: string;
|
|
39
|
+
externalRef?: string;
|
|
40
|
+
wallStart?: string;
|
|
41
|
+
wallEnd?: string;
|
|
42
|
+
startServerSeq?: number | string;
|
|
43
|
+
endServerSeq?: number | string;
|
|
44
|
+
checkpointSeqAtStart?: number | string;
|
|
45
|
+
bundleObjectKey?: string;
|
|
46
|
+
bundleChecksumSha256?: string;
|
|
47
|
+
bundleSizeBytes?: number | string;
|
|
48
|
+
bundleReadyAt?: string;
|
|
49
|
+
startedAt?: string;
|
|
50
|
+
stoppedAt?: string;
|
|
51
|
+
mediaAssets?: MediaAsset[];
|
|
52
|
+
};
|
|
53
|
+
export type RegisterMediaAssetInput = {
|
|
54
|
+
kind: string;
|
|
55
|
+
url: string;
|
|
56
|
+
contentType?: string;
|
|
57
|
+
durationMs?: number;
|
|
58
|
+
metadata?: Record<string, unknown>;
|
|
59
|
+
};
|
|
60
|
+
export type EnqueueVideoExportInput = {
|
|
61
|
+
profile?: 'standard' | 'hd';
|
|
62
|
+
sampleMode?: 'change' | 'fixed';
|
|
63
|
+
fps?: number;
|
|
64
|
+
width?: number;
|
|
65
|
+
height?: number;
|
|
66
|
+
};
|
|
67
|
+
export type VideoExportJob = {
|
|
68
|
+
jobId: string;
|
|
69
|
+
recordingId: string;
|
|
70
|
+
appId: string;
|
|
71
|
+
roomId: string;
|
|
72
|
+
status: string;
|
|
73
|
+
fromSeq?: number | string;
|
|
74
|
+
toSeq?: number | string;
|
|
75
|
+
wallStart?: string;
|
|
76
|
+
wallEnd?: string;
|
|
77
|
+
profile?: string;
|
|
78
|
+
sampleMode?: string;
|
|
79
|
+
fps?: number;
|
|
80
|
+
width?: number;
|
|
81
|
+
height?: number;
|
|
82
|
+
assetId?: string;
|
|
83
|
+
publicUrl?: string;
|
|
84
|
+
objectKey?: string;
|
|
85
|
+
lastError?: string;
|
|
86
|
+
createdAt?: string;
|
|
87
|
+
finishedAt?: string;
|
|
88
|
+
};
|
|
89
|
+
export type CreateCaptureInput = {
|
|
90
|
+
pageId: string;
|
|
91
|
+
url?: string;
|
|
92
|
+
};
|
|
93
|
+
export type Capture = {
|
|
94
|
+
captureId: string;
|
|
95
|
+
appId: string;
|
|
96
|
+
roomId: string;
|
|
97
|
+
pageId: string;
|
|
98
|
+
url: string;
|
|
99
|
+
createdAt: string;
|
|
100
|
+
};
|
|
101
|
+
export type CreateConvertJobInput = {
|
|
102
|
+
sourceUrl?: string;
|
|
103
|
+
fileName?: string;
|
|
104
|
+
objectKey?: string;
|
|
105
|
+
clientReference?: string;
|
|
106
|
+
};
|
|
107
|
+
export type ConvertJob = {
|
|
108
|
+
jobId: string;
|
|
109
|
+
appId: string;
|
|
110
|
+
sourceUrl?: string;
|
|
111
|
+
fileName?: string;
|
|
112
|
+
status: string;
|
|
113
|
+
progress: number;
|
|
114
|
+
stage?: string;
|
|
115
|
+
errorCode?: string;
|
|
116
|
+
errorMessage?: string;
|
|
117
|
+
resultManifestUrl?: string;
|
|
118
|
+
coursewareBaseUrl?: string;
|
|
119
|
+
resultManifestObjectKey?: string;
|
|
120
|
+
coursewareObjectPrefix?: string;
|
|
121
|
+
coursewareId?: string;
|
|
122
|
+
coursewareVersion?: string;
|
|
123
|
+
clientReference?: string;
|
|
124
|
+
createdAt: string;
|
|
125
|
+
updatedAt: string;
|
|
126
|
+
};
|
|
127
|
+
export type AttachRoomCoursewareInput = {
|
|
128
|
+
coursewareId: string;
|
|
129
|
+
convertJobId: string;
|
|
130
|
+
requestId: string;
|
|
131
|
+
};
|
|
132
|
+
export type AttachedRoomCourseware = {
|
|
133
|
+
coursewareId: string;
|
|
134
|
+
whiteboardFileId: string;
|
|
135
|
+
opId: string;
|
|
136
|
+
serverSeq: number;
|
|
137
|
+
serverReceivedAt: number;
|
|
138
|
+
pageCount: number;
|
|
139
|
+
duplicated: boolean;
|
|
140
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eduskit/server-sdk",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Eduskit B-end server SDK for classroom and whiteboard APIs",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/eduskit/server-sdk-node.git"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/src/index.js",
|
|
14
|
+
"types": "./dist/src/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/src/index.d.ts",
|
|
18
|
+
"import": "./dist/src/index.js",
|
|
19
|
+
"default": "./dist/src/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": ["dist/src"],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"test": "tsc && node --test dist/test/*.test.js"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"license": "UNLICENSED",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.13.0",
|
|
33
|
+
"typescript": "^5.7.3"
|
|
34
|
+
}
|
|
35
|
+
}
|