@appthen/sdk-node 0.0.0

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 ADDED
@@ -0,0 +1,48 @@
1
+ # @appthen/chat-hub-node
2
+
3
+ Node.js 服务端集成方适配包。
4
+
5
+ 它复用 `@appthen/chat-hub-core` 的业务 API,并提供:
6
+
7
+ - Node `fetch` HTTP adapter
8
+ - 内存 token provider
9
+ - AK/SK 自动换取并缓存 tenant app token
10
+ - `createChatHubNodeClient`
11
+ - `createChatHubNodeRuntime`
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createChatHubNodeClient } from '@appthen/chat-hub-node';
17
+
18
+ const client = createChatHubNodeClient({
19
+ baseUrl: 'http://127.0.0.1:1626',
20
+ tenantApp: {
21
+ accessKey: process.env.APPTHEN_ACCESS_KEY!,
22
+ accessSecret: process.env.APPTHEN_ACCESS_SECRET!,
23
+ actingMode: 'subject',
24
+ actingSubjectId: 'subject_customer_1',
25
+ requestedScopes: ['conversation.write'],
26
+ },
27
+ });
28
+
29
+ await client.conversationTypes.create({
30
+ typeId: 'support_room',
31
+ label: 'Support Room',
32
+ status: 'active',
33
+ });
34
+ ```
35
+
36
+ ## Verification
37
+
38
+ ```bash
39
+ pnpm --filter @appthen/chat-hub-node test
40
+ pnpm --filter @appthen/chat-hub-node typecheck
41
+ ```
42
+
43
+ 当前 smoke tests 覆盖:
44
+
45
+ - `NodeFetchHttpAdapter` 的 base URL、query、headers、JSON body 拼接。
46
+ - HTTP 非 2xx 时抛错并保留响应 payload。
47
+ - `TenantAppAuthProvider` 通过 AK/SK 换取 tenant app token,并在过期前复用缓存。
48
+ - `createChatHubNodeClient()` 能把 tenant app auth provider 接入 `ChatHubClient`。
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @appthen/sdk-node
3
+ *
4
+ * Node.js server adapter for AppThen Conversation Hub SDK.
5
+ */
6
+ import { ChatHubAppRuntime, ChatHubClient, type AuthProvider, type ChatHubAppConfig, type HttpAdapter, type HttpRequestOptions, type HttpResponse, type IssueAppTokenInput } from '@appthen/sdk-core';
7
+ export { SDK_VERSION } from '@appthen/sdk-core';
8
+ export * from '@appthen/sdk-core';
9
+ export interface NodeHttpAdapterOptions {
10
+ baseUrl: string;
11
+ fetch?: typeof fetch;
12
+ defaultHeaders?: Record<string, string>;
13
+ }
14
+ export interface NodeTenantAppAuthOptions extends IssueAppTokenInput {
15
+ refreshSkewMs?: number;
16
+ }
17
+ export interface CreateChatHubNodeClientOptions {
18
+ baseUrl: string;
19
+ token?: string | null;
20
+ tenantApp?: NodeTenantAppAuthOptions;
21
+ fetch?: typeof fetch;
22
+ defaultHeaders?: Record<string, string>;
23
+ }
24
+ export interface CreateChatHubNodeRuntimeOptions extends CreateChatHubNodeClientOptions {
25
+ appConfig: ChatHubAppConfig;
26
+ }
27
+ export declare class NodeFetchHttpAdapter implements HttpAdapter {
28
+ private readonly fetchImpl;
29
+ private readonly baseUrl;
30
+ private readonly defaultHeaders;
31
+ constructor(options: NodeHttpAdapterOptions);
32
+ request<T>(options: HttpRequestOptions): Promise<HttpResponse<T>>;
33
+ }
34
+ export declare class NodeMemoryAuthProvider implements AuthProvider {
35
+ private token;
36
+ constructor(token?: string | null);
37
+ getToken(): Promise<string | null>;
38
+ setToken(token: string | null): Promise<void>;
39
+ }
40
+ export declare class TenantAppAuthProvider implements AuthProvider {
41
+ private readonly http;
42
+ private readonly input;
43
+ private token;
44
+ private expiresAt;
45
+ private refreshPromise;
46
+ constructor(http: HttpAdapter, input: NodeTenantAppAuthOptions);
47
+ getToken(): Promise<string | null>;
48
+ setToken(token: string | null): Promise<void>;
49
+ refreshToken(): Promise<string | null>;
50
+ private get refreshSkewMs();
51
+ }
52
+ export declare function createChatHubNodeClient(options: CreateChatHubNodeClientOptions): ChatHubClient;
53
+ export declare function createChatHubNodeRuntime(options: CreateChatHubNodeRuntimeOptions): ChatHubAppRuntime;
54
+ import { PlatformApi, type PlatformProvisionTenantInput, type PlatformProvisionTenantResult, type PlatformResolveTenantResult, type PlatformSyncSubjectInput, type PlatformSyncSubjectResult } from '@appthen/sdk-core';
55
+ export { PlatformApi, type PlatformProvisionTenantInput, type PlatformProvisionTenantResult, type PlatformResolveTenantResult, type PlatformSyncSubjectInput, type PlatformSyncSubjectResult, } from '@appthen/sdk-core';
56
+ export interface CreateAppThenSdkOptions {
57
+ baseUrl: string;
58
+ accessKey: string;
59
+ accessSecret: string;
60
+ /** 缺省给全部 allowedScopes;建议显式声明所需 platform:* scope。 */
61
+ requestedScopes?: string[];
62
+ refreshSkewMs?: number;
63
+ fetch?: typeof fetch;
64
+ defaultHeaders?: Record<string, string>;
65
+ }
66
+ /** 单个被授权租户的操作客户端:调用自动携带 targetTenantId。 */
67
+ export declare class PlatformTenantClient {
68
+ private readonly platform;
69
+ readonly tenantId: string;
70
+ constructor(platform: PlatformApi, tenantId: string);
71
+ /** 幂等代同步成员:重复推送只产生一个 Subject,可安全重试。 */
72
+ syncSubject(input: PlatformSyncSubjectInput): Promise<PlatformSyncSubjectResult>;
73
+ }
74
+ export declare class AppThenPlatformSdk {
75
+ private readonly http;
76
+ readonly platform: PlatformApi;
77
+ constructor(platform: PlatformApi, http: NodeFetchHttpAdapter);
78
+ /** 代开通租户(目标租户尚不存在,故在 sdk 根上而非 tenant client)。 */
79
+ provisionTenant(input: PlatformProvisionTenantInput): Promise<PlatformProvisionTenantResult>;
80
+ /** 对账:externalCorrelationId → tenantId + grants。未命中抛 CORRELATION_MISMATCH。 */
81
+ resolveTenant(externalCorrelationId: string): Promise<PlatformResolveTenantResult>;
82
+ /** 启停授权:active ↔ revoked。 */
83
+ toggleGrant(grantId: string, status: 'active' | 'revoked'): Promise<Record<string, unknown>>;
84
+ /**
85
+ * 认领激活:管理员凭 claimToken 首登认领(租户 pending → active,grant → active)。
86
+ * 走**用户 JWT**(非 platform token)——与微信"公众号管理员扫码授权"同构的信任锚。
87
+ */
88
+ claimTenant(tenantId: string, claimToken: string, userToken: string): Promise<Record<string, unknown>>;
89
+ }
90
+ export declare function createAppThenSdk(options: CreateAppThenSdkOptions): AppThenPlatformSdk;
package/dist/index.js ADDED
@@ -0,0 +1,229 @@
1
+ /**
2
+ * @appthen/sdk-node
3
+ *
4
+ * Node.js server adapter for AppThen Conversation Hub SDK.
5
+ */
6
+ import { ChatHubAppRuntime, ChatHubClient, } from '@appthen/sdk-core';
7
+ export { SDK_VERSION } from '@appthen/sdk-core';
8
+ export * from '@appthen/sdk-core';
9
+ function joinUrl(baseUrl, path) {
10
+ if (/^https?:\/\//.test(path)) {
11
+ return path;
12
+ }
13
+ const normalizedBase = baseUrl.replace(/\/+$/, '');
14
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
15
+ return `${normalizedBase}${normalizedPath}`;
16
+ }
17
+ function appendQuery(url, query) {
18
+ if (!query) {
19
+ return url;
20
+ }
21
+ const params = new URLSearchParams();
22
+ Object.entries(query).forEach(([key, value]) => {
23
+ if (value === undefined) {
24
+ return;
25
+ }
26
+ params.set(key, String(value));
27
+ });
28
+ const serialized = params.toString();
29
+ return serialized ? `${url}?${serialized}` : url;
30
+ }
31
+ function isJsonResponse(contentType) {
32
+ return typeof contentType === 'string' && contentType.includes('application/json');
33
+ }
34
+ function normalizeIssueTokenResult(result) {
35
+ const accessToken = result
36
+ .accessToken;
37
+ return result.token || accessToken || null;
38
+ }
39
+ export class NodeFetchHttpAdapter {
40
+ constructor(options) {
41
+ if (!options.fetch && typeof globalThis.fetch !== 'function') {
42
+ throw new Error('NodeFetchHttpAdapter requires global fetch or an explicit fetch implementation');
43
+ }
44
+ this.fetchImpl = options.fetch || globalThis.fetch.bind(globalThis);
45
+ this.baseUrl = options.baseUrl;
46
+ this.defaultHeaders = options.defaultHeaders || {};
47
+ }
48
+ async request(options) {
49
+ const url = appendQuery(joinUrl(this.baseUrl, options.path), options.query);
50
+ const response = await this.fetchImpl(url, {
51
+ method: options.method,
52
+ headers: {
53
+ ...this.defaultHeaders,
54
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
55
+ ...(options.headers || {}),
56
+ },
57
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
58
+ });
59
+ const contentType = response.headers.get('content-type');
60
+ const data = isJsonResponse(contentType)
61
+ ? (await response.json())
62
+ : (await response.text());
63
+ if (!response.ok) {
64
+ const error = new Error(`HTTP ${response.status} for ${options.method} ${options.path}`);
65
+ error.response = data;
66
+ throw error;
67
+ }
68
+ const headers = {};
69
+ response.headers.forEach((value, key) => {
70
+ headers[key] = value;
71
+ });
72
+ return {
73
+ status: response.status,
74
+ data,
75
+ headers,
76
+ };
77
+ }
78
+ }
79
+ export class NodeMemoryAuthProvider {
80
+ constructor(token = null) {
81
+ this.token = token;
82
+ }
83
+ async getToken() {
84
+ return this.token;
85
+ }
86
+ async setToken(token) {
87
+ this.token = token;
88
+ }
89
+ }
90
+ export class TenantAppAuthProvider {
91
+ constructor(http, input) {
92
+ this.http = http;
93
+ this.input = input;
94
+ this.token = null;
95
+ this.expiresAt = 0;
96
+ this.refreshPromise = null;
97
+ }
98
+ async getToken() {
99
+ if (this.token && Date.now() < this.expiresAt - this.refreshSkewMs) {
100
+ return this.token;
101
+ }
102
+ return this.refreshToken();
103
+ }
104
+ async setToken(token) {
105
+ this.token = token;
106
+ this.expiresAt = token ? Number.MAX_SAFE_INTEGER : 0;
107
+ }
108
+ async refreshToken() {
109
+ if (this.refreshPromise) {
110
+ return this.refreshPromise;
111
+ }
112
+ this.refreshPromise = (async () => {
113
+ const response = await this.http.request({
114
+ method: 'POST',
115
+ path: '/open-platform/token',
116
+ body: {
117
+ accessKey: this.input.accessKey,
118
+ accessSecret: this.input.accessSecret,
119
+ actingSubjectId: this.input.actingSubjectId,
120
+ actingMode: this.input.actingMode,
121
+ requestedScopes: this.input.requestedScopes,
122
+ },
123
+ });
124
+ const token = normalizeIssueTokenResult(response.data);
125
+ this.token = token;
126
+ this.expiresAt = token
127
+ ? Date.now() + ((response.data.expiresIn || 7200) * 1000)
128
+ : 0;
129
+ return token;
130
+ })();
131
+ try {
132
+ return await this.refreshPromise;
133
+ }
134
+ finally {
135
+ this.refreshPromise = null;
136
+ }
137
+ }
138
+ get refreshSkewMs() {
139
+ return this.input.refreshSkewMs ?? 60000;
140
+ }
141
+ }
142
+ export function createChatHubNodeClient(options) {
143
+ const http = new NodeFetchHttpAdapter({
144
+ baseUrl: options.baseUrl,
145
+ fetch: options.fetch,
146
+ defaultHeaders: options.defaultHeaders,
147
+ });
148
+ const auth = options.tenantApp
149
+ ? new TenantAppAuthProvider(http, options.tenantApp)
150
+ : new NodeMemoryAuthProvider(options.token || null);
151
+ return new ChatHubClient({
152
+ http,
153
+ auth,
154
+ });
155
+ }
156
+ export function createChatHubNodeRuntime(options) {
157
+ return new ChatHubAppRuntime(createChatHubNodeClient(options), options.appConfig);
158
+ }
159
+ // ── Platform(服务商 / 代运营)SDK ──
160
+ //
161
+ // 面向 LIMS 等 SaaS 后端:持 AK/SK(accessSecret 永不出后端),
162
+ // token 2h 生命周期自动刷新,按租户组织调用(对齐微信第三方平台的心智):
163
+ //
164
+ // const sdk = createAppThenSdk({ baseUrl, accessKey, accessSecret });
165
+ // const provisioned = await sdk.provisionTenant({ name, externalCorrelationId });
166
+ // const lab = sdk.platformTenant(provisioned.tenantId);
167
+ // await lab.syncSubject({ classId, displayName, externalIdentity });
168
+ // await sdk.resolveTenant('lims_org_10086'); // 对账
169
+ import { PlatformApi, } from '@appthen/sdk-core';
170
+ export { PlatformApi, } from '@appthen/sdk-core';
171
+ /** 单个被授权租户的操作客户端:调用自动携带 targetTenantId。 */
172
+ export class PlatformTenantClient {
173
+ constructor(platform, tenantId) {
174
+ this.platform = platform;
175
+ this.tenantId = tenantId;
176
+ }
177
+ /** 幂等代同步成员:重复推送只产生一个 Subject,可安全重试。 */
178
+ async syncSubject(input) {
179
+ return this.platform.syncSubject(this.tenantId, input);
180
+ }
181
+ }
182
+ export class AppThenPlatformSdk {
183
+ constructor(platform, http) {
184
+ this.http = http;
185
+ this.platform = platform;
186
+ }
187
+ /** 代开通租户(目标租户尚不存在,故在 sdk 根上而非 tenant client)。 */
188
+ async provisionTenant(input) {
189
+ return this.platform.provisionTenant(input);
190
+ }
191
+ /** 对账:externalCorrelationId → tenantId + grants。未命中抛 CORRELATION_MISMATCH。 */
192
+ async resolveTenant(externalCorrelationId) {
193
+ return this.platform.resolveTenant(externalCorrelationId);
194
+ }
195
+ /** 启停授权:active ↔ revoked。 */
196
+ async toggleGrant(grantId, status) {
197
+ return this.platform.toggleGrant(grantId, status);
198
+ }
199
+ /**
200
+ * 认领激活:管理员凭 claimToken 首登认领(租户 pending → active,grant → active)。
201
+ * 走**用户 JWT**(非 platform token)——与微信"公众号管理员扫码授权"同构的信任锚。
202
+ */
203
+ async claimTenant(tenantId, claimToken, userToken) {
204
+ const response = await this.http.request({
205
+ method: 'POST',
206
+ path: `/open-platform/platform/tenants/${encodeURIComponent(tenantId)}/claim`,
207
+ body: { claimToken },
208
+ headers: { Authorization: `Bearer ${userToken}` },
209
+ });
210
+ return response.data;
211
+ }
212
+ }
213
+ export function createAppThenSdk(options) {
214
+ const http = new NodeFetchHttpAdapter({
215
+ baseUrl: options.baseUrl,
216
+ fetch: options.fetch,
217
+ defaultHeaders: options.defaultHeaders,
218
+ });
219
+ // 复用 TenantAppAuthProvider:actingMode='platform' 换 platform token,
220
+ // 2h 生命周期 + 60s skew + 并发刷新去重全部继承。
221
+ const auth = new TenantAppAuthProvider(http, {
222
+ accessKey: options.accessKey,
223
+ accessSecret: options.accessSecret,
224
+ actingMode: 'platform',
225
+ requestedScopes: options.requestedScopes,
226
+ refreshSkewMs: options.refreshSkewMs,
227
+ });
228
+ return new AppThenPlatformSdk(new PlatformApi(http, auth), http);
229
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @appthen/sdk-node
3
+ *
4
+ * Node.js server adapter for AppThen Conversation Hub SDK.
5
+ */
6
+ import { ChatHubAppRuntime, ChatHubClient, type AuthProvider, type ChatHubAppConfig, type HttpAdapter, type HttpRequestOptions, type HttpResponse, type IssueAppTokenInput } from '@appthen/sdk-core';
7
+ export { SDK_VERSION } from '@appthen/sdk-core';
8
+ export * from '@appthen/sdk-core';
9
+ export interface NodeHttpAdapterOptions {
10
+ baseUrl: string;
11
+ fetch?: typeof fetch;
12
+ defaultHeaders?: Record<string, string>;
13
+ }
14
+ export interface NodeTenantAppAuthOptions extends IssueAppTokenInput {
15
+ refreshSkewMs?: number;
16
+ }
17
+ export interface CreateChatHubNodeClientOptions {
18
+ baseUrl: string;
19
+ token?: string | null;
20
+ tenantApp?: NodeTenantAppAuthOptions;
21
+ fetch?: typeof fetch;
22
+ defaultHeaders?: Record<string, string>;
23
+ }
24
+ export interface CreateChatHubNodeRuntimeOptions extends CreateChatHubNodeClientOptions {
25
+ appConfig: ChatHubAppConfig;
26
+ }
27
+ export declare class NodeFetchHttpAdapter implements HttpAdapter {
28
+ private readonly fetchImpl;
29
+ private readonly baseUrl;
30
+ private readonly defaultHeaders;
31
+ constructor(options: NodeHttpAdapterOptions);
32
+ request<T>(options: HttpRequestOptions): Promise<HttpResponse<T>>;
33
+ }
34
+ export declare class NodeMemoryAuthProvider implements AuthProvider {
35
+ private token;
36
+ constructor(token?: string | null);
37
+ getToken(): Promise<string | null>;
38
+ setToken(token: string | null): Promise<void>;
39
+ }
40
+ export declare class TenantAppAuthProvider implements AuthProvider {
41
+ private readonly http;
42
+ private readonly input;
43
+ private token;
44
+ private expiresAt;
45
+ private refreshPromise;
46
+ constructor(http: HttpAdapter, input: NodeTenantAppAuthOptions);
47
+ getToken(): Promise<string | null>;
48
+ setToken(token: string | null): Promise<void>;
49
+ refreshToken(): Promise<string | null>;
50
+ private get refreshSkewMs();
51
+ }
52
+ export declare function createChatHubNodeClient(options: CreateChatHubNodeClientOptions): ChatHubClient;
53
+ export declare function createChatHubNodeRuntime(options: CreateChatHubNodeRuntimeOptions): ChatHubAppRuntime;
54
+ import { PlatformApi, type PlatformProvisionTenantInput, type PlatformProvisionTenantResult, type PlatformResolveTenantResult, type PlatformSyncSubjectInput, type PlatformSyncSubjectResult } from '@appthen/sdk-core';
55
+ export { PlatformApi, type PlatformProvisionTenantInput, type PlatformProvisionTenantResult, type PlatformResolveTenantResult, type PlatformSyncSubjectInput, type PlatformSyncSubjectResult, } from '@appthen/sdk-core';
56
+ export interface CreateAppThenSdkOptions {
57
+ baseUrl: string;
58
+ accessKey: string;
59
+ accessSecret: string;
60
+ /** 缺省给全部 allowedScopes;建议显式声明所需 platform:* scope。 */
61
+ requestedScopes?: string[];
62
+ refreshSkewMs?: number;
63
+ fetch?: typeof fetch;
64
+ defaultHeaders?: Record<string, string>;
65
+ }
66
+ /** 单个被授权租户的操作客户端:调用自动携带 targetTenantId。 */
67
+ export declare class PlatformTenantClient {
68
+ private readonly platform;
69
+ readonly tenantId: string;
70
+ constructor(platform: PlatformApi, tenantId: string);
71
+ /** 幂等代同步成员:重复推送只产生一个 Subject,可安全重试。 */
72
+ syncSubject(input: PlatformSyncSubjectInput): Promise<PlatformSyncSubjectResult>;
73
+ }
74
+ export declare class AppThenPlatformSdk {
75
+ private readonly http;
76
+ readonly platform: PlatformApi;
77
+ constructor(platform: PlatformApi, http: NodeFetchHttpAdapter);
78
+ /** 代开通租户(目标租户尚不存在,故在 sdk 根上而非 tenant client)。 */
79
+ provisionTenant(input: PlatformProvisionTenantInput): Promise<PlatformProvisionTenantResult>;
80
+ /** 对账:externalCorrelationId → tenantId + grants。未命中抛 CORRELATION_MISMATCH。 */
81
+ resolveTenant(externalCorrelationId: string): Promise<PlatformResolveTenantResult>;
82
+ /** 启停授权:active ↔ revoked。 */
83
+ toggleGrant(grantId: string, status: 'active' | 'revoked'): Promise<Record<string, unknown>>;
84
+ /**
85
+ * 认领激活:管理员凭 claimToken 首登认领(租户 pending → active,grant → active)。
86
+ * 走**用户 JWT**(非 platform token)——与微信"公众号管理员扫码授权"同构的信任锚。
87
+ */
88
+ claimTenant(tenantId: string, claimToken: string, userToken: string): Promise<Record<string, unknown>>;
89
+ }
90
+ export declare function createAppThenSdk(options: CreateAppThenSdkOptions): AppThenPlatformSdk;
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ /**
3
+ * @appthen/sdk-node
4
+ *
5
+ * Node.js server adapter for AppThen Conversation Hub SDK.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
19
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.AppThenPlatformSdk = exports.PlatformTenantClient = exports.PlatformApi = exports.TenantAppAuthProvider = exports.NodeMemoryAuthProvider = exports.NodeFetchHttpAdapter = exports.SDK_VERSION = void 0;
23
+ exports.createChatHubNodeClient = createChatHubNodeClient;
24
+ exports.createChatHubNodeRuntime = createChatHubNodeRuntime;
25
+ exports.createAppThenSdk = createAppThenSdk;
26
+ const sdk_core_1 = require("@appthen/sdk-core");
27
+ var sdk_core_2 = require("@appthen/sdk-core");
28
+ Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return sdk_core_2.SDK_VERSION; } });
29
+ __exportStar(require("@appthen/sdk-core"), exports);
30
+ function joinUrl(baseUrl, path) {
31
+ if (/^https?:\/\//.test(path)) {
32
+ return path;
33
+ }
34
+ const normalizedBase = baseUrl.replace(/\/+$/, '');
35
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
36
+ return `${normalizedBase}${normalizedPath}`;
37
+ }
38
+ function appendQuery(url, query) {
39
+ if (!query) {
40
+ return url;
41
+ }
42
+ const params = new URLSearchParams();
43
+ Object.entries(query).forEach(([key, value]) => {
44
+ if (value === undefined) {
45
+ return;
46
+ }
47
+ params.set(key, String(value));
48
+ });
49
+ const serialized = params.toString();
50
+ return serialized ? `${url}?${serialized}` : url;
51
+ }
52
+ function isJsonResponse(contentType) {
53
+ return typeof contentType === 'string' && contentType.includes('application/json');
54
+ }
55
+ function normalizeIssueTokenResult(result) {
56
+ const accessToken = result
57
+ .accessToken;
58
+ return result.token || accessToken || null;
59
+ }
60
+ class NodeFetchHttpAdapter {
61
+ constructor(options) {
62
+ if (!options.fetch && typeof globalThis.fetch !== 'function') {
63
+ throw new Error('NodeFetchHttpAdapter requires global fetch or an explicit fetch implementation');
64
+ }
65
+ this.fetchImpl = options.fetch || globalThis.fetch.bind(globalThis);
66
+ this.baseUrl = options.baseUrl;
67
+ this.defaultHeaders = options.defaultHeaders || {};
68
+ }
69
+ async request(options) {
70
+ const url = appendQuery(joinUrl(this.baseUrl, options.path), options.query);
71
+ const response = await this.fetchImpl(url, {
72
+ method: options.method,
73
+ headers: {
74
+ ...this.defaultHeaders,
75
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
76
+ ...(options.headers || {}),
77
+ },
78
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
79
+ });
80
+ const contentType = response.headers.get('content-type');
81
+ const data = isJsonResponse(contentType)
82
+ ? (await response.json())
83
+ : (await response.text());
84
+ if (!response.ok) {
85
+ const error = new Error(`HTTP ${response.status} for ${options.method} ${options.path}`);
86
+ error.response = data;
87
+ throw error;
88
+ }
89
+ const headers = {};
90
+ response.headers.forEach((value, key) => {
91
+ headers[key] = value;
92
+ });
93
+ return {
94
+ status: response.status,
95
+ data,
96
+ headers,
97
+ };
98
+ }
99
+ }
100
+ exports.NodeFetchHttpAdapter = NodeFetchHttpAdapter;
101
+ class NodeMemoryAuthProvider {
102
+ constructor(token = null) {
103
+ this.token = token;
104
+ }
105
+ async getToken() {
106
+ return this.token;
107
+ }
108
+ async setToken(token) {
109
+ this.token = token;
110
+ }
111
+ }
112
+ exports.NodeMemoryAuthProvider = NodeMemoryAuthProvider;
113
+ class TenantAppAuthProvider {
114
+ constructor(http, input) {
115
+ this.http = http;
116
+ this.input = input;
117
+ this.token = null;
118
+ this.expiresAt = 0;
119
+ this.refreshPromise = null;
120
+ }
121
+ async getToken() {
122
+ if (this.token && Date.now() < this.expiresAt - this.refreshSkewMs) {
123
+ return this.token;
124
+ }
125
+ return this.refreshToken();
126
+ }
127
+ async setToken(token) {
128
+ this.token = token;
129
+ this.expiresAt = token ? Number.MAX_SAFE_INTEGER : 0;
130
+ }
131
+ async refreshToken() {
132
+ if (this.refreshPromise) {
133
+ return this.refreshPromise;
134
+ }
135
+ this.refreshPromise = (async () => {
136
+ const response = await this.http.request({
137
+ method: 'POST',
138
+ path: '/open-platform/token',
139
+ body: {
140
+ accessKey: this.input.accessKey,
141
+ accessSecret: this.input.accessSecret,
142
+ actingSubjectId: this.input.actingSubjectId,
143
+ actingMode: this.input.actingMode,
144
+ requestedScopes: this.input.requestedScopes,
145
+ },
146
+ });
147
+ const token = normalizeIssueTokenResult(response.data);
148
+ this.token = token;
149
+ this.expiresAt = token
150
+ ? Date.now() + ((response.data.expiresIn || 7200) * 1000)
151
+ : 0;
152
+ return token;
153
+ })();
154
+ try {
155
+ return await this.refreshPromise;
156
+ }
157
+ finally {
158
+ this.refreshPromise = null;
159
+ }
160
+ }
161
+ get refreshSkewMs() {
162
+ return this.input.refreshSkewMs ?? 60000;
163
+ }
164
+ }
165
+ exports.TenantAppAuthProvider = TenantAppAuthProvider;
166
+ function createChatHubNodeClient(options) {
167
+ const http = new NodeFetchHttpAdapter({
168
+ baseUrl: options.baseUrl,
169
+ fetch: options.fetch,
170
+ defaultHeaders: options.defaultHeaders,
171
+ });
172
+ const auth = options.tenantApp
173
+ ? new TenantAppAuthProvider(http, options.tenantApp)
174
+ : new NodeMemoryAuthProvider(options.token || null);
175
+ return new sdk_core_1.ChatHubClient({
176
+ http,
177
+ auth,
178
+ });
179
+ }
180
+ function createChatHubNodeRuntime(options) {
181
+ return new sdk_core_1.ChatHubAppRuntime(createChatHubNodeClient(options), options.appConfig);
182
+ }
183
+ // ── Platform(服务商 / 代运营)SDK ──
184
+ //
185
+ // 面向 LIMS 等 SaaS 后端:持 AK/SK(accessSecret 永不出后端),
186
+ // token 2h 生命周期自动刷新,按租户组织调用(对齐微信第三方平台的心智):
187
+ //
188
+ // const sdk = createAppThenSdk({ baseUrl, accessKey, accessSecret });
189
+ // const provisioned = await sdk.provisionTenant({ name, externalCorrelationId });
190
+ // const lab = sdk.platformTenant(provisioned.tenantId);
191
+ // await lab.syncSubject({ classId, displayName, externalIdentity });
192
+ // await sdk.resolveTenant('lims_org_10086'); // 对账
193
+ const sdk_core_3 = require("@appthen/sdk-core");
194
+ var sdk_core_4 = require("@appthen/sdk-core");
195
+ Object.defineProperty(exports, "PlatformApi", { enumerable: true, get: function () { return sdk_core_4.PlatformApi; } });
196
+ /** 单个被授权租户的操作客户端:调用自动携带 targetTenantId。 */
197
+ class PlatformTenantClient {
198
+ constructor(platform, tenantId) {
199
+ this.platform = platform;
200
+ this.tenantId = tenantId;
201
+ }
202
+ /** 幂等代同步成员:重复推送只产生一个 Subject,可安全重试。 */
203
+ async syncSubject(input) {
204
+ return this.platform.syncSubject(this.tenantId, input);
205
+ }
206
+ }
207
+ exports.PlatformTenantClient = PlatformTenantClient;
208
+ class AppThenPlatformSdk {
209
+ constructor(platform, http) {
210
+ this.http = http;
211
+ this.platform = platform;
212
+ }
213
+ /** 代开通租户(目标租户尚不存在,故在 sdk 根上而非 tenant client)。 */
214
+ async provisionTenant(input) {
215
+ return this.platform.provisionTenant(input);
216
+ }
217
+ /** 对账:externalCorrelationId → tenantId + grants。未命中抛 CORRELATION_MISMATCH。 */
218
+ async resolveTenant(externalCorrelationId) {
219
+ return this.platform.resolveTenant(externalCorrelationId);
220
+ }
221
+ /** 启停授权:active ↔ revoked。 */
222
+ async toggleGrant(grantId, status) {
223
+ return this.platform.toggleGrant(grantId, status);
224
+ }
225
+ /**
226
+ * 认领激活:管理员凭 claimToken 首登认领(租户 pending → active,grant → active)。
227
+ * 走**用户 JWT**(非 platform token)——与微信"公众号管理员扫码授权"同构的信任锚。
228
+ */
229
+ async claimTenant(tenantId, claimToken, userToken) {
230
+ const response = await this.http.request({
231
+ method: 'POST',
232
+ path: `/open-platform/platform/tenants/${encodeURIComponent(tenantId)}/claim`,
233
+ body: { claimToken },
234
+ headers: { Authorization: `Bearer ${userToken}` },
235
+ });
236
+ return response.data;
237
+ }
238
+ }
239
+ exports.AppThenPlatformSdk = AppThenPlatformSdk;
240
+ function createAppThenSdk(options) {
241
+ const http = new NodeFetchHttpAdapter({
242
+ baseUrl: options.baseUrl,
243
+ fetch: options.fetch,
244
+ defaultHeaders: options.defaultHeaders,
245
+ });
246
+ // 复用 TenantAppAuthProvider:actingMode='platform' 换 platform token,
247
+ // 2h 生命周期 + 60s skew + 并发刷新去重全部继承。
248
+ const auth = new TenantAppAuthProvider(http, {
249
+ accessKey: options.accessKey,
250
+ accessSecret: options.accessSecret,
251
+ actingMode: 'platform',
252
+ requestedScopes: options.requestedScopes,
253
+ refreshSkewMs: options.refreshSkewMs,
254
+ });
255
+ return new AppThenPlatformSdk(new sdk_core_3.PlatformApi(http, auth), http);
256
+ }
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@appthen/sdk-node",
3
+ "version": "0.0.0",
4
+ "description": "AppThen Conversation Hub SDK - Node.js server adapter",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist-cjs/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "dist-cjs"
20
+ ],
21
+ "dependencies": {
22
+ "@appthen/sdk-core": "0.0.0"
23
+ },
24
+ "sideEffects": false,
25
+ "license": "UNLICENSED",
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist-cjs/package.json",
28
+ "dev": "tsc -p tsconfig.json --watch",
29
+ "test": "pnpm build && node --test test/*.test.mjs",
30
+ "typecheck": "tsc -p tsconfig.json --noEmit",
31
+ "clean": "rm -rf dist *.tsbuildinfo"
32
+ }
33
+ }