@nordbyte/nordrelay 0.6.0 → 0.8.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/.env.example +52 -0
- package/README.md +171 -50
- package/dist/access-control.js +6 -1
- package/dist/activity-events.js +2 -2
- package/dist/adapter-conformance.js +61 -0
- package/dist/bot-preferences.js +1 -0
- package/dist/bot.js +95 -37
- package/dist/channel-adapter.js +44 -11
- package/dist/channel-command-catalog.js +94 -0
- package/dist/channel-command-core.js +60 -0
- package/dist/channel-command-service.js +230 -1
- package/dist/channel-mirror-registry.js +84 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-prompt-engine.js +177 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/channel-turn-lifecycle.js +73 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +82 -8
- package/dist/config.js +79 -7
- package/dist/context-key.js +42 -0
- package/dist/discord-bot.js +173 -342
- package/dist/discord-command-surface.js +11 -73
- package/dist/index.js +29 -0
- package/dist/metrics.js +48 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +288 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-readiness.js +77 -0
- package/dist/peer-runtime-service.js +658 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +307 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-runtime-helpers.js +210 -0
- package/dist/relay-runtime.js +79 -274
- package/dist/remote-prompt.js +98 -0
- package/dist/settings-wizard-test.js +216 -0
- package/dist/slack-artifacts.js +165 -0
- package/dist/slack-bot.js +1461 -0
- package/dist/slack-channel-runtime.js +147 -0
- package/dist/slack-command-surface.js +46 -0
- package/dist/slack-diagnostics.js +116 -0
- package/dist/slack-rate-limit.js +139 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-general-commands.js +14 -0
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/user-management-crypto.js +38 -0
- package/dist/user-management-normalize.js +188 -0
- package/dist/user-management-types.js +1 -0
- package/dist/user-management.js +193 -196
- package/dist/web-api-contract.js +16 -0
- package/dist/web-dashboard-access-routes.js +62 -0
- package/dist/web-dashboard-assets.js +1 -0
- package/dist/web-dashboard-pages.js +26 -4
- package/dist/web-dashboard-peer-routes.js +225 -0
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +46 -0
- package/dist/web-state.js +2 -2
- package/dist/webui-assets/dashboard.css +193 -0
- package/dist/webui-assets/dashboard.js +870 -57
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +468 -11
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { capabilitiesOf, idOf, labelOf, parseToggle, } from "./bot-rendering.js";
|
|
3
|
-
import { friendlyErrorText } from "./error-messages.js";
|
|
1
|
+
import { capabilitiesOf, labelOf, } from "./bot-rendering.js";
|
|
4
2
|
import { escapeHTML } from "./format.js";
|
|
5
|
-
import { getAvailableBackends } from "./voice.js";
|
|
6
3
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "./workspace-policy.js";
|
|
7
4
|
import { safeReply } from "./telegram-output.js";
|
|
8
5
|
export function registerTelegramPreferenceCommands(options) {
|
|
@@ -18,28 +15,15 @@ export function registerTelegramPreferenceCommands(options) {
|
|
|
18
15
|
return;
|
|
19
16
|
}
|
|
20
17
|
const argument = (ctx.message?.text ?? "").replace(/^\/mirror(?:@\w+)?\s*/i, "").trim();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
const mode = options.getEffectiveMirrorMode(contextKey);
|
|
32
|
-
const plain = [
|
|
33
|
-
`CLI mirroring: ${mode}`,
|
|
34
|
-
`Minimum update interval: ${options.config.telegramMirrorMinUpdateMs} ms`,
|
|
35
|
-
"Modes: off, status, final, full",
|
|
36
|
-
].join("\n");
|
|
37
|
-
const html = [
|
|
38
|
-
`<b>CLI mirroring:</b> <code>${escapeHTML(mode)}</code>`,
|
|
39
|
-
`<b>Minimum update interval:</b> <code>${options.config.telegramMirrorMinUpdateMs} ms</code>`,
|
|
40
|
-
"<b>Modes:</b> <code>off</code>, <code>status</code>, <code>final</code>, <code>full</code>",
|
|
41
|
-
].join("\n");
|
|
42
|
-
await safeReply(ctx, html, { fallbackText: plain });
|
|
18
|
+
const response = options.commandService.renderMirrorPreference({
|
|
19
|
+
source: "telegram",
|
|
20
|
+
contextKey,
|
|
21
|
+
argument,
|
|
22
|
+
preferencesStore: options.preferencesStore,
|
|
23
|
+
cliMirrorSupported: capabilitiesOf(session.getInfo()).cliMirror,
|
|
24
|
+
agentLabel: labelOf(session.getInfo()),
|
|
25
|
+
});
|
|
26
|
+
await safeReply(ctx, response.html, { fallbackText: response.plain });
|
|
43
27
|
});
|
|
44
28
|
options.bot.command("notify", async (ctx) => {
|
|
45
29
|
const contextSession = await options.getContextSession(ctx, { deferThreadStart: true });
|
|
@@ -48,45 +32,13 @@ export function registerTelegramPreferenceCommands(options) {
|
|
|
48
32
|
}
|
|
49
33
|
const { contextKey } = contextSession;
|
|
50
34
|
const argument = (ctx.message?.text ?? "").replace(/^\/notify(?:@\w+)?\s*/i, "").trim();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
catch (error) {
|
|
59
|
-
await safeReply(ctx, escapeHTML(`Invalid quiet hours: ${friendlyErrorText(error)}`), {
|
|
60
|
-
fallbackText: `Invalid quiet hours: ${friendlyErrorText(error)}`,
|
|
61
|
-
});
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
options.preferencesStore.update(contextKey, { quietHours });
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
const mode = parseNotifyMode(argument, options.getEffectiveNotifyMode(contextKey));
|
|
68
|
-
if (!["off", "minimal", "all"].includes(argument.toLowerCase())) {
|
|
69
|
-
await safeReply(ctx, escapeHTML("Usage: /notify [off|minimal|all] or /notify quiet HH-HH"), {
|
|
70
|
-
fallbackText: "Usage: /notify [off|minimal|all] or /notify quiet HH-HH",
|
|
71
|
-
});
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
options.preferencesStore.update(contextKey, { notifyMode: mode });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const mode = options.getEffectiveNotifyMode(contextKey);
|
|
78
|
-
const quietHours = options.getEffectiveQuietHours(contextKey);
|
|
79
|
-
const plain = [
|
|
80
|
-
`Notifications: ${mode}`,
|
|
81
|
-
`Quiet hours: ${formatQuietHours(quietHours)}`,
|
|
82
|
-
`Currently quiet: ${isQuietNow(quietHours) ? "yes" : "no"}`,
|
|
83
|
-
].join("\n");
|
|
84
|
-
const html = [
|
|
85
|
-
`<b>Notifications:</b> <code>${escapeHTML(mode)}</code>`,
|
|
86
|
-
`<b>Quiet hours:</b> <code>${escapeHTML(formatQuietHours(quietHours))}</code>`,
|
|
87
|
-
`<b>Currently quiet:</b> <code>${isQuietNow(quietHours) ? "yes" : "no"}</code>`,
|
|
88
|
-
].join("\n");
|
|
89
|
-
await safeReply(ctx, html, { fallbackText: plain });
|
|
35
|
+
const response = options.commandService.renderNotifyPreference({
|
|
36
|
+
source: "telegram",
|
|
37
|
+
contextKey,
|
|
38
|
+
argument,
|
|
39
|
+
preferencesStore: options.preferencesStore,
|
|
40
|
+
});
|
|
41
|
+
await safeReply(ctx, response.html, { fallbackText: response.plain });
|
|
90
42
|
});
|
|
91
43
|
options.bot.command("workspaces", async (ctx) => {
|
|
92
44
|
const contextSession = await options.getContextSession(ctx, { deferThreadStart: true });
|
|
@@ -131,68 +83,12 @@ export function registerTelegramPreferenceCommands(options) {
|
|
|
131
83
|
}
|
|
132
84
|
const { contextKey } = contextSession;
|
|
133
85
|
const argument = (ctx.message?.text ?? "").replace(/^\/voice(?:@\w+)?\s*/i, "").trim();
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
options.preferencesStore.update(contextKey, { voiceBackend: parseVoiceBackendPreference(value) });
|
|
140
|
-
}
|
|
141
|
-
else if (key === "language") {
|
|
142
|
-
options.preferencesStore.update(contextKey, { voiceLanguage: value && value.toLowerCase() !== "auto" ? value : null });
|
|
143
|
-
}
|
|
144
|
-
else if (key === "transcribe_only" || key === "transcribe-only") {
|
|
145
|
-
const enabled = parseToggle(value);
|
|
146
|
-
if (enabled === undefined) {
|
|
147
|
-
await safeReply(ctx, escapeHTML("Usage: /voice transcribe_only on|off"), {
|
|
148
|
-
fallbackText: "Usage: /voice transcribe_only on|off",
|
|
149
|
-
});
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
options.preferencesStore.update(contextKey, { voiceTranscribeOnly: enabled });
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
await safeReply(ctx, escapeHTML("Usage: /voice, /voice backend auto|parakeet|faster-whisper|openai, /voice language auto|<code>, /voice transcribe_only on|off"), {
|
|
156
|
-
fallbackText: "Usage: /voice, /voice backend auto|parakeet|faster-whisper|openai, /voice language auto|<code>, /voice transcribe_only on|off",
|
|
157
|
-
});
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
const backends = await getAvailableBackends().catch(() => []);
|
|
162
|
-
if (backends.length === 0) {
|
|
163
|
-
await safeReply(ctx, [
|
|
164
|
-
"<b>Voice transcription is not available.</b>",
|
|
165
|
-
"",
|
|
166
|
-
"Install <code>faster-whisper</code> + ffmpeg, install <code>parakeet-coreml</code> on macOS Apple Silicon, or set <code>OPENAI_API_KEY</code>.",
|
|
167
|
-
"<i>Cloud transcription uses OPENAI_API_KEY, not CODEX_API_KEY.</i>",
|
|
168
|
-
].join("\n"), {
|
|
169
|
-
fallbackText: [
|
|
170
|
-
"Voice transcription is not available.",
|
|
171
|
-
"",
|
|
172
|
-
"Install faster-whisper + ffmpeg, install parakeet-coreml on macOS Apple Silicon, or set OPENAI_API_KEY.",
|
|
173
|
-
"Cloud transcription uses OPENAI_API_KEY, not CODEX_API_KEY.",
|
|
174
|
-
].join("\n"),
|
|
175
|
-
});
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const joined = backends.join(" + ");
|
|
179
|
-
const backendPreference = options.getEffectiveVoiceBackend(contextKey);
|
|
180
|
-
const language = options.getEffectiveVoiceLanguage(contextKey);
|
|
181
|
-
const transcribeOnly = options.isVoiceTranscribeOnly(contextKey);
|
|
182
|
-
const plain = [
|
|
183
|
-
`Voice backends: ${joined}`,
|
|
184
|
-
`Preferred backend: ${backendPreference}`,
|
|
185
|
-
`Language: ${language ?? "auto"}`,
|
|
186
|
-
`Transcribe only: ${transcribeOnly ? "on" : "off"}`,
|
|
187
|
-
].join("\n");
|
|
188
|
-
const html = [
|
|
189
|
-
`<b>Voice backends:</b> <code>${escapeHTML(joined)}</code>`,
|
|
190
|
-
`<b>Preferred backend:</b> <code>${escapeHTML(backendPreference)}</code>`,
|
|
191
|
-
`<b>Language:</b> <code>${escapeHTML(language ?? "auto")}</code>`,
|
|
192
|
-
`<b>Transcribe only:</b> <code>${transcribeOnly ? "on" : "off"}</code>`,
|
|
193
|
-
].join("\n");
|
|
194
|
-
await safeReply(ctx, html, {
|
|
195
|
-
fallbackText: plain,
|
|
86
|
+
const response = await options.commandService.renderVoicePreference({
|
|
87
|
+
source: "telegram",
|
|
88
|
+
contextKey,
|
|
89
|
+
argument,
|
|
90
|
+
preferencesStore: options.preferencesStore,
|
|
196
91
|
});
|
|
92
|
+
await safeReply(ctx, response.html, { fallbackText: response.plain });
|
|
197
93
|
});
|
|
198
94
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const PASSWORD_KEYLEN = 64;
|
|
3
|
+
export function sleepSync(ms) {
|
|
4
|
+
const end = Date.now() + ms;
|
|
5
|
+
while (Date.now() < end) {
|
|
6
|
+
// The lock is only held around tiny JSON mutations; a short spin keeps the implementation dependency-free.
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function hashPassword(password) {
|
|
10
|
+
if (password.length < 8) {
|
|
11
|
+
throw new Error("Password must be at least 8 characters.");
|
|
12
|
+
}
|
|
13
|
+
const salt = randomBytes(16).toString("hex");
|
|
14
|
+
const hash = scryptSync(password, salt, PASSWORD_KEYLEN).toString("hex");
|
|
15
|
+
return { salt, hash };
|
|
16
|
+
}
|
|
17
|
+
export function verifyPasswordHash(password, salt, expectedHash) {
|
|
18
|
+
const actual = scryptSync(password, salt, PASSWORD_KEYLEN);
|
|
19
|
+
const expected = Buffer.from(expectedHash, "hex");
|
|
20
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
21
|
+
}
|
|
22
|
+
export function hashToken(token) {
|
|
23
|
+
return createHash("sha256").update(token).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
export function constantTimeStringEqual(left, right) {
|
|
26
|
+
const leftBuffer = Buffer.from(left);
|
|
27
|
+
const rightBuffer = Buffer.from(right);
|
|
28
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
29
|
+
}
|
|
30
|
+
export function randomId() {
|
|
31
|
+
return randomUUID().replace(/-/g, "").slice(0, 12);
|
|
32
|
+
}
|
|
33
|
+
export function randomLinkCode() {
|
|
34
|
+
return `NR-${randomBytes(4).toString("hex").toUpperCase()}`;
|
|
35
|
+
}
|
|
36
|
+
export function randomSessionToken() {
|
|
37
|
+
return randomBytes(32).toString("hex");
|
|
38
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ADMIN_GROUP_ID, BUILTIN_GROUPS, READONLY_GROUP_ID, isPermission, } from "./access-control.js";
|
|
3
|
+
export function normalizePayload(payload) {
|
|
4
|
+
const now = new Date().toISOString();
|
|
5
|
+
const groupsById = new Map();
|
|
6
|
+
for (const group of BUILTIN_GROUPS) {
|
|
7
|
+
groupsById.set(group.id, {
|
|
8
|
+
...group,
|
|
9
|
+
permissions: group.id === ADMIN_GROUP_ID ? allPermissionsSafe() : group.permissions,
|
|
10
|
+
agentIds: [],
|
|
11
|
+
workspaceRoots: [],
|
|
12
|
+
telegramChatIds: [],
|
|
13
|
+
discordChannelIds: [],
|
|
14
|
+
slackChannelIds: [],
|
|
15
|
+
createdAt: now,
|
|
16
|
+
updatedAt: now,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
for (const group of payload?.groups ?? []) {
|
|
20
|
+
if (!isGroupRecord(group))
|
|
21
|
+
continue;
|
|
22
|
+
groupsById.set(group.id, {
|
|
23
|
+
...group,
|
|
24
|
+
permissions: group.id === ADMIN_GROUP_ID ? allPermissionsSafe() : normalizePermissions(group.permissions),
|
|
25
|
+
system: BUILTIN_GROUPS.some((builtin) => builtin.id === group.id) || group.system,
|
|
26
|
+
agentIds: normalizeStringList(group.agentIds),
|
|
27
|
+
workspaceRoots: normalizeStringList(group.workspaceRoots),
|
|
28
|
+
telegramChatIds: normalizeNumberList(group.telegramChatIds),
|
|
29
|
+
discordChannelIds: normalizeStringList(group.discordChannelIds),
|
|
30
|
+
slackChannelIds: normalizeStringList(group.slackChannelIds),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const groups = Array.from(groupsById.values());
|
|
34
|
+
const groupIds = new Set(groups.map((group) => group.id));
|
|
35
|
+
const users = (payload?.users ?? []).filter(isUserRecord);
|
|
36
|
+
const userIds = new Set(users.map((user) => user.id));
|
|
37
|
+
return {
|
|
38
|
+
version: 1,
|
|
39
|
+
users,
|
|
40
|
+
groups,
|
|
41
|
+
userGroups: (payload?.userGroups ?? []).filter((item) => isUserGroupRecord(item) && userIds.has(item.userId) && groupIds.has(item.groupId)),
|
|
42
|
+
telegramIdentities: (payload?.telegramIdentities ?? []).filter((item) => isTelegramIdentityRecord(item) && userIds.has(item.userId)),
|
|
43
|
+
telegramChats: (payload?.telegramChats ?? []).filter(isTelegramChatAccessRecord).map((chat) => ({
|
|
44
|
+
...chat,
|
|
45
|
+
allowedGroupIds: chat.allowedGroupIds.filter((groupId) => groupIds.has(groupId)),
|
|
46
|
+
})),
|
|
47
|
+
discordIdentities: (payload?.discordIdentities ?? []).filter((item) => isDiscordIdentityRecord(item) && userIds.has(item.userId)),
|
|
48
|
+
discordChannels: (payload?.discordChannels ?? []).filter(isDiscordChannelAccessRecord).map((channel) => ({
|
|
49
|
+
...channel,
|
|
50
|
+
allowedGroupIds: channel.allowedGroupIds.filter((groupId) => groupIds.has(groupId)),
|
|
51
|
+
})),
|
|
52
|
+
slackIdentities: (payload?.slackIdentities ?? []).filter((item) => isSlackIdentityRecord(item) && userIds.has(item.userId)),
|
|
53
|
+
slackChannels: (payload?.slackChannels ?? []).filter(isSlackChannelAccessRecord).map((channel) => ({
|
|
54
|
+
...channel,
|
|
55
|
+
allowedGroupIds: channel.allowedGroupIds.filter((groupId) => groupIds.has(groupId)),
|
|
56
|
+
})),
|
|
57
|
+
webSessions: (payload?.webSessions ?? []).filter((item) => isWebSessionRecord(item) && userIds.has(item.userId)),
|
|
58
|
+
telegramLinkCodes: (payload?.telegramLinkCodes ?? []).filter((item) => isTelegramLinkCodeRecord(item) && userIds.has(item.userId)),
|
|
59
|
+
discordLinkCodes: (payload?.discordLinkCodes ?? []).filter((item) => isDiscordLinkCodeRecord(item) && userIds.has(item.userId)),
|
|
60
|
+
slackLinkCodes: (payload?.slackLinkCodes ?? []).filter((item) => isSlackLinkCodeRecord(item) && userIds.has(item.userId)),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function normalizeEmail(email) {
|
|
64
|
+
return email.trim().toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
export function normalizeGroupIds(payload, values, emptyFallback = READONLY_GROUP_ID) {
|
|
67
|
+
const available = new Set(payload.groups.map((group) => group.id));
|
|
68
|
+
const groupIds = Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
|
|
69
|
+
for (const groupId of groupIds) {
|
|
70
|
+
if (!available.has(groupId)) {
|
|
71
|
+
throw new Error(`Unknown group: ${groupId}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return groupIds.length > 0 ? groupIds : (emptyFallback ? [emptyFallback] : []);
|
|
75
|
+
}
|
|
76
|
+
export function normalizePermissions(values, strict = false) {
|
|
77
|
+
const permissions = [];
|
|
78
|
+
for (const value of values ?? []) {
|
|
79
|
+
if (isPermission(value)) {
|
|
80
|
+
if (!permissions.includes(value)) {
|
|
81
|
+
permissions.push(value);
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (strict && value.trim()) {
|
|
86
|
+
throw new Error(`Unknown permission: ${value}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return permissions;
|
|
90
|
+
}
|
|
91
|
+
export function normalizeStringList(values) {
|
|
92
|
+
return Array.from(new Set((values ?? []).map((value) => value.trim()).filter(Boolean)));
|
|
93
|
+
}
|
|
94
|
+
export function normalizeNumberList(values) {
|
|
95
|
+
return Array.from(new Set((values ?? []).filter((value) => Number.isInteger(value))));
|
|
96
|
+
}
|
|
97
|
+
export function normalizeDiscordId(value) {
|
|
98
|
+
const normalized = String(value ?? "").trim();
|
|
99
|
+
return normalized || undefined;
|
|
100
|
+
}
|
|
101
|
+
export function normalizeSlackId(value) {
|
|
102
|
+
const normalized = String(value ?? "").trim();
|
|
103
|
+
return normalized || undefined;
|
|
104
|
+
}
|
|
105
|
+
export function assertActiveAdminExists(payload) {
|
|
106
|
+
const hasAdmin = payload.users.some((user) => user.active && payload.userGroups.some((item) => item.userId === user.id && item.groupId === ADMIN_GROUP_ID));
|
|
107
|
+
if (!hasAdmin) {
|
|
108
|
+
throw new Error("Cannot remove or disable the last active admin user.");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function normalizeWorkspacePath(value) {
|
|
112
|
+
return path.resolve(value);
|
|
113
|
+
}
|
|
114
|
+
export function isPathInside(candidate, root) {
|
|
115
|
+
const relative = path.relative(root, candidate);
|
|
116
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
117
|
+
}
|
|
118
|
+
export function slugify(value) {
|
|
119
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
120
|
+
}
|
|
121
|
+
export function allPermissionsSafe() {
|
|
122
|
+
return [...BUILTIN_GROUPS.find((group) => group.id === ADMIN_GROUP_ID).permissions];
|
|
123
|
+
}
|
|
124
|
+
function isUserRecord(value) {
|
|
125
|
+
const candidate = value;
|
|
126
|
+
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.email === "string" &&
|
|
127
|
+
typeof candidate.displayName === "string" && typeof candidate.passwordHash === "string" &&
|
|
128
|
+
typeof candidate.passwordSalt === "string" && typeof candidate.active === "boolean";
|
|
129
|
+
}
|
|
130
|
+
function isGroupRecord(value) {
|
|
131
|
+
const candidate = value;
|
|
132
|
+
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.name === "string" &&
|
|
133
|
+
Array.isArray(candidate.permissions);
|
|
134
|
+
}
|
|
135
|
+
function isUserGroupRecord(value) {
|
|
136
|
+
const candidate = value;
|
|
137
|
+
return Boolean(candidate) && typeof candidate.userId === "string" && typeof candidate.groupId === "string";
|
|
138
|
+
}
|
|
139
|
+
function isTelegramIdentityRecord(value) {
|
|
140
|
+
const candidate = value;
|
|
141
|
+
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
|
|
142
|
+
Number.isInteger(candidate.telegramUserId) && typeof candidate.active === "boolean";
|
|
143
|
+
}
|
|
144
|
+
function isTelegramChatAccessRecord(value) {
|
|
145
|
+
const candidate = value;
|
|
146
|
+
return Boolean(candidate) && typeof candidate.id === "string" && Number.isInteger(candidate.chatId) &&
|
|
147
|
+
typeof candidate.enabled === "boolean" && Array.isArray(candidate.allowedGroupIds);
|
|
148
|
+
}
|
|
149
|
+
function isDiscordIdentityRecord(value) {
|
|
150
|
+
const candidate = value;
|
|
151
|
+
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
|
|
152
|
+
typeof candidate.discordUserId === "string" && typeof candidate.active === "boolean";
|
|
153
|
+
}
|
|
154
|
+
function isDiscordChannelAccessRecord(value) {
|
|
155
|
+
const candidate = value;
|
|
156
|
+
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.channelId === "string" &&
|
|
157
|
+
typeof candidate.enabled === "boolean" && Array.isArray(candidate.allowedGroupIds);
|
|
158
|
+
}
|
|
159
|
+
function isSlackIdentityRecord(value) {
|
|
160
|
+
const candidate = value;
|
|
161
|
+
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
|
|
162
|
+
typeof candidate.slackUserId === "string" && typeof candidate.active === "boolean";
|
|
163
|
+
}
|
|
164
|
+
function isSlackChannelAccessRecord(value) {
|
|
165
|
+
const candidate = value;
|
|
166
|
+
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.channelId === "string" &&
|
|
167
|
+
typeof candidate.enabled === "boolean" && Array.isArray(candidate.allowedGroupIds);
|
|
168
|
+
}
|
|
169
|
+
function isWebSessionRecord(value) {
|
|
170
|
+
const candidate = value;
|
|
171
|
+
return Boolean(candidate) && typeof candidate.id === "string" && typeof candidate.userId === "string" &&
|
|
172
|
+
typeof candidate.tokenHash === "string" && typeof candidate.expiresAt === "string";
|
|
173
|
+
}
|
|
174
|
+
function isTelegramLinkCodeRecord(value) {
|
|
175
|
+
const candidate = value;
|
|
176
|
+
return Boolean(candidate) && typeof candidate.code === "string" && typeof candidate.userId === "string" &&
|
|
177
|
+
typeof candidate.expiresAt === "string";
|
|
178
|
+
}
|
|
179
|
+
function isDiscordLinkCodeRecord(value) {
|
|
180
|
+
const candidate = value;
|
|
181
|
+
return Boolean(candidate) && typeof candidate.code === "string" && typeof candidate.userId === "string" &&
|
|
182
|
+
typeof candidate.expiresAt === "string";
|
|
183
|
+
}
|
|
184
|
+
function isSlackLinkCodeRecord(value) {
|
|
185
|
+
const candidate = value;
|
|
186
|
+
return Boolean(candidate) && typeof candidate.code === "string" && typeof candidate.userId === "string" &&
|
|
187
|
+
typeof candidate.expiresAt === "string";
|
|
188
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|