@velanir/openclaw-participation-gate 0.1.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 +151 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +2718 -0
- package/openclaw.plugin.json +156 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2718 @@
|
|
|
1
|
+
// src/api.ts
|
|
2
|
+
import {
|
|
3
|
+
definePluginEntry
|
|
4
|
+
} from "openclaw/plugin-sdk/plugin-entry";
|
|
5
|
+
|
|
6
|
+
// ../../oct8-secrets/dist/index.js
|
|
7
|
+
import { createHash, randomUUID } from "crypto";
|
|
8
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
9
|
+
import fs from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var Oct8SecretsError = class extends Error {
|
|
17
|
+
constructor(message2, code = "OCT8_SECRETS_ERROR") {
|
|
18
|
+
super(message2);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.name = "Oct8SecretsError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
var KEY_ID_RE = /^[A-Za-z0-9._:-]{8,160}$/;
|
|
25
|
+
function requiredString(env, key) {
|
|
26
|
+
const value = env[key]?.trim();
|
|
27
|
+
if (!value) {
|
|
28
|
+
throw new Oct8SecretsError(`${key} is required.`, "MISSING_ENV");
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function normalizeBaseUrl(value, key) {
|
|
33
|
+
let url;
|
|
34
|
+
try {
|
|
35
|
+
url = new URL(value);
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Oct8SecretsError(`${key} must be a valid absolute URL.`, "INVALID_ENV");
|
|
38
|
+
}
|
|
39
|
+
if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
40
|
+
throw new Oct8SecretsError(`${key} must use https outside local development.`, "INVALID_ENV");
|
|
41
|
+
}
|
|
42
|
+
url.search = "";
|
|
43
|
+
url.hash = "";
|
|
44
|
+
return url.toString().replace(/\/+$/, "");
|
|
45
|
+
}
|
|
46
|
+
function readRuntimeSecretsEnv(env = process.env) {
|
|
47
|
+
const mode = env.OCT8_SECRETS_MODE?.trim();
|
|
48
|
+
if (mode && mode !== "runtime") {
|
|
49
|
+
throw new Oct8SecretsError("oct8-secrets-runtime-v2 only supports OCT8_SECRETS_MODE=runtime.", "UNSUPPORTED_MODE");
|
|
50
|
+
}
|
|
51
|
+
const apiUrl = normalizeBaseUrl(requiredString(env, "OCT8_API_URL"), "OCT8_API_URL");
|
|
52
|
+
const tokenIssuer = normalizeBaseUrl(env.OCT8_RUNTIME_TOKEN_ISSUER?.trim() || apiUrl, "OCT8_RUNTIME_TOKEN_ISSUER");
|
|
53
|
+
const runtimeIdentityId = requiredString(env, "OCT8_RUNTIME_IDENTITY_ID");
|
|
54
|
+
if (!UUID_RE.test(runtimeIdentityId)) {
|
|
55
|
+
throw new Oct8SecretsError("OCT8_RUNTIME_IDENTITY_ID must be a runtime identity UUID.", "INVALID_ENV");
|
|
56
|
+
}
|
|
57
|
+
const keyId = env.OCT8_RUNTIME_KEY_ID?.trim();
|
|
58
|
+
if (keyId && !KEY_ID_RE.test(keyId)) {
|
|
59
|
+
throw new Oct8SecretsError("OCT8_RUNTIME_KEY_ID must be 8-160 characters using letters, numbers, dots, underscores, colons, or hyphens.", "INVALID_ENV");
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
apiUrl,
|
|
63
|
+
tokenIssuer,
|
|
64
|
+
runtimeIdentityId,
|
|
65
|
+
stateDir: requiredString(env, "OCT8_RUNTIME_STATE_DIR"),
|
|
66
|
+
...env.OCT8_RUNTIME_ENROLLMENT_TOKEN?.trim() ? { enrollmentToken: env.OCT8_RUNTIME_ENROLLMENT_TOKEN.trim() } : {},
|
|
67
|
+
...keyId ? { keyId } : {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
var base64url_exports = {};
|
|
71
|
+
__export(base64url_exports, {
|
|
72
|
+
decode: () => decode,
|
|
73
|
+
encode: () => encode2
|
|
74
|
+
});
|
|
75
|
+
var encoder = new TextEncoder();
|
|
76
|
+
var decoder = new TextDecoder();
|
|
77
|
+
var MAX_INT32 = 2 ** 32;
|
|
78
|
+
function concat(...buffers) {
|
|
79
|
+
const size = buffers.reduce((acc, { length }) => acc + length, 0);
|
|
80
|
+
const buf = new Uint8Array(size);
|
|
81
|
+
let i = 0;
|
|
82
|
+
for (const buffer of buffers) {
|
|
83
|
+
buf.set(buffer, i);
|
|
84
|
+
i += buffer.length;
|
|
85
|
+
}
|
|
86
|
+
return buf;
|
|
87
|
+
}
|
|
88
|
+
function encode(string) {
|
|
89
|
+
const bytes = new Uint8Array(string.length);
|
|
90
|
+
for (let i = 0; i < string.length; i++) {
|
|
91
|
+
const code = string.charCodeAt(i);
|
|
92
|
+
if (code > 127) {
|
|
93
|
+
throw new TypeError("non-ASCII string encountered in encode()");
|
|
94
|
+
}
|
|
95
|
+
bytes[i] = code;
|
|
96
|
+
}
|
|
97
|
+
return bytes;
|
|
98
|
+
}
|
|
99
|
+
function encodeBase64(input) {
|
|
100
|
+
if (Uint8Array.prototype.toBase64) {
|
|
101
|
+
return input.toBase64();
|
|
102
|
+
}
|
|
103
|
+
const CHUNK_SIZE = 32768;
|
|
104
|
+
const arr = [];
|
|
105
|
+
for (let i = 0; i < input.length; i += CHUNK_SIZE) {
|
|
106
|
+
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
|
|
107
|
+
}
|
|
108
|
+
return btoa(arr.join(""));
|
|
109
|
+
}
|
|
110
|
+
function decodeBase64(encoded) {
|
|
111
|
+
if (Uint8Array.fromBase64) {
|
|
112
|
+
return Uint8Array.fromBase64(encoded);
|
|
113
|
+
}
|
|
114
|
+
const binary = atob(encoded);
|
|
115
|
+
const bytes = new Uint8Array(binary.length);
|
|
116
|
+
for (let i = 0; i < binary.length; i++) {
|
|
117
|
+
bytes[i] = binary.charCodeAt(i);
|
|
118
|
+
}
|
|
119
|
+
return bytes;
|
|
120
|
+
}
|
|
121
|
+
function decode(input) {
|
|
122
|
+
if (Uint8Array.fromBase64) {
|
|
123
|
+
return Uint8Array.fromBase64(typeof input === "string" ? input : decoder.decode(input), {
|
|
124
|
+
alphabet: "base64url"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
let encoded = input;
|
|
128
|
+
if (encoded instanceof Uint8Array) {
|
|
129
|
+
encoded = decoder.decode(encoded);
|
|
130
|
+
}
|
|
131
|
+
encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
132
|
+
try {
|
|
133
|
+
return decodeBase64(encoded);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new TypeError("The input to be decoded is not correctly encoded.");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function encode2(input) {
|
|
139
|
+
let unencoded = input;
|
|
140
|
+
if (typeof unencoded === "string") {
|
|
141
|
+
unencoded = encoder.encode(unencoded);
|
|
142
|
+
}
|
|
143
|
+
if (Uint8Array.prototype.toBase64) {
|
|
144
|
+
return unencoded.toBase64({ alphabet: "base64url", omitPadding: true });
|
|
145
|
+
}
|
|
146
|
+
return encodeBase64(unencoded).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
147
|
+
}
|
|
148
|
+
var unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
149
|
+
var isAlgorithm = (algorithm, name) => algorithm.name === name;
|
|
150
|
+
function getHashLength(hash) {
|
|
151
|
+
return parseInt(hash.name.slice(4), 10);
|
|
152
|
+
}
|
|
153
|
+
function checkHashLength(algorithm, expected) {
|
|
154
|
+
const actual = getHashLength(algorithm.hash);
|
|
155
|
+
if (actual !== expected)
|
|
156
|
+
throw unusable(`SHA-${expected}`, "algorithm.hash");
|
|
157
|
+
}
|
|
158
|
+
function getNamedCurve(alg) {
|
|
159
|
+
switch (alg) {
|
|
160
|
+
case "ES256":
|
|
161
|
+
return "P-256";
|
|
162
|
+
case "ES384":
|
|
163
|
+
return "P-384";
|
|
164
|
+
case "ES512":
|
|
165
|
+
return "P-521";
|
|
166
|
+
default:
|
|
167
|
+
throw new Error("unreachable");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function checkUsage(key, usage) {
|
|
171
|
+
if (usage && !key.usages.includes(usage)) {
|
|
172
|
+
throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function checkSigCryptoKey(key, alg, usage) {
|
|
176
|
+
switch (alg) {
|
|
177
|
+
case "HS256":
|
|
178
|
+
case "HS384":
|
|
179
|
+
case "HS512": {
|
|
180
|
+
if (!isAlgorithm(key.algorithm, "HMAC"))
|
|
181
|
+
throw unusable("HMAC");
|
|
182
|
+
checkHashLength(key.algorithm, parseInt(alg.slice(2), 10));
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "RS256":
|
|
186
|
+
case "RS384":
|
|
187
|
+
case "RS512": {
|
|
188
|
+
if (!isAlgorithm(key.algorithm, "RSASSA-PKCS1-v1_5"))
|
|
189
|
+
throw unusable("RSASSA-PKCS1-v1_5");
|
|
190
|
+
checkHashLength(key.algorithm, parseInt(alg.slice(2), 10));
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case "PS256":
|
|
194
|
+
case "PS384":
|
|
195
|
+
case "PS512": {
|
|
196
|
+
if (!isAlgorithm(key.algorithm, "RSA-PSS"))
|
|
197
|
+
throw unusable("RSA-PSS");
|
|
198
|
+
checkHashLength(key.algorithm, parseInt(alg.slice(2), 10));
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "Ed25519":
|
|
202
|
+
case "EdDSA": {
|
|
203
|
+
if (!isAlgorithm(key.algorithm, "Ed25519"))
|
|
204
|
+
throw unusable("Ed25519");
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case "ML-DSA-44":
|
|
208
|
+
case "ML-DSA-65":
|
|
209
|
+
case "ML-DSA-87": {
|
|
210
|
+
if (!isAlgorithm(key.algorithm, alg))
|
|
211
|
+
throw unusable(alg);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case "ES256":
|
|
215
|
+
case "ES384":
|
|
216
|
+
case "ES512": {
|
|
217
|
+
if (!isAlgorithm(key.algorithm, "ECDSA"))
|
|
218
|
+
throw unusable("ECDSA");
|
|
219
|
+
const expected = getNamedCurve(alg);
|
|
220
|
+
const actual = key.algorithm.namedCurve;
|
|
221
|
+
if (actual !== expected)
|
|
222
|
+
throw unusable(expected, "algorithm.namedCurve");
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
default:
|
|
226
|
+
throw new TypeError("CryptoKey does not support this operation");
|
|
227
|
+
}
|
|
228
|
+
checkUsage(key, usage);
|
|
229
|
+
}
|
|
230
|
+
function message(msg, actual, ...types) {
|
|
231
|
+
types = types.filter(Boolean);
|
|
232
|
+
if (types.length > 2) {
|
|
233
|
+
const last = types.pop();
|
|
234
|
+
msg += `one of type ${types.join(", ")}, or ${last}.`;
|
|
235
|
+
} else if (types.length === 2) {
|
|
236
|
+
msg += `one of type ${types[0]} or ${types[1]}.`;
|
|
237
|
+
} else {
|
|
238
|
+
msg += `of type ${types[0]}.`;
|
|
239
|
+
}
|
|
240
|
+
if (actual == null) {
|
|
241
|
+
msg += ` Received ${actual}`;
|
|
242
|
+
} else if (typeof actual === "function" && actual.name) {
|
|
243
|
+
msg += ` Received function ${actual.name}`;
|
|
244
|
+
} else if (typeof actual === "object" && actual != null) {
|
|
245
|
+
if (actual.constructor?.name) {
|
|
246
|
+
msg += ` Received an instance of ${actual.constructor.name}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return msg;
|
|
250
|
+
}
|
|
251
|
+
var invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types);
|
|
252
|
+
var withAlg = (alg, actual, ...types) => message(`Key for the ${alg} algorithm must be `, actual, ...types);
|
|
253
|
+
var JOSEError = class extends Error {
|
|
254
|
+
static code = "ERR_JOSE_GENERIC";
|
|
255
|
+
code = "ERR_JOSE_GENERIC";
|
|
256
|
+
constructor(message2, options) {
|
|
257
|
+
super(message2, options);
|
|
258
|
+
this.name = this.constructor.name;
|
|
259
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var JOSENotSupported = class extends JOSEError {
|
|
263
|
+
static code = "ERR_JOSE_NOT_SUPPORTED";
|
|
264
|
+
code = "ERR_JOSE_NOT_SUPPORTED";
|
|
265
|
+
};
|
|
266
|
+
var JWSInvalid = class extends JOSEError {
|
|
267
|
+
static code = "ERR_JWS_INVALID";
|
|
268
|
+
code = "ERR_JWS_INVALID";
|
|
269
|
+
};
|
|
270
|
+
var JWTInvalid = class extends JOSEError {
|
|
271
|
+
static code = "ERR_JWT_INVALID";
|
|
272
|
+
code = "ERR_JWT_INVALID";
|
|
273
|
+
};
|
|
274
|
+
var isCryptoKey = (key) => {
|
|
275
|
+
if (key?.[Symbol.toStringTag] === "CryptoKey")
|
|
276
|
+
return true;
|
|
277
|
+
try {
|
|
278
|
+
return key instanceof CryptoKey;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
var isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject";
|
|
284
|
+
var isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key);
|
|
285
|
+
function assertNotSet(value, name) {
|
|
286
|
+
if (value) {
|
|
287
|
+
throw new TypeError(`${name} can only be called once`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
var isObjectLike = (value) => typeof value === "object" && value !== null;
|
|
291
|
+
function isObject(input) {
|
|
292
|
+
if (!isObjectLike(input) || Object.prototype.toString.call(input) !== "[object Object]") {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
if (Object.getPrototypeOf(input) === null) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
let proto = input;
|
|
299
|
+
while (Object.getPrototypeOf(proto) !== null) {
|
|
300
|
+
proto = Object.getPrototypeOf(proto);
|
|
301
|
+
}
|
|
302
|
+
return Object.getPrototypeOf(input) === proto;
|
|
303
|
+
}
|
|
304
|
+
function isDisjoint(...headers) {
|
|
305
|
+
const sources = headers.filter(Boolean);
|
|
306
|
+
if (sources.length === 0 || sources.length === 1) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
let acc;
|
|
310
|
+
for (const header of sources) {
|
|
311
|
+
const parameters = Object.keys(header);
|
|
312
|
+
if (!acc || acc.size === 0) {
|
|
313
|
+
acc = new Set(parameters);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
for (const parameter of parameters) {
|
|
317
|
+
if (acc.has(parameter)) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
acc.add(parameter);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
var isJWK = (key) => isObject(key) && typeof key.kty === "string";
|
|
326
|
+
var isPrivateJWK = (key) => key.kty !== "oct" && (key.kty === "AKP" && typeof key.priv === "string" || typeof key.d === "string");
|
|
327
|
+
var isPublicJWK = (key) => key.kty !== "oct" && key.d === void 0 && key.priv === void 0;
|
|
328
|
+
var isSecretJWK = (key) => key.kty === "oct" && typeof key.k === "string";
|
|
329
|
+
function checkKeyLength(alg, key) {
|
|
330
|
+
if (alg.startsWith("RS") || alg.startsWith("PS")) {
|
|
331
|
+
const { modulusLength } = key.algorithm;
|
|
332
|
+
if (typeof modulusLength !== "number" || modulusLength < 2048) {
|
|
333
|
+
throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function subtleAlgorithm(alg, algorithm) {
|
|
338
|
+
const hash = `SHA-${alg.slice(-3)}`;
|
|
339
|
+
switch (alg) {
|
|
340
|
+
case "HS256":
|
|
341
|
+
case "HS384":
|
|
342
|
+
case "HS512":
|
|
343
|
+
return { hash, name: "HMAC" };
|
|
344
|
+
case "PS256":
|
|
345
|
+
case "PS384":
|
|
346
|
+
case "PS512":
|
|
347
|
+
return { hash, name: "RSA-PSS", saltLength: parseInt(alg.slice(-3), 10) >> 3 };
|
|
348
|
+
case "RS256":
|
|
349
|
+
case "RS384":
|
|
350
|
+
case "RS512":
|
|
351
|
+
return { hash, name: "RSASSA-PKCS1-v1_5" };
|
|
352
|
+
case "ES256":
|
|
353
|
+
case "ES384":
|
|
354
|
+
case "ES512":
|
|
355
|
+
return { hash, name: "ECDSA", namedCurve: algorithm.namedCurve };
|
|
356
|
+
case "Ed25519":
|
|
357
|
+
case "EdDSA":
|
|
358
|
+
return { name: "Ed25519" };
|
|
359
|
+
case "ML-DSA-44":
|
|
360
|
+
case "ML-DSA-65":
|
|
361
|
+
case "ML-DSA-87":
|
|
362
|
+
return { name: alg };
|
|
363
|
+
default:
|
|
364
|
+
throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function getSigKey(alg, key, usage) {
|
|
368
|
+
if (key instanceof Uint8Array) {
|
|
369
|
+
if (!alg.startsWith("HS")) {
|
|
370
|
+
throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key"));
|
|
371
|
+
}
|
|
372
|
+
return crypto.subtle.importKey("raw", key, { hash: `SHA-${alg.slice(-3)}`, name: "HMAC" }, false, [usage]);
|
|
373
|
+
}
|
|
374
|
+
checkSigCryptoKey(key, alg, usage);
|
|
375
|
+
return key;
|
|
376
|
+
}
|
|
377
|
+
async function sign(alg, key, data) {
|
|
378
|
+
const cryptoKey = await getSigKey(alg, key, "sign");
|
|
379
|
+
checkKeyLength(alg, cryptoKey);
|
|
380
|
+
const signature = await crypto.subtle.sign(subtleAlgorithm(alg, cryptoKey.algorithm), cryptoKey, data);
|
|
381
|
+
return new Uint8Array(signature);
|
|
382
|
+
}
|
|
383
|
+
var unsupportedAlg = 'Invalid or unsupported JWK "alg" (Algorithm) Parameter value';
|
|
384
|
+
function subtleMapping(jwk) {
|
|
385
|
+
let algorithm;
|
|
386
|
+
let keyUsages;
|
|
387
|
+
switch (jwk.kty) {
|
|
388
|
+
case "AKP": {
|
|
389
|
+
switch (jwk.alg) {
|
|
390
|
+
case "ML-DSA-44":
|
|
391
|
+
case "ML-DSA-65":
|
|
392
|
+
case "ML-DSA-87":
|
|
393
|
+
algorithm = { name: jwk.alg };
|
|
394
|
+
keyUsages = jwk.priv ? ["sign"] : ["verify"];
|
|
395
|
+
break;
|
|
396
|
+
default:
|
|
397
|
+
throw new JOSENotSupported(unsupportedAlg);
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case "RSA": {
|
|
402
|
+
switch (jwk.alg) {
|
|
403
|
+
case "PS256":
|
|
404
|
+
case "PS384":
|
|
405
|
+
case "PS512":
|
|
406
|
+
algorithm = { name: "RSA-PSS", hash: `SHA-${jwk.alg.slice(-3)}` };
|
|
407
|
+
keyUsages = jwk.d ? ["sign"] : ["verify"];
|
|
408
|
+
break;
|
|
409
|
+
case "RS256":
|
|
410
|
+
case "RS384":
|
|
411
|
+
case "RS512":
|
|
412
|
+
algorithm = { name: "RSASSA-PKCS1-v1_5", hash: `SHA-${jwk.alg.slice(-3)}` };
|
|
413
|
+
keyUsages = jwk.d ? ["sign"] : ["verify"];
|
|
414
|
+
break;
|
|
415
|
+
case "RSA-OAEP":
|
|
416
|
+
case "RSA-OAEP-256":
|
|
417
|
+
case "RSA-OAEP-384":
|
|
418
|
+
case "RSA-OAEP-512":
|
|
419
|
+
algorithm = {
|
|
420
|
+
name: "RSA-OAEP",
|
|
421
|
+
hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}`
|
|
422
|
+
};
|
|
423
|
+
keyUsages = jwk.d ? ["decrypt", "unwrapKey"] : ["encrypt", "wrapKey"];
|
|
424
|
+
break;
|
|
425
|
+
default:
|
|
426
|
+
throw new JOSENotSupported(unsupportedAlg);
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case "EC": {
|
|
431
|
+
switch (jwk.alg) {
|
|
432
|
+
case "ES256":
|
|
433
|
+
case "ES384":
|
|
434
|
+
case "ES512":
|
|
435
|
+
algorithm = {
|
|
436
|
+
name: "ECDSA",
|
|
437
|
+
namedCurve: { ES256: "P-256", ES384: "P-384", ES512: "P-521" }[jwk.alg]
|
|
438
|
+
};
|
|
439
|
+
keyUsages = jwk.d ? ["sign"] : ["verify"];
|
|
440
|
+
break;
|
|
441
|
+
case "ECDH-ES":
|
|
442
|
+
case "ECDH-ES+A128KW":
|
|
443
|
+
case "ECDH-ES+A192KW":
|
|
444
|
+
case "ECDH-ES+A256KW":
|
|
445
|
+
algorithm = { name: "ECDH", namedCurve: jwk.crv };
|
|
446
|
+
keyUsages = jwk.d ? ["deriveBits"] : [];
|
|
447
|
+
break;
|
|
448
|
+
default:
|
|
449
|
+
throw new JOSENotSupported(unsupportedAlg);
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
case "OKP": {
|
|
454
|
+
switch (jwk.alg) {
|
|
455
|
+
case "Ed25519":
|
|
456
|
+
case "EdDSA":
|
|
457
|
+
algorithm = { name: "Ed25519" };
|
|
458
|
+
keyUsages = jwk.d ? ["sign"] : ["verify"];
|
|
459
|
+
break;
|
|
460
|
+
case "ECDH-ES":
|
|
461
|
+
case "ECDH-ES+A128KW":
|
|
462
|
+
case "ECDH-ES+A192KW":
|
|
463
|
+
case "ECDH-ES+A256KW":
|
|
464
|
+
algorithm = { name: jwk.crv };
|
|
465
|
+
keyUsages = jwk.d ? ["deriveBits"] : [];
|
|
466
|
+
break;
|
|
467
|
+
default:
|
|
468
|
+
throw new JOSENotSupported(unsupportedAlg);
|
|
469
|
+
}
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
default:
|
|
473
|
+
throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value');
|
|
474
|
+
}
|
|
475
|
+
return { algorithm, keyUsages };
|
|
476
|
+
}
|
|
477
|
+
async function jwkToKey(jwk) {
|
|
478
|
+
if (!jwk.alg) {
|
|
479
|
+
throw new TypeError('"alg" argument is required when "jwk.alg" is not present');
|
|
480
|
+
}
|
|
481
|
+
const { algorithm, keyUsages } = subtleMapping(jwk);
|
|
482
|
+
const keyData = { ...jwk };
|
|
483
|
+
if (keyData.kty !== "AKP") {
|
|
484
|
+
delete keyData.alg;
|
|
485
|
+
}
|
|
486
|
+
delete keyData.use;
|
|
487
|
+
return crypto.subtle.importKey("jwk", keyData, algorithm, jwk.ext ?? (jwk.d || jwk.priv ? false : true), jwk.key_ops ?? keyUsages);
|
|
488
|
+
}
|
|
489
|
+
var unusableForAlg = "given KeyObject instance cannot be used for this algorithm";
|
|
490
|
+
var cache;
|
|
491
|
+
var handleJWK = async (key, jwk, alg, freeze = false) => {
|
|
492
|
+
cache ||= /* @__PURE__ */ new WeakMap();
|
|
493
|
+
let cached = cache.get(key);
|
|
494
|
+
if (cached?.[alg]) {
|
|
495
|
+
return cached[alg];
|
|
496
|
+
}
|
|
497
|
+
const cryptoKey = await jwkToKey({ ...jwk, alg });
|
|
498
|
+
if (freeze)
|
|
499
|
+
Object.freeze(key);
|
|
500
|
+
if (!cached) {
|
|
501
|
+
cache.set(key, { [alg]: cryptoKey });
|
|
502
|
+
} else {
|
|
503
|
+
cached[alg] = cryptoKey;
|
|
504
|
+
}
|
|
505
|
+
return cryptoKey;
|
|
506
|
+
};
|
|
507
|
+
var handleKeyObject = (keyObject, alg) => {
|
|
508
|
+
cache ||= /* @__PURE__ */ new WeakMap();
|
|
509
|
+
let cached = cache.get(keyObject);
|
|
510
|
+
if (cached?.[alg]) {
|
|
511
|
+
return cached[alg];
|
|
512
|
+
}
|
|
513
|
+
const isPublic = keyObject.type === "public";
|
|
514
|
+
const extractable = isPublic ? true : false;
|
|
515
|
+
let cryptoKey;
|
|
516
|
+
if (keyObject.asymmetricKeyType === "x25519") {
|
|
517
|
+
switch (alg) {
|
|
518
|
+
case "ECDH-ES":
|
|
519
|
+
case "ECDH-ES+A128KW":
|
|
520
|
+
case "ECDH-ES+A192KW":
|
|
521
|
+
case "ECDH-ES+A256KW":
|
|
522
|
+
break;
|
|
523
|
+
default:
|
|
524
|
+
throw new TypeError(unusableForAlg);
|
|
525
|
+
}
|
|
526
|
+
cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, isPublic ? [] : ["deriveBits"]);
|
|
527
|
+
}
|
|
528
|
+
if (keyObject.asymmetricKeyType === "ed25519") {
|
|
529
|
+
if (alg !== "EdDSA" && alg !== "Ed25519") {
|
|
530
|
+
throw new TypeError(unusableForAlg);
|
|
531
|
+
}
|
|
532
|
+
cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, [
|
|
533
|
+
isPublic ? "verify" : "sign"
|
|
534
|
+
]);
|
|
535
|
+
}
|
|
536
|
+
switch (keyObject.asymmetricKeyType) {
|
|
537
|
+
case "ml-dsa-44":
|
|
538
|
+
case "ml-dsa-65":
|
|
539
|
+
case "ml-dsa-87": {
|
|
540
|
+
if (alg !== keyObject.asymmetricKeyType.toUpperCase()) {
|
|
541
|
+
throw new TypeError(unusableForAlg);
|
|
542
|
+
}
|
|
543
|
+
cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, [
|
|
544
|
+
isPublic ? "verify" : "sign"
|
|
545
|
+
]);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (keyObject.asymmetricKeyType === "rsa") {
|
|
549
|
+
let hash;
|
|
550
|
+
switch (alg) {
|
|
551
|
+
case "RSA-OAEP":
|
|
552
|
+
hash = "SHA-1";
|
|
553
|
+
break;
|
|
554
|
+
case "RS256":
|
|
555
|
+
case "PS256":
|
|
556
|
+
case "RSA-OAEP-256":
|
|
557
|
+
hash = "SHA-256";
|
|
558
|
+
break;
|
|
559
|
+
case "RS384":
|
|
560
|
+
case "PS384":
|
|
561
|
+
case "RSA-OAEP-384":
|
|
562
|
+
hash = "SHA-384";
|
|
563
|
+
break;
|
|
564
|
+
case "RS512":
|
|
565
|
+
case "PS512":
|
|
566
|
+
case "RSA-OAEP-512":
|
|
567
|
+
hash = "SHA-512";
|
|
568
|
+
break;
|
|
569
|
+
default:
|
|
570
|
+
throw new TypeError(unusableForAlg);
|
|
571
|
+
}
|
|
572
|
+
if (alg.startsWith("RSA-OAEP")) {
|
|
573
|
+
return keyObject.toCryptoKey({
|
|
574
|
+
name: "RSA-OAEP",
|
|
575
|
+
hash
|
|
576
|
+
}, extractable, isPublic ? ["encrypt"] : ["decrypt"]);
|
|
577
|
+
}
|
|
578
|
+
cryptoKey = keyObject.toCryptoKey({
|
|
579
|
+
name: alg.startsWith("PS") ? "RSA-PSS" : "RSASSA-PKCS1-v1_5",
|
|
580
|
+
hash
|
|
581
|
+
}, extractable, [isPublic ? "verify" : "sign"]);
|
|
582
|
+
}
|
|
583
|
+
if (keyObject.asymmetricKeyType === "ec") {
|
|
584
|
+
const nist = /* @__PURE__ */ new Map([
|
|
585
|
+
["prime256v1", "P-256"],
|
|
586
|
+
["secp384r1", "P-384"],
|
|
587
|
+
["secp521r1", "P-521"]
|
|
588
|
+
]);
|
|
589
|
+
const namedCurve = nist.get(keyObject.asymmetricKeyDetails?.namedCurve);
|
|
590
|
+
if (!namedCurve) {
|
|
591
|
+
throw new TypeError(unusableForAlg);
|
|
592
|
+
}
|
|
593
|
+
const expectedCurve = { ES256: "P-256", ES384: "P-384", ES512: "P-521" };
|
|
594
|
+
if (expectedCurve[alg] && namedCurve === expectedCurve[alg]) {
|
|
595
|
+
cryptoKey = keyObject.toCryptoKey({
|
|
596
|
+
name: "ECDSA",
|
|
597
|
+
namedCurve
|
|
598
|
+
}, extractable, [isPublic ? "verify" : "sign"]);
|
|
599
|
+
}
|
|
600
|
+
if (alg.startsWith("ECDH-ES")) {
|
|
601
|
+
cryptoKey = keyObject.toCryptoKey({
|
|
602
|
+
name: "ECDH",
|
|
603
|
+
namedCurve
|
|
604
|
+
}, extractable, isPublic ? [] : ["deriveBits"]);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (!cryptoKey) {
|
|
608
|
+
throw new TypeError(unusableForAlg);
|
|
609
|
+
}
|
|
610
|
+
if (!cached) {
|
|
611
|
+
cache.set(keyObject, { [alg]: cryptoKey });
|
|
612
|
+
} else {
|
|
613
|
+
cached[alg] = cryptoKey;
|
|
614
|
+
}
|
|
615
|
+
return cryptoKey;
|
|
616
|
+
};
|
|
617
|
+
async function normalizeKey(key, alg) {
|
|
618
|
+
if (key instanceof Uint8Array) {
|
|
619
|
+
return key;
|
|
620
|
+
}
|
|
621
|
+
if (isCryptoKey(key)) {
|
|
622
|
+
return key;
|
|
623
|
+
}
|
|
624
|
+
if (isKeyObject(key)) {
|
|
625
|
+
if (key.type === "secret") {
|
|
626
|
+
return key.export();
|
|
627
|
+
}
|
|
628
|
+
if ("toCryptoKey" in key && typeof key.toCryptoKey === "function") {
|
|
629
|
+
try {
|
|
630
|
+
return handleKeyObject(key, alg);
|
|
631
|
+
} catch (err) {
|
|
632
|
+
if (err instanceof TypeError) {
|
|
633
|
+
throw err;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
let jwk = key.export({ format: "jwk" });
|
|
638
|
+
return handleJWK(key, jwk, alg);
|
|
639
|
+
}
|
|
640
|
+
if (isJWK(key)) {
|
|
641
|
+
if (key.k) {
|
|
642
|
+
return decode(key.k);
|
|
643
|
+
}
|
|
644
|
+
return handleJWK(key, key, alg, true);
|
|
645
|
+
}
|
|
646
|
+
throw new Error("unreachable");
|
|
647
|
+
}
|
|
648
|
+
async function importJWK(jwk, alg, options) {
|
|
649
|
+
if (!isObject(jwk)) {
|
|
650
|
+
throw new TypeError("JWK must be an object");
|
|
651
|
+
}
|
|
652
|
+
let ext;
|
|
653
|
+
alg ??= jwk.alg;
|
|
654
|
+
ext ??= options?.extractable ?? jwk.ext;
|
|
655
|
+
switch (jwk.kty) {
|
|
656
|
+
case "oct":
|
|
657
|
+
if (typeof jwk.k !== "string" || !jwk.k) {
|
|
658
|
+
throw new TypeError('missing "k" (Key Value) Parameter value');
|
|
659
|
+
}
|
|
660
|
+
return decode(jwk.k);
|
|
661
|
+
case "RSA":
|
|
662
|
+
if ("oth" in jwk && jwk.oth !== void 0) {
|
|
663
|
+
throw new JOSENotSupported('RSA JWK "oth" (Other Primes Info) Parameter value is not supported');
|
|
664
|
+
}
|
|
665
|
+
return jwkToKey({ ...jwk, alg, ext });
|
|
666
|
+
case "AKP": {
|
|
667
|
+
if (typeof jwk.alg !== "string" || !jwk.alg) {
|
|
668
|
+
throw new TypeError('missing "alg" (Algorithm) Parameter value');
|
|
669
|
+
}
|
|
670
|
+
if (alg !== void 0 && alg !== jwk.alg) {
|
|
671
|
+
throw new TypeError("JWK alg and alg option value mismatch");
|
|
672
|
+
}
|
|
673
|
+
return jwkToKey({ ...jwk, ext });
|
|
674
|
+
}
|
|
675
|
+
case "EC":
|
|
676
|
+
case "OKP":
|
|
677
|
+
return jwkToKey({ ...jwk, alg, ext });
|
|
678
|
+
default:
|
|
679
|
+
throw new JOSENotSupported('Unsupported "kty" (Key Type) Parameter value');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function keyToJWK(key) {
|
|
683
|
+
if (isKeyObject(key)) {
|
|
684
|
+
if (key.type === "secret") {
|
|
685
|
+
key = key.export();
|
|
686
|
+
} else {
|
|
687
|
+
return key.export({ format: "jwk" });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (key instanceof Uint8Array) {
|
|
691
|
+
return {
|
|
692
|
+
kty: "oct",
|
|
693
|
+
k: encode2(key)
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
if (!isCryptoKey(key)) {
|
|
697
|
+
throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "Uint8Array"));
|
|
698
|
+
}
|
|
699
|
+
if (!key.extractable) {
|
|
700
|
+
throw new TypeError("non-extractable CryptoKey cannot be exported as a JWK");
|
|
701
|
+
}
|
|
702
|
+
const { ext, key_ops, alg, use, ...jwk } = await crypto.subtle.exportKey("jwk", key);
|
|
703
|
+
if (jwk.kty === "AKP") {
|
|
704
|
+
;
|
|
705
|
+
jwk.alg = alg;
|
|
706
|
+
}
|
|
707
|
+
return jwk;
|
|
708
|
+
}
|
|
709
|
+
async function exportJWK(key) {
|
|
710
|
+
return keyToJWK(key);
|
|
711
|
+
}
|
|
712
|
+
function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
|
|
713
|
+
if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) {
|
|
714
|
+
throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
|
|
715
|
+
}
|
|
716
|
+
if (!protectedHeader || protectedHeader.crit === void 0) {
|
|
717
|
+
return /* @__PURE__ */ new Set();
|
|
718
|
+
}
|
|
719
|
+
if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input !== "string" || input.length === 0)) {
|
|
720
|
+
throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');
|
|
721
|
+
}
|
|
722
|
+
let recognized;
|
|
723
|
+
if (recognizedOption !== void 0) {
|
|
724
|
+
recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]);
|
|
725
|
+
} else {
|
|
726
|
+
recognized = recognizedDefault;
|
|
727
|
+
}
|
|
728
|
+
for (const parameter of protectedHeader.crit) {
|
|
729
|
+
if (!recognized.has(parameter)) {
|
|
730
|
+
throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
|
|
731
|
+
}
|
|
732
|
+
if (joseHeader[parameter] === void 0) {
|
|
733
|
+
throw new Err(`Extension Header Parameter "${parameter}" is missing`);
|
|
734
|
+
}
|
|
735
|
+
if (recognized.get(parameter) && protectedHeader[parameter] === void 0) {
|
|
736
|
+
throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return new Set(protectedHeader.crit);
|
|
740
|
+
}
|
|
741
|
+
var tag = (key) => key?.[Symbol.toStringTag];
|
|
742
|
+
var jwkMatchesOp = (alg, key, usage) => {
|
|
743
|
+
if (key.use !== void 0) {
|
|
744
|
+
let expected;
|
|
745
|
+
switch (usage) {
|
|
746
|
+
case "sign":
|
|
747
|
+
case "verify":
|
|
748
|
+
expected = "sig";
|
|
749
|
+
break;
|
|
750
|
+
case "encrypt":
|
|
751
|
+
case "decrypt":
|
|
752
|
+
expected = "enc";
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
if (key.use !== expected) {
|
|
756
|
+
throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (key.alg !== void 0 && key.alg !== alg) {
|
|
760
|
+
throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
|
|
761
|
+
}
|
|
762
|
+
if (Array.isArray(key.key_ops)) {
|
|
763
|
+
let expectedKeyOp;
|
|
764
|
+
switch (true) {
|
|
765
|
+
case (usage === "sign" || usage === "verify"):
|
|
766
|
+
case alg === "dir":
|
|
767
|
+
case alg.includes("CBC-HS"):
|
|
768
|
+
expectedKeyOp = usage;
|
|
769
|
+
break;
|
|
770
|
+
case alg.startsWith("PBES2"):
|
|
771
|
+
expectedKeyOp = "deriveBits";
|
|
772
|
+
break;
|
|
773
|
+
case /^A\d{3}(?:GCM)?(?:KW)?$/.test(alg):
|
|
774
|
+
if (!alg.includes("GCM") && alg.endsWith("KW")) {
|
|
775
|
+
expectedKeyOp = usage === "encrypt" ? "wrapKey" : "unwrapKey";
|
|
776
|
+
} else {
|
|
777
|
+
expectedKeyOp = usage;
|
|
778
|
+
}
|
|
779
|
+
break;
|
|
780
|
+
case (usage === "encrypt" && alg.startsWith("RSA")):
|
|
781
|
+
expectedKeyOp = "wrapKey";
|
|
782
|
+
break;
|
|
783
|
+
case usage === "decrypt":
|
|
784
|
+
expectedKeyOp = alg.startsWith("RSA") ? "unwrapKey" : "deriveBits";
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
if (expectedKeyOp && key.key_ops?.includes?.(expectedKeyOp) === false) {
|
|
788
|
+
throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return true;
|
|
792
|
+
};
|
|
793
|
+
var symmetricTypeCheck = (alg, key, usage) => {
|
|
794
|
+
if (key instanceof Uint8Array)
|
|
795
|
+
return;
|
|
796
|
+
if (isJWK(key)) {
|
|
797
|
+
if (isSecretJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
798
|
+
return;
|
|
799
|
+
throw new TypeError(`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`);
|
|
800
|
+
}
|
|
801
|
+
if (!isKeyLike(key)) {
|
|
802
|
+
throw new TypeError(withAlg(alg, key, "CryptoKey", "KeyObject", "JSON Web Key", "Uint8Array"));
|
|
803
|
+
}
|
|
804
|
+
if (key.type !== "secret") {
|
|
805
|
+
throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`);
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
var asymmetricTypeCheck = (alg, key, usage) => {
|
|
809
|
+
if (isJWK(key)) {
|
|
810
|
+
switch (usage) {
|
|
811
|
+
case "decrypt":
|
|
812
|
+
case "sign":
|
|
813
|
+
if (isPrivateJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
814
|
+
return;
|
|
815
|
+
throw new TypeError(`JSON Web Key for this operation must be a private JWK`);
|
|
816
|
+
case "encrypt":
|
|
817
|
+
case "verify":
|
|
818
|
+
if (isPublicJWK(key) && jwkMatchesOp(alg, key, usage))
|
|
819
|
+
return;
|
|
820
|
+
throw new TypeError(`JSON Web Key for this operation must be a public JWK`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (!isKeyLike(key)) {
|
|
824
|
+
throw new TypeError(withAlg(alg, key, "CryptoKey", "KeyObject", "JSON Web Key"));
|
|
825
|
+
}
|
|
826
|
+
if (key.type === "secret") {
|
|
827
|
+
throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`);
|
|
828
|
+
}
|
|
829
|
+
if (key.type === "public") {
|
|
830
|
+
switch (usage) {
|
|
831
|
+
case "sign":
|
|
832
|
+
throw new TypeError(`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`);
|
|
833
|
+
case "decrypt":
|
|
834
|
+
throw new TypeError(`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (key.type === "private") {
|
|
838
|
+
switch (usage) {
|
|
839
|
+
case "verify":
|
|
840
|
+
throw new TypeError(`${tag(key)} instances for asymmetric algorithm verifying must be of type "public"`);
|
|
841
|
+
case "encrypt":
|
|
842
|
+
throw new TypeError(`${tag(key)} instances for asymmetric algorithm encryption must be of type "public"`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
function checkKeyType(alg, key, usage) {
|
|
847
|
+
switch (alg.substring(0, 2)) {
|
|
848
|
+
case "A1":
|
|
849
|
+
case "A2":
|
|
850
|
+
case "di":
|
|
851
|
+
case "HS":
|
|
852
|
+
case "PB":
|
|
853
|
+
symmetricTypeCheck(alg, key, usage);
|
|
854
|
+
break;
|
|
855
|
+
default:
|
|
856
|
+
asymmetricTypeCheck(alg, key, usage);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
var epoch = (date) => Math.floor(date.getTime() / 1e3);
|
|
860
|
+
var minute = 60;
|
|
861
|
+
var hour = minute * 60;
|
|
862
|
+
var day = hour * 24;
|
|
863
|
+
var week = day * 7;
|
|
864
|
+
var year = day * 365.25;
|
|
865
|
+
var REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i;
|
|
866
|
+
function secs(str) {
|
|
867
|
+
const matched = REGEX.exec(str);
|
|
868
|
+
if (!matched || matched[4] && matched[1]) {
|
|
869
|
+
throw new TypeError("Invalid time period format");
|
|
870
|
+
}
|
|
871
|
+
const value = parseFloat(matched[2]);
|
|
872
|
+
const unit = matched[3].toLowerCase();
|
|
873
|
+
let numericDate;
|
|
874
|
+
switch (unit) {
|
|
875
|
+
case "sec":
|
|
876
|
+
case "secs":
|
|
877
|
+
case "second":
|
|
878
|
+
case "seconds":
|
|
879
|
+
case "s":
|
|
880
|
+
numericDate = Math.round(value);
|
|
881
|
+
break;
|
|
882
|
+
case "minute":
|
|
883
|
+
case "minutes":
|
|
884
|
+
case "min":
|
|
885
|
+
case "mins":
|
|
886
|
+
case "m":
|
|
887
|
+
numericDate = Math.round(value * minute);
|
|
888
|
+
break;
|
|
889
|
+
case "hour":
|
|
890
|
+
case "hours":
|
|
891
|
+
case "hr":
|
|
892
|
+
case "hrs":
|
|
893
|
+
case "h":
|
|
894
|
+
numericDate = Math.round(value * hour);
|
|
895
|
+
break;
|
|
896
|
+
case "day":
|
|
897
|
+
case "days":
|
|
898
|
+
case "d":
|
|
899
|
+
numericDate = Math.round(value * day);
|
|
900
|
+
break;
|
|
901
|
+
case "week":
|
|
902
|
+
case "weeks":
|
|
903
|
+
case "w":
|
|
904
|
+
numericDate = Math.round(value * week);
|
|
905
|
+
break;
|
|
906
|
+
default:
|
|
907
|
+
numericDate = Math.round(value * year);
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
if (matched[1] === "-" || matched[4] === "ago") {
|
|
911
|
+
return -numericDate;
|
|
912
|
+
}
|
|
913
|
+
return numericDate;
|
|
914
|
+
}
|
|
915
|
+
function validateInput(label, input) {
|
|
916
|
+
if (!Number.isFinite(input)) {
|
|
917
|
+
throw new TypeError(`Invalid ${label} input`);
|
|
918
|
+
}
|
|
919
|
+
return input;
|
|
920
|
+
}
|
|
921
|
+
var JWTClaimsBuilder = class {
|
|
922
|
+
#payload;
|
|
923
|
+
constructor(payload) {
|
|
924
|
+
if (!isObject(payload)) {
|
|
925
|
+
throw new TypeError("JWT Claims Set MUST be an object");
|
|
926
|
+
}
|
|
927
|
+
this.#payload = structuredClone(payload);
|
|
928
|
+
}
|
|
929
|
+
data() {
|
|
930
|
+
return encoder.encode(JSON.stringify(this.#payload));
|
|
931
|
+
}
|
|
932
|
+
get iss() {
|
|
933
|
+
return this.#payload.iss;
|
|
934
|
+
}
|
|
935
|
+
set iss(value) {
|
|
936
|
+
this.#payload.iss = value;
|
|
937
|
+
}
|
|
938
|
+
get sub() {
|
|
939
|
+
return this.#payload.sub;
|
|
940
|
+
}
|
|
941
|
+
set sub(value) {
|
|
942
|
+
this.#payload.sub = value;
|
|
943
|
+
}
|
|
944
|
+
get aud() {
|
|
945
|
+
return this.#payload.aud;
|
|
946
|
+
}
|
|
947
|
+
set aud(value) {
|
|
948
|
+
this.#payload.aud = value;
|
|
949
|
+
}
|
|
950
|
+
set jti(value) {
|
|
951
|
+
this.#payload.jti = value;
|
|
952
|
+
}
|
|
953
|
+
set nbf(value) {
|
|
954
|
+
if (typeof value === "number") {
|
|
955
|
+
this.#payload.nbf = validateInput("setNotBefore", value);
|
|
956
|
+
} else if (value instanceof Date) {
|
|
957
|
+
this.#payload.nbf = validateInput("setNotBefore", epoch(value));
|
|
958
|
+
} else {
|
|
959
|
+
this.#payload.nbf = epoch(/* @__PURE__ */ new Date()) + secs(value);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
set exp(value) {
|
|
963
|
+
if (typeof value === "number") {
|
|
964
|
+
this.#payload.exp = validateInput("setExpirationTime", value);
|
|
965
|
+
} else if (value instanceof Date) {
|
|
966
|
+
this.#payload.exp = validateInput("setExpirationTime", epoch(value));
|
|
967
|
+
} else {
|
|
968
|
+
this.#payload.exp = epoch(/* @__PURE__ */ new Date()) + secs(value);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
set iat(value) {
|
|
972
|
+
if (value === void 0) {
|
|
973
|
+
this.#payload.iat = epoch(/* @__PURE__ */ new Date());
|
|
974
|
+
} else if (value instanceof Date) {
|
|
975
|
+
this.#payload.iat = validateInput("setIssuedAt", epoch(value));
|
|
976
|
+
} else if (typeof value === "string") {
|
|
977
|
+
this.#payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value));
|
|
978
|
+
} else {
|
|
979
|
+
this.#payload.iat = validateInput("setIssuedAt", value);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
var FlattenedSign = class {
|
|
984
|
+
#payload;
|
|
985
|
+
#protectedHeader;
|
|
986
|
+
#unprotectedHeader;
|
|
987
|
+
constructor(payload) {
|
|
988
|
+
if (!(payload instanceof Uint8Array)) {
|
|
989
|
+
throw new TypeError("payload must be an instance of Uint8Array");
|
|
990
|
+
}
|
|
991
|
+
this.#payload = payload;
|
|
992
|
+
}
|
|
993
|
+
setProtectedHeader(protectedHeader) {
|
|
994
|
+
assertNotSet(this.#protectedHeader, "setProtectedHeader");
|
|
995
|
+
this.#protectedHeader = protectedHeader;
|
|
996
|
+
return this;
|
|
997
|
+
}
|
|
998
|
+
setUnprotectedHeader(unprotectedHeader) {
|
|
999
|
+
assertNotSet(this.#unprotectedHeader, "setUnprotectedHeader");
|
|
1000
|
+
this.#unprotectedHeader = unprotectedHeader;
|
|
1001
|
+
return this;
|
|
1002
|
+
}
|
|
1003
|
+
async sign(key, options) {
|
|
1004
|
+
if (!this.#protectedHeader && !this.#unprotectedHeader) {
|
|
1005
|
+
throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()");
|
|
1006
|
+
}
|
|
1007
|
+
if (!isDisjoint(this.#protectedHeader, this.#unprotectedHeader)) {
|
|
1008
|
+
throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
|
|
1009
|
+
}
|
|
1010
|
+
const joseHeader = {
|
|
1011
|
+
...this.#protectedHeader,
|
|
1012
|
+
...this.#unprotectedHeader
|
|
1013
|
+
};
|
|
1014
|
+
const extensions = validateCrit(JWSInvalid, /* @__PURE__ */ new Map([["b64", true]]), options?.crit, this.#protectedHeader, joseHeader);
|
|
1015
|
+
let b64 = true;
|
|
1016
|
+
if (extensions.has("b64")) {
|
|
1017
|
+
b64 = this.#protectedHeader.b64;
|
|
1018
|
+
if (typeof b64 !== "boolean") {
|
|
1019
|
+
throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const { alg } = joseHeader;
|
|
1023
|
+
if (typeof alg !== "string" || !alg) {
|
|
1024
|
+
throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
|
|
1025
|
+
}
|
|
1026
|
+
checkKeyType(alg, key, "sign");
|
|
1027
|
+
let payloadS;
|
|
1028
|
+
let payloadB;
|
|
1029
|
+
if (b64) {
|
|
1030
|
+
payloadS = encode2(this.#payload);
|
|
1031
|
+
payloadB = encode(payloadS);
|
|
1032
|
+
} else {
|
|
1033
|
+
payloadB = this.#payload;
|
|
1034
|
+
payloadS = "";
|
|
1035
|
+
}
|
|
1036
|
+
let protectedHeaderString;
|
|
1037
|
+
let protectedHeaderBytes;
|
|
1038
|
+
if (this.#protectedHeader) {
|
|
1039
|
+
protectedHeaderString = encode2(JSON.stringify(this.#protectedHeader));
|
|
1040
|
+
protectedHeaderBytes = encode(protectedHeaderString);
|
|
1041
|
+
} else {
|
|
1042
|
+
protectedHeaderString = "";
|
|
1043
|
+
protectedHeaderBytes = new Uint8Array();
|
|
1044
|
+
}
|
|
1045
|
+
const data = concat(protectedHeaderBytes, encode("."), payloadB);
|
|
1046
|
+
const k = await normalizeKey(key, alg);
|
|
1047
|
+
const signature = await sign(alg, k, data);
|
|
1048
|
+
const jws = {
|
|
1049
|
+
signature: encode2(signature),
|
|
1050
|
+
payload: payloadS
|
|
1051
|
+
};
|
|
1052
|
+
if (this.#unprotectedHeader) {
|
|
1053
|
+
jws.header = this.#unprotectedHeader;
|
|
1054
|
+
}
|
|
1055
|
+
if (this.#protectedHeader) {
|
|
1056
|
+
jws.protected = protectedHeaderString;
|
|
1057
|
+
}
|
|
1058
|
+
return jws;
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
var CompactSign = class {
|
|
1062
|
+
#flattened;
|
|
1063
|
+
constructor(payload) {
|
|
1064
|
+
this.#flattened = new FlattenedSign(payload);
|
|
1065
|
+
}
|
|
1066
|
+
setProtectedHeader(protectedHeader) {
|
|
1067
|
+
this.#flattened.setProtectedHeader(protectedHeader);
|
|
1068
|
+
return this;
|
|
1069
|
+
}
|
|
1070
|
+
async sign(key, options) {
|
|
1071
|
+
const jws = await this.#flattened.sign(key, options);
|
|
1072
|
+
if (jws.payload === void 0) {
|
|
1073
|
+
throw new TypeError("use the flattened module for creating JWS with b64: false");
|
|
1074
|
+
}
|
|
1075
|
+
return `${jws.protected}.${jws.payload}.${jws.signature}`;
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
var SignJWT = class {
|
|
1079
|
+
#protectedHeader;
|
|
1080
|
+
#jwt;
|
|
1081
|
+
constructor(payload = {}) {
|
|
1082
|
+
this.#jwt = new JWTClaimsBuilder(payload);
|
|
1083
|
+
}
|
|
1084
|
+
setIssuer(issuer) {
|
|
1085
|
+
this.#jwt.iss = issuer;
|
|
1086
|
+
return this;
|
|
1087
|
+
}
|
|
1088
|
+
setSubject(subject) {
|
|
1089
|
+
this.#jwt.sub = subject;
|
|
1090
|
+
return this;
|
|
1091
|
+
}
|
|
1092
|
+
setAudience(audience) {
|
|
1093
|
+
this.#jwt.aud = audience;
|
|
1094
|
+
return this;
|
|
1095
|
+
}
|
|
1096
|
+
setJti(jwtId) {
|
|
1097
|
+
this.#jwt.jti = jwtId;
|
|
1098
|
+
return this;
|
|
1099
|
+
}
|
|
1100
|
+
setNotBefore(input) {
|
|
1101
|
+
this.#jwt.nbf = input;
|
|
1102
|
+
return this;
|
|
1103
|
+
}
|
|
1104
|
+
setExpirationTime(input) {
|
|
1105
|
+
this.#jwt.exp = input;
|
|
1106
|
+
return this;
|
|
1107
|
+
}
|
|
1108
|
+
setIssuedAt(input) {
|
|
1109
|
+
this.#jwt.iat = input;
|
|
1110
|
+
return this;
|
|
1111
|
+
}
|
|
1112
|
+
setProtectedHeader(protectedHeader) {
|
|
1113
|
+
this.#protectedHeader = protectedHeader;
|
|
1114
|
+
return this;
|
|
1115
|
+
}
|
|
1116
|
+
async sign(key, options) {
|
|
1117
|
+
const sig = new CompactSign(this.#jwt.data());
|
|
1118
|
+
sig.setProtectedHeader(this.#protectedHeader);
|
|
1119
|
+
if (Array.isArray(this.#protectedHeader?.crit) && this.#protectedHeader.crit.includes("b64") && this.#protectedHeader.b64 === false) {
|
|
1120
|
+
throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
|
|
1121
|
+
}
|
|
1122
|
+
return sig.sign(key, options);
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
function getModulusLengthOption(options) {
|
|
1126
|
+
const modulusLength = options?.modulusLength ?? 2048;
|
|
1127
|
+
if (typeof modulusLength !== "number" || modulusLength < 2048) {
|
|
1128
|
+
throw new JOSENotSupported("Invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used");
|
|
1129
|
+
}
|
|
1130
|
+
return modulusLength;
|
|
1131
|
+
}
|
|
1132
|
+
async function generateKeyPair(alg, options) {
|
|
1133
|
+
let algorithm;
|
|
1134
|
+
let keyUsages;
|
|
1135
|
+
switch (alg) {
|
|
1136
|
+
case "PS256":
|
|
1137
|
+
case "PS384":
|
|
1138
|
+
case "PS512":
|
|
1139
|
+
algorithm = {
|
|
1140
|
+
name: "RSA-PSS",
|
|
1141
|
+
hash: `SHA-${alg.slice(-3)}`,
|
|
1142
|
+
publicExponent: Uint8Array.of(1, 0, 1),
|
|
1143
|
+
modulusLength: getModulusLengthOption(options)
|
|
1144
|
+
};
|
|
1145
|
+
keyUsages = ["sign", "verify"];
|
|
1146
|
+
break;
|
|
1147
|
+
case "RS256":
|
|
1148
|
+
case "RS384":
|
|
1149
|
+
case "RS512":
|
|
1150
|
+
algorithm = {
|
|
1151
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
1152
|
+
hash: `SHA-${alg.slice(-3)}`,
|
|
1153
|
+
publicExponent: Uint8Array.of(1, 0, 1),
|
|
1154
|
+
modulusLength: getModulusLengthOption(options)
|
|
1155
|
+
};
|
|
1156
|
+
keyUsages = ["sign", "verify"];
|
|
1157
|
+
break;
|
|
1158
|
+
case "RSA-OAEP":
|
|
1159
|
+
case "RSA-OAEP-256":
|
|
1160
|
+
case "RSA-OAEP-384":
|
|
1161
|
+
case "RSA-OAEP-512":
|
|
1162
|
+
algorithm = {
|
|
1163
|
+
name: "RSA-OAEP",
|
|
1164
|
+
hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`,
|
|
1165
|
+
publicExponent: Uint8Array.of(1, 0, 1),
|
|
1166
|
+
modulusLength: getModulusLengthOption(options)
|
|
1167
|
+
};
|
|
1168
|
+
keyUsages = ["decrypt", "unwrapKey", "encrypt", "wrapKey"];
|
|
1169
|
+
break;
|
|
1170
|
+
case "ES256":
|
|
1171
|
+
algorithm = { name: "ECDSA", namedCurve: "P-256" };
|
|
1172
|
+
keyUsages = ["sign", "verify"];
|
|
1173
|
+
break;
|
|
1174
|
+
case "ES384":
|
|
1175
|
+
algorithm = { name: "ECDSA", namedCurve: "P-384" };
|
|
1176
|
+
keyUsages = ["sign", "verify"];
|
|
1177
|
+
break;
|
|
1178
|
+
case "ES512":
|
|
1179
|
+
algorithm = { name: "ECDSA", namedCurve: "P-521" };
|
|
1180
|
+
keyUsages = ["sign", "verify"];
|
|
1181
|
+
break;
|
|
1182
|
+
case "Ed25519":
|
|
1183
|
+
case "EdDSA": {
|
|
1184
|
+
keyUsages = ["sign", "verify"];
|
|
1185
|
+
algorithm = { name: "Ed25519" };
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
case "ML-DSA-44":
|
|
1189
|
+
case "ML-DSA-65":
|
|
1190
|
+
case "ML-DSA-87": {
|
|
1191
|
+
keyUsages = ["sign", "verify"];
|
|
1192
|
+
algorithm = { name: alg };
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1195
|
+
case "ECDH-ES":
|
|
1196
|
+
case "ECDH-ES+A128KW":
|
|
1197
|
+
case "ECDH-ES+A192KW":
|
|
1198
|
+
case "ECDH-ES+A256KW": {
|
|
1199
|
+
keyUsages = ["deriveBits"];
|
|
1200
|
+
const crv = options?.crv ?? "P-256";
|
|
1201
|
+
switch (crv) {
|
|
1202
|
+
case "P-256":
|
|
1203
|
+
case "P-384":
|
|
1204
|
+
case "P-521": {
|
|
1205
|
+
algorithm = { name: "ECDH", namedCurve: crv };
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
case "X25519":
|
|
1209
|
+
algorithm = { name: "X25519" };
|
|
1210
|
+
break;
|
|
1211
|
+
default:
|
|
1212
|
+
throw new JOSENotSupported("Invalid or unsupported crv option provided, supported values are P-256, P-384, P-521, and X25519");
|
|
1213
|
+
}
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
default:
|
|
1217
|
+
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
|
|
1218
|
+
}
|
|
1219
|
+
return crypto.subtle.generateKey(algorithm, options?.extractable ?? false, keyUsages);
|
|
1220
|
+
}
|
|
1221
|
+
var CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
1222
|
+
var ASSERTION_TTL_SECONDS = 60;
|
|
1223
|
+
var DPOP_PROOF_TYP = "dpop+jwt";
|
|
1224
|
+
function nowSeconds() {
|
|
1225
|
+
return Math.floor(Date.now() / 1e3);
|
|
1226
|
+
}
|
|
1227
|
+
function accessTokenHash(accessToken) {
|
|
1228
|
+
return base64url_exports.encode(createHash("sha256").update(accessToken, "ascii").digest());
|
|
1229
|
+
}
|
|
1230
|
+
function publicDpopJwk(publicKeyJwk) {
|
|
1231
|
+
return {
|
|
1232
|
+
...publicKeyJwk,
|
|
1233
|
+
alg: "ES256",
|
|
1234
|
+
use: "sig"
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
async function signRuntimeAssertion(params) {
|
|
1238
|
+
const now = nowSeconds();
|
|
1239
|
+
return new SignJWT({ jti: randomUUID() }).setProtectedHeader({ alg: "ES256", kid: params.key.keyId }).setIssuer(params.runtimeIdentityId).setSubject(params.runtimeIdentityId).setAudience(params.audience).setIssuedAt(now).setExpirationTime(now + ASSERTION_TTL_SECONDS).sign(params.key.privateKey);
|
|
1240
|
+
}
|
|
1241
|
+
async function signDpopProof(params) {
|
|
1242
|
+
const now = nowSeconds();
|
|
1243
|
+
const payload = {
|
|
1244
|
+
htm: params.method,
|
|
1245
|
+
htu: params.htu,
|
|
1246
|
+
jti: randomUUID()
|
|
1247
|
+
};
|
|
1248
|
+
if (params.accessToken) {
|
|
1249
|
+
payload.ath = accessTokenHash(params.accessToken);
|
|
1250
|
+
}
|
|
1251
|
+
return new SignJWT(payload).setProtectedHeader({
|
|
1252
|
+
typ: DPOP_PROOF_TYP,
|
|
1253
|
+
alg: "ES256",
|
|
1254
|
+
jwk: publicDpopJwk(params.key.publicKeyJwk)
|
|
1255
|
+
}).setIssuedAt(now).sign(params.key.privateKey);
|
|
1256
|
+
}
|
|
1257
|
+
var PRIVATE_KEY_FILE = "private-key.jwk";
|
|
1258
|
+
var RUNTIME_METADATA_FILE = "runtime.json";
|
|
1259
|
+
var PRIVATE_JWK_FIELDS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth", "k"]);
|
|
1260
|
+
function isRecord3(value) {
|
|
1261
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1262
|
+
}
|
|
1263
|
+
function runtimeStatePaths(stateDir) {
|
|
1264
|
+
const root = path.join(stateDir, "oct8-secrets");
|
|
1265
|
+
return {
|
|
1266
|
+
root,
|
|
1267
|
+
privateKeyPath: path.join(root, PRIVATE_KEY_FILE),
|
|
1268
|
+
runtimeMetadataPath: path.join(root, RUNTIME_METADATA_FILE)
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
async function ensureStateRoot(root) {
|
|
1272
|
+
await fs.mkdir(root, { recursive: true, mode: 448 });
|
|
1273
|
+
await fs.chmod(root, 448).catch(() => void 0);
|
|
1274
|
+
}
|
|
1275
|
+
async function readJsonFile(pathname) {
|
|
1276
|
+
try {
|
|
1277
|
+
return JSON.parse(await fs.readFile(pathname, "utf8"));
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
1280
|
+
return void 0;
|
|
1281
|
+
}
|
|
1282
|
+
throw new Oct8SecretsError(`Failed to read runtime state file ${path.basename(pathname)}.`, "STATE_READ_FAILED");
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function isNodeError(error) {
|
|
1286
|
+
return error instanceof Error && "code" in error;
|
|
1287
|
+
}
|
|
1288
|
+
async function createJsonFileExclusive(pathname, value) {
|
|
1289
|
+
let handle;
|
|
1290
|
+
try {
|
|
1291
|
+
handle = await fs.open(pathname, "wx", 384);
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
if (isNodeError(error) && error.code === "EEXIST") {
|
|
1294
|
+
return "exists";
|
|
1295
|
+
}
|
|
1296
|
+
throw new Oct8SecretsError(`Failed to create runtime state file ${path.basename(pathname)}.`, "STATE_WRITE_FAILED");
|
|
1297
|
+
}
|
|
1298
|
+
try {
|
|
1299
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}
|
|
1300
|
+
`, "utf8");
|
|
1301
|
+
} finally {
|
|
1302
|
+
await handle.close();
|
|
1303
|
+
}
|
|
1304
|
+
await fs.chmod(pathname, 384).catch(() => void 0);
|
|
1305
|
+
return "created";
|
|
1306
|
+
}
|
|
1307
|
+
function publicJwkFromPrivate(privateJwk) {
|
|
1308
|
+
const publicJwk = Object.fromEntries(
|
|
1309
|
+
Object.entries(privateJwk).filter(([key]) => !PRIVATE_JWK_FIELDS.has(key))
|
|
1310
|
+
);
|
|
1311
|
+
return {
|
|
1312
|
+
...publicJwk,
|
|
1313
|
+
kty: "EC",
|
|
1314
|
+
crv: "P-256",
|
|
1315
|
+
kid: privateJwk.kid,
|
|
1316
|
+
alg: "ES256",
|
|
1317
|
+
use: "sig"
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
function assertPrivateRuntimeJwk(value) {
|
|
1321
|
+
if (!isRecord3(value)) {
|
|
1322
|
+
throw new Oct8SecretsError("Runtime private key state must be a JSON object.", "INVALID_STATE");
|
|
1323
|
+
}
|
|
1324
|
+
if (value.kty !== "EC" || value.crv !== "P-256" || value.alg !== "ES256" || value.use !== "sig" || typeof value.kid !== "string" || typeof value.d !== "string" || typeof value.x !== "string" || typeof value.y !== "string") {
|
|
1325
|
+
throw new Oct8SecretsError("Runtime private key state is not an ES256 private JWK.", "INVALID_STATE");
|
|
1326
|
+
}
|
|
1327
|
+
return value;
|
|
1328
|
+
}
|
|
1329
|
+
async function generatePrivateRuntimeJwk(keyId) {
|
|
1330
|
+
const { privateKey } = await generateKeyPair("ES256", { extractable: true });
|
|
1331
|
+
const exported = await exportJWK(privateKey);
|
|
1332
|
+
return {
|
|
1333
|
+
...exported,
|
|
1334
|
+
kty: "EC",
|
|
1335
|
+
crv: "P-256",
|
|
1336
|
+
kid: keyId,
|
|
1337
|
+
alg: "ES256",
|
|
1338
|
+
use: "sig"
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
async function loadRuntimeKey(env) {
|
|
1342
|
+
const paths = runtimeStatePaths(env.stateDir);
|
|
1343
|
+
await ensureStateRoot(paths.root);
|
|
1344
|
+
const stored = await readJsonFile(paths.privateKeyPath);
|
|
1345
|
+
let privateJwk;
|
|
1346
|
+
if (stored === void 0) {
|
|
1347
|
+
privateJwk = await generatePrivateRuntimeJwk(env.keyId ?? `oct8-runtime-${randomUUID2()}`);
|
|
1348
|
+
const result = await createJsonFileExclusive(paths.privateKeyPath, privateJwk);
|
|
1349
|
+
if (result === "exists") {
|
|
1350
|
+
privateJwk = assertPrivateRuntimeJwk(await readJsonFile(paths.privateKeyPath));
|
|
1351
|
+
}
|
|
1352
|
+
} else {
|
|
1353
|
+
privateJwk = assertPrivateRuntimeJwk(stored);
|
|
1354
|
+
}
|
|
1355
|
+
if (env.keyId && privateJwk.kid !== env.keyId) {
|
|
1356
|
+
throw new Oct8SecretsError("OCT8_RUNTIME_KEY_ID does not match the stored runtime key.", "INVALID_STATE");
|
|
1357
|
+
}
|
|
1358
|
+
const privateKey = await importJWK(privateJwk, "ES256");
|
|
1359
|
+
return {
|
|
1360
|
+
keyId: privateJwk.kid,
|
|
1361
|
+
algorithm: "ES256",
|
|
1362
|
+
privateKey,
|
|
1363
|
+
publicKeyJwk: publicJwkFromPrivate(privateJwk)
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/runtime-auth.ts
|
|
1368
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
1369
|
+
var TOKEN_REFRESH_SKEW_MS = 3e4;
|
|
1370
|
+
var PARTICIPATION_CONTEXT_SCOPE = "participation-context:read";
|
|
1371
|
+
function runtimeUrl(baseUrl, path3) {
|
|
1372
|
+
const normalizedPath = path3.startsWith("/") ? path3 : `/${path3}`;
|
|
1373
|
+
return `${baseUrl.replace(/\/+$/, "")}${normalizedPath}`;
|
|
1374
|
+
}
|
|
1375
|
+
function participationContextHtu(tokenIssuer, requestUrl) {
|
|
1376
|
+
const parsed = new URL(requestUrl);
|
|
1377
|
+
return runtimeUrl(tokenIssuer, parsed.pathname);
|
|
1378
|
+
}
|
|
1379
|
+
function isRecord(value) {
|
|
1380
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1381
|
+
}
|
|
1382
|
+
async function parseJson(response) {
|
|
1383
|
+
try {
|
|
1384
|
+
return await response.json();
|
|
1385
|
+
} catch {
|
|
1386
|
+
return void 0;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
async function fetchWithTimeout(fetchImpl, url, init) {
|
|
1390
|
+
const controller = new AbortController();
|
|
1391
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
1392
|
+
try {
|
|
1393
|
+
return await fetchImpl(url, {
|
|
1394
|
+
...init,
|
|
1395
|
+
signal: controller.signal
|
|
1396
|
+
});
|
|
1397
|
+
} finally {
|
|
1398
|
+
clearTimeout(timeout);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function apiErrorMessage(response) {
|
|
1402
|
+
const statusText = response.statusText ? ` ${response.statusText}` : "";
|
|
1403
|
+
return `Runtime token request failed with ${response.status}${statusText}.`;
|
|
1404
|
+
}
|
|
1405
|
+
var RuntimeParticipationContextAuthClient = class {
|
|
1406
|
+
constructor(options = {}) {
|
|
1407
|
+
this.options = options;
|
|
1408
|
+
}
|
|
1409
|
+
runtimeEnv;
|
|
1410
|
+
keyPromise;
|
|
1411
|
+
token;
|
|
1412
|
+
async authorizationHeaders(requestUrl) {
|
|
1413
|
+
const accessToken = await this.accessToken();
|
|
1414
|
+
const env = this.env();
|
|
1415
|
+
const key = await this.key();
|
|
1416
|
+
const dpopProof = await signDpopProof({
|
|
1417
|
+
key,
|
|
1418
|
+
method: "GET",
|
|
1419
|
+
htu: participationContextHtu(env.tokenIssuer, requestUrl),
|
|
1420
|
+
accessToken
|
|
1421
|
+
});
|
|
1422
|
+
return {
|
|
1423
|
+
Authorization: `DPoP ${accessToken}`,
|
|
1424
|
+
DPoP: dpopProof
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
env() {
|
|
1428
|
+
this.runtimeEnv ??= readRuntimeSecretsEnv(this.options.env);
|
|
1429
|
+
return this.runtimeEnv;
|
|
1430
|
+
}
|
|
1431
|
+
key() {
|
|
1432
|
+
this.keyPromise ??= loadRuntimeKey(this.env());
|
|
1433
|
+
return this.keyPromise;
|
|
1434
|
+
}
|
|
1435
|
+
now() {
|
|
1436
|
+
return this.options.now?.() ?? Date.now();
|
|
1437
|
+
}
|
|
1438
|
+
async accessToken() {
|
|
1439
|
+
const now = this.now();
|
|
1440
|
+
if (this.token && this.token.expiresAtMs - TOKEN_REFRESH_SKEW_MS > now) {
|
|
1441
|
+
return this.token.accessToken;
|
|
1442
|
+
}
|
|
1443
|
+
const env = this.env();
|
|
1444
|
+
const key = await this.key();
|
|
1445
|
+
const endpoint = runtimeUrl(env.apiUrl, "/v1/runtime/token");
|
|
1446
|
+
const audience = runtimeUrl(env.tokenIssuer, "/v1/runtime/token");
|
|
1447
|
+
const clientAssertion = await signRuntimeAssertion({
|
|
1448
|
+
key,
|
|
1449
|
+
runtimeIdentityId: env.runtimeIdentityId,
|
|
1450
|
+
audience
|
|
1451
|
+
});
|
|
1452
|
+
const dpopProof = await signDpopProof({
|
|
1453
|
+
key,
|
|
1454
|
+
method: "POST",
|
|
1455
|
+
htu: audience
|
|
1456
|
+
});
|
|
1457
|
+
const form = new URLSearchParams({
|
|
1458
|
+
grant_type: "client_credentials",
|
|
1459
|
+
client_id: env.runtimeIdentityId,
|
|
1460
|
+
client_assertion_type: CLIENT_ASSERTION_TYPE,
|
|
1461
|
+
client_assertion: clientAssertion,
|
|
1462
|
+
scope: PARTICIPATION_CONTEXT_SCOPE
|
|
1463
|
+
});
|
|
1464
|
+
const response = await fetchWithTimeout(this.options.fetchImpl ?? fetch, endpoint, {
|
|
1465
|
+
method: "POST",
|
|
1466
|
+
headers: {
|
|
1467
|
+
Accept: "application/json",
|
|
1468
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1469
|
+
DPoP: dpopProof
|
|
1470
|
+
},
|
|
1471
|
+
body: form.toString()
|
|
1472
|
+
});
|
|
1473
|
+
const payload = await parseJson(response);
|
|
1474
|
+
if (!response.ok) {
|
|
1475
|
+
throw new Error(apiErrorMessage(response));
|
|
1476
|
+
}
|
|
1477
|
+
if (!isRecord(payload) || typeof payload.access_token !== "string" || payload.token_type !== "DPoP" || payload.scope !== PARTICIPATION_CONTEXT_SCOPE || typeof payload.expires_in !== "number" || !Number.isFinite(payload.expires_in) || payload.expires_in <= 0) {
|
|
1478
|
+
throw new Error("Runtime token response was invalid.");
|
|
1479
|
+
}
|
|
1480
|
+
this.token = {
|
|
1481
|
+
accessToken: payload.access_token,
|
|
1482
|
+
expiresAtMs: now + payload.expires_in * 1e3
|
|
1483
|
+
};
|
|
1484
|
+
return payload.access_token;
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
function createRuntimeParticipationContextAuthClient(options) {
|
|
1488
|
+
return new RuntimeParticipationContextAuthClient(options);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// src/context.ts
|
|
1492
|
+
var CONTEXT_FETCH_TIMEOUT_MS = 5e3;
|
|
1493
|
+
function isRecord2(value) {
|
|
1494
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1495
|
+
}
|
|
1496
|
+
function readString(value) {
|
|
1497
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
1498
|
+
}
|
|
1499
|
+
function readNames(value) {
|
|
1500
|
+
if (!Array.isArray(value)) {
|
|
1501
|
+
return [];
|
|
1502
|
+
}
|
|
1503
|
+
const names = value.map(readString).filter((entry) => Boolean(entry));
|
|
1504
|
+
return [...new Set(names)];
|
|
1505
|
+
}
|
|
1506
|
+
function parseIdentity(value) {
|
|
1507
|
+
if (!isRecord2(value)) {
|
|
1508
|
+
return void 0;
|
|
1509
|
+
}
|
|
1510
|
+
const id = readString(value.id);
|
|
1511
|
+
const names = readNames(value.names);
|
|
1512
|
+
if (!id || names.length === 0) {
|
|
1513
|
+
return void 0;
|
|
1514
|
+
}
|
|
1515
|
+
const roleSummary = readString(value.roleSummary);
|
|
1516
|
+
return {
|
|
1517
|
+
id,
|
|
1518
|
+
names,
|
|
1519
|
+
...roleSummary ? { roleSummary } : {}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function parseParticipationContext(payload) {
|
|
1523
|
+
const candidate = isRecord2(payload) && isRecord2(payload.data) ? payload.data : payload;
|
|
1524
|
+
if (!isRecord2(candidate)) {
|
|
1525
|
+
throw new Error("participation context response must be an object");
|
|
1526
|
+
}
|
|
1527
|
+
const self = parseIdentity(candidate.self);
|
|
1528
|
+
if (!self) {
|
|
1529
|
+
throw new Error("participation context response is missing self identity");
|
|
1530
|
+
}
|
|
1531
|
+
const coworkers = Array.isArray(candidate.coworkers) ? candidate.coworkers.map(parseIdentity).filter((entry) => Boolean(entry)).filter((entry) => entry.id !== self.id) : [];
|
|
1532
|
+
return { self, coworkers };
|
|
1533
|
+
}
|
|
1534
|
+
function buildParticipationContextUrl(config) {
|
|
1535
|
+
if (!config.baseUrl) {
|
|
1536
|
+
throw new Error("platform context baseUrl is not configured");
|
|
1537
|
+
}
|
|
1538
|
+
if (!config.coworkerId) {
|
|
1539
|
+
throw new Error("platform context coworkerId is not configured");
|
|
1540
|
+
}
|
|
1541
|
+
const path3 = config.endpointPath.replace(
|
|
1542
|
+
"{coworkerId}",
|
|
1543
|
+
encodeURIComponent(config.coworkerId)
|
|
1544
|
+
);
|
|
1545
|
+
return new URL(path3, config.baseUrl.endsWith("/") ? config.baseUrl : `${config.baseUrl}/`).href;
|
|
1546
|
+
}
|
|
1547
|
+
async function fetchJsonWithTimeout(fetchImpl, url, init, timeoutMs) {
|
|
1548
|
+
const controller = new AbortController();
|
|
1549
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1550
|
+
try {
|
|
1551
|
+
const response = await fetchImpl(url, {
|
|
1552
|
+
...init,
|
|
1553
|
+
signal: controller.signal
|
|
1554
|
+
});
|
|
1555
|
+
if (!response.ok) {
|
|
1556
|
+
throw new Error(`platform context request failed with ${response.status}`);
|
|
1557
|
+
}
|
|
1558
|
+
return await response.json();
|
|
1559
|
+
} finally {
|
|
1560
|
+
clearTimeout(timeout);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
function createStaticParticipationContextProvider(context) {
|
|
1564
|
+
return {
|
|
1565
|
+
async load() {
|
|
1566
|
+
if (!context) {
|
|
1567
|
+
throw new Error("static participation context is not configured");
|
|
1568
|
+
}
|
|
1569
|
+
return context;
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function createPlatformParticipationContextProvider(config, options = {}) {
|
|
1574
|
+
let cached;
|
|
1575
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1576
|
+
const runtimeAuthClient = options.runtimeAuthClient ?? createRuntimeParticipationContextAuthClient();
|
|
1577
|
+
return {
|
|
1578
|
+
async load() {
|
|
1579
|
+
const now = Date.now();
|
|
1580
|
+
if (cached && cached.expiresAt > now) {
|
|
1581
|
+
return cached.context;
|
|
1582
|
+
}
|
|
1583
|
+
const url = buildParticipationContextUrl(config.platform);
|
|
1584
|
+
const headers = {
|
|
1585
|
+
Accept: "application/json"
|
|
1586
|
+
};
|
|
1587
|
+
if (config.platform.authMode === "static-token") {
|
|
1588
|
+
if (!config.platform.token) {
|
|
1589
|
+
throw new Error("static platform context token is not configured");
|
|
1590
|
+
}
|
|
1591
|
+
headers.Authorization = `Bearer ${config.platform.token}`;
|
|
1592
|
+
if (config.platform.coworkerId) {
|
|
1593
|
+
headers["X-Velanir-Coworker-Id"] = config.platform.coworkerId;
|
|
1594
|
+
}
|
|
1595
|
+
} else {
|
|
1596
|
+
Object.assign(headers, await runtimeAuthClient.authorizationHeaders(url));
|
|
1597
|
+
}
|
|
1598
|
+
const payload = await fetchJsonWithTimeout(
|
|
1599
|
+
fetchImpl,
|
|
1600
|
+
url,
|
|
1601
|
+
{ method: "GET", headers },
|
|
1602
|
+
CONTEXT_FETCH_TIMEOUT_MS
|
|
1603
|
+
);
|
|
1604
|
+
const context = parseParticipationContext(payload);
|
|
1605
|
+
cached = {
|
|
1606
|
+
context,
|
|
1607
|
+
expiresAt: now + config.context.refreshMs
|
|
1608
|
+
};
|
|
1609
|
+
return context;
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
function createParticipationContextProvider(config) {
|
|
1614
|
+
if (config.context.source === "static") {
|
|
1615
|
+
return createStaticParticipationContextProvider(config.staticContext);
|
|
1616
|
+
}
|
|
1617
|
+
return createPlatformParticipationContextProvider(config);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/config.ts
|
|
1621
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
1622
|
+
var DEFAULT_MAX_OUTPUT_TOKENS = 32;
|
|
1623
|
+
var DEFAULT_CLASSIFIER_THRESHOLD = 0.7;
|
|
1624
|
+
var DEFAULT_MAX_MESSAGES = 5;
|
|
1625
|
+
var DEFAULT_REFRESH_MS = 5 * 60 * 1e3;
|
|
1626
|
+
var DEFAULT_PLATFORM_ENDPOINT_PATH = "/v1/runtime/coworkers/{coworkerId}/participation-context";
|
|
1627
|
+
function isRecord4(value) {
|
|
1628
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1629
|
+
}
|
|
1630
|
+
function readString2(value) {
|
|
1631
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
1632
|
+
}
|
|
1633
|
+
function readPositiveInteger(value, fallback) {
|
|
1634
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
|
|
1635
|
+
}
|
|
1636
|
+
function readRatio(value, fallback) {
|
|
1637
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : fallback;
|
|
1638
|
+
}
|
|
1639
|
+
function normalizeMode(value) {
|
|
1640
|
+
return value === "enforce" ? "enforce" : "shadow";
|
|
1641
|
+
}
|
|
1642
|
+
function normalizeContextSource(value) {
|
|
1643
|
+
return value === "static" ? "static" : "platform";
|
|
1644
|
+
}
|
|
1645
|
+
function normalizePlatformAuthMode(value) {
|
|
1646
|
+
return value === "static-token" ? "static-token" : "runtime";
|
|
1647
|
+
}
|
|
1648
|
+
function normalizeClassifier(value) {
|
|
1649
|
+
const record = isRecord4(value) ? value : {};
|
|
1650
|
+
return {
|
|
1651
|
+
provider: readString2(record.provider),
|
|
1652
|
+
model: readString2(record.model),
|
|
1653
|
+
authProfileId: readString2(record.authProfileId),
|
|
1654
|
+
timeoutMs: readPositiveInteger(record.timeoutMs, DEFAULT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS,
|
|
1655
|
+
maxOutputTokens: readPositiveInteger(record.maxOutputTokens, DEFAULT_MAX_OUTPUT_TOKENS) || DEFAULT_MAX_OUTPUT_TOKENS,
|
|
1656
|
+
threshold: readRatio(record.threshold, DEFAULT_CLASSIFIER_THRESHOLD)
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function normalizePlatform(value, env) {
|
|
1660
|
+
const record = isRecord4(value) ? value : {};
|
|
1661
|
+
const authMode = normalizePlatformAuthMode(record.authMode);
|
|
1662
|
+
return {
|
|
1663
|
+
authMode,
|
|
1664
|
+
baseUrl: readString2(record.baseUrl) ?? readString2(env.OCT8_API_URL),
|
|
1665
|
+
coworkerId: readString2(record.coworkerId) ?? readString2(env.OCT8_COWORKER_ID),
|
|
1666
|
+
token: authMode === "static-token" ? readString2(record.token) ?? readString2(env.OCT8_PARTICIPATION_CONTEXT_TOKEN) : void 0,
|
|
1667
|
+
endpointPath: readString2(record.endpointPath) ?? DEFAULT_PLATFORM_ENDPOINT_PATH
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
function normalizeNames(value) {
|
|
1671
|
+
if (!Array.isArray(value)) {
|
|
1672
|
+
return [];
|
|
1673
|
+
}
|
|
1674
|
+
const names = value.map(readString2).filter((entry) => Boolean(entry));
|
|
1675
|
+
return [...new Set(names)];
|
|
1676
|
+
}
|
|
1677
|
+
function normalizeIdentity(value) {
|
|
1678
|
+
if (!isRecord4(value)) {
|
|
1679
|
+
return void 0;
|
|
1680
|
+
}
|
|
1681
|
+
const id = readString2(value.id);
|
|
1682
|
+
const names = normalizeNames(value.names);
|
|
1683
|
+
if (!id || names.length === 0) {
|
|
1684
|
+
return void 0;
|
|
1685
|
+
}
|
|
1686
|
+
const roleSummary = readString2(value.roleSummary);
|
|
1687
|
+
return {
|
|
1688
|
+
id,
|
|
1689
|
+
names,
|
|
1690
|
+
...roleSummary ? { roleSummary } : {}
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function normalizeStaticContext(value) {
|
|
1694
|
+
if (!isRecord4(value)) {
|
|
1695
|
+
return void 0;
|
|
1696
|
+
}
|
|
1697
|
+
const self = normalizeIdentity(value.self);
|
|
1698
|
+
if (!self) {
|
|
1699
|
+
return void 0;
|
|
1700
|
+
}
|
|
1701
|
+
const coworkers = Array.isArray(value.coworkers) ? value.coworkers.map(normalizeIdentity).filter((entry) => Boolean(entry)) : [];
|
|
1702
|
+
return {
|
|
1703
|
+
self,
|
|
1704
|
+
coworkers: coworkers.filter((entry) => entry.id !== self.id)
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function normalizeConfig(pluginConfig, env = process.env) {
|
|
1708
|
+
const record = isRecord4(pluginConfig) ? pluginConfig : {};
|
|
1709
|
+
const contextRecord = isRecord4(record.context) ? record.context : {};
|
|
1710
|
+
const loggingRecord = isRecord4(record.logging) ? record.logging : {};
|
|
1711
|
+
return {
|
|
1712
|
+
mode: normalizeMode(record.mode),
|
|
1713
|
+
classifier: normalizeClassifier(record.classifier),
|
|
1714
|
+
context: {
|
|
1715
|
+
source: normalizeContextSource(contextRecord.source),
|
|
1716
|
+
maxMessages: readPositiveInteger(contextRecord.maxMessages, DEFAULT_MAX_MESSAGES),
|
|
1717
|
+
refreshMs: readPositiveInteger(contextRecord.refreshMs, DEFAULT_REFRESH_MS) || DEFAULT_REFRESH_MS
|
|
1718
|
+
},
|
|
1719
|
+
platform: normalizePlatform(record.platform, env),
|
|
1720
|
+
staticContext: normalizeStaticContext(record.staticContext),
|
|
1721
|
+
logging: {
|
|
1722
|
+
decisions: loggingRecord.decisions !== false,
|
|
1723
|
+
includeContent: loggingRecord.includeContent === true,
|
|
1724
|
+
classifierDebug: loggingRecord.classifierDebug === true
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/classifier.ts
|
|
1730
|
+
import { createHash as createHash2 } from "crypto";
|
|
1731
|
+
|
|
1732
|
+
// src/embedded-model.ts
|
|
1733
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1734
|
+
import fs2 from "fs/promises";
|
|
1735
|
+
import os from "os";
|
|
1736
|
+
import path2 from "path";
|
|
1737
|
+
function collectAssistantText(result) {
|
|
1738
|
+
if (typeof result === "string") {
|
|
1739
|
+
return result.trim();
|
|
1740
|
+
}
|
|
1741
|
+
if (!result || typeof result !== "object") {
|
|
1742
|
+
return "";
|
|
1743
|
+
}
|
|
1744
|
+
const payloads = result.payloads;
|
|
1745
|
+
if (!Array.isArray(payloads)) {
|
|
1746
|
+
return "";
|
|
1747
|
+
}
|
|
1748
|
+
return payloads.map((payload) => {
|
|
1749
|
+
if (!payload || typeof payload !== "object") {
|
|
1750
|
+
return "";
|
|
1751
|
+
}
|
|
1752
|
+
const record = payload;
|
|
1753
|
+
return record.isError === true || typeof record.text !== "string" ? "" : record.text;
|
|
1754
|
+
}).filter(Boolean).join("\n").trim();
|
|
1755
|
+
}
|
|
1756
|
+
async function runEmbeddedClassifierModel(params) {
|
|
1757
|
+
const agentRuntime = params.api.runtime?.agent;
|
|
1758
|
+
const runEmbeddedAgent = agentRuntime?.runEmbeddedAgent ?? agentRuntime?.runEmbeddedPiAgent;
|
|
1759
|
+
if (typeof runEmbeddedAgent !== "function") {
|
|
1760
|
+
throw new Error("OpenClaw embedded agent runtime is unavailable");
|
|
1761
|
+
}
|
|
1762
|
+
const provider = params.config.classifier.provider;
|
|
1763
|
+
const model = params.config.classifier.model;
|
|
1764
|
+
if (!provider || !model) {
|
|
1765
|
+
throw new Error("classifier provider/model is not configured");
|
|
1766
|
+
}
|
|
1767
|
+
let tmpDir;
|
|
1768
|
+
try {
|
|
1769
|
+
tmpDir = await fs2.mkdtemp(path2.join(os.tmpdir(), "velanir-participation-gate-"));
|
|
1770
|
+
const runId = `participation-gate-${randomUUID3()}`;
|
|
1771
|
+
const result = await runEmbeddedAgent({
|
|
1772
|
+
sessionId: runId,
|
|
1773
|
+
sessionFile: path2.join(tmpDir, "session.json"),
|
|
1774
|
+
workspaceDir: agentRuntime?.resolveAgentWorkspaceDir?.(params.api.config) ?? process.cwd(),
|
|
1775
|
+
config: params.api.config,
|
|
1776
|
+
prompt: params.prompt,
|
|
1777
|
+
timeoutMs: params.config.classifier.timeoutMs,
|
|
1778
|
+
runId,
|
|
1779
|
+
modelRun: true,
|
|
1780
|
+
provider,
|
|
1781
|
+
model,
|
|
1782
|
+
authProfileId: params.config.classifier.authProfileId,
|
|
1783
|
+
authProfileIdSource: params.config.classifier.authProfileId ? "user" : "auto",
|
|
1784
|
+
explicitStreamParamsOnly: true,
|
|
1785
|
+
streamParams: {
|
|
1786
|
+
maxTokens: params.config.classifier.maxOutputTokens
|
|
1787
|
+
},
|
|
1788
|
+
disableTools: true,
|
|
1789
|
+
disableMessageTool: true,
|
|
1790
|
+
thinkLevel: "off",
|
|
1791
|
+
reasoningLevel: "off",
|
|
1792
|
+
verboseLevel: "off",
|
|
1793
|
+
fastMode: true,
|
|
1794
|
+
bootstrapContextMode: "lightweight"
|
|
1795
|
+
});
|
|
1796
|
+
const text = collectAssistantText(result);
|
|
1797
|
+
if (!text) {
|
|
1798
|
+
throw new Error("classifier returned empty output");
|
|
1799
|
+
}
|
|
1800
|
+
return text;
|
|
1801
|
+
} finally {
|
|
1802
|
+
if (tmpDir) {
|
|
1803
|
+
await fs2.rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// src/classifier.ts
|
|
1809
|
+
var CLASSIFIER_PROMPT_VERSION = "participation-score-v2";
|
|
1810
|
+
function stripCodeFences(value) {
|
|
1811
|
+
const trimmed = value.trim();
|
|
1812
|
+
const match = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(trimmed);
|
|
1813
|
+
return (match?.[1] ?? trimmed).trim();
|
|
1814
|
+
}
|
|
1815
|
+
function hashValue(value) {
|
|
1816
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
1817
|
+
}
|
|
1818
|
+
function stableStringify(value) {
|
|
1819
|
+
if (Array.isArray(value)) {
|
|
1820
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
1821
|
+
}
|
|
1822
|
+
if (value && typeof value === "object") {
|
|
1823
|
+
const record = value;
|
|
1824
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
1825
|
+
}
|
|
1826
|
+
return JSON.stringify(value);
|
|
1827
|
+
}
|
|
1828
|
+
function trimForPrompt(value, maxLength = 1200) {
|
|
1829
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
1830
|
+
if (normalized.length <= maxLength) {
|
|
1831
|
+
return normalized;
|
|
1832
|
+
}
|
|
1833
|
+
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
1834
|
+
}
|
|
1835
|
+
function formatIdentity(identity) {
|
|
1836
|
+
const role = identity.roleSummary ? ` Role: ${identity.roleSummary}` : "";
|
|
1837
|
+
return `- ${identity.names.join(", ")}.${role}`;
|
|
1838
|
+
}
|
|
1839
|
+
function buildContextSection(context) {
|
|
1840
|
+
const coworkers = context.coworkers.length ? context.coworkers.map(formatIdentity).join("\n") : "- None known.";
|
|
1841
|
+
return [
|
|
1842
|
+
"This digital coworker:",
|
|
1843
|
+
formatIdentity(context.self),
|
|
1844
|
+
"",
|
|
1845
|
+
"Other digital coworkers in this organization:",
|
|
1846
|
+
coworkers
|
|
1847
|
+
].join("\n");
|
|
1848
|
+
}
|
|
1849
|
+
function formatFact(name, value) {
|
|
1850
|
+
if (value === void 0 || value === null || value === "") {
|
|
1851
|
+
return void 0;
|
|
1852
|
+
}
|
|
1853
|
+
return `- ${name}: ${String(value)}`;
|
|
1854
|
+
}
|
|
1855
|
+
function buildConversationSection(input) {
|
|
1856
|
+
const facts = [
|
|
1857
|
+
formatFact("provider", input.conversation.provider),
|
|
1858
|
+
formatFact("surface", input.conversation.surface),
|
|
1859
|
+
formatFact("chatType", input.conversation.chatType),
|
|
1860
|
+
formatFact("channelId", input.conversation.channelId),
|
|
1861
|
+
formatFact("conversationId", input.conversation.conversationId),
|
|
1862
|
+
formatFact("conversationLabel", input.conversation.conversationLabel),
|
|
1863
|
+
formatFact("groupSubject", input.conversation.groupSubject),
|
|
1864
|
+
formatFact("sessionKey", input.conversation.sessionKey),
|
|
1865
|
+
formatFact("senderName", input.conversation.senderName),
|
|
1866
|
+
formatFact("senderId", input.conversation.senderId),
|
|
1867
|
+
formatFact("wasMentioned", input.conversation.wasMentioned),
|
|
1868
|
+
formatFact("isThread", input.conversation.isThread),
|
|
1869
|
+
formatFact("messageThreadId", input.conversation.messageThreadId),
|
|
1870
|
+
formatFact("parentSessionKey", input.conversation.parentSessionKey),
|
|
1871
|
+
formatFact("threadLabel", input.conversation.threadLabel),
|
|
1872
|
+
formatFact("isFirstThreadTurn", input.conversation.isFirstThreadTurn)
|
|
1873
|
+
].filter((entry) => Boolean(entry));
|
|
1874
|
+
return `Conversation facts:
|
|
1875
|
+
${facts.length ? facts.join("\n") : "- None available."}`;
|
|
1876
|
+
}
|
|
1877
|
+
function buildThreadSection(input) {
|
|
1878
|
+
const starter = input.thread?.starterBody ? trimForPrompt(input.thread.starterBody) : void 0;
|
|
1879
|
+
const history = input.thread?.historyBody ? trimForPrompt(input.thread.historyBody, 1800) : void 0;
|
|
1880
|
+
if (!starter && !history) {
|
|
1881
|
+
return "Thread context:\n- No thread context available.";
|
|
1882
|
+
}
|
|
1883
|
+
return [
|
|
1884
|
+
"Thread context:",
|
|
1885
|
+
starter ? `- Starter: ${starter}` : void 0,
|
|
1886
|
+
history ? `- Recent thread history: ${history}` : void 0
|
|
1887
|
+
].filter((entry) => Boolean(entry)).join("\n");
|
|
1888
|
+
}
|
|
1889
|
+
function formatHistoryMessage(message2) {
|
|
1890
|
+
const sender = message2.senderName ?? message2.senderId ?? "unknown";
|
|
1891
|
+
const role = message2.role === "assistant" ? "assistant" : "user";
|
|
1892
|
+
const timestamp = message2.timestamp ? ` at ${message2.timestamp}` : "";
|
|
1893
|
+
return `- ${role} ${sender}${timestamp}: ${trimForPrompt(message2.content, 700)}`;
|
|
1894
|
+
}
|
|
1895
|
+
function buildRecentMessagesSection(input) {
|
|
1896
|
+
if (input.recentMessages.length === 0) {
|
|
1897
|
+
return "Recent conversation:\n- No recent messages available.";
|
|
1898
|
+
}
|
|
1899
|
+
return `Recent conversation:
|
|
1900
|
+
${input.recentMessages.map(formatHistoryMessage).join("\n")}`;
|
|
1901
|
+
}
|
|
1902
|
+
function buildCurrentMessageSection(input) {
|
|
1903
|
+
const sender = input.currentMessage.senderName ?? input.currentMessage.senderId;
|
|
1904
|
+
return [
|
|
1905
|
+
"Current message:",
|
|
1906
|
+
sender ? `${sender}: ${trimForPrompt(input.currentMessage.content, 1200)}` : trimForPrompt(input.currentMessage.content, 1200)
|
|
1907
|
+
].join("\n");
|
|
1908
|
+
}
|
|
1909
|
+
function buildClassifierPrompt(input) {
|
|
1910
|
+
return [
|
|
1911
|
+
"You decide whether this digital coworker should participate in the current shared group, channel, room, or thread turn.",
|
|
1912
|
+
"The goal is useful participation, not name matching. Decide from the conversation context, current request, thread ownership, prior replies, and the coworker's role.",
|
|
1913
|
+
"Direct messages are admitted before this classifier; this classifier handles shared rooms, channels, groups, and threads.",
|
|
1914
|
+
"",
|
|
1915
|
+
buildContextSection(input.context),
|
|
1916
|
+
"",
|
|
1917
|
+
buildConversationSection(input),
|
|
1918
|
+
"",
|
|
1919
|
+
buildThreadSection(input),
|
|
1920
|
+
"",
|
|
1921
|
+
buildRecentMessagesSection(input),
|
|
1922
|
+
"",
|
|
1923
|
+
buildCurrentMessageSection(input),
|
|
1924
|
+
"",
|
|
1925
|
+
"Return exactly one complete minified JSON object with this exact shape:",
|
|
1926
|
+
'{"participationScore":0.0}',
|
|
1927
|
+
"",
|
|
1928
|
+
"Score guidance:",
|
|
1929
|
+
"- 1.0 means this coworker clearly should reply now.",
|
|
1930
|
+
"- 0.7 or higher means this coworker should participate.",
|
|
1931
|
+
"- Below 0.7 means this coworker should stay silent.",
|
|
1932
|
+
"- Use 0.9 or higher for direct requests to this coworker, clear follow-ups after this coworker replied, or requests where this coworker is the responsible participant.",
|
|
1933
|
+
"- Use 0.7 or higher for open channel asks such as 'can someone', 'does anyone know', or 'could the digital coworker in this channel' when this coworker can help.",
|
|
1934
|
+
"- Use 0.8 or higher when the message is in a thread where this coworker already answered or owns the thread context.",
|
|
1935
|
+
"- Use below 0.7 for ambient chat, messages clearly meant for another person, acknowledgements that do not need a reply, or interruptions.",
|
|
1936
|
+
"",
|
|
1937
|
+
"Do not include markdown, code fences, prose outside JSON, or extra keys."
|
|
1938
|
+
].join("\n");
|
|
1939
|
+
}
|
|
1940
|
+
function buildClassifierRetryPrompt(params) {
|
|
1941
|
+
return [
|
|
1942
|
+
buildClassifierPrompt(params.input),
|
|
1943
|
+
"",
|
|
1944
|
+
"The previous classifier response was invalid JSON.",
|
|
1945
|
+
params.parseError ? `Previous parse error: ${trimForPrompt(params.parseError, 240)}` : void 0,
|
|
1946
|
+
"Repair the response now. Return exactly one complete minified JSON object and nothing else.",
|
|
1947
|
+
'{"participationScore":0.0}'
|
|
1948
|
+
].filter((line) => Boolean(line)).join("\n");
|
|
1949
|
+
}
|
|
1950
|
+
function normalizeScore(value) {
|
|
1951
|
+
const score = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
1952
|
+
if (!Number.isFinite(score) || score < 0 || score > 1) {
|
|
1953
|
+
return void 0;
|
|
1954
|
+
}
|
|
1955
|
+
return score;
|
|
1956
|
+
}
|
|
1957
|
+
function failOpen(parseError) {
|
|
1958
|
+
return {
|
|
1959
|
+
participationScore: 1,
|
|
1960
|
+
parseStatus: "malformed",
|
|
1961
|
+
parseError
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
function recoverParticipationScore(output) {
|
|
1965
|
+
const stripped = stripCodeFences(output);
|
|
1966
|
+
const match = /["']?participationScore["']?\s*:\s*["']?(-?(?:\d+(?:\.\d+)?|\.\d+))["']?/i.exec(stripped);
|
|
1967
|
+
return match ? normalizeScore(match[1]) : void 0;
|
|
1968
|
+
}
|
|
1969
|
+
function extractFirstJsonObject(value) {
|
|
1970
|
+
const start = value.indexOf("{");
|
|
1971
|
+
if (start < 0) {
|
|
1972
|
+
return void 0;
|
|
1973
|
+
}
|
|
1974
|
+
let depth = 0;
|
|
1975
|
+
let inString = false;
|
|
1976
|
+
let escaped = false;
|
|
1977
|
+
for (let index = start; index < value.length; index += 1) {
|
|
1978
|
+
const char = value[index];
|
|
1979
|
+
if (inString) {
|
|
1980
|
+
if (escaped) {
|
|
1981
|
+
escaped = false;
|
|
1982
|
+
} else if (char === "\\") {
|
|
1983
|
+
escaped = true;
|
|
1984
|
+
} else if (char === '"') {
|
|
1985
|
+
inString = false;
|
|
1986
|
+
}
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
if (char === '"') {
|
|
1990
|
+
inString = true;
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
if (char === "{") {
|
|
1994
|
+
depth += 1;
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
if (char === "}") {
|
|
1998
|
+
depth -= 1;
|
|
1999
|
+
if (depth === 0) {
|
|
2000
|
+
return value.slice(start, index + 1);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return void 0;
|
|
2005
|
+
}
|
|
2006
|
+
function parseJsonObject(output) {
|
|
2007
|
+
const stripped = stripCodeFences(output);
|
|
2008
|
+
try {
|
|
2009
|
+
return { parsed: JSON.parse(stripped) };
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
const primaryError = error instanceof Error ? error.message : String(error);
|
|
2012
|
+
const extracted = extractFirstJsonObject(stripped);
|
|
2013
|
+
if (extracted && extracted !== stripped) {
|
|
2014
|
+
try {
|
|
2015
|
+
return { parsed: JSON.parse(extracted) };
|
|
2016
|
+
} catch (extractError) {
|
|
2017
|
+
return {
|
|
2018
|
+
parseError: extractError instanceof Error ? extractError.message : String(extractError)
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return { parseError: primaryError };
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
function parseClassifierDecision(output) {
|
|
2026
|
+
const { parsed, parseError } = parseJsonObject(output);
|
|
2027
|
+
if (parseError) {
|
|
2028
|
+
const recoveredScore = recoverParticipationScore(output);
|
|
2029
|
+
if (recoveredScore !== void 0) {
|
|
2030
|
+
return {
|
|
2031
|
+
participationScore: recoveredScore,
|
|
2032
|
+
parseStatus: "recovered_score",
|
|
2033
|
+
parseError
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
return failOpen(parseError);
|
|
2037
|
+
}
|
|
2038
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2039
|
+
return failOpen("not_object");
|
|
2040
|
+
}
|
|
2041
|
+
const record = parsed;
|
|
2042
|
+
if (typeof record.shouldRespond === "boolean" && record.participationScore === void 0) {
|
|
2043
|
+
return {
|
|
2044
|
+
participationScore: record.shouldRespond ? 1 : 0,
|
|
2045
|
+
parseStatus: "legacy_boolean"
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
const participationScore = normalizeScore(record.participationScore);
|
|
2049
|
+
if (participationScore === void 0) {
|
|
2050
|
+
return failOpen("invalid_score");
|
|
2051
|
+
}
|
|
2052
|
+
return {
|
|
2053
|
+
participationScore,
|
|
2054
|
+
parseStatus: "valid_json"
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
async function classifyParticipation(params) {
|
|
2058
|
+
const inputHash = hashValue(stableStringify(params.input));
|
|
2059
|
+
const firstPrompt = buildClassifierPrompt(params.input);
|
|
2060
|
+
const firstAttempt = await runClassifierAttempt({
|
|
2061
|
+
api: params.api,
|
|
2062
|
+
config: params.config,
|
|
2063
|
+
prompt: firstPrompt,
|
|
2064
|
+
attempt: 1
|
|
2065
|
+
});
|
|
2066
|
+
const attempts = [firstAttempt];
|
|
2067
|
+
let finalAttempt = firstAttempt;
|
|
2068
|
+
if (firstAttempt.parseStatus === "malformed") {
|
|
2069
|
+
const retryPrompt = buildClassifierRetryPrompt({
|
|
2070
|
+
input: params.input,
|
|
2071
|
+
parseError: firstAttempt.parseError
|
|
2072
|
+
});
|
|
2073
|
+
finalAttempt = await runClassifierAttempt({
|
|
2074
|
+
api: params.api,
|
|
2075
|
+
config: params.config,
|
|
2076
|
+
prompt: retryPrompt,
|
|
2077
|
+
attempt: 2
|
|
2078
|
+
});
|
|
2079
|
+
attempts.push(finalAttempt);
|
|
2080
|
+
}
|
|
2081
|
+
return {
|
|
2082
|
+
participationScore: finalAttempt.participationScore,
|
|
2083
|
+
parseStatus: finalAttempt.parseStatus,
|
|
2084
|
+
parseError: finalAttempt.parseError,
|
|
2085
|
+
rawOutput: finalAttempt.rawOutput,
|
|
2086
|
+
prompt: finalAttempt.prompt,
|
|
2087
|
+
promptVersion: CLASSIFIER_PROMPT_VERSION,
|
|
2088
|
+
promptHash: finalAttempt.promptHash,
|
|
2089
|
+
inputHash,
|
|
2090
|
+
outputHash: finalAttempt.outputHash,
|
|
2091
|
+
outputLength: finalAttempt.outputLength,
|
|
2092
|
+
attempts
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
async function runClassifierAttempt(params) {
|
|
2096
|
+
const output = await runEmbeddedClassifierModel({
|
|
2097
|
+
api: params.api,
|
|
2098
|
+
config: params.config,
|
|
2099
|
+
prompt: params.prompt
|
|
2100
|
+
});
|
|
2101
|
+
const parsed = parseClassifierDecision(output);
|
|
2102
|
+
return {
|
|
2103
|
+
attempt: params.attempt,
|
|
2104
|
+
prompt: params.prompt,
|
|
2105
|
+
promptHash: hashValue(params.prompt),
|
|
2106
|
+
rawOutput: output,
|
|
2107
|
+
outputHash: hashValue(output),
|
|
2108
|
+
outputLength: output.length,
|
|
2109
|
+
...parsed
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/message.ts
|
|
2114
|
+
var THREAD_HISTORY_ENVELOPE_RE = /^\s*\[Thread history\]\s*([\s\S]*?)\s*\[\/Thread history\]\s*([\s\S]*)$/i;
|
|
2115
|
+
function nonBlank(value) {
|
|
2116
|
+
const text = value?.trim();
|
|
2117
|
+
return text ? text : void 0;
|
|
2118
|
+
}
|
|
2119
|
+
function threadHistoryText(event) {
|
|
2120
|
+
const explicit = nonBlank(event.threadHistoryBody);
|
|
2121
|
+
if (explicit) {
|
|
2122
|
+
return explicit;
|
|
2123
|
+
}
|
|
2124
|
+
const body = event.body;
|
|
2125
|
+
if (!body) {
|
|
2126
|
+
return void 0;
|
|
2127
|
+
}
|
|
2128
|
+
const match = THREAD_HISTORY_ENVELOPE_RE.exec(body);
|
|
2129
|
+
return nonBlank(match?.[1]);
|
|
2130
|
+
}
|
|
2131
|
+
function stripThreadHistoryEnvelope(body) {
|
|
2132
|
+
if (!body) {
|
|
2133
|
+
return "";
|
|
2134
|
+
}
|
|
2135
|
+
const match = THREAD_HISTORY_ENVELOPE_RE.exec(body);
|
|
2136
|
+
return (match?.[2] ?? body).trim();
|
|
2137
|
+
}
|
|
2138
|
+
function messageText(event) {
|
|
2139
|
+
for (const value of [event.rawBody, event.content]) {
|
|
2140
|
+
const text = nonBlank(value);
|
|
2141
|
+
if (text) {
|
|
2142
|
+
return text;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return stripThreadHistoryEnvelope(event.body);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/decision.ts
|
|
2149
|
+
function escapeRegExp(value) {
|
|
2150
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2151
|
+
}
|
|
2152
|
+
function namePattern(name) {
|
|
2153
|
+
const trimmed = name.trim();
|
|
2154
|
+
if (trimmed.length < 2) {
|
|
2155
|
+
return void 0;
|
|
2156
|
+
}
|
|
2157
|
+
return escapeRegExp(trimmed).replace(/\s+/g, "\\s+");
|
|
2158
|
+
}
|
|
2159
|
+
function matchesAnyPattern(text, patterns) {
|
|
2160
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
2161
|
+
}
|
|
2162
|
+
function buildDirectAddressPatterns(name) {
|
|
2163
|
+
const pattern = namePattern(name);
|
|
2164
|
+
if (!pattern) {
|
|
2165
|
+
return [];
|
|
2166
|
+
}
|
|
2167
|
+
const left = "(^|[^A-Za-z0-9_])";
|
|
2168
|
+
const right = "(?=$|[^A-Za-z0-9_])";
|
|
2169
|
+
const sentenceStart = "(^|[.!?][\\s\\n]+)";
|
|
2170
|
+
const requestAfterName = "(I\\s+(need|want|would\\s+like)\\s+you|we\\s+(need|want)\\s+you)";
|
|
2171
|
+
return [
|
|
2172
|
+
new RegExp(`${left}@${pattern}${right}`, "i"),
|
|
2173
|
+
new RegExp(`(^|[\\s\\n])${pattern}\\s*[:,]`, "i"),
|
|
2174
|
+
new RegExp(`${left}(hey|hi|hello|yo)\\s+${pattern}${right}`, "i"),
|
|
2175
|
+
new RegExp(`${sentenceStart}${pattern}\\s+${requestAfterName}${right}`, "i"),
|
|
2176
|
+
new RegExp(
|
|
2177
|
+
`${left}${pattern}\\s+(can|could|would|will|please|do|take|help|look|check|own|handle|review|summarize|find|send|create|update)${right}`,
|
|
2178
|
+
"i"
|
|
2179
|
+
),
|
|
2180
|
+
new RegExp(`${left}(can|could|would|will)\\s+${pattern}\\s+`, "i")
|
|
2181
|
+
];
|
|
2182
|
+
}
|
|
2183
|
+
function messageClearlyAddressesIdentity(content, identity) {
|
|
2184
|
+
if (!content.trim()) {
|
|
2185
|
+
return false;
|
|
2186
|
+
}
|
|
2187
|
+
return identity.names.some((name) => matchesAnyPattern(content, buildDirectAddressPatterns(name)));
|
|
2188
|
+
}
|
|
2189
|
+
function messageClearlyAddressesAnotherCoworker(content, context) {
|
|
2190
|
+
return context.coworkers.some((coworker) => messageClearlyAddressesIdentity(content, coworker));
|
|
2191
|
+
}
|
|
2192
|
+
function decision(shouldRespond, reason, source, startedAt, error, metadata) {
|
|
2193
|
+
const message2 = error instanceof Error ? error.message : error ? String(error) : void 0;
|
|
2194
|
+
return {
|
|
2195
|
+
shouldRespond,
|
|
2196
|
+
reason,
|
|
2197
|
+
source,
|
|
2198
|
+
latencyMs: Date.now() - startedAt,
|
|
2199
|
+
...message2 ? { error: message2 } : {},
|
|
2200
|
+
...metadata
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
function messageIsThread(event, ctx) {
|
|
2204
|
+
const sessionKey = ctx.sessionKey ?? event.sessionKey ?? "";
|
|
2205
|
+
return Boolean(
|
|
2206
|
+
event.messageThreadId || ctx.messageThreadId || event.parentSessionKey || ctx.parentSessionKey || event.threadStarterBody || threadHistoryText(event) || sessionKey.includes(":thread:") || sessionKey.includes("thread")
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
function eventRecentMessages(event, maxMessages) {
|
|
2210
|
+
if (!Array.isArray(event.inboundHistory) || maxMessages <= 0) {
|
|
2211
|
+
return [];
|
|
2212
|
+
}
|
|
2213
|
+
return event.inboundHistory.filter((message2) => message2.content.trim().length > 0).slice(-maxMessages);
|
|
2214
|
+
}
|
|
2215
|
+
function providerIsSlack(event, ctx) {
|
|
2216
|
+
const provider = event.provider ?? ctx.provider ?? event.surface ?? ctx.surface ?? event.channel;
|
|
2217
|
+
return provider?.toLowerCase() === "slack";
|
|
2218
|
+
}
|
|
2219
|
+
function mergeRecentMessages(params) {
|
|
2220
|
+
if (params.inboundHistory.length === 0) {
|
|
2221
|
+
return params.localHistory;
|
|
2222
|
+
}
|
|
2223
|
+
if (providerIsSlack(params.event, params.ctx)) {
|
|
2224
|
+
return params.inboundHistory;
|
|
2225
|
+
}
|
|
2226
|
+
const merged = [];
|
|
2227
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2228
|
+
for (const message2 of [...params.localHistory, ...params.inboundHistory]) {
|
|
2229
|
+
const key = [
|
|
2230
|
+
message2.role ?? "user",
|
|
2231
|
+
message2.senderId ?? "",
|
|
2232
|
+
message2.senderName ?? "",
|
|
2233
|
+
message2.timestamp ?? "",
|
|
2234
|
+
message2.content
|
|
2235
|
+
].join("\0");
|
|
2236
|
+
if (seen.has(key)) {
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
seen.add(key);
|
|
2240
|
+
merged.push(message2);
|
|
2241
|
+
}
|
|
2242
|
+
return merged.slice(-params.maxMessages);
|
|
2243
|
+
}
|
|
2244
|
+
function buildClassifierInput(params) {
|
|
2245
|
+
const inboundHistory = eventRecentMessages(params.event, params.maxMessages);
|
|
2246
|
+
const threadHistoryBody = threadHistoryText(params.event);
|
|
2247
|
+
return {
|
|
2248
|
+
context: params.context,
|
|
2249
|
+
conversation: {
|
|
2250
|
+
provider: params.event.provider ?? params.ctx.provider,
|
|
2251
|
+
surface: params.event.surface ?? params.ctx.surface,
|
|
2252
|
+
chatType: params.event.chatType ?? params.ctx.chatType ?? (params.event.isGroup ? "group" : "dm"),
|
|
2253
|
+
channelId: params.ctx.channelId ?? params.event.channel,
|
|
2254
|
+
conversationId: params.ctx.conversationId,
|
|
2255
|
+
conversationLabel: params.event.conversationLabel ?? params.ctx.conversationLabel,
|
|
2256
|
+
groupSubject: params.event.groupSubject ?? params.ctx.groupSubject,
|
|
2257
|
+
sessionKey: params.ctx.sessionKey ?? params.event.sessionKey,
|
|
2258
|
+
senderId: params.event.senderId ?? params.ctx.senderId,
|
|
2259
|
+
senderName: params.event.senderName ?? params.ctx.senderName,
|
|
2260
|
+
wasMentioned: params.event.wasMentioned ?? params.ctx.wasMentioned,
|
|
2261
|
+
isThread: messageIsThread(params.event, params.ctx),
|
|
2262
|
+
messageThreadId: params.event.messageThreadId ?? params.ctx.messageThreadId,
|
|
2263
|
+
parentSessionKey: params.event.parentSessionKey ?? params.ctx.parentSessionKey,
|
|
2264
|
+
threadLabel: params.event.threadLabel,
|
|
2265
|
+
isFirstThreadTurn: params.event.isFirstThreadTurn
|
|
2266
|
+
},
|
|
2267
|
+
thread: params.event.threadStarterBody || threadHistoryBody ? {
|
|
2268
|
+
starterBody: params.event.threadStarterBody,
|
|
2269
|
+
historyBody: threadHistoryBody
|
|
2270
|
+
} : void 0,
|
|
2271
|
+
recentMessages: mergeRecentMessages({
|
|
2272
|
+
event: params.event,
|
|
2273
|
+
ctx: params.ctx,
|
|
2274
|
+
inboundHistory,
|
|
2275
|
+
localHistory: params.recentMessages,
|
|
2276
|
+
maxMessages: params.maxMessages
|
|
2277
|
+
}),
|
|
2278
|
+
currentMessage: {
|
|
2279
|
+
senderId: params.event.senderId ?? params.ctx.senderId,
|
|
2280
|
+
senderName: params.event.senderName ?? params.ctx.senderName,
|
|
2281
|
+
content: params.content,
|
|
2282
|
+
timestamp: params.event.timestamp
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
async function decideParticipation(params) {
|
|
2287
|
+
const startedAt = Date.now();
|
|
2288
|
+
const classify = params.classify ?? classifyParticipation;
|
|
2289
|
+
if (params.event.isGroup !== true) {
|
|
2290
|
+
return decision(true, "dm", "rule", startedAt);
|
|
2291
|
+
}
|
|
2292
|
+
const content = messageText(params.event);
|
|
2293
|
+
if (!content) {
|
|
2294
|
+
return decision(true, "empty_message", "rule", startedAt);
|
|
2295
|
+
}
|
|
2296
|
+
const recentMessages = params.history.recent(
|
|
2297
|
+
params.event,
|
|
2298
|
+
params.ctx,
|
|
2299
|
+
params.config.context.maxMessages
|
|
2300
|
+
);
|
|
2301
|
+
let context;
|
|
2302
|
+
try {
|
|
2303
|
+
context = await params.contextProvider.load();
|
|
2304
|
+
} catch (error) {
|
|
2305
|
+
params.history.recordInbound(params.event, params.ctx, params.config.context.maxMessages);
|
|
2306
|
+
return decision(true, "context_unavailable", "fallback", startedAt, error);
|
|
2307
|
+
}
|
|
2308
|
+
try {
|
|
2309
|
+
if (messageClearlyAddressesIdentity(content, context.self)) {
|
|
2310
|
+
return decision(true, "direct_address_self", "rule", startedAt);
|
|
2311
|
+
}
|
|
2312
|
+
if (messageClearlyAddressesAnotherCoworker(content, context)) {
|
|
2313
|
+
return decision(false, "direct_address_other_coworker", "rule", startedAt);
|
|
2314
|
+
}
|
|
2315
|
+
const classifierInput = buildClassifierInput({
|
|
2316
|
+
context,
|
|
2317
|
+
event: params.event,
|
|
2318
|
+
ctx: params.ctx,
|
|
2319
|
+
content,
|
|
2320
|
+
recentMessages,
|
|
2321
|
+
maxMessages: params.config.context.maxMessages
|
|
2322
|
+
});
|
|
2323
|
+
const classifierDecision = await classify({
|
|
2324
|
+
api: params.api,
|
|
2325
|
+
config: params.config,
|
|
2326
|
+
input: classifierInput
|
|
2327
|
+
});
|
|
2328
|
+
const classifierFields = {
|
|
2329
|
+
participationScore: classifierDecision.participationScore,
|
|
2330
|
+
threshold: params.config.classifier.threshold,
|
|
2331
|
+
classifierPromptVersion: classifierDecision.promptVersion,
|
|
2332
|
+
classifierPromptHash: classifierDecision.promptHash,
|
|
2333
|
+
classifierInputHash: classifierDecision.inputHash,
|
|
2334
|
+
classifierParseStatus: classifierDecision.parseStatus,
|
|
2335
|
+
classifierAttemptCount: classifierDecision.attempts.length,
|
|
2336
|
+
classifierOutputHash: classifierDecision.outputHash,
|
|
2337
|
+
classifierOutputLength: classifierDecision.outputLength,
|
|
2338
|
+
classifierParseError: classifierDecision.parseError,
|
|
2339
|
+
classifierRecentMessageCount: classifierInput.recentMessages.length,
|
|
2340
|
+
classifierThreadContext: Boolean(classifierInput.thread?.starterBody || classifierInput.thread?.historyBody),
|
|
2341
|
+
classifierCurrentMessageLength: classifierInput.currentMessage.content.length,
|
|
2342
|
+
...params.config.logging.classifierDebug ? {
|
|
2343
|
+
classifierRawOutput: classifierDecision.rawOutput,
|
|
2344
|
+
classifierPrompt: classifierDecision.prompt,
|
|
2345
|
+
classifierInput,
|
|
2346
|
+
classifierAttempts: classifierDecision.attempts
|
|
2347
|
+
} : {}
|
|
2348
|
+
};
|
|
2349
|
+
if (classifierDecision.parseStatus === "malformed") {
|
|
2350
|
+
return decision(
|
|
2351
|
+
true,
|
|
2352
|
+
"classifier_malformed",
|
|
2353
|
+
"fallback",
|
|
2354
|
+
startedAt,
|
|
2355
|
+
void 0,
|
|
2356
|
+
classifierFields
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
const shouldRespond = classifierDecision.participationScore >= params.config.classifier.threshold;
|
|
2360
|
+
return decision(
|
|
2361
|
+
shouldRespond,
|
|
2362
|
+
shouldRespond ? "classifier_true" : "classifier_false",
|
|
2363
|
+
"classifier",
|
|
2364
|
+
startedAt,
|
|
2365
|
+
void 0,
|
|
2366
|
+
classifierFields
|
|
2367
|
+
);
|
|
2368
|
+
} catch (error) {
|
|
2369
|
+
return decision(true, "classifier_error", "fallback", startedAt, error);
|
|
2370
|
+
} finally {
|
|
2371
|
+
params.history.recordInbound(params.event, params.ctx, params.config.context.maxMessages);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// src/history.ts
|
|
2376
|
+
function normalizeKey2(value) {
|
|
2377
|
+
const trimmed = value?.trim();
|
|
2378
|
+
if (!trimmed) {
|
|
2379
|
+
return void 0;
|
|
2380
|
+
}
|
|
2381
|
+
for (const prefix of ["conversation:", "channel:", "chat:", "user:"]) {
|
|
2382
|
+
if (trimmed.startsWith(prefix)) {
|
|
2383
|
+
return trimmed.slice(prefix.length);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
return trimmed;
|
|
2387
|
+
}
|
|
2388
|
+
function stripTeamsMessageId(value) {
|
|
2389
|
+
return value.replace(/;messageid=[^;]+/i, "");
|
|
2390
|
+
}
|
|
2391
|
+
function teamsThreadKey(conversationId, threadId) {
|
|
2392
|
+
const normalizedConversationId = normalizeKey2(conversationId);
|
|
2393
|
+
const normalizedThreadId = normalizeKey2(threadId);
|
|
2394
|
+
if (!normalizedConversationId || !normalizedThreadId) {
|
|
2395
|
+
return void 0;
|
|
2396
|
+
}
|
|
2397
|
+
return `${stripTeamsMessageId(normalizedConversationId)};messageid=${normalizedThreadId}`;
|
|
2398
|
+
}
|
|
2399
|
+
function addKey(keys, value) {
|
|
2400
|
+
const key = normalizeKey2(value);
|
|
2401
|
+
if (key && !keys.includes(key)) {
|
|
2402
|
+
keys.push(key);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
function addTargetKeys(keys, value) {
|
|
2406
|
+
const key = normalizeKey2(value);
|
|
2407
|
+
if (!key) {
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
addKey(keys, key);
|
|
2411
|
+
const baseKey = stripTeamsMessageId(key);
|
|
2412
|
+
if (baseKey !== key) {
|
|
2413
|
+
addKey(keys, baseKey);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
function inboundKeys(event, ctx) {
|
|
2417
|
+
const keys = [];
|
|
2418
|
+
const conversationId = ctx.conversationId ?? event.channel ?? ctx.channelId;
|
|
2419
|
+
const threadId = event.messageThreadId ?? ctx.messageThreadId;
|
|
2420
|
+
addKey(keys, ctx.sessionKey);
|
|
2421
|
+
addKey(keys, event.sessionKey);
|
|
2422
|
+
addKey(keys, teamsThreadKey(conversationId, threadId));
|
|
2423
|
+
addKey(keys, ctx.parentSessionKey);
|
|
2424
|
+
addKey(keys, event.parentSessionKey);
|
|
2425
|
+
addTargetKeys(keys, conversationId);
|
|
2426
|
+
addTargetKeys(keys, event.channel);
|
|
2427
|
+
addTargetKeys(keys, ctx.channelId);
|
|
2428
|
+
return keys.length > 0 ? keys : ["unknown"];
|
|
2429
|
+
}
|
|
2430
|
+
function outboundKeys(event, ctx) {
|
|
2431
|
+
const keys = [];
|
|
2432
|
+
addKey(keys, ctx.sessionKey);
|
|
2433
|
+
addKey(keys, event.sessionKey);
|
|
2434
|
+
addTargetKeys(keys, event.to);
|
|
2435
|
+
addTargetKeys(keys, ctx.conversationId);
|
|
2436
|
+
addTargetKeys(keys, ctx.channelId);
|
|
2437
|
+
return keys.length > 0 ? keys : ["unknown"];
|
|
2438
|
+
}
|
|
2439
|
+
function publicMessages(messages, maxMessages) {
|
|
2440
|
+
return messages.sort((left, right) => left.sequence - right.sequence).slice(-maxMessages).map(({ sequence: _sequence, ...message2 }) => message2);
|
|
2441
|
+
}
|
|
2442
|
+
function createParticipationHistoryStore() {
|
|
2443
|
+
const messagesByConversation = /* @__PURE__ */ new Map();
|
|
2444
|
+
let nextSequence = 1;
|
|
2445
|
+
function recordForKeys(keys, message2, maxMessages) {
|
|
2446
|
+
const stored = {
|
|
2447
|
+
...message2,
|
|
2448
|
+
sequence: nextSequence
|
|
2449
|
+
};
|
|
2450
|
+
nextSequence += 1;
|
|
2451
|
+
for (const key of keys) {
|
|
2452
|
+
const current = messagesByConversation.get(key) ?? [];
|
|
2453
|
+
current.push(stored);
|
|
2454
|
+
if (current.length > maxMessages) {
|
|
2455
|
+
current.splice(0, current.length - maxMessages);
|
|
2456
|
+
}
|
|
2457
|
+
messagesByConversation.set(key, current);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
function recordInbound(event, ctx, maxMessages) {
|
|
2461
|
+
if (maxMessages <= 0 || event.isGroup !== true) {
|
|
2462
|
+
return false;
|
|
2463
|
+
}
|
|
2464
|
+
const content = messageText(event);
|
|
2465
|
+
if (!content) {
|
|
2466
|
+
return false;
|
|
2467
|
+
}
|
|
2468
|
+
recordForKeys(
|
|
2469
|
+
inboundKeys(event, ctx),
|
|
2470
|
+
{
|
|
2471
|
+
role: "user",
|
|
2472
|
+
senderId: event.senderId ?? ctx.senderId,
|
|
2473
|
+
senderName: event.senderName ?? ctx.senderName,
|
|
2474
|
+
content,
|
|
2475
|
+
timestamp: event.timestamp
|
|
2476
|
+
},
|
|
2477
|
+
maxMessages
|
|
2478
|
+
);
|
|
2479
|
+
return true;
|
|
2480
|
+
}
|
|
2481
|
+
return {
|
|
2482
|
+
recent(event, ctx, maxMessages) {
|
|
2483
|
+
if (maxMessages <= 0) {
|
|
2484
|
+
return [];
|
|
2485
|
+
}
|
|
2486
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2487
|
+
const messages = [];
|
|
2488
|
+
for (const key of inboundKeys(event, ctx)) {
|
|
2489
|
+
for (const message2 of messagesByConversation.get(key) ?? []) {
|
|
2490
|
+
if (!seen.has(message2)) {
|
|
2491
|
+
seen.add(message2);
|
|
2492
|
+
messages.push(message2);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
return publicMessages(messages, maxMessages);
|
|
2497
|
+
},
|
|
2498
|
+
recordInbound,
|
|
2499
|
+
recordOutbound(event, ctx, maxMessages) {
|
|
2500
|
+
if (maxMessages <= 0 || event.success !== true) {
|
|
2501
|
+
return false;
|
|
2502
|
+
}
|
|
2503
|
+
const content = event.content?.trim();
|
|
2504
|
+
if (!content) {
|
|
2505
|
+
return false;
|
|
2506
|
+
}
|
|
2507
|
+
recordForKeys(
|
|
2508
|
+
outboundKeys(event, ctx),
|
|
2509
|
+
{
|
|
2510
|
+
role: "assistant",
|
|
2511
|
+
senderId: "self",
|
|
2512
|
+
senderName: "This coworker",
|
|
2513
|
+
content
|
|
2514
|
+
},
|
|
2515
|
+
maxMessages
|
|
2516
|
+
);
|
|
2517
|
+
return true;
|
|
2518
|
+
},
|
|
2519
|
+
record(event, ctx, maxMessages) {
|
|
2520
|
+
return recordInbound(event, ctx, maxMessages);
|
|
2521
|
+
}
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// src/logging.ts
|
|
2526
|
+
function eventContent(event) {
|
|
2527
|
+
return messageText(event);
|
|
2528
|
+
}
|
|
2529
|
+
function field(name, value) {
|
|
2530
|
+
if (value === void 0 || value === null || value === "") {
|
|
2531
|
+
return void 0;
|
|
2532
|
+
}
|
|
2533
|
+
return `${name}=${String(value)}`;
|
|
2534
|
+
}
|
|
2535
|
+
function scoreField(value) {
|
|
2536
|
+
return typeof value === "number" ? value.toFixed(3) : void 0;
|
|
2537
|
+
}
|
|
2538
|
+
function contentLoggingEnabled(config) {
|
|
2539
|
+
return config.logging.includeContent === true;
|
|
2540
|
+
}
|
|
2541
|
+
function logParticipationDecision(params) {
|
|
2542
|
+
if (!params.config.logging.decisions) {
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
const outcome = params.decision.shouldRespond ? "respond" : params.config.mode === "enforce" ? "skip" : "would_skip";
|
|
2546
|
+
const parts = [
|
|
2547
|
+
"velanir-participation-gate:",
|
|
2548
|
+
field("mode", params.config.mode),
|
|
2549
|
+
field("outcome", outcome),
|
|
2550
|
+
field("reason", params.decision.reason),
|
|
2551
|
+
field("source", params.decision.source),
|
|
2552
|
+
field("channel", params.ctx.channelId ?? params.event.channel),
|
|
2553
|
+
field("conversation", params.ctx.conversationId),
|
|
2554
|
+
field("session", params.ctx.sessionKey ?? params.event.sessionKey),
|
|
2555
|
+
field("sender", params.event.senderId ?? params.ctx.senderId),
|
|
2556
|
+
field("latencyMs", params.decision.latencyMs),
|
|
2557
|
+
field("provider", params.config.classifier.provider),
|
|
2558
|
+
field("model", params.config.classifier.model),
|
|
2559
|
+
field("score", scoreField(params.decision.participationScore)),
|
|
2560
|
+
field("threshold", scoreField(params.decision.threshold)),
|
|
2561
|
+
field("promptVersion", params.decision.classifierPromptVersion),
|
|
2562
|
+
field("promptHash", params.decision.classifierPromptHash),
|
|
2563
|
+
field("inputHash", params.decision.classifierInputHash),
|
|
2564
|
+
field("parseStatus", params.decision.classifierParseStatus),
|
|
2565
|
+
field("attemptCount", params.decision.classifierAttemptCount),
|
|
2566
|
+
field("outputHash", params.decision.classifierOutputHash),
|
|
2567
|
+
field("outputLength", params.decision.classifierOutputLength),
|
|
2568
|
+
field("recentMessages", params.decision.classifierRecentMessageCount),
|
|
2569
|
+
field("threadContext", params.decision.classifierThreadContext),
|
|
2570
|
+
field("currentLength", params.decision.classifierCurrentMessageLength),
|
|
2571
|
+
contentLoggingEnabled(params.config) ? field("parseError", params.decision.classifierParseError) : void 0,
|
|
2572
|
+
field("error", params.decision.error),
|
|
2573
|
+
contentLoggingEnabled(params.config) ? field("content", JSON.stringify(eventContent(params.event))) : void 0
|
|
2574
|
+
].filter((entry) => Boolean(entry));
|
|
2575
|
+
params.logger?.info?.(parts.join(" "));
|
|
2576
|
+
if (params.config.logging.classifierDebug && (params.decision.source === "classifier" || params.decision.classifierAttempts)) {
|
|
2577
|
+
const record = {
|
|
2578
|
+
promptVersion: params.decision.classifierPromptVersion,
|
|
2579
|
+
promptHash: params.decision.classifierPromptHash,
|
|
2580
|
+
inputHash: params.decision.classifierInputHash,
|
|
2581
|
+
provider: params.config.classifier.provider,
|
|
2582
|
+
model: params.config.classifier.model,
|
|
2583
|
+
score: params.decision.participationScore,
|
|
2584
|
+
threshold: params.decision.threshold,
|
|
2585
|
+
parseStatus: params.decision.classifierParseStatus,
|
|
2586
|
+
attemptCount: params.decision.classifierAttemptCount,
|
|
2587
|
+
outputHash: params.decision.classifierOutputHash,
|
|
2588
|
+
outputLength: params.decision.classifierOutputLength,
|
|
2589
|
+
recentMessages: params.decision.classifierRecentMessageCount,
|
|
2590
|
+
threadContext: params.decision.classifierThreadContext,
|
|
2591
|
+
currentMessageLength: params.decision.classifierCurrentMessageLength,
|
|
2592
|
+
outcome
|
|
2593
|
+
};
|
|
2594
|
+
if (params.decision.classifierAttempts) {
|
|
2595
|
+
record.attempts = params.decision.classifierAttempts.map((attempt) => ({
|
|
2596
|
+
attempt: attempt.attempt,
|
|
2597
|
+
promptHash: attempt.promptHash,
|
|
2598
|
+
outputHash: attempt.outputHash,
|
|
2599
|
+
outputLength: attempt.outputLength,
|
|
2600
|
+
parseStatus: attempt.parseStatus,
|
|
2601
|
+
...contentLoggingEnabled(params.config) ? {
|
|
2602
|
+
parseError: attempt.parseError,
|
|
2603
|
+
prompt: attempt.prompt,
|
|
2604
|
+
rawOutput: attempt.rawOutput
|
|
2605
|
+
} : {}
|
|
2606
|
+
}));
|
|
2607
|
+
}
|
|
2608
|
+
if (contentLoggingEnabled(params.config)) {
|
|
2609
|
+
record.parseError = params.decision.classifierParseError;
|
|
2610
|
+
record.prompt = params.decision.classifierPrompt;
|
|
2611
|
+
record.input = params.decision.classifierInput;
|
|
2612
|
+
record.rawOutput = params.decision.classifierRawOutput;
|
|
2613
|
+
}
|
|
2614
|
+
params.logger?.info?.(
|
|
2615
|
+
`velanir-participation-gate-classifier-debug: ${JSON.stringify(record)}`
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
function logParticipationHistoryEvent(params) {
|
|
2620
|
+
if (!params.config.logging.decisions) {
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
const parts = [
|
|
2624
|
+
"velanir-participation-gate-history:",
|
|
2625
|
+
field("event", params.event),
|
|
2626
|
+
...Object.entries(params.fields).map(([name, value]) => field(name, value)),
|
|
2627
|
+
contentLoggingEnabled(params.config) ? field("content", JSON.stringify(params.content ?? "")) : void 0
|
|
2628
|
+
].filter((entry) => Boolean(entry));
|
|
2629
|
+
params.logger?.info?.(parts.join(" "));
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// src/index.ts
|
|
2633
|
+
var PLUGIN_ID = "velanir-participation-gate";
|
|
2634
|
+
var index_default = definePluginEntry({
|
|
2635
|
+
id: PLUGIN_ID,
|
|
2636
|
+
name: "Velanir Participation Gate",
|
|
2637
|
+
description: "Decides whether a digital coworker should respond in group and channel conversations before the main agent runs.",
|
|
2638
|
+
register(api) {
|
|
2639
|
+
const runtimeApi = api;
|
|
2640
|
+
const config = normalizeConfig(runtimeApi.pluginConfig);
|
|
2641
|
+
const contextProvider = createParticipationContextProvider(config);
|
|
2642
|
+
const history = createParticipationHistoryStore();
|
|
2643
|
+
api.on(
|
|
2644
|
+
"before_dispatch",
|
|
2645
|
+
async (event, ctx) => {
|
|
2646
|
+
const beforeDispatchEvent = event;
|
|
2647
|
+
const beforeDispatchContext = ctx;
|
|
2648
|
+
if (beforeDispatchEvent.wasMentioned === true || beforeDispatchContext.wasMentioned === true) {
|
|
2649
|
+
const recorded = history.recordInbound(
|
|
2650
|
+
beforeDispatchEvent,
|
|
2651
|
+
beforeDispatchContext,
|
|
2652
|
+
config.context.maxMessages
|
|
2653
|
+
);
|
|
2654
|
+
logParticipationHistoryEvent({
|
|
2655
|
+
logger: runtimeApi.logger,
|
|
2656
|
+
config,
|
|
2657
|
+
event: "mention_bypass_inbound",
|
|
2658
|
+
fields: {
|
|
2659
|
+
recorded,
|
|
2660
|
+
provider: beforeDispatchEvent.provider ?? beforeDispatchContext.provider,
|
|
2661
|
+
channel: beforeDispatchContext.channelId ?? beforeDispatchEvent.channel,
|
|
2662
|
+
conversation: beforeDispatchContext.conversationId,
|
|
2663
|
+
session: beforeDispatchContext.sessionKey ?? beforeDispatchEvent.sessionKey,
|
|
2664
|
+
thread: beforeDispatchEvent.messageThreadId ?? beforeDispatchContext.messageThreadId,
|
|
2665
|
+
sender: beforeDispatchEvent.senderId ?? beforeDispatchContext.senderId
|
|
2666
|
+
},
|
|
2667
|
+
content: messageText(beforeDispatchEvent)
|
|
2668
|
+
});
|
|
2669
|
+
return void 0;
|
|
2670
|
+
}
|
|
2671
|
+
const decision2 = await decideParticipation({
|
|
2672
|
+
api: runtimeApi,
|
|
2673
|
+
config,
|
|
2674
|
+
event: beforeDispatchEvent,
|
|
2675
|
+
ctx: beforeDispatchContext,
|
|
2676
|
+
contextProvider,
|
|
2677
|
+
history
|
|
2678
|
+
});
|
|
2679
|
+
logParticipationDecision({
|
|
2680
|
+
logger: runtimeApi.logger,
|
|
2681
|
+
config,
|
|
2682
|
+
decision: decision2,
|
|
2683
|
+
event: beforeDispatchEvent,
|
|
2684
|
+
ctx: beforeDispatchContext
|
|
2685
|
+
});
|
|
2686
|
+
if (!decision2.shouldRespond && config.mode === "enforce") {
|
|
2687
|
+
return { handled: true };
|
|
2688
|
+
}
|
|
2689
|
+
return void 0;
|
|
2690
|
+
},
|
|
2691
|
+
{ timeoutMs: config.classifier.timeoutMs + 2e3 }
|
|
2692
|
+
);
|
|
2693
|
+
api.on("message_sent", (event, ctx) => {
|
|
2694
|
+
const messageSentEvent = event;
|
|
2695
|
+
const messageSentContext = ctx;
|
|
2696
|
+
const recorded = history.recordOutbound(messageSentEvent, messageSentContext, config.context.maxMessages);
|
|
2697
|
+
logParticipationHistoryEvent({
|
|
2698
|
+
logger: runtimeApi.logger,
|
|
2699
|
+
config,
|
|
2700
|
+
event: "message_sent_outbound",
|
|
2701
|
+
fields: {
|
|
2702
|
+
recorded,
|
|
2703
|
+
success: messageSentEvent.success,
|
|
2704
|
+
channel: messageSentContext.channelId,
|
|
2705
|
+
conversation: messageSentContext.conversationId ?? messageSentEvent.to,
|
|
2706
|
+
session: messageSentContext.sessionKey ?? messageSentEvent.sessionKey,
|
|
2707
|
+
messageId: messageSentEvent.messageId ?? messageSentContext.messageId
|
|
2708
|
+
},
|
|
2709
|
+
content: messageSentEvent.content
|
|
2710
|
+
});
|
|
2711
|
+
return void 0;
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
export {
|
|
2716
|
+
PLUGIN_ID,
|
|
2717
|
+
index_default as default
|
|
2718
|
+
};
|