lagora-cli 1.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 +138 -0
- package/dist/help.txt +70 -0
- package/dist/lagora.js +342 -0
- package/dist/report-help.txt +5 -0
- package/dist/scripts/agora_playground_harness.py +263 -0
- package/dist/scripts/announce.js +41 -0
- package/dist/scripts/check-kernel-submission.py +90 -0
- package/dist/scripts/chunk-2EAJVB5D.js +100 -0
- package/dist/scripts/chunk-2KTLCUFI.js +29 -0
- package/dist/scripts/chunk-AZ3EEBVD.js +137 -0
- package/dist/scripts/chunk-NBJMYAOA.js +2128 -0
- package/dist/scripts/chunk-NCJMUBTG.js +125 -0
- package/dist/scripts/chunk-QJPQHKIO.js +23 -0
- package/dist/scripts/chunk-RIR5KGHC.js +33 -0
- package/dist/scripts/chunk-TJZVQYBL.js +8 -0
- package/dist/scripts/chunk-UHJXD4TG.js +18 -0
- package/dist/scripts/chunk-UQ6I6VTY.js +117 -0
- package/dist/scripts/cli-auth.js +348 -0
- package/dist/scripts/cli-config-IA7EOSYD.js +7 -0
- package/dist/scripts/install-skill.js +199 -0
- package/dist/scripts/issue-local-client-DZUXZOKY.js +22 -0
- package/dist/scripts/issue-search.js +1823 -0
- package/dist/scripts/issue.js +386 -0
- package/dist/scripts/keycloak-provision.js +986 -0
- package/dist/scripts/legato-fsim-runner.py +126 -0
- package/dist/scripts/legato-lowering-runner.py +156 -0
- package/dist/scripts/legato_runner_annotations.py +235 -0
- package/dist/scripts/legato_runner_env.py +91 -0
- package/dist/scripts/legato_runner_launchers.py +287 -0
- package/dist/scripts/legato_runner_script_wrapper.py +193 -0
- package/dist/scripts/notifications-EU43SIEV.js +624 -0
- package/dist/scripts/playground.js +408 -0
- package/dist/scripts/report-bundle-sync-3U7QTP4Z.js +215 -0
- package/dist/scripts/report.js +104 -0
- package/dist/scripts/resolve-sdk-package-version.py +151 -0
- package/dist/scripts/sdk-runtime-JE6H2PB2.js +992 -0
- package/dist/scripts/sdk-runtime-kubernetes-job-KOWL4ITV.js +479 -0
- package/dist/scripts/sdk-runtime-smoke.py +168 -0
- package/dist/scripts/sdk.js +256 -0
- package/dist/scripts/site-feedback-CAPE5MPX.js +136 -0
- package/dist/scripts/site-feedback-rate-limit-5BU2WSFE.js +86 -0
- package/dist/scripts/site-feedback.js +117 -0
- package/dist/scripts/storage-234FBH54.js +67 -0
- package/dist/scripts/submit-issue.sh +489 -0
- package/dist/scripts/verification-3QCY66QW.js +772 -0
- package/dist/scripts/verify-issue.js +144 -0
- package/dist/skills/legato-agora-cli/SKILL.md +556 -0
- package/dist/skills/legato-agora-cli/agents/openai.yaml +7 -0
- package/dist/skills/legato-agora-cli/reference/kernel-with-golden.py +84 -0
- package/dist/skills/legato-site-feedback/SKILL.md +49 -0
- package/dist/skills/legato-site-feedback/agents/openai.yaml +7 -0
- package/package.json +16 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import "./chunk-TJZVQYBL.js";
|
|
2
|
+
|
|
3
|
+
// lib/server/notifications.ts
|
|
4
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
// lib/server/notification-events.ts
|
|
7
|
+
var listenersByUserId = /* @__PURE__ */ new Map();
|
|
8
|
+
function publishNotificationUpdates(userIds) {
|
|
9
|
+
for (const userId of new Set(userIds)) {
|
|
10
|
+
const listeners = listenersByUserId.get(userId);
|
|
11
|
+
if (!listeners) continue;
|
|
12
|
+
for (const listener of listeners) listener();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// lib/server/notification-store.ts
|
|
17
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
18
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
|
|
19
|
+
import path4 from "node:path";
|
|
20
|
+
|
|
21
|
+
// lib/server/user-auth-crypto.ts
|
|
22
|
+
var browserDeviceMaxAgeSeconds = 400 * 24 * 60 * 60;
|
|
23
|
+
|
|
24
|
+
// lib/server/user-auth-files.ts
|
|
25
|
+
import path2 from "node:path";
|
|
26
|
+
|
|
27
|
+
// lib/server/user-auth-records.ts
|
|
28
|
+
var legacyAccountScope = "hyperaccel";
|
|
29
|
+
function publicUser(user) {
|
|
30
|
+
return {
|
|
31
|
+
id: user.id,
|
|
32
|
+
displayName: user.displayName,
|
|
33
|
+
displayNameKey: user.displayNameKey,
|
|
34
|
+
accountScope: user.accountScope,
|
|
35
|
+
principalSubject: user.principalSubject,
|
|
36
|
+
isActive: user.isActive,
|
|
37
|
+
createdAt: user.createdAt,
|
|
38
|
+
lastSeenAt: user.lastSeenAt
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function parseUsers(values) {
|
|
42
|
+
return values.map(parseStoredUser).filter((user) => Boolean(user));
|
|
43
|
+
}
|
|
44
|
+
function parseStoredUser(value) {
|
|
45
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.displayName !== "string" || typeof value.displayNameKey !== "string" || value.passwordHash !== void 0 && typeof value.passwordHash !== "string" || typeof value.isActive !== "boolean" || typeof value.createdAt !== "string" || value.principalSubject !== void 0 && typeof value.principalSubject !== "string" || value.lastSeenAt !== void 0 && typeof value.lastSeenAt !== "string") {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
const accountScope = normalizeStoredAccountScope(value.accountScope);
|
|
49
|
+
if (!accountScope) return void 0;
|
|
50
|
+
const principalSubject = normalizeStoredPrincipalSubject(value.principalSubject);
|
|
51
|
+
return {
|
|
52
|
+
id: value.id,
|
|
53
|
+
displayName: value.displayName,
|
|
54
|
+
displayNameKey: value.displayNameKey,
|
|
55
|
+
accountScope,
|
|
56
|
+
...principalSubject ? { principalSubject } : {},
|
|
57
|
+
...typeof value.passwordHash === "string" ? { passwordHash: value.passwordHash } : {},
|
|
58
|
+
isActive: value.isActive,
|
|
59
|
+
createdAt: value.createdAt,
|
|
60
|
+
...value.lastSeenAt ? { lastSeenAt: value.lastSeenAt } : {}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function normalizeStoredAccountScope(value) {
|
|
64
|
+
if (value === void 0) return legacyAccountScope;
|
|
65
|
+
if (value === "admin" || value === "hyperaccel") return value;
|
|
66
|
+
if (typeof value === "string" && /^vendor:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value)) return value;
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
function normalizeStoredPrincipalSubject(value) {
|
|
70
|
+
if (typeof value !== "string") return void 0;
|
|
71
|
+
const normalized = value.trim();
|
|
72
|
+
return normalized || void 0;
|
|
73
|
+
}
|
|
74
|
+
function isRecord(value) {
|
|
75
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// lib/server/native-auth-records.ts
|
|
79
|
+
var nativeTokenScopes = {
|
|
80
|
+
notifications: "notifications",
|
|
81
|
+
cli: "cli"
|
|
82
|
+
};
|
|
83
|
+
var nativeTokenScope = nativeTokenScopes.notifications;
|
|
84
|
+
|
|
85
|
+
// lib/server/user-auth-file-store.ts
|
|
86
|
+
import { randomUUID } from "node:crypto";
|
|
87
|
+
import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
88
|
+
import path from "node:path";
|
|
89
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
90
|
+
var LOCK_WAIT_MS = 1e4;
|
|
91
|
+
var LOCK_STALE_MS = 6e4;
|
|
92
|
+
var LOCK_POLL_MS = 25;
|
|
93
|
+
async function readJsonArray(filePath) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
96
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (hasNodeCode(error, "ENOENT")) return [];
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function withFileLock(filePath, callback) {
|
|
103
|
+
const lockPath = `${filePath}.lock`;
|
|
104
|
+
const owner = `${process.pid}-${randomUUID()}`;
|
|
105
|
+
const startedAt = Date.now();
|
|
106
|
+
while (Date.now() - startedAt < LOCK_WAIT_MS) {
|
|
107
|
+
const handle = await tryCreateLock(lockPath, owner);
|
|
108
|
+
if (handle) {
|
|
109
|
+
try {
|
|
110
|
+
return await callback();
|
|
111
|
+
} finally {
|
|
112
|
+
await handle.close();
|
|
113
|
+
await releaseLock(lockPath, owner);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await removeStaleLock(lockPath);
|
|
117
|
+
await delay(LOCK_POLL_MS);
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`Timed out waiting for user auth file lock: ${lockPath}`);
|
|
120
|
+
}
|
|
121
|
+
async function tryCreateLock(lockPath, owner) {
|
|
122
|
+
try {
|
|
123
|
+
const handle = await open(lockPath, "wx");
|
|
124
|
+
await handle.writeFile(`${JSON.stringify({ owner, pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
125
|
+
`);
|
|
126
|
+
return handle;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (hasNodeCode(error, "ENOENT")) {
|
|
129
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
130
|
+
return tryCreateLock(lockPath, owner);
|
|
131
|
+
}
|
|
132
|
+
if (hasNodeCode(error, "EEXIST")) return void 0;
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function removeStaleLock(lockPath) {
|
|
137
|
+
try {
|
|
138
|
+
const details = await stat(lockPath);
|
|
139
|
+
if (Date.now() - details.mtimeMs <= LOCK_STALE_MS) return;
|
|
140
|
+
await rm(lockPath, { force: true });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (hasNodeCode(error, "ENOENT")) return;
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function releaseLock(lockPath, owner) {
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(await readFile(lockPath, "utf8"));
|
|
149
|
+
if (isRecord2(parsed) && parsed.owner === owner) await rm(lockPath, { force: true });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (hasNodeCode(error, "ENOENT")) return;
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function hasNodeCode(error, code) {
|
|
156
|
+
return error instanceof Error && Reflect.get(error, "code") === code;
|
|
157
|
+
}
|
|
158
|
+
function isRecord2(value) {
|
|
159
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// lib/server/user-auth-files.ts
|
|
163
|
+
function userAuthRoot() {
|
|
164
|
+
if (process.env.AGORA_AUTH_DIR?.trim()) return path2.resolve(process.env.AGORA_AUTH_DIR);
|
|
165
|
+
if (process.env.AGORA_SETTINGS_DIR?.trim()) return path2.join(path2.resolve(process.env.AGORA_SETTINGS_DIR), "auth");
|
|
166
|
+
return path2.join(
|
|
167
|
+
/*turbopackIgnore: true*/
|
|
168
|
+
process.cwd(),
|
|
169
|
+
".local-data",
|
|
170
|
+
"auth"
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
async function readUsers() {
|
|
174
|
+
return withFileLock(usersPath(), async () => parseUsers(await readJsonArray(usersPath())));
|
|
175
|
+
}
|
|
176
|
+
function usersPath() {
|
|
177
|
+
return path2.join(userAuthRoot(), "users.json");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// lib/server/user-auth.ts
|
|
181
|
+
var userSessionMaxAgeSeconds = 30 * 24 * 60 * 60;
|
|
182
|
+
var sessionTtlMs = userSessionMaxAgeSeconds * 1e3;
|
|
183
|
+
async function resolveUserByDisplayName(displayName, accountScope = legacyAccountScope, principalSubject) {
|
|
184
|
+
const normalizedName = displayName.trim();
|
|
185
|
+
if (!normalizedName) return void 0;
|
|
186
|
+
const normalizedScope = normalizeAccountScope(accountScope);
|
|
187
|
+
const normalizedSubject = normalizePrincipalSubject(principalSubject);
|
|
188
|
+
const user = (await readUsers()).find((item) => sameProfile(item, normalizedName, normalizedScope, normalizedSubject) && item.isActive);
|
|
189
|
+
return user ? publicUser(user) : void 0;
|
|
190
|
+
}
|
|
191
|
+
async function listUsersForAdmin() {
|
|
192
|
+
return (await readUsers()).map(publicUser);
|
|
193
|
+
}
|
|
194
|
+
function sameProfile(user, displayName, accountScope, principalSubject) {
|
|
195
|
+
return user.displayNameKey === displayNameKey(displayName) && user.accountScope === accountScope && user.principalSubject === principalSubject;
|
|
196
|
+
}
|
|
197
|
+
function accountScopeCanViewVendor(accountScope, vendorId) {
|
|
198
|
+
if (accountScope === "admin" || accountScope === "hyperaccel") return true;
|
|
199
|
+
return accountScope === `vendor:${vendorId ?? legacyAccountScope}`;
|
|
200
|
+
}
|
|
201
|
+
function normalizeAccountScope(accountScope) {
|
|
202
|
+
if (accountScope === "admin" || accountScope === "hyperaccel") return accountScope;
|
|
203
|
+
if (/^vendor:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(accountScope)) return accountScope;
|
|
204
|
+
throw new Error(`Invalid account scope: ${accountScope}`);
|
|
205
|
+
}
|
|
206
|
+
function normalizePrincipalSubject(principalSubject) {
|
|
207
|
+
return normalizeStoredPrincipalSubject(principalSubject);
|
|
208
|
+
}
|
|
209
|
+
function displayNameKey(displayName) {
|
|
210
|
+
return displayName.trim().toLocaleLowerCase();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// lib/server/notification-store-lock.ts
|
|
214
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
215
|
+
import { mkdir as mkdir2, open as open2, readFile as readFile2, rm as rm2, stat as stat2 } from "node:fs/promises";
|
|
216
|
+
import path3 from "node:path";
|
|
217
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
218
|
+
var DEFAULT_LOCK_WAIT_MS = 1e4;
|
|
219
|
+
var DEFAULT_LOCK_STALE_MS = 6e4;
|
|
220
|
+
var LOCK_POLL_MS2 = 50;
|
|
221
|
+
async function withNotificationStoreLock(root, callback) {
|
|
222
|
+
const lock = await acquireNotificationStoreLock(root);
|
|
223
|
+
try {
|
|
224
|
+
return await callback();
|
|
225
|
+
} finally {
|
|
226
|
+
await lock.release();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function acquireNotificationStoreLock(root) {
|
|
230
|
+
const lockPath = path3.join(root, "notifications.lock");
|
|
231
|
+
const owner = `${process.pid}-${randomUUID2()}`;
|
|
232
|
+
const startedAt = Date.now();
|
|
233
|
+
while (Date.now() - startedAt < notificationLockWaitMs()) {
|
|
234
|
+
const handle = await tryCreateLock2(lockPath, owner);
|
|
235
|
+
if (handle) return lockHandle(lockPath, owner, handle);
|
|
236
|
+
await removeStaleLock2(lockPath);
|
|
237
|
+
await delay2(LOCK_POLL_MS2);
|
|
238
|
+
}
|
|
239
|
+
throw new Error(`Timed out waiting for notification store lock: ${lockPath}`);
|
|
240
|
+
}
|
|
241
|
+
async function tryCreateLock2(lockPath, owner) {
|
|
242
|
+
try {
|
|
243
|
+
const handle = await open2(lockPath, "wx");
|
|
244
|
+
await handle.writeFile(`${JSON.stringify({ owner, pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
245
|
+
`);
|
|
246
|
+
return handle;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (hasNodeCode2(error, "ENOENT")) {
|
|
249
|
+
await mkdir2(path3.dirname(lockPath), { recursive: true });
|
|
250
|
+
return await tryCreateLock2(lockPath, owner);
|
|
251
|
+
}
|
|
252
|
+
if (hasNodeCode2(error, "EEXIST")) return void 0;
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function removeStaleLock2(lockPath) {
|
|
257
|
+
try {
|
|
258
|
+
const details = await stat2(lockPath);
|
|
259
|
+
if (Date.now() - details.mtimeMs <= notificationLockStaleMs()) return;
|
|
260
|
+
await rm2(lockPath, { force: true });
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (hasNodeCode2(error, "ENOENT")) return;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function lockHandle(lockPath, owner, handle) {
|
|
267
|
+
return {
|
|
268
|
+
release: async () => {
|
|
269
|
+
await handle.close();
|
|
270
|
+
try {
|
|
271
|
+
const current = JSON.parse(await readFile2(lockPath, "utf8"));
|
|
272
|
+
if (isRecord3(current) && current.owner === owner) await rm2(lockPath, { force: true });
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (hasNodeCode2(error, "ENOENT")) return;
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function notificationLockWaitMs() {
|
|
281
|
+
return readPositiveInt(process.env.AGORA_NOTIFICATION_LOCK_WAIT_MS, DEFAULT_LOCK_WAIT_MS);
|
|
282
|
+
}
|
|
283
|
+
function notificationLockStaleMs() {
|
|
284
|
+
return readPositiveInt(process.env.AGORA_NOTIFICATION_LOCK_STALE_MS, DEFAULT_LOCK_STALE_MS);
|
|
285
|
+
}
|
|
286
|
+
function readPositiveInt(value, fallback) {
|
|
287
|
+
if (!value) return fallback;
|
|
288
|
+
const parsed = Number.parseInt(value, 10);
|
|
289
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
290
|
+
}
|
|
291
|
+
function isRecord3(value) {
|
|
292
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
293
|
+
}
|
|
294
|
+
function hasNodeCode2(error, code) {
|
|
295
|
+
return error instanceof Error && Reflect.get(error, "code") === code;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// lib/server/notification-store.ts
|
|
299
|
+
async function readNotificationRecords() {
|
|
300
|
+
try {
|
|
301
|
+
const parsed = JSON.parse(await readFile3(notificationsPath(), "utf8"));
|
|
302
|
+
return Array.isArray(parsed) ? parsed.filter(isIssueNotification) : [];
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (hasNodeCode3(error, "ENOENT")) return [];
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function updateNotificationRecords(callback) {
|
|
309
|
+
return await withNotificationStoreLock(notificationsRoot(), async () => {
|
|
310
|
+
const current = await readNotificationRecords();
|
|
311
|
+
const output = callback(current);
|
|
312
|
+
await writeNotificationRecords(output.next);
|
|
313
|
+
return output.result;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async function compactNotificationRecords() {
|
|
317
|
+
}
|
|
318
|
+
async function notificationStoreStatus() {
|
|
319
|
+
const statusError = await readStatusError();
|
|
320
|
+
const notifications = statusError.notifications;
|
|
321
|
+
const newest = notifications.toSorted(compareNewestFirst)[0];
|
|
322
|
+
return {
|
|
323
|
+
path: notificationsPath(),
|
|
324
|
+
totalCount: notifications.length,
|
|
325
|
+
unreadCount: notifications.filter((notification) => !notification.readAt).length,
|
|
326
|
+
lastCreatedAt: newest?.createdAt,
|
|
327
|
+
lastError: statusError.error ?? await readNotificationStoreError()
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
async function recordNotificationStoreError(context, error) {
|
|
331
|
+
try {
|
|
332
|
+
await mkdir3(notificationsRoot(), { recursive: true });
|
|
333
|
+
const payload = {
|
|
334
|
+
context,
|
|
335
|
+
message: error instanceof Error ? error.message : String(error),
|
|
336
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
337
|
+
};
|
|
338
|
+
await writeFile2(notificationErrorPath(), `${JSON.stringify(payload, null, 2)}
|
|
339
|
+
`, { mode: 384 });
|
|
340
|
+
} catch (writeError) {
|
|
341
|
+
if (writeError instanceof Error) return;
|
|
342
|
+
throw writeError;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function clearNotificationStoreError() {
|
|
346
|
+
await rm3(notificationErrorPath(), { force: true });
|
|
347
|
+
}
|
|
348
|
+
function notificationsRoot() {
|
|
349
|
+
if (process.env.AGORA_NOTIFICATIONS_DIR?.trim()) return path4.resolve(process.env.AGORA_NOTIFICATIONS_DIR);
|
|
350
|
+
return path4.join(userAuthRoot(), "notifications");
|
|
351
|
+
}
|
|
352
|
+
function notificationsPath() {
|
|
353
|
+
return path4.join(notificationsRoot(), "notifications.json");
|
|
354
|
+
}
|
|
355
|
+
async function writeNotificationRecords(notifications) {
|
|
356
|
+
await mkdir3(notificationsRoot(), { recursive: true });
|
|
357
|
+
const target = notificationsPath();
|
|
358
|
+
const temp = `${target}.${process.pid}.${randomUUID3()}.tmp`;
|
|
359
|
+
await writeFile2(temp, `${JSON.stringify(notifications, null, 2)}
|
|
360
|
+
`, { mode: 384 });
|
|
361
|
+
await rename2(temp, target);
|
|
362
|
+
await rm3(notificationErrorPath(), { force: true });
|
|
363
|
+
}
|
|
364
|
+
async function readNotificationStoreError() {
|
|
365
|
+
try {
|
|
366
|
+
const parsed = JSON.parse(await readFile3(notificationErrorPath(), "utf8"));
|
|
367
|
+
return isNotificationStoreError(parsed) ? parsed : void 0;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (hasNodeCode3(error, "ENOENT")) return void 0;
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function readStatusError() {
|
|
374
|
+
try {
|
|
375
|
+
return { notifications: await readNotificationRecords() };
|
|
376
|
+
} catch (error) {
|
|
377
|
+
const storeError = {
|
|
378
|
+
context: "notification_store_status",
|
|
379
|
+
message: error instanceof Error ? error.message : String(error),
|
|
380
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
381
|
+
};
|
|
382
|
+
await recordNotificationStoreError(storeError.context, error);
|
|
383
|
+
return { notifications: [], error: storeError };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function notificationErrorPath() {
|
|
387
|
+
return path4.join(notificationsRoot(), "last-error.json");
|
|
388
|
+
}
|
|
389
|
+
function compareNewestFirst(left, right) {
|
|
390
|
+
return createdAtMs(right) - createdAtMs(left);
|
|
391
|
+
}
|
|
392
|
+
function createdAtMs(notification) {
|
|
393
|
+
const timestamp = new Date(notification.createdAt).getTime();
|
|
394
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
395
|
+
}
|
|
396
|
+
function isIssueNotification(value) {
|
|
397
|
+
if (!isRecord4(value)) return false;
|
|
398
|
+
return typeof value.id === "string" && typeof value.recipientUserId === "string" && (value.issueId === void 0 || typeof value.issueId === "string") && (value.eventId === void 0 || typeof value.eventId === "string") && typeof value.eventType === "string" && typeof value.title === "string" && typeof value.body === "string" && (value.linkUrl === void 0 || typeof value.linkUrl === "string") && (value.readAt === void 0 || typeof value.readAt === "string") && typeof value.createdAt === "string";
|
|
399
|
+
}
|
|
400
|
+
function isNotificationStoreError(value) {
|
|
401
|
+
return isRecord4(value) && typeof value.context === "string" && typeof value.message === "string" && typeof value.createdAt === "string";
|
|
402
|
+
}
|
|
403
|
+
function isRecord4(value) {
|
|
404
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
405
|
+
}
|
|
406
|
+
function hasNodeCode3(error, code) {
|
|
407
|
+
return error instanceof Error && Reflect.get(error, "code") === code;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// lib/server/notifications.ts
|
|
411
|
+
async function createIssueCreatedNotifications(issue, actorUserId) {
|
|
412
|
+
try {
|
|
413
|
+
const recipients = (await listUsersForAdmin()).filter((user) => user.isActive && user.id !== actorUserId && accountScopeCanViewVendor(user.accountScope, issue.creatorVendorId));
|
|
414
|
+
return await createNotifications(recipients.map((user) => notificationInput(user.id, issue, "issue_created", {
|
|
415
|
+
title: issue.title,
|
|
416
|
+
body: "\uC0C8 \uC774\uC288\uAC00 \uB4F1\uB85D\uB418\uC5C8\uC2B5\uB2C8\uB2E4"
|
|
417
|
+
})));
|
|
418
|
+
} catch (error) {
|
|
419
|
+
await recordNotificationStoreError("issue_created", error);
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function createIssueChangeNotifications(input) {
|
|
424
|
+
try {
|
|
425
|
+
const recipientIds = await issueRecipientIds(input.issue, input.actorUserId);
|
|
426
|
+
return await createNotifications(recipientIds.map((recipientUserId) => notificationInput(recipientUserId, input.issue, input.eventType, {
|
|
427
|
+
eventId: input.eventId,
|
|
428
|
+
title: input.title ?? "\uC774\uC288\uC5D0 \uBCC0\uACBD\uC0AC\uD56D\uC774 \uC788\uC2B5\uB2C8\uB2E4",
|
|
429
|
+
body: input.body ?? input.issue.title
|
|
430
|
+
})));
|
|
431
|
+
} catch (error) {
|
|
432
|
+
await recordNotificationStoreError(input.eventType, error);
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function createIssueDeletedNotifications(issue, actorUserId) {
|
|
437
|
+
try {
|
|
438
|
+
const recipientIds = await issueRecipientIds(issue, actorUserId);
|
|
439
|
+
return await createNotifications(recipientIds.map((recipientUserId) => ({
|
|
440
|
+
recipientUserId,
|
|
441
|
+
issueId: issue.id,
|
|
442
|
+
eventType: "issue_deleted",
|
|
443
|
+
title: "\uC774\uC288\uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
|
|
444
|
+
body: issue.title,
|
|
445
|
+
linkUrl: "/"
|
|
446
|
+
})));
|
|
447
|
+
} catch (error) {
|
|
448
|
+
await recordNotificationStoreError("issue_deleted", error);
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function createAdminTestNotification(recipientUserId) {
|
|
453
|
+
return await createNotifications([{
|
|
454
|
+
recipientUserId,
|
|
455
|
+
eventType: "admin_test",
|
|
456
|
+
title: "\uC54C\uB9BC \uD14C\uC2A4\uD2B8",
|
|
457
|
+
body: "\uAD00\uB9AC\uC790 \uC124\uC815\uC5D0\uC11C \uBCF4\uB0B8 \uD14C\uC2A4\uD2B8 \uC54C\uB9BC\uC785\uB2C8\uB2E4.",
|
|
458
|
+
linkUrl: "/settings?section=notifications#notifications"
|
|
459
|
+
}]);
|
|
460
|
+
}
|
|
461
|
+
async function createSiteAnnouncementNotifications(input) {
|
|
462
|
+
try {
|
|
463
|
+
const recipients = (await listUsersForAdmin()).filter((user) => user.isActive);
|
|
464
|
+
return await createNotifications(recipients.map((user) => ({
|
|
465
|
+
recipientUserId: user.id,
|
|
466
|
+
eventType: "site_announcement",
|
|
467
|
+
title: input.title,
|
|
468
|
+
body: input.body,
|
|
469
|
+
linkUrl: input.linkUrl
|
|
470
|
+
})));
|
|
471
|
+
} catch (error) {
|
|
472
|
+
await recordNotificationStoreError("site_announcement", error);
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function createNotifications(inputs) {
|
|
477
|
+
if (inputs.length === 0) return [];
|
|
478
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
479
|
+
const notifications = inputs.map((input) => ({
|
|
480
|
+
id: randomUUID4(),
|
|
481
|
+
recipientUserId: input.recipientUserId,
|
|
482
|
+
issueId: input.issueId,
|
|
483
|
+
eventId: input.eventId,
|
|
484
|
+
eventType: input.eventType,
|
|
485
|
+
title: input.title,
|
|
486
|
+
body: input.body,
|
|
487
|
+
linkUrl: input.linkUrl,
|
|
488
|
+
readAt: input.readAt,
|
|
489
|
+
createdAt: input.createdAt ?? createdAt
|
|
490
|
+
}));
|
|
491
|
+
await updateNotificationRecords((current) => ({
|
|
492
|
+
next: [...current, ...notifications],
|
|
493
|
+
result: void 0
|
|
494
|
+
}));
|
|
495
|
+
publishNotificationUpdates(notifications.map((notification) => notification.recipientUserId));
|
|
496
|
+
return notifications;
|
|
497
|
+
}
|
|
498
|
+
async function listNotificationsForUser(userId) {
|
|
499
|
+
return notificationsForUser(await readNotificationRecords(), userId);
|
|
500
|
+
}
|
|
501
|
+
async function unreadNotificationCount(userId) {
|
|
502
|
+
return (await listNotificationsForUser(userId)).filter((notification) => !notification.readAt).length;
|
|
503
|
+
}
|
|
504
|
+
async function markNotificationRead(userId, notificationId, now = /* @__PURE__ */ new Date()) {
|
|
505
|
+
const readAt = now.toISOString();
|
|
506
|
+
const result = await updateNotificationRecords((notifications2) => {
|
|
507
|
+
let found = false;
|
|
508
|
+
const next = notifications2.map((notification) => {
|
|
509
|
+
if (notification.recipientUserId !== userId || notification.id !== notificationId) return notification;
|
|
510
|
+
found = true;
|
|
511
|
+
return notification.readAt ? notification : { ...notification, readAt };
|
|
512
|
+
});
|
|
513
|
+
return {
|
|
514
|
+
next,
|
|
515
|
+
result: { found }
|
|
516
|
+
};
|
|
517
|
+
});
|
|
518
|
+
const notifications = await listNotificationsForUser(userId);
|
|
519
|
+
if (result.found) publishNotificationUpdates([userId]);
|
|
520
|
+
return result.found ? { kind: "updated", notifications } : { kind: "not_found", notifications };
|
|
521
|
+
}
|
|
522
|
+
async function markNotificationUnread(userId, notificationId) {
|
|
523
|
+
const normalizedID = notificationId.trim();
|
|
524
|
+
if (!normalizedID) {
|
|
525
|
+
return { kind: "not_found", notifications: await listNotificationsForUser(userId) };
|
|
526
|
+
}
|
|
527
|
+
const result = await updateNotificationRecords((notifications2) => {
|
|
528
|
+
let found = false;
|
|
529
|
+
const next = notifications2.map((notification) => {
|
|
530
|
+
if (notification.recipientUserId !== userId || notification.id !== normalizedID) return notification;
|
|
531
|
+
found = true;
|
|
532
|
+
if (!notification.readAt) return notification;
|
|
533
|
+
const { readAt: _readAt, ...unreadNotification } = notification;
|
|
534
|
+
return unreadNotification;
|
|
535
|
+
});
|
|
536
|
+
return {
|
|
537
|
+
next,
|
|
538
|
+
result: { found }
|
|
539
|
+
};
|
|
540
|
+
});
|
|
541
|
+
const notifications = await listNotificationsForUser(userId);
|
|
542
|
+
if (result.found) publishNotificationUpdates([userId]);
|
|
543
|
+
return result.found ? { kind: "updated", notifications } : { kind: "not_found", notifications };
|
|
544
|
+
}
|
|
545
|
+
async function markAllNotificationsRead(userId, now = /* @__PURE__ */ new Date()) {
|
|
546
|
+
const readAt = now.toISOString();
|
|
547
|
+
await updateNotificationRecords((notifications) => ({
|
|
548
|
+
next: notifications.map((notification) => notification.recipientUserId === userId && !notification.readAt ? { ...notification, readAt } : notification),
|
|
549
|
+
result: void 0
|
|
550
|
+
}));
|
|
551
|
+
publishNotificationUpdates([userId]);
|
|
552
|
+
return await listNotificationsForUser(userId);
|
|
553
|
+
}
|
|
554
|
+
async function markIssueNotificationsRead(userId, issueId, now = /* @__PURE__ */ new Date()) {
|
|
555
|
+
const normalizedIssueId = issueId.trim();
|
|
556
|
+
if (!normalizedIssueId) return await listNotificationsForUser(userId);
|
|
557
|
+
const readAt = now.toISOString();
|
|
558
|
+
const changed = await updateNotificationRecords((notifications) => {
|
|
559
|
+
let mutated = false;
|
|
560
|
+
const next = notifications.map((notification) => {
|
|
561
|
+
if (notification.recipientUserId !== userId || notification.issueId !== normalizedIssueId || notification.readAt) return notification;
|
|
562
|
+
mutated = true;
|
|
563
|
+
return { ...notification, readAt };
|
|
564
|
+
});
|
|
565
|
+
return { next, result: mutated };
|
|
566
|
+
});
|
|
567
|
+
if (changed) publishNotificationUpdates([userId]);
|
|
568
|
+
return await listNotificationsForUser(userId);
|
|
569
|
+
}
|
|
570
|
+
async function issueRecipientIds(issue, actorUserId) {
|
|
571
|
+
const ids = /* @__PURE__ */ new Set();
|
|
572
|
+
const reporterId = issue.creatorUserId ?? (await resolveUserByDisplayName(issue.creator, accountScopeForIssue(issue)))?.id;
|
|
573
|
+
if (reporterId) ids.add(reporterId);
|
|
574
|
+
for (const assignee of issueAssignees(issue)) {
|
|
575
|
+
const assigneeId = assignee.userId ?? (await resolveUserByDisplayName(assignee.name, accountScopeForIssue(issue)))?.id;
|
|
576
|
+
if (assigneeId) ids.add(assigneeId);
|
|
577
|
+
}
|
|
578
|
+
if (actorUserId) ids.delete(actorUserId);
|
|
579
|
+
const allowedIds = new Set((await listUsersForAdmin()).filter((user) => user.isActive && accountScopeCanViewVendor(user.accountScope, issue.creatorVendorId)).map((user) => user.id));
|
|
580
|
+
return [...ids].filter((id) => allowedIds.has(id));
|
|
581
|
+
}
|
|
582
|
+
function accountScopeForIssue(issue) {
|
|
583
|
+
const vendorId = issue.creatorVendorId ?? "hyperaccel";
|
|
584
|
+
return vendorId === "hyperaccel" ? "hyperaccel" : `vendor:${vendorId}`;
|
|
585
|
+
}
|
|
586
|
+
function issueAssignees(issue) {
|
|
587
|
+
const merged = [...issue.assignees ?? [], ...issue.assignee ? [issue.assignee] : []];
|
|
588
|
+
const byName = /* @__PURE__ */ new Map();
|
|
589
|
+
for (const assignee of merged) {
|
|
590
|
+
if (assignee.name.trim()) byName.set(assignee.name.trim().toLowerCase(), assignee);
|
|
591
|
+
}
|
|
592
|
+
return [...byName.values()];
|
|
593
|
+
}
|
|
594
|
+
function notificationsForUser(notifications, userId) {
|
|
595
|
+
return notifications.map((notification, index) => ({ notification, index })).filter((item) => item.notification.recipientUserId === userId).toSorted((left, right) => new Date(right.notification.createdAt).getTime() - new Date(left.notification.createdAt).getTime() || right.index - left.index).map((item) => item.notification);
|
|
596
|
+
}
|
|
597
|
+
function notificationInput(recipientUserId, issue, eventType, details) {
|
|
598
|
+
return {
|
|
599
|
+
recipientUserId,
|
|
600
|
+
issueId: issue.id,
|
|
601
|
+
eventId: details.eventId,
|
|
602
|
+
eventType,
|
|
603
|
+
title: details.title,
|
|
604
|
+
body: details.body,
|
|
605
|
+
linkUrl: `/issues/${issue.id}`
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
export {
|
|
609
|
+
clearNotificationStoreError,
|
|
610
|
+
compactNotificationRecords as compactNotificationStore,
|
|
611
|
+
createAdminTestNotification,
|
|
612
|
+
createIssueChangeNotifications,
|
|
613
|
+
createIssueCreatedNotifications,
|
|
614
|
+
createIssueDeletedNotifications,
|
|
615
|
+
createNotifications,
|
|
616
|
+
createSiteAnnouncementNotifications,
|
|
617
|
+
listNotificationsForUser,
|
|
618
|
+
markAllNotificationsRead,
|
|
619
|
+
markIssueNotificationsRead,
|
|
620
|
+
markNotificationRead,
|
|
621
|
+
markNotificationUnread,
|
|
622
|
+
notificationStoreStatus,
|
|
623
|
+
unreadNotificationCount
|
|
624
|
+
};
|