@imenam/mcp-organizer 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +529 -2
- package/dist/shared/logic/first-account.d.ts +13 -0
- package/dist/shared/logic/first-account.js +15 -0
- package/dist/shared/logic/labels.d.ts +5 -0
- package/dist/shared/logic/labels.js +17 -0
- package/dist/shared/logic/positions.d.ts +9 -0
- package/dist/shared/logic/positions.js +19 -0
- package/dist/shared/logic/spaces.d.ts +13 -0
- package/dist/shared/logic/spaces.js +48 -0
- package/dist/shared/logic/test-summary.d.ts +28 -0
- package/dist/shared/logic/test-summary.js +35 -0
- package/dist/shared/schemas/agent-api.schema.d.ts +268 -0
- package/dist/shared/schemas/agent-api.schema.js +94 -0
- package/dist/shared/schemas/agent-tools.schema.d.ts +724 -0
- package/dist/shared/schemas/agent-tools.schema.js +165 -0
- package/dist/shared/schemas/api.schema.d.ts +249 -0
- package/dist/shared/schemas/api.schema.js +116 -0
- package/dist/shared/schemas/entities.schema.d.ts +472 -0
- package/dist/shared/schemas/entities.schema.js +262 -0
- package/dist/shared/schemas/enums.schema.d.ts +135 -0
- package/dist/shared/schemas/enums.schema.js +58 -0
- package/dist/shared/schemas/gui-api.schema.d.ts +53 -0
- package/dist/shared/schemas/gui-api.schema.js +20 -0
- package/dist/shared/schemas/ideas-api.schema.d.ts +81 -0
- package/dist/shared/schemas/ideas-api.schema.js +40 -0
- package/dist/shared/schemas/notifications-api.schema.d.ts +87 -0
- package/dist/shared/schemas/notifications-api.schema.js +58 -0
- package/dist/shared/schemas/tasks-api.schema.d.ts +675 -0
- package/dist/shared/schemas/tasks-api.schema.js +225 -0
- package/dist/shared/schemas/tokens-api.schema.d.ts +106 -0
- package/dist/shared/schemas/tokens-api.schema.js +42 -0
- package/dist/shared/schemas/transcription-api.schema.d.ts +15 -0
- package/dist/shared/schemas/transcription-api.schema.js +12 -0
- package/dist/src/agent-api/checklist.d.ts +3 -0
- package/dist/src/agent-api/checklist.js +127 -0
- package/dist/src/agent-api/ideas.d.ts +3 -0
- package/dist/src/agent-api/ideas.js +93 -0
- package/dist/src/agent-api/requests.d.ts +3 -0
- package/dist/src/agent-api/requests.js +60 -0
- package/dist/src/agent-api/router.d.ts +3 -0
- package/dist/src/agent-api/router.js +40 -0
- package/dist/src/agent-api/tasks.d.ts +2 -0
- package/dist/src/agent-api/tasks.js +290 -0
- package/dist/src/agent-api/types.d.ts +41 -0
- package/dist/src/agent-api/types.js +14 -0
- package/dist/src/cli/backup.d.ts +12 -0
- package/dist/src/cli/backup.js +126 -0
- package/dist/src/cli/prompt.d.ts +7 -0
- package/dist/src/cli/prompt.js +40 -0
- package/dist/src/cli/set-user.d.ts +2 -0
- package/dist/src/cli/set-user.js +114 -0
- package/dist/src/cli/setup-mcp.d.ts +10 -0
- package/dist/src/cli/setup-mcp.js +110 -0
- package/dist/src/cli/setup.d.ts +39 -0
- package/dist/src/cli/setup.js +626 -0
- package/dist/src/core/accounts.d.ts +129 -0
- package/dist/src/core/accounts.js +284 -0
- package/dist/src/core/agent-tokens.d.ts +31 -0
- package/dist/src/core/agent-tokens.js +120 -0
- package/dist/src/core/agent.d.ts +71 -0
- package/dist/src/core/agent.js +358 -0
- package/dist/src/core/attachments.d.ts +21 -0
- package/dist/src/core/attachments.js +44 -0
- package/dist/src/core/data-dir.d.ts +5 -0
- package/dist/src/core/data-dir.js +11 -0
- package/dist/src/core/db.d.ts +13 -0
- package/dist/src/core/db.js +38 -0
- package/dist/src/core/env-file.d.ts +11 -0
- package/dist/src/core/env-file.js +36 -0
- package/dist/src/core/errors.d.ts +24 -0
- package/dist/src/core/errors.js +42 -0
- package/dist/src/core/history.d.ts +34 -0
- package/dist/src/core/history.js +279 -0
- package/dist/src/core/ideas.d.ts +16 -0
- package/dist/src/core/ideas.js +100 -0
- package/dist/src/core/ids.d.ts +2 -0
- package/dist/src/core/ids.js +5 -0
- package/dist/src/core/log-dir.d.ts +2 -0
- package/dist/src/core/log-dir.js +9 -0
- package/dist/src/core/logger.d.ts +14 -0
- package/dist/src/core/logger.js +37 -0
- package/dist/src/core/mail.d.ts +43 -0
- package/dist/src/core/mail.js +44 -0
- package/dist/src/core/mcp-entry.d.ts +20 -0
- package/dist/src/core/mcp-entry.js +15 -0
- package/dist/src/core/migrations.d.ts +18 -0
- package/dist/src/core/migrations.js +404 -0
- package/dist/src/core/native-sqlite.d.ts +2 -0
- package/dist/src/core/native-sqlite.js +23 -0
- package/dist/src/core/notifications.d.ts +46 -0
- package/dist/src/core/notifications.js +173 -0
- package/dist/src/core/project-root.d.ts +2 -0
- package/dist/src/core/project-root.js +13 -0
- package/dist/src/core/requests.d.ts +49 -0
- package/dist/src/core/requests.js +239 -0
- package/dist/src/core/snapshots.d.ts +32 -0
- package/dist/src/core/snapshots.js +35 -0
- package/dist/src/core/spaces.d.ts +66 -0
- package/dist/src/core/spaces.js +252 -0
- package/dist/src/core/store.d.ts +565 -0
- package/dist/src/core/store.js +2093 -0
- package/dist/src/core/task-history.d.ts +7 -0
- package/dist/src/core/task-history.js +41 -0
- package/dist/src/core/tasks.d.ts +58 -0
- package/dist/src/core/tasks.js +389 -0
- package/dist/src/core/test-tasks.d.ts +19 -0
- package/dist/src/core/test-tasks.js +135 -0
- package/dist/src/core/transcription.d.ts +45 -0
- package/dist/src/core/transcription.js +92 -0
- package/dist/src/deploy/env-render.d.ts +26 -0
- package/dist/src/deploy/env-render.js +98 -0
- package/dist/src/deploy/templates.d.ts +38 -0
- package/dist/src/deploy/templates.js +130 -0
- package/dist/src/gui-api.d.ts +37 -0
- package/dist/src/gui-api.js +383 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +100 -0
- package/dist/src/mcp/client.d.ts +23 -0
- package/dist/src/mcp/client.js +81 -0
- package/dist/src/mcp/start.d.ts +14 -0
- package/dist/src/mcp/start.js +104 -0
- package/dist/src/mcp/tools.d.ts +46 -0
- package/dist/src/mcp/tools.js +348 -0
- package/dist/src/server/app.d.ts +25 -0
- package/dist/src/server/app.js +290 -0
- package/dist/src/server/config.d.ts +23 -0
- package/dist/src/server/config.js +75 -0
- package/dist/src/server/limits.d.ts +2 -0
- package/dist/src/server/limits.js +4 -0
- package/dist/src/server/login-limiter.d.ts +36 -0
- package/dist/src/server/login-limiter.js +81 -0
- package/dist/src/server/start.d.ts +5 -0
- package/dist/src/server/start.js +88 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +16 -0
- package/gui/dist/assets/index-DwkXx4Q2.js +70 -0
- package/gui/dist/assets/index-lnvnkDdg.css +1 -0
- package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/gui/dist/index.html +13 -0
- package/package.json +66 -4
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Mutation, Store } from "./store.js";
|
|
2
|
+
import { type AccessLink, type User } from "../../shared/schemas/entities.schema.js";
|
|
3
|
+
import type { AccessLinkPurpose } from "../../shared/schemas/enums.schema.js";
|
|
4
|
+
/** Durée d'une session sans activité. */
|
|
5
|
+
export declare const SESSION_TTL_MS: number;
|
|
6
|
+
/** Intervalle minimal entre deux prolongations d'une même session. */
|
|
7
|
+
export declare const SESSION_REFRESH_INTERVAL_MS: number;
|
|
8
|
+
/** Durée de validité d'un lien d'invitation ou de réinitialisation. */
|
|
9
|
+
export declare const ACCESS_LINK_TTL_MS: number;
|
|
10
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
11
|
+
/** Vérifie un mot de passe ; un compte sans mot de passe et un hash illisible sont refusés. */
|
|
12
|
+
export declare function verifyPassword(hash: string | null, password: string): Promise<boolean>;
|
|
13
|
+
/** Jeton opaque de 32 octets, en base64url. */
|
|
14
|
+
export declare function newToken(): string;
|
|
15
|
+
/** Hash sha256 (hexadécimal) sous lequel un jeton est stocké. */
|
|
16
|
+
export declare function hashToken(token: string): string;
|
|
17
|
+
/** Adresse validée et normalisée, ou erreur explicite. */
|
|
18
|
+
export declare function normalizeEmail(email: string): string;
|
|
19
|
+
/** Nom affiché validé (non vide, longueur bornée), ou erreur explicite. */
|
|
20
|
+
export declare function normalizeDisplayName(name: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Rend un compte actif : nom affiché et mot de passe posés, espace personnel
|
|
23
|
+
* créé s'il manque. Les invitations en attente du compte restent en attente :
|
|
24
|
+
* il les accepte ou les refuse une fois connecté.
|
|
25
|
+
*/
|
|
26
|
+
export declare function activateAccount(tx: Mutation, user: User, input: {
|
|
27
|
+
displayName: string;
|
|
28
|
+
passwordHash: string;
|
|
29
|
+
}): User;
|
|
30
|
+
/** Accepte une invitation en attente pour le compte `userId`, qui rejoint l'espace. */
|
|
31
|
+
export declare function joinInvitedSpace(tx: Mutation, userId: string, invitationId: string): void;
|
|
32
|
+
/** Crée l'espace personnel d'un compte s'il n'en a pas. */
|
|
33
|
+
export declare function ensurePersonalSpace(tx: Mutation, userId: string): void;
|
|
34
|
+
export interface BootstrapResult {
|
|
35
|
+
/** `created` : compte créé ; `exists` : des comptes existent ; `missing` : aucun compte ni variable. */
|
|
36
|
+
outcome: "created" | "exists" | "missing";
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Crée le premier compte depuis les variables d'environnement, si et
|
|
41
|
+
* seulement si la table est vide. Une adresse et un mot de passe sans nom
|
|
42
|
+
* affiché sont refusés.
|
|
43
|
+
*/
|
|
44
|
+
export declare function bootstrapAdmin(store: Store, credentials: {
|
|
45
|
+
email?: string;
|
|
46
|
+
displayName?: string;
|
|
47
|
+
password?: string;
|
|
48
|
+
}): Promise<BootstrapResult>;
|
|
49
|
+
/** Compte à créer ou à réactiver. `displayName` est requis pour un compte qui n'en a pas encore. */
|
|
50
|
+
export interface SetAccountInput {
|
|
51
|
+
email: string;
|
|
52
|
+
password: string;
|
|
53
|
+
displayName?: string;
|
|
54
|
+
}
|
|
55
|
+
/** Message d'un compte créé sans nom affiché. */
|
|
56
|
+
export declare const DISPLAY_NAME_REQUIRED_MESSAGE = "Le nom affich\u00E9 du compte est requis.";
|
|
57
|
+
/**
|
|
58
|
+
* Crée un compte actif, ou remplace le mot de passe d'un compte existant, qui
|
|
59
|
+
* redevient actif (et prend le nom affiché donné). Les sessions ouvertes du
|
|
60
|
+
* compte sont fermées.
|
|
61
|
+
*/
|
|
62
|
+
export declare function setAccount(store: Store, input: SetAccountInput): Promise<{
|
|
63
|
+
created: boolean;
|
|
64
|
+
user: User;
|
|
65
|
+
}>;
|
|
66
|
+
/** Change le nom affiché d'un compte. */
|
|
67
|
+
export declare function updateDisplayName(store: Store, userId: string, displayName: string): User;
|
|
68
|
+
/**
|
|
69
|
+
* Change le mot de passe d'un compte après vérification de l'ancien. Les
|
|
70
|
+
* autres sessions du compte sont fermées ; celle de `keepSessionToken` reste ouverte.
|
|
71
|
+
*/
|
|
72
|
+
export declare function changePassword(store: Store, userId: string, input: {
|
|
73
|
+
currentPassword: string;
|
|
74
|
+
newPassword: string;
|
|
75
|
+
}, keepSessionToken: string | null): Promise<void>;
|
|
76
|
+
/** Session ouverte : le jeton à placer dans le cookie et sa fin de validité. */
|
|
77
|
+
export interface IssuedSession {
|
|
78
|
+
token: string;
|
|
79
|
+
expiresAt: string;
|
|
80
|
+
}
|
|
81
|
+
/** Ouvre une session pour `userId` dans la mutation en cours. */
|
|
82
|
+
export declare function openSession(tx: Mutation, userId: string, now: Date): IssuedSession;
|
|
83
|
+
/** Vérifie e-mail et mot de passe et ouvre une session. */
|
|
84
|
+
export declare function login(store: Store, email: string, password: string, now?: Date): Promise<{
|
|
85
|
+
user: User;
|
|
86
|
+
session: IssuedSession;
|
|
87
|
+
}>;
|
|
88
|
+
/**
|
|
89
|
+
* Utilisateur d'une session valide, ou null. Une session expirée, ou d'un
|
|
90
|
+
* compte qui n'est plus actif, est supprimée. L'expiration glisse : une
|
|
91
|
+
* session utilisée est prolongée (au plus une fois par
|
|
92
|
+
* `SESSION_REFRESH_INTERVAL_MS`), et `refreshed` porte alors le cookie à renvoyer.
|
|
93
|
+
*/
|
|
94
|
+
export declare function authenticate(store: Store, token: string | null, now?: Date): {
|
|
95
|
+
user: User;
|
|
96
|
+
refreshed: IssuedSession | null;
|
|
97
|
+
} | null;
|
|
98
|
+
/** Ferme la session du jeton, si elle existe. */
|
|
99
|
+
export declare function logout(store: Store, token: string | null): void;
|
|
100
|
+
/** Lien émis : le jeton en clair, qui n'est montré qu'une fois. */
|
|
101
|
+
export interface IssuedAccessLink {
|
|
102
|
+
token: string;
|
|
103
|
+
expiresAt: string;
|
|
104
|
+
}
|
|
105
|
+
/** Chemin de la GUI qui ouvre l'écran d'activation d'un lien. */
|
|
106
|
+
export declare function accessLinkPath(token: string): string;
|
|
107
|
+
/**
|
|
108
|
+
* Émet un lien pour un compte, dans la mutation en cours. Un lien
|
|
109
|
+
* d'invitation est rattaché à `invitationId` et remplace le lien précédent de
|
|
110
|
+
* cette invitation ; les liens des autres invitations restent valables.
|
|
111
|
+
*/
|
|
112
|
+
export declare function issueAccessLink(tx: Mutation, userId: string, purpose: AccessLinkPurpose, invitationId: string | null, now: Date): IssuedAccessLink;
|
|
113
|
+
/** Lien valide et son compte ; sinon une erreur qui dit pourquoi le lien ne fonctionne pas. */
|
|
114
|
+
export declare function resolveAccessLink(store: Store, token: string, now?: Date): {
|
|
115
|
+
link: AccessLink;
|
|
116
|
+
user: User;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Consomme un lien : nom affiché et mot de passe posés, compte activé (espace
|
|
120
|
+
* personnel créé), session ouverte. Un lien d'invitation accepte l'invitation
|
|
121
|
+
* qui l'a émis, et elle seule.
|
|
122
|
+
*/
|
|
123
|
+
export declare function redeemAccessLink(store: Store, token: string, input: {
|
|
124
|
+
displayName: string;
|
|
125
|
+
password: string;
|
|
126
|
+
}, now?: Date): Promise<{
|
|
127
|
+
user: User;
|
|
128
|
+
session: IssuedSession;
|
|
129
|
+
}>;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import argon2 from "argon2";
|
|
3
|
+
import { InvariantError, NotFoundError, OrganizerError, UnauthorizedError } from "./errors.js";
|
|
4
|
+
import { newId } from "./ids.js";
|
|
5
|
+
import { DisplayNameSchema, MIN_PASSWORD_LENGTH } from "../../shared/schemas/api.schema.js";
|
|
6
|
+
import { EmailSchema } from "../../shared/schemas/entities.schema.js";
|
|
7
|
+
import { PERSONAL_KEY_PREFIX, PERSONAL_SPACE_NAME } from "../../shared/logic/spaces.js";
|
|
8
|
+
import { NO_ACCOUNT_MESSAGE } from "../../shared/logic/first-account.js";
|
|
9
|
+
/**
|
|
10
|
+
* Comptes, mots de passe, sessions et liens d'accès.
|
|
11
|
+
*
|
|
12
|
+
* Les comptes se créent sur invitation. Le premier compte vient des variables
|
|
13
|
+
* `ORGANIZER_ADMIN_EMAIL` / `ORGANIZER_ADMIN_NAME` / `ORGANIZER_ADMIN_PASSWORD` (au démarrage, si la
|
|
14
|
+
* table est vide) ou de la commande `--set-user`. Les mots de passe sont hachés
|
|
15
|
+
* en argon2id ; les jetons de session et de lien ne sont stockés que hachés
|
|
16
|
+
* (sha256).
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Paramètres argon2id (recommandation OWASP : 19 Mio, 2 passes, 1 fil), fixés
|
|
20
|
+
* explicitement pour qu'une mise à jour de la bibliothèque ne change pas le coût.
|
|
21
|
+
*/
|
|
22
|
+
const ARGON2_OPTIONS = {
|
|
23
|
+
type: argon2.argon2id,
|
|
24
|
+
memoryCost: 19_456,
|
|
25
|
+
timeCost: 2,
|
|
26
|
+
parallelism: 1,
|
|
27
|
+
};
|
|
28
|
+
/** Durée d'une session sans activité. */
|
|
29
|
+
export const SESSION_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
30
|
+
/** Intervalle minimal entre deux prolongations d'une même session. */
|
|
31
|
+
export const SESSION_REFRESH_INTERVAL_MS = 60 * 60 * 1000;
|
|
32
|
+
/** Durée de validité d'un lien d'invitation ou de réinitialisation. */
|
|
33
|
+
export const ACCESS_LINK_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
34
|
+
const BAD_CREDENTIALS_MESSAGE = "Adresse e-mail ou mot de passe incorrect.";
|
|
35
|
+
export async function hashPassword(password) {
|
|
36
|
+
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
37
|
+
throw new OrganizerError(`Le mot de passe doit faire au moins ${MIN_PASSWORD_LENGTH} caractères.`);
|
|
38
|
+
}
|
|
39
|
+
return argon2.hash(password, ARGON2_OPTIONS);
|
|
40
|
+
}
|
|
41
|
+
/** Vérifie un mot de passe ; un compte sans mot de passe et un hash illisible sont refusés. */
|
|
42
|
+
export async function verifyPassword(hash, password) {
|
|
43
|
+
if (hash === null)
|
|
44
|
+
return false;
|
|
45
|
+
try {
|
|
46
|
+
return await argon2.verify(hash, password);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Hash d'un mot de passe arbitraire, vérifié quand l'adresse est inconnue pour garder un temps de réponse constant. */
|
|
53
|
+
let decoyHash = null;
|
|
54
|
+
function decoy() {
|
|
55
|
+
decoyHash ??= argon2.hash(randomBytes(16).toString("hex"), ARGON2_OPTIONS);
|
|
56
|
+
return decoyHash;
|
|
57
|
+
}
|
|
58
|
+
/** Jeton opaque de 32 octets, en base64url. */
|
|
59
|
+
export function newToken() {
|
|
60
|
+
return randomBytes(32).toString("base64url");
|
|
61
|
+
}
|
|
62
|
+
/** Hash sha256 (hexadécimal) sous lequel un jeton est stocké. */
|
|
63
|
+
export function hashToken(token) {
|
|
64
|
+
return createHash("sha256").update(token, "utf8").digest("hex");
|
|
65
|
+
}
|
|
66
|
+
/** Adresse validée et normalisée, ou erreur explicite. */
|
|
67
|
+
export function normalizeEmail(email) {
|
|
68
|
+
const parsed = EmailSchema.safeParse(email);
|
|
69
|
+
if (!parsed.success)
|
|
70
|
+
throw new OrganizerError(`Adresse e-mail invalide : « ${email} ».`);
|
|
71
|
+
return parsed.data;
|
|
72
|
+
}
|
|
73
|
+
/** Nom affiché validé (non vide, longueur bornée), ou erreur explicite. */
|
|
74
|
+
export function normalizeDisplayName(name) {
|
|
75
|
+
const parsed = DisplayNameSchema.safeParse(name);
|
|
76
|
+
if (!parsed.success)
|
|
77
|
+
throw new OrganizerError(parsed.error.issues.map((issue) => issue.message).join(" "));
|
|
78
|
+
return parsed.data;
|
|
79
|
+
}
|
|
80
|
+
// --- Activation ---------------------------------------------------------------
|
|
81
|
+
/**
|
|
82
|
+
* Rend un compte actif : nom affiché et mot de passe posés, espace personnel
|
|
83
|
+
* créé s'il manque. Les invitations en attente du compte restent en attente :
|
|
84
|
+
* il les accepte ou les refuse une fois connecté.
|
|
85
|
+
*/
|
|
86
|
+
export function activateAccount(tx, user, input) {
|
|
87
|
+
const active = tx.updateUser(user.id, { displayName: input.displayName, passwordHash: input.passwordHash, status: "active" });
|
|
88
|
+
ensurePersonalSpace(tx, user.id);
|
|
89
|
+
return active;
|
|
90
|
+
}
|
|
91
|
+
/** Accepte une invitation en attente pour le compte `userId`, qui rejoint l'espace. */
|
|
92
|
+
export function joinInvitedSpace(tx, userId, invitationId) {
|
|
93
|
+
const invitation = tx.acceptInvitation(invitationId);
|
|
94
|
+
if (!tx.store.isMember(invitation.spaceId, userId))
|
|
95
|
+
tx.addMember(invitation.spaceId, userId);
|
|
96
|
+
}
|
|
97
|
+
/** Crée l'espace personnel d'un compte s'il n'en a pas. */
|
|
98
|
+
export function ensurePersonalSpace(tx, userId) {
|
|
99
|
+
if (tx.store.findPersonalSpace(userId))
|
|
100
|
+
return;
|
|
101
|
+
tx.createSpace({ name: PERSONAL_SPACE_NAME, kind: "personal", ownerId: userId, keyPrefix: PERSONAL_KEY_PREFIX });
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Crée le premier compte depuis les variables d'environnement, si et
|
|
105
|
+
* seulement si la table est vide. Une adresse et un mot de passe sans nom
|
|
106
|
+
* affiché sont refusés.
|
|
107
|
+
*/
|
|
108
|
+
export async function bootstrapAdmin(store, credentials) {
|
|
109
|
+
const count = store.countUsers();
|
|
110
|
+
if (count > 0)
|
|
111
|
+
return { outcome: "exists", message: `${count} account(s) in the database.` };
|
|
112
|
+
const email = credentials.email?.trim();
|
|
113
|
+
const password = credentials.password;
|
|
114
|
+
if (!email || !password) {
|
|
115
|
+
return {
|
|
116
|
+
outcome: "missing",
|
|
117
|
+
message: "No account exists: the GUI refuses every login. Create the first one with ORGANIZER_ADMIN_EMAIL, ORGANIZER_ADMIN_NAME and ORGANIZER_ADMIN_PASSWORD, " +
|
|
118
|
+
'or with: node dist/src/index.js --set-user <email> --name="First Last" (cloned site) or npx -y @imenam/mcp-organizer --set-user <email> --name="First Last" (npm package)',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const displayName = credentials.displayName?.trim();
|
|
122
|
+
if (!displayName)
|
|
123
|
+
throw new OrganizerError("ORGANIZER_ADMIN_NAME is not set: give the display name of the first account (for example: Matthieu Pin).");
|
|
124
|
+
const result = await setAccount(store, { email, password, displayName });
|
|
125
|
+
return { outcome: "created", message: `First account ${result.user.email} created from ORGANIZER_ADMIN_EMAIL / ORGANIZER_ADMIN_NAME / ORGANIZER_ADMIN_PASSWORD.` };
|
|
126
|
+
}
|
|
127
|
+
/** Message d'un compte créé sans nom affiché. */
|
|
128
|
+
export const DISPLAY_NAME_REQUIRED_MESSAGE = "Le nom affiché du compte est requis.";
|
|
129
|
+
/**
|
|
130
|
+
* Crée un compte actif, ou remplace le mot de passe d'un compte existant, qui
|
|
131
|
+
* redevient actif (et prend le nom affiché donné). Les sessions ouvertes du
|
|
132
|
+
* compte sont fermées.
|
|
133
|
+
*/
|
|
134
|
+
export async function setAccount(store, input) {
|
|
135
|
+
const normalized = normalizeEmail(input.email);
|
|
136
|
+
const displayName = input.displayName === undefined ? undefined : normalizeDisplayName(input.displayName);
|
|
137
|
+
const passwordHash = await hashPassword(input.password);
|
|
138
|
+
const userId = store.findUserByEmail(normalized)?.id ?? newId();
|
|
139
|
+
return store.mutate({ type: "user", userId }, (tx) => {
|
|
140
|
+
const existing = tx.store.findUserByEmail(normalized);
|
|
141
|
+
const name = displayName ?? existing?.displayName;
|
|
142
|
+
if (!name)
|
|
143
|
+
throw new OrganizerError(DISPLAY_NAME_REQUIRED_MESSAGE);
|
|
144
|
+
if (existing) {
|
|
145
|
+
const user = activateAccount(tx, existing, { displayName: name, passwordHash });
|
|
146
|
+
tx.deleteSessionsOf(user.id);
|
|
147
|
+
tx.discardUnusedAccessLinks(user.id);
|
|
148
|
+
return { created: false, user };
|
|
149
|
+
}
|
|
150
|
+
const created = tx.createUser({ id: userId, email: normalized, displayName: name, passwordHash, status: "active" });
|
|
151
|
+
ensurePersonalSpace(tx, created.id);
|
|
152
|
+
return { created: true, user: created };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// --- Profil ---------------------------------------------------------------------
|
|
156
|
+
/** Change le nom affiché d'un compte. */
|
|
157
|
+
export function updateDisplayName(store, userId, displayName) {
|
|
158
|
+
const name = normalizeDisplayName(displayName);
|
|
159
|
+
return store.mutate({ type: "user", userId }, (tx) => tx.updateUser(userId, { displayName: name }));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Change le mot de passe d'un compte après vérification de l'ancien. Les
|
|
163
|
+
* autres sessions du compte sont fermées ; celle de `keepSessionToken` reste ouverte.
|
|
164
|
+
*/
|
|
165
|
+
export async function changePassword(store, userId, input, keepSessionToken) {
|
|
166
|
+
const user = store.getUser(userId);
|
|
167
|
+
if (!(await verifyPassword(user.passwordHash, input.currentPassword)))
|
|
168
|
+
throw new OrganizerError("Le mot de passe actuel est incorrect.");
|
|
169
|
+
const passwordHash = await hashPassword(input.newPassword);
|
|
170
|
+
store.mutate({ type: "user", userId }, (tx) => {
|
|
171
|
+
tx.updateUser(userId, { passwordHash });
|
|
172
|
+
tx.deleteSessionsOf(userId, keepSessionToken === null ? null : hashToken(keepSessionToken));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/** Ouvre une session pour `userId` dans la mutation en cours. */
|
|
176
|
+
export function openSession(tx, userId, now) {
|
|
177
|
+
const token = newToken();
|
|
178
|
+
const expiresAt = new Date(now.getTime() + SESSION_TTL_MS).toISOString();
|
|
179
|
+
tx.deleteExpiredSessions(now.toISOString());
|
|
180
|
+
tx.createSession({ idHash: hashToken(token), userId, createdAt: now.toISOString(), expiresAt });
|
|
181
|
+
return { token, expiresAt };
|
|
182
|
+
}
|
|
183
|
+
/** Vérifie e-mail et mot de passe et ouvre une session. */
|
|
184
|
+
export async function login(store, email, password, now = new Date()) {
|
|
185
|
+
if (store.countUsers() === 0)
|
|
186
|
+
throw new UnauthorizedError(NO_ACCOUNT_MESSAGE);
|
|
187
|
+
const user = store.findUserByEmail(email);
|
|
188
|
+
if (!user || user.passwordHash === null) {
|
|
189
|
+
await verifyPassword(await decoy(), password);
|
|
190
|
+
throw new UnauthorizedError(BAD_CREDENTIALS_MESSAGE);
|
|
191
|
+
}
|
|
192
|
+
if (!(await verifyPassword(user.passwordHash, password)))
|
|
193
|
+
throw new UnauthorizedError(BAD_CREDENTIALS_MESSAGE);
|
|
194
|
+
if (user.status !== "active")
|
|
195
|
+
throw new UnauthorizedError("Ce compte n'est pas actif.");
|
|
196
|
+
const session = store.mutate({ type: "user", userId: user.id }, (tx) => openSession(tx, user.id, now));
|
|
197
|
+
return { user, session };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Utilisateur d'une session valide, ou null. Une session expirée, ou d'un
|
|
201
|
+
* compte qui n'est plus actif, est supprimée. L'expiration glisse : une
|
|
202
|
+
* session utilisée est prolongée (au plus une fois par
|
|
203
|
+
* `SESSION_REFRESH_INTERVAL_MS`), et `refreshed` porte alors le cookie à renvoyer.
|
|
204
|
+
*/
|
|
205
|
+
export function authenticate(store, token, now = new Date()) {
|
|
206
|
+
if (!token)
|
|
207
|
+
return null;
|
|
208
|
+
const idHash = hashToken(token);
|
|
209
|
+
const session = store.findSessionByHash(idHash);
|
|
210
|
+
if (!session)
|
|
211
|
+
return null;
|
|
212
|
+
const user = store.findUser(session.userId);
|
|
213
|
+
if (!user || user.status !== "active" || Date.parse(session.expiresAt) <= now.getTime()) {
|
|
214
|
+
store.mutate({ type: "user", userId: session.userId }, (tx) => tx.deleteSession(idHash));
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const remaining = Date.parse(session.expiresAt) - now.getTime();
|
|
218
|
+
if (remaining > SESSION_TTL_MS - SESSION_REFRESH_INTERVAL_MS)
|
|
219
|
+
return { user, refreshed: null };
|
|
220
|
+
const expiresAt = new Date(now.getTime() + SESSION_TTL_MS).toISOString();
|
|
221
|
+
store.mutate({ type: "user", userId: user.id }, (tx) => tx.extendSession(idHash, expiresAt));
|
|
222
|
+
return { user, refreshed: { token, expiresAt } };
|
|
223
|
+
}
|
|
224
|
+
/** Ferme la session du jeton, si elle existe. */
|
|
225
|
+
export function logout(store, token) {
|
|
226
|
+
if (!token)
|
|
227
|
+
return;
|
|
228
|
+
const session = store.findSessionByHash(hashToken(token));
|
|
229
|
+
if (!session)
|
|
230
|
+
return;
|
|
231
|
+
store.mutate({ type: "user", userId: session.userId }, (tx) => tx.deleteSession(session.idHash));
|
|
232
|
+
}
|
|
233
|
+
/** Chemin de la GUI qui ouvre l'écran d'activation d'un lien. */
|
|
234
|
+
export function accessLinkPath(token) {
|
|
235
|
+
return `#/activation/${token}`;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Émet un lien pour un compte, dans la mutation en cours. Un lien
|
|
239
|
+
* d'invitation est rattaché à `invitationId` et remplace le lien précédent de
|
|
240
|
+
* cette invitation ; les liens des autres invitations restent valables.
|
|
241
|
+
*/
|
|
242
|
+
export function issueAccessLink(tx, userId, purpose, invitationId, now) {
|
|
243
|
+
const token = newToken();
|
|
244
|
+
const expiresAt = new Date(now.getTime() + ACCESS_LINK_TTL_MS).toISOString();
|
|
245
|
+
tx.createAccessLink({ userId, purpose, invitationId, tokenHash: hashToken(token), expiresAt });
|
|
246
|
+
return { token, expiresAt };
|
|
247
|
+
}
|
|
248
|
+
/** Lien valide et son compte ; sinon une erreur qui dit pourquoi le lien ne fonctionne pas. */
|
|
249
|
+
export function resolveAccessLink(store, token, now = new Date()) {
|
|
250
|
+
const link = store.findAccessLinkByHash(hashToken(token));
|
|
251
|
+
if (!link) {
|
|
252
|
+
throw new NotFoundError("Ce lien n'est pas valide : il a été révoqué ou remplacé par un lien plus récent. Demandez un nouveau lien à la personne qui vous a invité.");
|
|
253
|
+
}
|
|
254
|
+
if (link.usedAt !== null)
|
|
255
|
+
throw new InvariantError("Ce lien a déjà été utilisé. Connectez-vous avec votre adresse e-mail et votre mot de passe.");
|
|
256
|
+
if (Date.parse(link.expiresAt) <= now.getTime()) {
|
|
257
|
+
throw new InvariantError("Ce lien a expiré. Demandez un nouveau lien à la personne qui vous a invité.");
|
|
258
|
+
}
|
|
259
|
+
const user = store.getUser(link.userId);
|
|
260
|
+
if (user.status === "disabled")
|
|
261
|
+
throw new InvariantError("Ce compte est désactivé.");
|
|
262
|
+
if (link.purpose === "invite" && user.status !== "invited") {
|
|
263
|
+
throw new InvariantError("Ce compte est déjà activé : connectez-vous avec votre adresse e-mail et votre mot de passe. L'invitation vous attend dans « Invitations reçues », dans le menu de l'espace.");
|
|
264
|
+
}
|
|
265
|
+
return { link, user };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Consomme un lien : nom affiché et mot de passe posés, compte activé (espace
|
|
269
|
+
* personnel créé), session ouverte. Un lien d'invitation accepte l'invitation
|
|
270
|
+
* qui l'a émis, et elle seule.
|
|
271
|
+
*/
|
|
272
|
+
export async function redeemAccessLink(store, token, input, now = new Date()) {
|
|
273
|
+
const { user: owner } = resolveAccessLink(store, token, now);
|
|
274
|
+
const passwordHash = await hashPassword(input.password);
|
|
275
|
+
return store.mutate({ type: "user", userId: owner.id }, (tx) => {
|
|
276
|
+
const { link, user } = resolveAccessLink(tx.store, token, now);
|
|
277
|
+
tx.markAccessLinkUsed(link.id);
|
|
278
|
+
const active = activateAccount(tx, user, { displayName: input.displayName, passwordHash });
|
|
279
|
+
if (link.invitationId !== null)
|
|
280
|
+
joinInvitedSpace(tx, user.id, link.invitationId);
|
|
281
|
+
tx.deleteSessionsOf(user.id);
|
|
282
|
+
return { user: active, session: openSession(tx, user.id, now) };
|
|
283
|
+
});
|
|
284
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
import type { AgentToken, Space, User } from "../../shared/schemas/entities.schema.js";
|
|
3
|
+
import { type AgentTokenView, type CreateTokenRequest, type CreatedTokenResponse, type TokenListResponse, type TokenState } from "../../shared/schemas/tokens-api.schema.js";
|
|
4
|
+
/** Nouveau jeton d'agent en clair. */
|
|
5
|
+
export declare function generateAgentToken(): string;
|
|
6
|
+
/** Hash sha256 (hexadécimal) d'un jeton. */
|
|
7
|
+
export declare function hashAgentToken(token: string): string;
|
|
8
|
+
/** État d'un jeton à l'instant `now` ; la révocation l'emporte sur l'expiration, l'expiration sur la perte d'accès. */
|
|
9
|
+
export declare function tokenState(store: Store, token: AgentToken, now: Date): TokenState;
|
|
10
|
+
/** Jetons de l'utilisateur : actifs d'abord, puis du plus récent au plus ancien. */
|
|
11
|
+
export declare function listAgentTokens(store: Store, userId: string, now?: Date): TokenListResponse;
|
|
12
|
+
/**
|
|
13
|
+
* Crée un jeton de l'utilisateur sur un espace dont il est membre ; renvoie
|
|
14
|
+
* sa valeur, montrée une seule fois, et l'entrée `.mcp.json` du mode MCP
|
|
15
|
+
* relié au site `publicUrl`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createAgentToken(store: Store, userId: string, input: CreateTokenRequest, publicUrl: string, now?: Date): CreatedTokenResponse;
|
|
18
|
+
/** Révoque un jeton de l'utilisateur ; un jeton d'un autre est introuvable. */
|
|
19
|
+
export declare function revokeAgentToken(store: Store, userId: string, tokenId: string, now?: Date): AgentTokenView;
|
|
20
|
+
/** Jeton résolu par l'API agent : le jeton, son propriétaire et l'espace couvert. */
|
|
21
|
+
export interface ResolvedAgentToken {
|
|
22
|
+
token: AgentToken;
|
|
23
|
+
user: User;
|
|
24
|
+
space: Space;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Résout le jeton présenté par un agent et note son usage. Un jeton absent,
|
|
28
|
+
* inconnu, révoqué ou expiré est refusé (401) ; un propriétaire inactif ou
|
|
29
|
+
* qui n'est plus membre de l'espace couvert aussi (403).
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveAgentToken(store: Store, raw: string | null, now?: Date): ResolvedAgentToken;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { ForbiddenError, NotFoundError, UnauthorizedError } from "./errors.js";
|
|
3
|
+
import { requireMember } from "./spaces.js";
|
|
4
|
+
import { buildAgentEntry, mcpJsonText } from "./mcp-entry.js";
|
|
5
|
+
import { AGENT_TOKEN_PREFIX, } from "../../shared/schemas/tokens-api.schema.js";
|
|
6
|
+
/**
|
|
7
|
+
* Jetons d'agent. Un utilisateur crée un jeton pour un espace dont il est
|
|
8
|
+
* membre ; le jeton (`org_` + 32 octets aléatoires en base64url) n'est
|
|
9
|
+
* montré qu'à sa création, seul son hash sha256 est stocké. À chaque appel
|
|
10
|
+
* de l'API agent, le jeton est résolu : non révoqué, non expiré, propriétaire
|
|
11
|
+
* actif et toujours membre de l'espace couvert. Quitter un espace, ou en être
|
|
12
|
+
* retiré, révoque les jetons du membre sur cet espace.
|
|
13
|
+
*/
|
|
14
|
+
/** Nombre de caractères aléatoires gardés dans le préfixe affiché d'un jeton. */
|
|
15
|
+
const PREFIX_RANDOM_CHARS = 8;
|
|
16
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const asUser = (userId) => ({ type: "user", userId });
|
|
18
|
+
/** Nouveau jeton d'agent en clair. */
|
|
19
|
+
export function generateAgentToken() {
|
|
20
|
+
return `${AGENT_TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
|
|
21
|
+
}
|
|
22
|
+
/** Hash sha256 (hexadécimal) d'un jeton. */
|
|
23
|
+
export function hashAgentToken(token) {
|
|
24
|
+
return createHash("sha256").update(token, "utf8").digest("hex");
|
|
25
|
+
}
|
|
26
|
+
/** État d'un jeton à l'instant `now` ; la révocation l'emporte sur l'expiration, l'expiration sur la perte d'accès. */
|
|
27
|
+
export function tokenState(store, token, now) {
|
|
28
|
+
if (token.revokedAt !== null)
|
|
29
|
+
return "revoked";
|
|
30
|
+
if (token.expiresAt !== null && Date.parse(token.expiresAt) <= now.getTime())
|
|
31
|
+
return "expired";
|
|
32
|
+
const user = store.findUser(token.userId);
|
|
33
|
+
if (!user || user.status !== "active" || !store.isMember(token.spaceId, token.userId))
|
|
34
|
+
return "inactive";
|
|
35
|
+
return "active";
|
|
36
|
+
}
|
|
37
|
+
function tokenView(store, token, now) {
|
|
38
|
+
const space = store.getSpace(token.spaceId);
|
|
39
|
+
return {
|
|
40
|
+
id: token.id,
|
|
41
|
+
name: token.name,
|
|
42
|
+
prefix: token.prefix,
|
|
43
|
+
space: { id: space.id, name: space.name, kind: space.kind },
|
|
44
|
+
state: tokenState(store, token, now),
|
|
45
|
+
createdAt: token.createdAt,
|
|
46
|
+
lastUsedAt: token.lastUsedAt,
|
|
47
|
+
expiresAt: token.expiresAt,
|
|
48
|
+
revokedAt: token.revokedAt,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const STATE_ORDER = { active: 0, inactive: 1, expired: 2, revoked: 3 };
|
|
52
|
+
/** Jetons de l'utilisateur : actifs d'abord, puis du plus récent au plus ancien. */
|
|
53
|
+
export function listAgentTokens(store, userId, now = new Date()) {
|
|
54
|
+
const tokens = store.listAgentTokensOf(userId).map((token) => tokenView(store, token, now));
|
|
55
|
+
tokens.sort((a, b) => STATE_ORDER[a.state] - STATE_ORDER[b.state] || b.createdAt.localeCompare(a.createdAt));
|
|
56
|
+
return { tokens };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Crée un jeton de l'utilisateur sur un espace dont il est membre ; renvoie
|
|
60
|
+
* sa valeur, montrée une seule fois, et l'entrée `.mcp.json` du mode MCP
|
|
61
|
+
* relié au site `publicUrl`.
|
|
62
|
+
*/
|
|
63
|
+
export function createAgentToken(store, userId, input, publicUrl, now = new Date()) {
|
|
64
|
+
const value = generateAgentToken();
|
|
65
|
+
const token = store.mutate(asUser(userId), (tx) => {
|
|
66
|
+
requireMember(tx.store, input.spaceId, userId);
|
|
67
|
+
return tx.createAgentToken({
|
|
68
|
+
spaceId: input.spaceId,
|
|
69
|
+
name: input.name,
|
|
70
|
+
prefix: value.slice(0, AGENT_TOKEN_PREFIX.length + PREFIX_RANDOM_CHARS),
|
|
71
|
+
tokenHash: hashAgentToken(value),
|
|
72
|
+
expiresAt: input.expiresInDays === null ? null : new Date(now.getTime() + input.expiresInDays * DAY_MS).toISOString(),
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
return { token: tokenView(store, token, now), value, mcpJson: mcpJsonText(buildAgentEntry({ url: publicUrl, token: value })) };
|
|
76
|
+
}
|
|
77
|
+
/** Révoque un jeton de l'utilisateur ; un jeton d'un autre est introuvable. */
|
|
78
|
+
export function revokeAgentToken(store, userId, tokenId, now = new Date()) {
|
|
79
|
+
const token = store.mutate(asUser(userId), (tx) => {
|
|
80
|
+
const found = tx.store.findAgentToken(tokenId);
|
|
81
|
+
if (!found || found.userId !== userId)
|
|
82
|
+
throw new NotFoundError("Jeton introuvable.");
|
|
83
|
+
return tx.revokeAgentToken(tokenId);
|
|
84
|
+
});
|
|
85
|
+
return tokenView(store, token, now);
|
|
86
|
+
}
|
|
87
|
+
/** Marche à suivre pour obtenir un jeton valide, rappelée dans les refus. */
|
|
88
|
+
const NEW_TOKEN_HINT = "Create a token in the organizer web app (Paramètres > Agents) and set it as ORGANIZER_TOKEN.";
|
|
89
|
+
/**
|
|
90
|
+
* Résout le jeton présenté par un agent et note son usage. Un jeton absent,
|
|
91
|
+
* inconnu, révoqué ou expiré est refusé (401) ; un propriétaire inactif ou
|
|
92
|
+
* qui n'est plus membre de l'espace couvert aussi (403).
|
|
93
|
+
*/
|
|
94
|
+
export function resolveAgentToken(store, raw, now = new Date()) {
|
|
95
|
+
if (raw === null || raw.trim() === "") {
|
|
96
|
+
throw new UnauthorizedError(`Missing agent token: send "Authorization: Bearer org_…". ${NEW_TOKEN_HINT}`);
|
|
97
|
+
}
|
|
98
|
+
const value = raw.trim();
|
|
99
|
+
if (!value.startsWith(AGENT_TOKEN_PREFIX))
|
|
100
|
+
throw new UnauthorizedError(`This is not an organizer agent token (they start with "${AGENT_TOKEN_PREFIX}"). ${NEW_TOKEN_HINT}`);
|
|
101
|
+
const token = store.findAgentTokenByHash(hashAgentToken(value));
|
|
102
|
+
if (!token)
|
|
103
|
+
throw new UnauthorizedError(`Unknown agent token. ${NEW_TOKEN_HINT}`);
|
|
104
|
+
if (token.revokedAt !== null) {
|
|
105
|
+
throw new UnauthorizedError(`Agent token "${token.name}" was revoked on ${token.revokedAt.slice(0, 10)}, by its owner or because its owner left or was removed from the space it covers. ${NEW_TOKEN_HINT}`);
|
|
106
|
+
}
|
|
107
|
+
if (token.expiresAt !== null && Date.parse(token.expiresAt) <= now.getTime()) {
|
|
108
|
+
throw new UnauthorizedError(`Agent token "${token.name}" expired on ${token.expiresAt.slice(0, 10)}. ${NEW_TOKEN_HINT}`);
|
|
109
|
+
}
|
|
110
|
+
const user = store.getUser(token.userId);
|
|
111
|
+
if (user.status !== "active")
|
|
112
|
+
throw new ForbiddenError(`The account that owns agent token "${token.name}" is not active: the token cannot be used.`);
|
|
113
|
+
const space = store.getSpace(token.spaceId);
|
|
114
|
+
if (!store.isMember(space.id, user.id)) {
|
|
115
|
+
throw new ForbiddenError(`The owner of agent token "${token.name}" is no longer a member of space "${space.name}": the token cannot be used. ${NEW_TOKEN_HINT}`);
|
|
116
|
+
}
|
|
117
|
+
const usedAt = now.toISOString();
|
|
118
|
+
store.mutate(asUser(user.id), (tx) => tx.recordAgentTokenUse(token.id, usedAt));
|
|
119
|
+
return { token: { ...token, lastUsedAt: usedAt }, user, space };
|
|
120
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Actor, Mutation, Store } from "./store.js";
|
|
2
|
+
import type { AgentRequest, Idea, Space, Task, TaskSnapshot, User } from "../../shared/schemas/entities.schema.js";
|
|
3
|
+
import type { AgentIdea, AgentMember, AgentRequestSummary, AgentSpace, AgentTaskSummary } from "../../shared/schemas/agent-tools.schema.js";
|
|
4
|
+
import type { ResolvedAgentToken } from "./agent-tokens.js";
|
|
5
|
+
/**
|
|
6
|
+
* Logique de l'API agent : agent appelant (jeton résolu), demande pour
|
|
7
|
+
* laquelle il agit, résolution des tâches et des membres dans l'espace du
|
|
8
|
+
* jeton, contexte textuel et structuré d'une demande, et résumés français
|
|
9
|
+
* des actions, affichés dans la GUI.
|
|
10
|
+
*/
|
|
11
|
+
/** Agent qui appelle l'API : son jeton, le propriétaire du jeton et l'espace couvert. */
|
|
12
|
+
export type AgentCaller = ResolvedAgentToken;
|
|
13
|
+
export declare function agentActor(agent: AgentCaller, requestId: string): Actor;
|
|
14
|
+
/**
|
|
15
|
+
* Exécute `fn` dans une mutation de l'agent pour la demande `requestId`. La
|
|
16
|
+
* demande et tout ce que `fn` lit sont vérifiés et lus dans la transaction
|
|
17
|
+
* de l'écriture.
|
|
18
|
+
*/
|
|
19
|
+
export declare function actFor<T>(store: Store, agent: AgentCaller, requestId: string, fn: (tx: Mutation, space: Space) => T): T;
|
|
20
|
+
/** Demande de l'espace du jeton ; une demande d'un autre espace est introuvable pour l'agent. */
|
|
21
|
+
export declare function requestOfSpace(store: Store, agent: AgentCaller, requestId: string): AgentRequest;
|
|
22
|
+
/**
|
|
23
|
+
* Demande pour laquelle l'agent veut écrire : elle vise l'espace du jeton,
|
|
24
|
+
* elle est `processing`, prise par ce jeton, et le propriétaire du jeton est
|
|
25
|
+
* encore membre de l'espace. Vérifié à chaque appel.
|
|
26
|
+
*/
|
|
27
|
+
export declare function actingRequest(store: Store, agent: AgentCaller, requestId: string): {
|
|
28
|
+
request: AgentRequest;
|
|
29
|
+
space: Space;
|
|
30
|
+
};
|
|
31
|
+
export declare function userLabel(user: User): string;
|
|
32
|
+
export declare function taskKey(store: Store, task: Task): string;
|
|
33
|
+
/** Tâche de l'espace désignée par son identifiant ou sa clé (`ORG-41`). */
|
|
34
|
+
export declare function resolveTask(store: Store, space: Space, ref: string): Task;
|
|
35
|
+
/** Idée de l'espace. */
|
|
36
|
+
export declare function resolveIdea(store: Store, space: Space, ideaId: string): Idea;
|
|
37
|
+
/** Membres de l'espace désignés par leur identifiant ou leur adresse e-mail. */
|
|
38
|
+
export declare function resolveMembers(store: Store, space: Space, refs: readonly string[]): User[];
|
|
39
|
+
/** Arborescence de l'espace : chaque tâche puis ses sous-tâches, par colonne et rang. */
|
|
40
|
+
export declare function spaceTree(store: Store, space: Space): string[];
|
|
41
|
+
/** Membres de l'espace : nom, adresse, rôle et identifiant (pour set_assignees). */
|
|
42
|
+
export declare function memberLines(store: Store, space: Space): string[];
|
|
43
|
+
/** Durée écoulée depuis `iso`, en anglais. */
|
|
44
|
+
export declare function ago(iso: string, now?: Date): string;
|
|
45
|
+
/** Résumé d'une demande pour list_requests. */
|
|
46
|
+
export declare function requestLine(store: Store, agent: AgentCaller, request: AgentRequest): string;
|
|
47
|
+
/** Contexte complet d'une demande, renvoyé par claim_request. */
|
|
48
|
+
export declare function requestContext(store: Store, agent: AgentCaller, request: AgentRequest): string;
|
|
49
|
+
/** Idées actives récentes de la boîte à idées d'un espace. */
|
|
50
|
+
export declare function recentIdeas(store: Store, space: Space): Idea[];
|
|
51
|
+
/** Arbre des tâches, membres et idées récentes d'un espace, pour claim_request et get_context. */
|
|
52
|
+
export declare function spaceContextLines(store: Store, space: Space): string[];
|
|
53
|
+
export declare function spaceData(space: Space): AgentSpace;
|
|
54
|
+
export declare function memberData(store: Store, space: Space): AgentMember[];
|
|
55
|
+
export declare function taskData(store: Store, task: Task): AgentTaskSummary;
|
|
56
|
+
/** Tâches d'un espace dans l'ordre de l'arbre : chaque tâche puis ses sous-tâches, par colonne et rang. */
|
|
57
|
+
export declare function treeData(store: Store, space: Space): AgentTaskSummary[];
|
|
58
|
+
export declare function ideaData(store: Store, idea: Idea): AgentIdea;
|
|
59
|
+
export declare function requestData(store: Store, agent: AgentCaller, request: AgentRequest): AgentRequestSummary;
|
|
60
|
+
export declare function quote(text: string, max?: number): string;
|
|
61
|
+
/** Résumé d'une création de tâche. */
|
|
62
|
+
export declare function createdTaskSummary(store: Store, task: Task): string;
|
|
63
|
+
/** Résumé des champs changés d'une tâche, et nature de l'opération. */
|
|
64
|
+
export declare function taskUpdateSummary(store: Store, before: TaskSnapshot, after: TaskSnapshot): {
|
|
65
|
+
summary: string;
|
|
66
|
+
operation: "update" | "move";
|
|
67
|
+
};
|
|
68
|
+
/** Résumé d'un changement d'assignés. */
|
|
69
|
+
export declare function assigneesSummary(store: Store, before: readonly string[], after: readonly string[], test: boolean): string;
|
|
70
|
+
/** Vérifie qu'un texte libre n'est pas vide ; le message dit quoi fournir. */
|
|
71
|
+
export declare function requireText(value: string, what: string): string;
|