@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 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
- if (!config.userId) throw new Error("[rei-standard-amsg-client] userId is required");
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
- * The payload uses the same field names as `scheduleMessage`, minus
110
- * `firstSendTime` and `recurrenceType`. It is auto-encrypted with the
111
- * same `userKey` fetched by `init()`, so the upstream deployment of
112
- * amsg-instant must share the same `masterKey` as the amsg-server
113
- * tenant.
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 encrypted = await this._encrypt(JSON.stringify(payload));
124
- const headers = {
125
- "Content-Type": "application/json",
126
- "X-User-Id": this._userId,
127
- "X-Payload-Encrypted": "true",
128
- "X-Encryption-Version": "1"
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: JSON.stringify(encrypted)
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 - Current user identifier (UUID v4).
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
- if (!config.userId) throw new Error('[rei-standard-amsg-client] userId is required');
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
- * The payload uses the same field names as `scheduleMessage`, minus
140
- * `firstSendTime` and `recurrenceType`. It is auto-encrypted with the
141
- * same `userKey` fetched by `init()`, so the upstream deployment of
142
- * amsg-instant must share the same `masterKey` as the amsg-server
143
- * tenant.
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 encrypted = await this._encrypt(JSON.stringify(payload));
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: JSON.stringify(encrypted)
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 - Current user identifier (UUID v4).
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
- if (!config.userId) throw new Error('[rei-standard-amsg-client] userId is required');
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
- * The payload uses the same field names as `scheduleMessage`, minus
140
- * `firstSendTime` and `recurrenceType`. It is auto-encrypted with the
141
- * same `userKey` fetched by `init()`, so the upstream deployment of
142
- * amsg-instant must share the same `masterKey` as the amsg-server
143
- * tenant.
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 encrypted = await this._encrypt(JSON.stringify(payload));
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: JSON.stringify(encrypted)
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
- if (!config.userId) throw new Error("[rei-standard-amsg-client] userId is required");
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
- * The payload uses the same field names as `scheduleMessage`, minus
87
- * `firstSendTime` and `recurrenceType`. It is auto-encrypted with the
88
- * same `userKey` fetched by `init()`, so the upstream deployment of
89
- * amsg-instant must share the same `masterKey` as the amsg-server
90
- * tenant.
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 encrypted = await this._encrypt(JSON.stringify(payload));
101
- const headers = {
102
- "Content-Type": "application/json",
103
- "X-User-Id": this._userId,
104
- "X-Payload-Encrypted": "true",
105
- "X-Encryption-Version": "1"
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: JSON.stringify(encrypted)
142
+ body
115
143
  });
116
144
  return res.json();
117
145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-client",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "ReiStandard Active Messaging browser client SDK",
5
5
  "repository": {
6
6
  "type": "git",