@kispi/chat 0.1.2 → 0.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.en.md +404 -0
- package/README.md +142 -11
- package/dist/{chunk-T3QOUFOM.js → chunk-5QQRM7BH.js} +2 -0
- package/dist/index.cjs +265 -38
- package/dist/index.d.cts +173 -36
- package/dist/index.d.ts +173 -36
- package/dist/index.js +264 -39
- package/dist/server/index.cjs +59 -10
- package/dist/server/index.d.cts +52 -1
- package/dist/server/index.d.ts +52 -1
- package/dist/server/index.js +57 -11
- package/package.json +3 -2
package/dist/server/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var server_exports = {};
|
|
22
22
|
__export(server_exports, {
|
|
23
23
|
DefaultTokenTtlSeconds: () => DefaultTokenTtlSeconds,
|
|
24
|
+
GuestIdPrefix: () => GuestIdPrefix,
|
|
24
25
|
MaxTokenTtlSeconds: () => MaxTokenTtlSeconds,
|
|
25
26
|
SignatureToleranceMs: () => SignatureToleranceMs,
|
|
26
27
|
createChatServer: () => createChatServer
|
|
@@ -34,6 +35,8 @@ var ChatError = class extends Error {
|
|
|
34
35
|
retryAfterMs;
|
|
35
36
|
/** The consumer's own code, when a before_publish hook denied this. */
|
|
36
37
|
appCode;
|
|
38
|
+
/** The HTTP status, when this came from a REST call. */
|
|
39
|
+
status;
|
|
37
40
|
constructor(code, message, extra) {
|
|
38
41
|
super(message);
|
|
39
42
|
this.name = "ChatError";
|
|
@@ -90,12 +93,15 @@ var ServerRest = class {
|
|
|
90
93
|
}
|
|
91
94
|
};
|
|
92
95
|
|
|
96
|
+
// src/server/guest.ts
|
|
97
|
+
var import_node_crypto2 = require("crypto");
|
|
98
|
+
|
|
93
99
|
// src/server/jwt.ts
|
|
94
100
|
var import_node_crypto = require("crypto");
|
|
95
101
|
function signHS256(header, claims, secret) {
|
|
96
102
|
const signingInput = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(claims))}`;
|
|
97
|
-
const
|
|
98
|
-
return `${signingInput}.${
|
|
103
|
+
const mac2 = (0, import_node_crypto.createHmac)("sha256", secret).update(signingInput).digest();
|
|
104
|
+
return `${signingInput}.${mac2.toString("base64url")}`;
|
|
99
105
|
}
|
|
100
106
|
function macMatches(expected, given) {
|
|
101
107
|
return expected.length === given.length && (0, import_node_crypto.timingSafeEqual)(expected, given);
|
|
@@ -104,6 +110,32 @@ function b64url(s) {
|
|
|
104
110
|
return Buffer.from(s, "utf8").toString("base64url");
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
// src/server/guest.ts
|
|
114
|
+
var GuestIdPrefix = "g_";
|
|
115
|
+
var CredentialVersion = "g1";
|
|
116
|
+
var MacLabel = "kispi-chat/guest/v1\n";
|
|
117
|
+
var MinGuestSecretLength = 32;
|
|
118
|
+
function newGuestId() {
|
|
119
|
+
return `${GuestIdPrefix}${(0, import_node_crypto2.randomBytes)(16).toString("base64url")}`;
|
|
120
|
+
}
|
|
121
|
+
function signGuestCredential(secret, userId) {
|
|
122
|
+
return `${CredentialVersion}.${userId}.${mac(secret, userId).toString("base64url")}`;
|
|
123
|
+
}
|
|
124
|
+
function verifyGuestCredential(secrets, credential) {
|
|
125
|
+
const parts = credential.split(".");
|
|
126
|
+
if (parts.length !== 3) return void 0;
|
|
127
|
+
const [version, userId, given] = parts;
|
|
128
|
+
if (version !== CredentialVersion || !userId.startsWith(GuestIdPrefix)) return void 0;
|
|
129
|
+
const givenMac = Buffer.from(given, "base64url");
|
|
130
|
+
for (const [i, secret] of secrets.entries()) {
|
|
131
|
+
if (macMatches(mac(secret, userId), givenMac)) return { userId, secretIndex: i };
|
|
132
|
+
}
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
function mac(secret, userId) {
|
|
136
|
+
return (0, import_node_crypto2.createHmac)("sha256", secret).update(MacLabel + userId).digest();
|
|
137
|
+
}
|
|
138
|
+
|
|
107
139
|
// src/server/token.ts
|
|
108
140
|
var MaxTokenTtlSeconds = 24 * 60 * 60;
|
|
109
141
|
var DefaultTokenTtlSeconds = 60 * 60;
|
|
@@ -135,7 +167,7 @@ function tokenClaims(input, now) {
|
|
|
135
167
|
}
|
|
136
168
|
|
|
137
169
|
// src/server/webhooks.ts
|
|
138
|
-
var
|
|
170
|
+
var import_node_crypto3 = require("crypto");
|
|
139
171
|
var SignatureToleranceMs = 5 * 60 * 1e3;
|
|
140
172
|
var EventBeforePublish = "before_publish";
|
|
141
173
|
function verifyWebhook(secret, headers, rawBody, options = {}) {
|
|
@@ -173,7 +205,7 @@ function verifyWebhook(secret, headers, rawBody, options = {}) {
|
|
|
173
205
|
if (age > tolerance) {
|
|
174
206
|
throw new Error(`webhook: signature is ${Math.round(age / 1e3)}s old, tolerance is ${tolerance / 1e3}s`);
|
|
175
207
|
}
|
|
176
|
-
const expected = (0,
|
|
208
|
+
const expected = (0, import_node_crypto3.createHmac)("sha256", secret).update(`${timestamp}
|
|
177
209
|
${event}
|
|
178
210
|
${delivery}
|
|
179
211
|
${rawBody}`).digest();
|
|
@@ -207,13 +239,29 @@ function createChatServer(options) {
|
|
|
207
239
|
throw new TypeError("keyId is required: it becomes the token's kid, which tells the server which key signed it");
|
|
208
240
|
}
|
|
209
241
|
const rest = new ServerRest(options.url, options.secretKey, options.fetch ?? globalThis.fetch.bind(globalThis));
|
|
242
|
+
const guestSecrets = options.guestSecret === void 0 ? [] : typeof options.guestSecret === "string" ? [options.guestSecret] : options.guestSecret;
|
|
243
|
+
for (const secret of guestSecrets) {
|
|
244
|
+
if (secret.length < MinGuestSecretLength) {
|
|
245
|
+
throw new RangeError(`guestSecret must be at least ${MinGuestSecretLength} characters`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const sign = (input) => signHS256({ alg: "HS256", typ: "JWT", kid: options.keyId }, tokenClaims(input, /* @__PURE__ */ new Date()), options.secretKey);
|
|
210
249
|
return {
|
|
211
|
-
token
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
250
|
+
token: sign,
|
|
251
|
+
guest(input) {
|
|
252
|
+
const signer = guestSecrets[0];
|
|
253
|
+
if (signer === void 0) {
|
|
254
|
+
throw new TypeError("guestSecret was not given to createChatServer, so guest credentials cannot be signed");
|
|
255
|
+
}
|
|
256
|
+
const { credential, ...claims } = input;
|
|
257
|
+
const verified = credential ? verifyGuestCredential(guestSecrets, credential) : void 0;
|
|
258
|
+
const userId = verified?.userId ?? newGuestId();
|
|
259
|
+
return {
|
|
260
|
+
userId,
|
|
261
|
+
credential: verified !== void 0 && verified.secretIndex === 0 ? credential : signGuestCredential(signer, userId),
|
|
262
|
+
token: sign({ ...claims, userId }),
|
|
263
|
+
created: verified === void 0
|
|
264
|
+
};
|
|
217
265
|
},
|
|
218
266
|
rooms: {
|
|
219
267
|
ensure: (input) => rest.call("PUT", "/v1/rooms", input),
|
|
@@ -305,6 +353,7 @@ function createChatServer(options) {
|
|
|
305
353
|
// Annotate the CommonJS export names for ESM import in node:
|
|
306
354
|
0 && (module.exports = {
|
|
307
355
|
DefaultTokenTtlSeconds,
|
|
356
|
+
GuestIdPrefix,
|
|
308
357
|
MaxTokenTtlSeconds,
|
|
309
358
|
SignatureToleranceMs,
|
|
310
359
|
createChatServer
|
package/dist/server/index.d.cts
CHANGED
|
@@ -39,6 +39,24 @@ type VerifyOptions = {
|
|
|
39
39
|
tolerance?: number;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Guest identities without a database.
|
|
44
|
+
*
|
|
45
|
+
* 비로그인 방문자에게 안정적인 id를 주려면 브라우저가 무언가를 들고 있다가
|
|
46
|
+
* 돌려줘야 한다. 그것이 **id 자체**이면 안 된다 — `sender.id`는 방의 모든
|
|
47
|
+
* 메시지에 실려 공개되므로, 백엔드가 "돌려받은 id를 그대로 서명"하면 누구나
|
|
48
|
+
* 남의 메시지에서 id를 복사해 그 게스트로 말할 수 있다. 그래서 브라우저가 드는
|
|
49
|
+
* 것은 id에 대한 MAC이 붙은 자격증명이고, id는 그 안에서 꺼낸다. MAC은 소비자
|
|
50
|
+
* 백엔드만 아는 비밀로 만들고 검증하므로 저장소가 필요 없다.
|
|
51
|
+
*
|
|
52
|
+
* 비밀을 `sk_`에서 유도하지 않는 이유: `sk_`는 유출되면 돌리는 키이고, 돌리는
|
|
53
|
+
* 순간 모든 게스트가 새 사람이 된다(자기 메시지·읽음 커서를 잃는다). 게스트
|
|
54
|
+
* 신원의 수명은 키의 수명과 무관해야 하므로 비밀을 따로 받고, 배열로 받아
|
|
55
|
+
* 교체 중에도 옛 자격증명을 받아 준다.
|
|
56
|
+
*/
|
|
57
|
+
/** Every guest id starts with this, so it cannot collide with a member id that does not. */
|
|
58
|
+
declare const GuestIdPrefix = "g_";
|
|
59
|
+
|
|
42
60
|
type ChatServerOptions = {
|
|
43
61
|
/** The chat server's base URL, e.g. `https://chat.example.com`. */
|
|
44
62
|
url: string;
|
|
@@ -56,6 +74,15 @@ type ChatServerOptions = {
|
|
|
56
74
|
keyId: string;
|
|
57
75
|
/** Needed only by `webhooks.verify`. */
|
|
58
76
|
webhookSecret?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Needed only by `guest()`: the secret guest credentials are signed with,
|
|
79
|
+
* at least 32 characters, kept only on your backend.
|
|
80
|
+
*
|
|
81
|
+
* An array while rotating: the first signs, every one verifies, and a
|
|
82
|
+
* credential verified by an older one is re-issued under the first --
|
|
83
|
+
* same `userId` -- so guests migrate on their next visit.
|
|
84
|
+
*/
|
|
85
|
+
guestSecret?: string | readonly string[];
|
|
59
86
|
/** Defaults to the global. */
|
|
60
87
|
fetch?: typeof globalThis.fetch;
|
|
61
88
|
};
|
|
@@ -82,9 +109,33 @@ type ServerSendInput = {
|
|
|
82
109
|
replyTo?: string;
|
|
83
110
|
threadId?: string;
|
|
84
111
|
};
|
|
112
|
+
type GuestInput = Omit<TokenInput, 'userId'> & {
|
|
113
|
+
/** What `guest()` returned last time, as the browser kept it. Missing or invalid means a new guest. */
|
|
114
|
+
credential?: string | null;
|
|
115
|
+
};
|
|
116
|
+
type GuestIdentity = {
|
|
117
|
+
/** `g_…`. Public -- it is `sender.id` on every message -- and never proof of anything. */
|
|
118
|
+
userId: string;
|
|
119
|
+
/** Give this back to the browser to keep, and take it back next time. This is the proof. */
|
|
120
|
+
credential: string;
|
|
121
|
+
/** A user token for `userId`, as `token()` would sign it. */
|
|
122
|
+
token: string;
|
|
123
|
+
/** True when a new guest was issued (no credential, or one that did not verify). */
|
|
124
|
+
created: boolean;
|
|
125
|
+
};
|
|
85
126
|
type ChatServer = {
|
|
86
127
|
/** Signs a user token locally. No network call. */
|
|
87
128
|
token: (input: TokenInput) => string;
|
|
129
|
+
/**
|
|
130
|
+
* Issues or resumes a guest: verifies `credential` and signs a token for
|
|
131
|
+
* the guest id inside it, or mints a new guest when there is none. No
|
|
132
|
+
* network call and no storage. Needs `guestSecret`.
|
|
133
|
+
*
|
|
134
|
+
* **Never sign a user id the browser sends back.** A guest id is public,
|
|
135
|
+
* so re-signing it lets anyone speak as any guest. Only the credential
|
|
136
|
+
* proves who a guest is.
|
|
137
|
+
*/
|
|
138
|
+
guest: (input: GuestInput) => GuestIdentity;
|
|
88
139
|
rooms: {
|
|
89
140
|
/** get-or-create. The same call whether the room exists or not. */
|
|
90
141
|
ensure: (input: EnsureRoomInput) => Promise<{
|
|
@@ -205,4 +256,4 @@ type ChatServer = {
|
|
|
205
256
|
};
|
|
206
257
|
declare function createChatServer(options: ChatServerOptions): ChatServer;
|
|
207
258
|
|
|
208
|
-
export { type ChatServer, type ChatServerOptions, DefaultTokenTtlSeconds, type EnsureRoomInput, MaxTokenTtlSeconds, type ServerSendInput, SignatureToleranceMs, type TokenInput, type VerifiedDelivery, type VerifyOptions, createChatServer };
|
|
259
|
+
export { type ChatServer, type ChatServerOptions, DefaultTokenTtlSeconds, type EnsureRoomInput, GuestIdPrefix, type GuestIdentity, type GuestInput, MaxTokenTtlSeconds, type ServerSendInput, SignatureToleranceMs, type TokenInput, type VerifiedDelivery, type VerifyOptions, createChatServer };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -39,6 +39,24 @@ type VerifyOptions = {
|
|
|
39
39
|
tolerance?: number;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Guest identities without a database.
|
|
44
|
+
*
|
|
45
|
+
* 비로그인 방문자에게 안정적인 id를 주려면 브라우저가 무언가를 들고 있다가
|
|
46
|
+
* 돌려줘야 한다. 그것이 **id 자체**이면 안 된다 — `sender.id`는 방의 모든
|
|
47
|
+
* 메시지에 실려 공개되므로, 백엔드가 "돌려받은 id를 그대로 서명"하면 누구나
|
|
48
|
+
* 남의 메시지에서 id를 복사해 그 게스트로 말할 수 있다. 그래서 브라우저가 드는
|
|
49
|
+
* 것은 id에 대한 MAC이 붙은 자격증명이고, id는 그 안에서 꺼낸다. MAC은 소비자
|
|
50
|
+
* 백엔드만 아는 비밀로 만들고 검증하므로 저장소가 필요 없다.
|
|
51
|
+
*
|
|
52
|
+
* 비밀을 `sk_`에서 유도하지 않는 이유: `sk_`는 유출되면 돌리는 키이고, 돌리는
|
|
53
|
+
* 순간 모든 게스트가 새 사람이 된다(자기 메시지·읽음 커서를 잃는다). 게스트
|
|
54
|
+
* 신원의 수명은 키의 수명과 무관해야 하므로 비밀을 따로 받고, 배열로 받아
|
|
55
|
+
* 교체 중에도 옛 자격증명을 받아 준다.
|
|
56
|
+
*/
|
|
57
|
+
/** Every guest id starts with this, so it cannot collide with a member id that does not. */
|
|
58
|
+
declare const GuestIdPrefix = "g_";
|
|
59
|
+
|
|
42
60
|
type ChatServerOptions = {
|
|
43
61
|
/** The chat server's base URL, e.g. `https://chat.example.com`. */
|
|
44
62
|
url: string;
|
|
@@ -56,6 +74,15 @@ type ChatServerOptions = {
|
|
|
56
74
|
keyId: string;
|
|
57
75
|
/** Needed only by `webhooks.verify`. */
|
|
58
76
|
webhookSecret?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Needed only by `guest()`: the secret guest credentials are signed with,
|
|
79
|
+
* at least 32 characters, kept only on your backend.
|
|
80
|
+
*
|
|
81
|
+
* An array while rotating: the first signs, every one verifies, and a
|
|
82
|
+
* credential verified by an older one is re-issued under the first --
|
|
83
|
+
* same `userId` -- so guests migrate on their next visit.
|
|
84
|
+
*/
|
|
85
|
+
guestSecret?: string | readonly string[];
|
|
59
86
|
/** Defaults to the global. */
|
|
60
87
|
fetch?: typeof globalThis.fetch;
|
|
61
88
|
};
|
|
@@ -82,9 +109,33 @@ type ServerSendInput = {
|
|
|
82
109
|
replyTo?: string;
|
|
83
110
|
threadId?: string;
|
|
84
111
|
};
|
|
112
|
+
type GuestInput = Omit<TokenInput, 'userId'> & {
|
|
113
|
+
/** What `guest()` returned last time, as the browser kept it. Missing or invalid means a new guest. */
|
|
114
|
+
credential?: string | null;
|
|
115
|
+
};
|
|
116
|
+
type GuestIdentity = {
|
|
117
|
+
/** `g_…`. Public -- it is `sender.id` on every message -- and never proof of anything. */
|
|
118
|
+
userId: string;
|
|
119
|
+
/** Give this back to the browser to keep, and take it back next time. This is the proof. */
|
|
120
|
+
credential: string;
|
|
121
|
+
/** A user token for `userId`, as `token()` would sign it. */
|
|
122
|
+
token: string;
|
|
123
|
+
/** True when a new guest was issued (no credential, or one that did not verify). */
|
|
124
|
+
created: boolean;
|
|
125
|
+
};
|
|
85
126
|
type ChatServer = {
|
|
86
127
|
/** Signs a user token locally. No network call. */
|
|
87
128
|
token: (input: TokenInput) => string;
|
|
129
|
+
/**
|
|
130
|
+
* Issues or resumes a guest: verifies `credential` and signs a token for
|
|
131
|
+
* the guest id inside it, or mints a new guest when there is none. No
|
|
132
|
+
* network call and no storage. Needs `guestSecret`.
|
|
133
|
+
*
|
|
134
|
+
* **Never sign a user id the browser sends back.** A guest id is public,
|
|
135
|
+
* so re-signing it lets anyone speak as any guest. Only the credential
|
|
136
|
+
* proves who a guest is.
|
|
137
|
+
*/
|
|
138
|
+
guest: (input: GuestInput) => GuestIdentity;
|
|
88
139
|
rooms: {
|
|
89
140
|
/** get-or-create. The same call whether the room exists or not. */
|
|
90
141
|
ensure: (input: EnsureRoomInput) => Promise<{
|
|
@@ -205,4 +256,4 @@ type ChatServer = {
|
|
|
205
256
|
};
|
|
206
257
|
declare function createChatServer(options: ChatServerOptions): ChatServer;
|
|
207
258
|
|
|
208
|
-
export { type ChatServer, type ChatServerOptions, DefaultTokenTtlSeconds, type EnsureRoomInput, MaxTokenTtlSeconds, type ServerSendInput, SignatureToleranceMs, type TokenInput, type VerifiedDelivery, type VerifyOptions, createChatServer };
|
|
259
|
+
export { type ChatServer, type ChatServerOptions, DefaultTokenTtlSeconds, type EnsureRoomInput, GuestIdPrefix, type GuestIdentity, type GuestInput, MaxTokenTtlSeconds, type ServerSendInput, SignatureToleranceMs, type TokenInput, type VerifiedDelivery, type VerifyOptions, createChatServer };
|
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
errorFrom
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-5QQRM7BH.js";
|
|
4
4
|
|
|
5
5
|
// src/server/api.ts
|
|
6
6
|
var ServerRest = class {
|
|
@@ -36,12 +36,15 @@ var ServerRest = class {
|
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
// src/server/guest.ts
|
|
40
|
+
import { createHmac as createHmac2, randomBytes } from "crypto";
|
|
41
|
+
|
|
39
42
|
// src/server/jwt.ts
|
|
40
43
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
41
44
|
function signHS256(header, claims, secret) {
|
|
42
45
|
const signingInput = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(claims))}`;
|
|
43
|
-
const
|
|
44
|
-
return `${signingInput}.${
|
|
46
|
+
const mac2 = createHmac("sha256", secret).update(signingInput).digest();
|
|
47
|
+
return `${signingInput}.${mac2.toString("base64url")}`;
|
|
45
48
|
}
|
|
46
49
|
function macMatches(expected, given) {
|
|
47
50
|
return expected.length === given.length && timingSafeEqual(expected, given);
|
|
@@ -50,6 +53,32 @@ function b64url(s) {
|
|
|
50
53
|
return Buffer.from(s, "utf8").toString("base64url");
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
// src/server/guest.ts
|
|
57
|
+
var GuestIdPrefix = "g_";
|
|
58
|
+
var CredentialVersion = "g1";
|
|
59
|
+
var MacLabel = "kispi-chat/guest/v1\n";
|
|
60
|
+
var MinGuestSecretLength = 32;
|
|
61
|
+
function newGuestId() {
|
|
62
|
+
return `${GuestIdPrefix}${randomBytes(16).toString("base64url")}`;
|
|
63
|
+
}
|
|
64
|
+
function signGuestCredential(secret, userId) {
|
|
65
|
+
return `${CredentialVersion}.${userId}.${mac(secret, userId).toString("base64url")}`;
|
|
66
|
+
}
|
|
67
|
+
function verifyGuestCredential(secrets, credential) {
|
|
68
|
+
const parts = credential.split(".");
|
|
69
|
+
if (parts.length !== 3) return void 0;
|
|
70
|
+
const [version, userId, given] = parts;
|
|
71
|
+
if (version !== CredentialVersion || !userId.startsWith(GuestIdPrefix)) return void 0;
|
|
72
|
+
const givenMac = Buffer.from(given, "base64url");
|
|
73
|
+
for (const [i, secret] of secrets.entries()) {
|
|
74
|
+
if (macMatches(mac(secret, userId), givenMac)) return { userId, secretIndex: i };
|
|
75
|
+
}
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
function mac(secret, userId) {
|
|
79
|
+
return createHmac2("sha256", secret).update(MacLabel + userId).digest();
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
// src/server/token.ts
|
|
54
83
|
var MaxTokenTtlSeconds = 24 * 60 * 60;
|
|
55
84
|
var DefaultTokenTtlSeconds = 60 * 60;
|
|
@@ -81,7 +110,7 @@ function tokenClaims(input, now) {
|
|
|
81
110
|
}
|
|
82
111
|
|
|
83
112
|
// src/server/webhooks.ts
|
|
84
|
-
import { createHmac as
|
|
113
|
+
import { createHmac as createHmac3 } from "crypto";
|
|
85
114
|
var SignatureToleranceMs = 5 * 60 * 1e3;
|
|
86
115
|
var EventBeforePublish = "before_publish";
|
|
87
116
|
function verifyWebhook(secret, headers, rawBody, options = {}) {
|
|
@@ -119,7 +148,7 @@ function verifyWebhook(secret, headers, rawBody, options = {}) {
|
|
|
119
148
|
if (age > tolerance) {
|
|
120
149
|
throw new Error(`webhook: signature is ${Math.round(age / 1e3)}s old, tolerance is ${tolerance / 1e3}s`);
|
|
121
150
|
}
|
|
122
|
-
const expected =
|
|
151
|
+
const expected = createHmac3("sha256", secret).update(`${timestamp}
|
|
123
152
|
${event}
|
|
124
153
|
${delivery}
|
|
125
154
|
${rawBody}`).digest();
|
|
@@ -153,13 +182,29 @@ function createChatServer(options) {
|
|
|
153
182
|
throw new TypeError("keyId is required: it becomes the token's kid, which tells the server which key signed it");
|
|
154
183
|
}
|
|
155
184
|
const rest = new ServerRest(options.url, options.secretKey, options.fetch ?? globalThis.fetch.bind(globalThis));
|
|
185
|
+
const guestSecrets = options.guestSecret === void 0 ? [] : typeof options.guestSecret === "string" ? [options.guestSecret] : options.guestSecret;
|
|
186
|
+
for (const secret of guestSecrets) {
|
|
187
|
+
if (secret.length < MinGuestSecretLength) {
|
|
188
|
+
throw new RangeError(`guestSecret must be at least ${MinGuestSecretLength} characters`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const sign = (input) => signHS256({ alg: "HS256", typ: "JWT", kid: options.keyId }, tokenClaims(input, /* @__PURE__ */ new Date()), options.secretKey);
|
|
156
192
|
return {
|
|
157
|
-
token
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
193
|
+
token: sign,
|
|
194
|
+
guest(input) {
|
|
195
|
+
const signer = guestSecrets[0];
|
|
196
|
+
if (signer === void 0) {
|
|
197
|
+
throw new TypeError("guestSecret was not given to createChatServer, so guest credentials cannot be signed");
|
|
198
|
+
}
|
|
199
|
+
const { credential, ...claims } = input;
|
|
200
|
+
const verified = credential ? verifyGuestCredential(guestSecrets, credential) : void 0;
|
|
201
|
+
const userId = verified?.userId ?? newGuestId();
|
|
202
|
+
return {
|
|
203
|
+
userId,
|
|
204
|
+
credential: verified !== void 0 && verified.secretIndex === 0 ? credential : signGuestCredential(signer, userId),
|
|
205
|
+
token: sign({ ...claims, userId }),
|
|
206
|
+
created: verified === void 0
|
|
207
|
+
};
|
|
163
208
|
},
|
|
164
209
|
rooms: {
|
|
165
210
|
ensure: (input) => rest.call("PUT", "/v1/rooms", input),
|
|
@@ -250,6 +295,7 @@ function createChatServer(options) {
|
|
|
250
295
|
}
|
|
251
296
|
export {
|
|
252
297
|
DefaultTokenTtlSeconds,
|
|
298
|
+
GuestIdPrefix,
|
|
253
299
|
MaxTokenTtlSeconds,
|
|
254
300
|
SignatureToleranceMs,
|
|
255
301
|
createChatServer
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kispi/chat",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Client SDK for the chat server: one WebSocket, many rooms, ordered history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"README.en.md"
|
|
10
11
|
],
|
|
11
12
|
"exports": {
|
|
12
13
|
".": {
|