@rei-standard/amsg-client 2.1.0 → 2.2.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 +51 -2
- package/dist/index.cjs +43 -15
- package/dist/index.d.cts +70 -19
- package/dist/index.d.ts +70 -19
- package/dist/index.mjs +43 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ await client.scheduleMessage({
|
|
|
42
42
|
|
|
43
43
|
新代码用 `client.sendInstant(payload)`,走 [`@rei-standard/amsg-instant`](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/instant/README.md)。
|
|
44
44
|
|
|
45
|
+
### 加密模式(默认;兼容 amsg-server / amsg-instant 0.1.x)
|
|
46
|
+
|
|
45
47
|
```js
|
|
46
48
|
const client = new ReiClient({
|
|
47
49
|
baseUrl: '/api/v1',
|
|
@@ -65,8 +67,55 @@ await client.sendInstant({
|
|
|
65
67
|
|
|
66
68
|
> `customBaseUrls` 是按端点名(如 `instant`)覆盖 `baseUrl` 的通用机制;后续其他端点也可以用同一字段独立指定 base URL,不会再加新的命名字段。
|
|
67
69
|
|
|
70
|
+
### 明文模式(配 amsg-instant 0.2.x,单租户自部署)
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const client = new ReiClient({
|
|
74
|
+
baseUrl: 'https://instant.example.com', // amsg-instant Worker URL
|
|
75
|
+
instantEncryption: false,
|
|
76
|
+
instantClientToken: 'shared-secret-xyz', // 可选;Worker 端配了再填
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// init() 在明文模式下是 no-op,调用与否都跑得通
|
|
80
|
+
await client.sendInstant({
|
|
81
|
+
contactName: 'Rei',
|
|
82
|
+
completePrompt: '你是 Rei,用一句话提醒用户带伞',
|
|
83
|
+
apiUrl: 'https://api.openai.com/v1/chat/completions',
|
|
84
|
+
apiKey: '...',
|
|
85
|
+
primaryModel: 'gpt-4o-mini',
|
|
86
|
+
pushSubscription: subscription.toJSON(),
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> ⚠️ **`instantClientToken` 是弱共享密钥**:它会随前端 bundle 发出去,devtools 一开就能看到。它只防 URL 直接被脚本小子打,不防有心人。要真正的鉴权,用 amsg-instant 的 `tokenSigningKey`(HMAC JWT,配合后端签发短期 token)。
|
|
91
|
+
|
|
92
|
+
> ⚠️ **双模式陷阱**:`instantEncryption: false` 时 `init()` 变成 no-op,`scheduleMessage` / `listMessages` / `updateMessage` 这类**仍走加密**的方法会因 `userKey` 没初始化抛 "Not initialised"。如果同一前端既要 `sendInstant`(明文走 amsg-instant)又要 `scheduleMessage`(加密走 amsg-server),请改回 `instantEncryption: true`(默认)—— amsg-instant 0.1.x 与 amsg-server 用同一份 `userKey` 都吃得下。
|
|
93
|
+
|
|
68
94
|
旧路径 `scheduleMessage({ ...payload, messageType: 'instant' })` 仍然可用(兼容保留,多一次 DB 来回)。
|
|
69
95
|
|
|
96
|
+
### `messages` 模式(多轮上下文 / 带 system role,对接 amsg-instant 0.5.0+ / amsg-server 2.2.0+)
|
|
97
|
+
|
|
98
|
+
需要 system role、保留多轮历史、tool role 这些场景时,把 `completePrompt` 换成标准 OpenAI 格式的 `messages` 数组。client 本身**完全透传**,所以 SDK 端零额外配置:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
await client.sendInstant({
|
|
102
|
+
contactName: 'Rei',
|
|
103
|
+
messages: [
|
|
104
|
+
{ role: 'system', content: '你是 Rei,回复要简短自然。' },
|
|
105
|
+
{ role: 'user', content: '今天会下雨吗?' },
|
|
106
|
+
{ role: 'assistant', content: '看了下,下午有阵雨。' },
|
|
107
|
+
{ role: 'user', content: '那提醒我一下带伞' },
|
|
108
|
+
],
|
|
109
|
+
apiUrl: 'https://api.openai.com/v1/chat/completions',
|
|
110
|
+
apiKey: '...',
|
|
111
|
+
primaryModel: 'gpt-4o-mini',
|
|
112
|
+
temperature: 0.7, // 可选
|
|
113
|
+
pushSubscription: subscription.toJSON(),
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
注意 `completePrompt` 和 `messages` **必须恰好二选一**——两者同时给会被 Worker / Server 端返回 `400 INVALID_PAYLOAD_FORMAT` / `INVALID_PARAMETERS`。`scheduleMessage` 也接受同样的 `messages` 字段(amsg-server 2.2.0+ 起持久化层一并支持),用法相同。
|
|
118
|
+
|
|
70
119
|
## 导出 API(Exports)
|
|
71
120
|
|
|
72
121
|
- `ReiClient`
|
|
@@ -91,8 +140,8 @@ await client.sendInstant({
|
|
|
91
140
|
|
|
92
141
|
- 浏览器环境(需 `fetch`、`crypto.subtle`)
|
|
93
142
|
- Push 订阅需可用 Service Worker 与 Push API
|
|
94
|
-
- 需要可用的 `baseUrl`(示例:`/api/v1
|
|
95
|
-
- `userId` 必须是 UUID v4
|
|
143
|
+
- 需要可用的 `baseUrl`(示例:`/api/v1`;明文 instant 模式下可直接是 Worker URL)
|
|
144
|
+
- `userId` 必须是 UUID v4(明文 instant 模式 `instantEncryption: false` 下可省)
|
|
96
145
|
|
|
97
146
|
## 相关链接(绝对 URL)
|
|
98
147
|
|
package/dist/index.cjs
CHANGED
|
@@ -28,7 +28,12 @@ var ReiClient = class {
|
|
|
28
28
|
*/
|
|
29
29
|
constructor(config) {
|
|
30
30
|
if (!config || !config.baseUrl) throw new Error("[rei-standard-amsg-client] baseUrl is required");
|
|
31
|
-
|
|
31
|
+
const instantEncryption = config.instantEncryption !== false;
|
|
32
|
+
if (!config.userId && instantEncryption) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"[rei-standard-amsg-client] userId is required (omit only when instantEncryption: false)"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
32
37
|
this._baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
33
38
|
this._customBaseUrls = {};
|
|
34
39
|
if (config.customBaseUrls && typeof config.customBaseUrls === "object") {
|
|
@@ -38,8 +43,10 @@ var ReiClient = class {
|
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
|
-
this._userId = config.userId;
|
|
46
|
+
this._userId = config.userId || "";
|
|
42
47
|
this._userKey = null;
|
|
48
|
+
this._instantEncryption = instantEncryption;
|
|
49
|
+
this._instantClientToken = typeof config.instantClientToken === "string" && config.instantClientToken ? config.instantClientToken : "";
|
|
43
50
|
}
|
|
44
51
|
/**
|
|
45
52
|
* Resolve the base URL for a given endpoint, falling back to `baseUrl`.
|
|
@@ -55,8 +62,17 @@ var ReiClient = class {
|
|
|
55
62
|
/**
|
|
56
63
|
* Fetch the user-specific encryption key.
|
|
57
64
|
* Must be called before any encrypted request.
|
|
65
|
+
*
|
|
66
|
+
* In plaintext-instant mode (`instantEncryption: false`) this is a no-op:
|
|
67
|
+
* `sendInstant()` does not need a userKey. Note that if you also intend to
|
|
68
|
+
* call `scheduleMessage` / `listMessages` / `updateMessage` (which always
|
|
69
|
+
* use AES-256-GCM), you must construct with `instantEncryption: true`
|
|
70
|
+
* (the default) — those methods will throw "Not initialised" otherwise.
|
|
58
71
|
*/
|
|
59
72
|
async init() {
|
|
73
|
+
if (this._instantEncryption === false) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
60
76
|
const res = await fetch(`${this._baseUrl}/get-user-key`, {
|
|
61
77
|
method: "GET",
|
|
62
78
|
headers: { "X-User-Id": this._userId }
|
|
@@ -106,11 +122,16 @@ var ReiClient = class {
|
|
|
106
122
|
* - Deployable to Cloudflare Workers / Deno Deploy / Vercel Edge
|
|
107
123
|
* - Rejects scheduled-only fields (`firstSendTime`, `recurrenceType`)
|
|
108
124
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
125
|
+
* Two transport modes (chosen by constructor `instantEncryption`):
|
|
126
|
+
*
|
|
127
|
+
* - **Encrypted (default)** — payload is AES-256-GCM encrypted with the
|
|
128
|
+
* `userKey` fetched by `init()`. Compatible with amsg-instant 0.1.x and
|
|
129
|
+
* with amsg-server's `schedule-message` instant path. Sends
|
|
130
|
+
* `X-User-Id` + `X-Payload-Encrypted: true` + `X-Encryption-Version: 1`.
|
|
131
|
+
*
|
|
132
|
+
* - **Plaintext** (`instantEncryption: false`) — payload is sent as raw
|
|
133
|
+
* JSON. Targets amsg-instant 0.2.x. Sends `X-Client-Token` if
|
|
134
|
+
* `instantClientToken` was configured.
|
|
114
135
|
*
|
|
115
136
|
* Routes to `customBaseUrls.instant` if configured, otherwise `baseUrl`.
|
|
116
137
|
*
|
|
@@ -120,13 +141,20 @@ var ReiClient = class {
|
|
|
120
141
|
* @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
|
|
121
142
|
*/
|
|
122
143
|
async sendInstant(payload, endpointPath = "/instant", opts = {}) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
const headers = { "Content-Type": "application/json" };
|
|
145
|
+
let body;
|
|
146
|
+
if (this._instantEncryption === false) {
|
|
147
|
+
body = JSON.stringify(payload);
|
|
148
|
+
if (this._instantClientToken) {
|
|
149
|
+
headers["X-Client-Token"] = this._instantClientToken;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
const encrypted = await this._encrypt(JSON.stringify(payload));
|
|
153
|
+
headers["X-User-Id"] = this._userId;
|
|
154
|
+
headers["X-Payload-Encrypted"] = "true";
|
|
155
|
+
headers["X-Encryption-Version"] = "1";
|
|
156
|
+
body = JSON.stringify(encrypted);
|
|
157
|
+
}
|
|
130
158
|
if (opts.authorization) {
|
|
131
159
|
headers["Authorization"] = opts.authorization;
|
|
132
160
|
}
|
|
@@ -134,7 +162,7 @@ var ReiClient = class {
|
|
|
134
162
|
const res = await fetch(`${this._resolveBaseUrl("instant")}${path}`, {
|
|
135
163
|
method: "POST",
|
|
136
164
|
headers,
|
|
137
|
-
body
|
|
165
|
+
body
|
|
138
166
|
});
|
|
139
167
|
return res.json();
|
|
140
168
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ReiStandard Client SDK
|
|
3
|
-
* v2.0.1
|
|
4
3
|
*
|
|
5
4
|
* Lightweight browser client that handles:
|
|
6
|
-
* - AES-256-GCM encryption using the Web Crypto API
|
|
5
|
+
* - AES-256-GCM encryption using the Web Crypto API (for amsg-server's
|
|
6
|
+
* schedule path and amsg-instant 0.1.x)
|
|
7
|
+
* - Optional plaintext mode for amsg-instant 0.2.x (instantEncryption: false)
|
|
7
8
|
* - Push subscription management via the Push API
|
|
8
|
-
* - Convenient request helpers for amsg-server endpoints + amsg-instant
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
11
|
* import { ReiClient } from '@rei-standard/amsg-client';
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
/**
|
|
26
26
|
* @typedef {Object} ReiClientConfig
|
|
27
27
|
* @property {string} baseUrl - Default base URL of the API (e.g. https://host/api/v1).
|
|
28
|
+
* In plaintext-instant mode (`instantEncryption: false`)
|
|
29
|
+
* this can be the amsg-instant Worker URL directly.
|
|
28
30
|
* @property {Record<string, string>} [customBaseUrls] - Optional per-endpoint base URL overrides.
|
|
29
31
|
* Key is the endpoint name (e.g. `instant`); value is
|
|
30
32
|
* the base URL to use for that endpoint instead of
|
|
@@ -33,7 +35,21 @@
|
|
|
33
35
|
* Workers while the rest run on Netlify). Future
|
|
34
36
|
* endpoints (e.g. `schedule`, `messages`) can be
|
|
35
37
|
* overridden the same way without an API change.
|
|
36
|
-
* @property {string} userId
|
|
38
|
+
* @property {string} [userId] - Current user identifier (UUID v4). Required for the
|
|
39
|
+
* encrypted path (default `instantEncryption: true`,
|
|
40
|
+
* and for `scheduleMessage` / `listMessages` /
|
|
41
|
+
* `updateMessage` always). Can be omitted only when
|
|
42
|
+
* `instantEncryption: false` AND you do not call any
|
|
43
|
+
* encrypted method.
|
|
44
|
+
* @property {boolean} [instantEncryption=true] - When `false`, `sendInstant()` posts plaintext JSON
|
|
45
|
+
* to amsg-instant 0.2.x. `init()` becomes a no-op.
|
|
46
|
+
* All other methods (`scheduleMessage` etc.) keep
|
|
47
|
+
* using AES-256-GCM regardless of this flag.
|
|
48
|
+
* @property {string} [instantClientToken] - When set, sent as the `X-Client-Token` header by
|
|
49
|
+
* `sendInstant()` in plaintext mode. Note: this is
|
|
50
|
+
* a *weak* shared secret — it ships inside any
|
|
51
|
+
* frontend bundle that uses it, so devtools can
|
|
52
|
+
* read it. Use for casual URL-direct abuse only.
|
|
37
53
|
*/
|
|
38
54
|
|
|
39
55
|
class ReiClient {
|
|
@@ -42,7 +58,13 @@ class ReiClient {
|
|
|
42
58
|
*/
|
|
43
59
|
constructor(config) {
|
|
44
60
|
if (!config || !config.baseUrl) throw new Error('[rei-standard-amsg-client] baseUrl is required');
|
|
45
|
-
|
|
61
|
+
|
|
62
|
+
const instantEncryption = config.instantEncryption !== false;
|
|
63
|
+
if (!config.userId && instantEncryption) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'[rei-standard-amsg-client] userId is required (omit only when instantEncryption: false)'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
46
68
|
|
|
47
69
|
/** @private */
|
|
48
70
|
this._baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
@@ -56,9 +78,15 @@ class ReiClient {
|
|
|
56
78
|
}
|
|
57
79
|
}
|
|
58
80
|
/** @private */
|
|
59
|
-
this._userId = config.userId;
|
|
81
|
+
this._userId = config.userId || '';
|
|
60
82
|
/** @private */
|
|
61
83
|
this._userKey = null;
|
|
84
|
+
/** @private */
|
|
85
|
+
this._instantEncryption = instantEncryption;
|
|
86
|
+
/** @private */
|
|
87
|
+
this._instantClientToken = typeof config.instantClientToken === 'string' && config.instantClientToken
|
|
88
|
+
? config.instantClientToken
|
|
89
|
+
: '';
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
/**
|
|
@@ -77,8 +105,18 @@ class ReiClient {
|
|
|
77
105
|
/**
|
|
78
106
|
* Fetch the user-specific encryption key.
|
|
79
107
|
* Must be called before any encrypted request.
|
|
108
|
+
*
|
|
109
|
+
* In plaintext-instant mode (`instantEncryption: false`) this is a no-op:
|
|
110
|
+
* `sendInstant()` does not need a userKey. Note that if you also intend to
|
|
111
|
+
* call `scheduleMessage` / `listMessages` / `updateMessage` (which always
|
|
112
|
+
* use AES-256-GCM), you must construct with `instantEncryption: true`
|
|
113
|
+
* (the default) — those methods will throw "Not initialised" otherwise.
|
|
80
114
|
*/
|
|
81
115
|
async init() {
|
|
116
|
+
if (this._instantEncryption === false) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
const res = await fetch(`${this._baseUrl}/get-user-key`, {
|
|
83
121
|
method: 'GET',
|
|
84
122
|
headers: { 'X-User-Id': this._userId }
|
|
@@ -136,11 +174,16 @@ class ReiClient {
|
|
|
136
174
|
* - Deployable to Cloudflare Workers / Deno Deploy / Vercel Edge
|
|
137
175
|
* - Rejects scheduled-only fields (`firstSendTime`, `recurrenceType`)
|
|
138
176
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
177
|
+
* Two transport modes (chosen by constructor `instantEncryption`):
|
|
178
|
+
*
|
|
179
|
+
* - **Encrypted (default)** — payload is AES-256-GCM encrypted with the
|
|
180
|
+
* `userKey` fetched by `init()`. Compatible with amsg-instant 0.1.x and
|
|
181
|
+
* with amsg-server's `schedule-message` instant path. Sends
|
|
182
|
+
* `X-User-Id` + `X-Payload-Encrypted: true` + `X-Encryption-Version: 1`.
|
|
183
|
+
*
|
|
184
|
+
* - **Plaintext** (`instantEncryption: false`) — payload is sent as raw
|
|
185
|
+
* JSON. Targets amsg-instant 0.2.x. Sends `X-Client-Token` if
|
|
186
|
+
* `instantClientToken` was configured.
|
|
144
187
|
*
|
|
145
188
|
* Routes to `customBaseUrls.instant` if configured, otherwise `baseUrl`.
|
|
146
189
|
*
|
|
@@ -150,14 +193,22 @@ class ReiClient {
|
|
|
150
193
|
* @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
|
|
151
194
|
*/
|
|
152
195
|
async sendInstant(payload, endpointPath = '/instant', opts = {}) {
|
|
153
|
-
const
|
|
196
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
197
|
+
let body;
|
|
198
|
+
|
|
199
|
+
if (this._instantEncryption === false) {
|
|
200
|
+
body = JSON.stringify(payload);
|
|
201
|
+
if (this._instantClientToken) {
|
|
202
|
+
headers['X-Client-Token'] = this._instantClientToken;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
const encrypted = await this._encrypt(JSON.stringify(payload));
|
|
206
|
+
headers['X-User-Id'] = this._userId;
|
|
207
|
+
headers['X-Payload-Encrypted'] = 'true';
|
|
208
|
+
headers['X-Encryption-Version'] = '1';
|
|
209
|
+
body = JSON.stringify(encrypted);
|
|
210
|
+
}
|
|
154
211
|
|
|
155
|
-
const headers = {
|
|
156
|
-
'Content-Type': 'application/json',
|
|
157
|
-
'X-User-Id': this._userId,
|
|
158
|
-
'X-Payload-Encrypted': 'true',
|
|
159
|
-
'X-Encryption-Version': '1'
|
|
160
|
-
};
|
|
161
212
|
if (opts.authorization) {
|
|
162
213
|
headers['Authorization'] = opts.authorization;
|
|
163
214
|
}
|
|
@@ -166,7 +217,7 @@ class ReiClient {
|
|
|
166
217
|
const res = await fetch(`${this._resolveBaseUrl('instant')}${path}`, {
|
|
167
218
|
method: 'POST',
|
|
168
219
|
headers,
|
|
169
|
-
body
|
|
220
|
+
body
|
|
170
221
|
});
|
|
171
222
|
|
|
172
223
|
return res.json();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ReiStandard Client SDK
|
|
3
|
-
* v2.0.1
|
|
4
3
|
*
|
|
5
4
|
* Lightweight browser client that handles:
|
|
6
|
-
* - AES-256-GCM encryption using the Web Crypto API
|
|
5
|
+
* - AES-256-GCM encryption using the Web Crypto API (for amsg-server's
|
|
6
|
+
* schedule path and amsg-instant 0.1.x)
|
|
7
|
+
* - Optional plaintext mode for amsg-instant 0.2.x (instantEncryption: false)
|
|
7
8
|
* - Push subscription management via the Push API
|
|
8
|
-
* - Convenient request helpers for amsg-server endpoints + amsg-instant
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
11
|
* import { ReiClient } from '@rei-standard/amsg-client';
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
/**
|
|
26
26
|
* @typedef {Object} ReiClientConfig
|
|
27
27
|
* @property {string} baseUrl - Default base URL of the API (e.g. https://host/api/v1).
|
|
28
|
+
* In plaintext-instant mode (`instantEncryption: false`)
|
|
29
|
+
* this can be the amsg-instant Worker URL directly.
|
|
28
30
|
* @property {Record<string, string>} [customBaseUrls] - Optional per-endpoint base URL overrides.
|
|
29
31
|
* Key is the endpoint name (e.g. `instant`); value is
|
|
30
32
|
* the base URL to use for that endpoint instead of
|
|
@@ -33,7 +35,21 @@
|
|
|
33
35
|
* Workers while the rest run on Netlify). Future
|
|
34
36
|
* endpoints (e.g. `schedule`, `messages`) can be
|
|
35
37
|
* overridden the same way without an API change.
|
|
36
|
-
* @property {string} userId
|
|
38
|
+
* @property {string} [userId] - Current user identifier (UUID v4). Required for the
|
|
39
|
+
* encrypted path (default `instantEncryption: true`,
|
|
40
|
+
* and for `scheduleMessage` / `listMessages` /
|
|
41
|
+
* `updateMessage` always). Can be omitted only when
|
|
42
|
+
* `instantEncryption: false` AND you do not call any
|
|
43
|
+
* encrypted method.
|
|
44
|
+
* @property {boolean} [instantEncryption=true] - When `false`, `sendInstant()` posts plaintext JSON
|
|
45
|
+
* to amsg-instant 0.2.x. `init()` becomes a no-op.
|
|
46
|
+
* All other methods (`scheduleMessage` etc.) keep
|
|
47
|
+
* using AES-256-GCM regardless of this flag.
|
|
48
|
+
* @property {string} [instantClientToken] - When set, sent as the `X-Client-Token` header by
|
|
49
|
+
* `sendInstant()` in plaintext mode. Note: this is
|
|
50
|
+
* a *weak* shared secret — it ships inside any
|
|
51
|
+
* frontend bundle that uses it, so devtools can
|
|
52
|
+
* read it. Use for casual URL-direct abuse only.
|
|
37
53
|
*/
|
|
38
54
|
|
|
39
55
|
class ReiClient {
|
|
@@ -42,7 +58,13 @@ class ReiClient {
|
|
|
42
58
|
*/
|
|
43
59
|
constructor(config) {
|
|
44
60
|
if (!config || !config.baseUrl) throw new Error('[rei-standard-amsg-client] baseUrl is required');
|
|
45
|
-
|
|
61
|
+
|
|
62
|
+
const instantEncryption = config.instantEncryption !== false;
|
|
63
|
+
if (!config.userId && instantEncryption) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'[rei-standard-amsg-client] userId is required (omit only when instantEncryption: false)'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
46
68
|
|
|
47
69
|
/** @private */
|
|
48
70
|
this._baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
@@ -56,9 +78,15 @@ class ReiClient {
|
|
|
56
78
|
}
|
|
57
79
|
}
|
|
58
80
|
/** @private */
|
|
59
|
-
this._userId = config.userId;
|
|
81
|
+
this._userId = config.userId || '';
|
|
60
82
|
/** @private */
|
|
61
83
|
this._userKey = null;
|
|
84
|
+
/** @private */
|
|
85
|
+
this._instantEncryption = instantEncryption;
|
|
86
|
+
/** @private */
|
|
87
|
+
this._instantClientToken = typeof config.instantClientToken === 'string' && config.instantClientToken
|
|
88
|
+
? config.instantClientToken
|
|
89
|
+
: '';
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
/**
|
|
@@ -77,8 +105,18 @@ class ReiClient {
|
|
|
77
105
|
/**
|
|
78
106
|
* Fetch the user-specific encryption key.
|
|
79
107
|
* Must be called before any encrypted request.
|
|
108
|
+
*
|
|
109
|
+
* In plaintext-instant mode (`instantEncryption: false`) this is a no-op:
|
|
110
|
+
* `sendInstant()` does not need a userKey. Note that if you also intend to
|
|
111
|
+
* call `scheduleMessage` / `listMessages` / `updateMessage` (which always
|
|
112
|
+
* use AES-256-GCM), you must construct with `instantEncryption: true`
|
|
113
|
+
* (the default) — those methods will throw "Not initialised" otherwise.
|
|
80
114
|
*/
|
|
81
115
|
async init() {
|
|
116
|
+
if (this._instantEncryption === false) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
const res = await fetch(`${this._baseUrl}/get-user-key`, {
|
|
83
121
|
method: 'GET',
|
|
84
122
|
headers: { 'X-User-Id': this._userId }
|
|
@@ -136,11 +174,16 @@ class ReiClient {
|
|
|
136
174
|
* - Deployable to Cloudflare Workers / Deno Deploy / Vercel Edge
|
|
137
175
|
* - Rejects scheduled-only fields (`firstSendTime`, `recurrenceType`)
|
|
138
176
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
177
|
+
* Two transport modes (chosen by constructor `instantEncryption`):
|
|
178
|
+
*
|
|
179
|
+
* - **Encrypted (default)** — payload is AES-256-GCM encrypted with the
|
|
180
|
+
* `userKey` fetched by `init()`. Compatible with amsg-instant 0.1.x and
|
|
181
|
+
* with amsg-server's `schedule-message` instant path. Sends
|
|
182
|
+
* `X-User-Id` + `X-Payload-Encrypted: true` + `X-Encryption-Version: 1`.
|
|
183
|
+
*
|
|
184
|
+
* - **Plaintext** (`instantEncryption: false`) — payload is sent as raw
|
|
185
|
+
* JSON. Targets amsg-instant 0.2.x. Sends `X-Client-Token` if
|
|
186
|
+
* `instantClientToken` was configured.
|
|
144
187
|
*
|
|
145
188
|
* Routes to `customBaseUrls.instant` if configured, otherwise `baseUrl`.
|
|
146
189
|
*
|
|
@@ -150,14 +193,22 @@ class ReiClient {
|
|
|
150
193
|
* @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
|
|
151
194
|
*/
|
|
152
195
|
async sendInstant(payload, endpointPath = '/instant', opts = {}) {
|
|
153
|
-
const
|
|
196
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
197
|
+
let body;
|
|
198
|
+
|
|
199
|
+
if (this._instantEncryption === false) {
|
|
200
|
+
body = JSON.stringify(payload);
|
|
201
|
+
if (this._instantClientToken) {
|
|
202
|
+
headers['X-Client-Token'] = this._instantClientToken;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
const encrypted = await this._encrypt(JSON.stringify(payload));
|
|
206
|
+
headers['X-User-Id'] = this._userId;
|
|
207
|
+
headers['X-Payload-Encrypted'] = 'true';
|
|
208
|
+
headers['X-Encryption-Version'] = '1';
|
|
209
|
+
body = JSON.stringify(encrypted);
|
|
210
|
+
}
|
|
154
211
|
|
|
155
|
-
const headers = {
|
|
156
|
-
'Content-Type': 'application/json',
|
|
157
|
-
'X-User-Id': this._userId,
|
|
158
|
-
'X-Payload-Encrypted': 'true',
|
|
159
|
-
'X-Encryption-Version': '1'
|
|
160
|
-
};
|
|
161
212
|
if (opts.authorization) {
|
|
162
213
|
headers['Authorization'] = opts.authorization;
|
|
163
214
|
}
|
|
@@ -166,7 +217,7 @@ class ReiClient {
|
|
|
166
217
|
const res = await fetch(`${this._resolveBaseUrl('instant')}${path}`, {
|
|
167
218
|
method: 'POST',
|
|
168
219
|
headers,
|
|
169
|
-
body
|
|
220
|
+
body
|
|
170
221
|
});
|
|
171
222
|
|
|
172
223
|
return res.json();
|
package/dist/index.mjs
CHANGED
|
@@ -5,7 +5,12 @@ var ReiClient = class {
|
|
|
5
5
|
*/
|
|
6
6
|
constructor(config) {
|
|
7
7
|
if (!config || !config.baseUrl) throw new Error("[rei-standard-amsg-client] baseUrl is required");
|
|
8
|
-
|
|
8
|
+
const instantEncryption = config.instantEncryption !== false;
|
|
9
|
+
if (!config.userId && instantEncryption) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
"[rei-standard-amsg-client] userId is required (omit only when instantEncryption: false)"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
9
14
|
this._baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
10
15
|
this._customBaseUrls = {};
|
|
11
16
|
if (config.customBaseUrls && typeof config.customBaseUrls === "object") {
|
|
@@ -15,8 +20,10 @@ var ReiClient = class {
|
|
|
15
20
|
}
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
|
-
this._userId = config.userId;
|
|
23
|
+
this._userId = config.userId || "";
|
|
19
24
|
this._userKey = null;
|
|
25
|
+
this._instantEncryption = instantEncryption;
|
|
26
|
+
this._instantClientToken = typeof config.instantClientToken === "string" && config.instantClientToken ? config.instantClientToken : "";
|
|
20
27
|
}
|
|
21
28
|
/**
|
|
22
29
|
* Resolve the base URL for a given endpoint, falling back to `baseUrl`.
|
|
@@ -32,8 +39,17 @@ var ReiClient = class {
|
|
|
32
39
|
/**
|
|
33
40
|
* Fetch the user-specific encryption key.
|
|
34
41
|
* Must be called before any encrypted request.
|
|
42
|
+
*
|
|
43
|
+
* In plaintext-instant mode (`instantEncryption: false`) this is a no-op:
|
|
44
|
+
* `sendInstant()` does not need a userKey. Note that if you also intend to
|
|
45
|
+
* call `scheduleMessage` / `listMessages` / `updateMessage` (which always
|
|
46
|
+
* use AES-256-GCM), you must construct with `instantEncryption: true`
|
|
47
|
+
* (the default) — those methods will throw "Not initialised" otherwise.
|
|
35
48
|
*/
|
|
36
49
|
async init() {
|
|
50
|
+
if (this._instantEncryption === false) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
37
53
|
const res = await fetch(`${this._baseUrl}/get-user-key`, {
|
|
38
54
|
method: "GET",
|
|
39
55
|
headers: { "X-User-Id": this._userId }
|
|
@@ -83,11 +99,16 @@ var ReiClient = class {
|
|
|
83
99
|
* - Deployable to Cloudflare Workers / Deno Deploy / Vercel Edge
|
|
84
100
|
* - Rejects scheduled-only fields (`firstSendTime`, `recurrenceType`)
|
|
85
101
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
102
|
+
* Two transport modes (chosen by constructor `instantEncryption`):
|
|
103
|
+
*
|
|
104
|
+
* - **Encrypted (default)** — payload is AES-256-GCM encrypted with the
|
|
105
|
+
* `userKey` fetched by `init()`. Compatible with amsg-instant 0.1.x and
|
|
106
|
+
* with amsg-server's `schedule-message` instant path. Sends
|
|
107
|
+
* `X-User-Id` + `X-Payload-Encrypted: true` + `X-Encryption-Version: 1`.
|
|
108
|
+
*
|
|
109
|
+
* - **Plaintext** (`instantEncryption: false`) — payload is sent as raw
|
|
110
|
+
* JSON. Targets amsg-instant 0.2.x. Sends `X-Client-Token` if
|
|
111
|
+
* `instantClientToken` was configured.
|
|
91
112
|
*
|
|
92
113
|
* Routes to `customBaseUrls.instant` if configured, otherwise `baseUrl`.
|
|
93
114
|
*
|
|
@@ -97,13 +118,20 @@ var ReiClient = class {
|
|
|
97
118
|
* @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
|
|
98
119
|
*/
|
|
99
120
|
async sendInstant(payload, endpointPath = "/instant", opts = {}) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
121
|
+
const headers = { "Content-Type": "application/json" };
|
|
122
|
+
let body;
|
|
123
|
+
if (this._instantEncryption === false) {
|
|
124
|
+
body = JSON.stringify(payload);
|
|
125
|
+
if (this._instantClientToken) {
|
|
126
|
+
headers["X-Client-Token"] = this._instantClientToken;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
const encrypted = await this._encrypt(JSON.stringify(payload));
|
|
130
|
+
headers["X-User-Id"] = this._userId;
|
|
131
|
+
headers["X-Payload-Encrypted"] = "true";
|
|
132
|
+
headers["X-Encryption-Version"] = "1";
|
|
133
|
+
body = JSON.stringify(encrypted);
|
|
134
|
+
}
|
|
107
135
|
if (opts.authorization) {
|
|
108
136
|
headers["Authorization"] = opts.authorization;
|
|
109
137
|
}
|
|
@@ -111,7 +139,7 @@ var ReiClient = class {
|
|
|
111
139
|
const res = await fetch(`${this._resolveBaseUrl("instant")}${path}`, {
|
|
112
140
|
method: "POST",
|
|
113
141
|
headers,
|
|
114
|
-
body
|
|
142
|
+
body
|
|
115
143
|
});
|
|
116
144
|
return res.json();
|
|
117
145
|
}
|