@hgdc/auth 0.2.0 → 0.3.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 CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  Connexion d'un produit HGDC à HGDC Account (OIDC) : bouton « Se connecter avec HGDC », configuration du client better-auth, proxy `/api/auth` pour TanStack Start et gardes Convex.
4
4
 
5
- | Import | Contenu |
6
- | ------------------- | ------------------------------------------------------------------------------------------------------ |
7
- | `@hgdc/auth` | `HGDC_ISSUER`, `HGDC_PROVIDER_ID`, `HGDC_SCOPES`, rôles (`highestRole`, `hasRole`), `parseHgdcProfile` |
8
- | `@hgdc/auth/server` | `hgdcProvider` (plugin `genericOAuth`), `createAuthProxy`, `restoreFetchMode` |
9
- | `@hgdc/auth/convex` | `requireIdentity`, `requireOrgMember`, `isAuthError` |
10
- | `@hgdc/auth/react` | `HgdcSignInButton`, `startHgdcSignIn`, `HgdcMark` |
5
+ | Import | Contenu |
6
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `@hgdc/auth` | `HGDC_ISSUER`, `HGDC_PROVIDER_ID`, `HGDC_SCOPES`, rôles (`highestRole`, `hasRole`), `parseHgdcProfile`, `organizationsFromIdToken`, `planOrganizationSync` |
8
+ | `@hgdc/auth/server` | `hgdcProvider` (plugin `genericOAuth`), `createAuthProxy`, `restoreFetchMode` |
9
+ | `@hgdc/auth/convex` | `requireIdentity`, `requireOrgMember`, `isAuthError` |
10
+ | `@hgdc/auth/react` | `HgdcSignInButton`, `startHgdcSignIn`, `HgdcMark` |
11
11
 
12
12
  Pairs : better-auth 1.6, convex 1.46+, React 19 (chacun facultatif selon l'import).
13
13
 
@@ -71,4 +71,21 @@ const { userId, role } = await requireOrgMember(ctx, organizationId, {
71
71
 
72
72
  Les organisations arrivent par la portée `organizations` : claim `organizations: [{ id, slug, name, role }]` de l'id_token et de userinfo.
73
73
 
74
+ ## Synchroniser les organisations
75
+
76
+ Les organisations vivent dans Account ; le produit en garde une copie (espace par organisation, appartenances) que lit le chargeur de `requireOrgMember`. Elle se met à jour à chaque connexion :
77
+
78
+ ```ts
79
+ import { organizationsFromIdToken, planOrganizationSync } from "@hgdc/auth"
80
+
81
+ // better-auth : databaseHooks.session.create.after → mutation interne du produit
82
+ const account = /* ligne `account` de la personne avec providerId "hgdc" */
83
+ const claimed = organizationsFromIdToken(account.idToken) // jeton reçu du point de jeton, déjà validé
84
+ const plan = planOrganizationSync(claimed, membershipsOfThisUser)
85
+ // plan.upsert : créer l'espace (index unique sur organizationId) et l'appartenance, ou mettre à jour rôle, nom, slug
86
+ // plan.remove : retirer les appartenances des organisations quittées
87
+ ```
88
+
89
+ Un rôle changé dans Account n'arrive qu'à la connexion suivante : garder des sessions courtes (7 jours au plus).
90
+
74
91
  Tous droits réservés, Hugo Gebel Développement Conseil.
package/dist/index.d.ts CHANGED
@@ -38,3 +38,28 @@ export type HgdcProfile = {
38
38
  * l'adresse n'est pas vérifiée : un tel profil ne doit jamais ouvrir de session.
39
39
  */
40
40
  export declare function parseHgdcProfile(claims: unknown): HgdcProfile;
41
+ /**
42
+ * Organisations d'un id_token de HGDC Account. À n'appeler que sur un jeton reçu directement
43
+ * du point de jeton d'Account par le serveur (champ `idToken` de la table `account` de
44
+ * better-auth) : sa signature n'est pas revérifiée ici (OpenID Connect Core 3.1.3.7, point 6).
45
+ * Lève une erreur si le jeton est illisible ou si un claim est invalide ; `[]` sans la portée.
46
+ */
47
+ export declare function organizationsFromIdToken(idToken: string): HgdcOrganization[];
48
+ /** Appartenance d'une personne telle qu'un produit l'a copiée depuis Account. */
49
+ export type SyncedMembership = {
50
+ organizationId: string;
51
+ slug: string;
52
+ name: string;
53
+ role: OrgRole;
54
+ };
55
+ export type OrganizationSyncPlan = {
56
+ /** Organisations à créer ou à mettre à jour chez le produit (espace et appartenance). */
57
+ upsert: HgdcOrganization[];
58
+ /** Organisations dont la personne ne fait plus partie : appartenance à retirer. */
59
+ remove: string[];
60
+ };
61
+ /**
62
+ * Écart entre les organisations reçues d'Account et la copie du produit, pour une personne.
63
+ * Ne renvoie que ce qui change : une connexion sans changement n'écrit rien.
64
+ */
65
+ export declare function planOrganizationSync(claimed: readonly HgdcOrganization[], current: readonly SyncedMembership[]): OrganizationSyncPlan;
package/dist/index.js CHANGED
@@ -56,3 +56,42 @@ export function parseHgdcProfile(claims) {
56
56
  throw new Error(`Profil HGDC invalide : ${z.prettifyError(result.error)}`);
57
57
  return result.data;
58
58
  }
59
+ const organizationsClaim = z.object({ organizations: z.array(organizationClaim).optional() });
60
+ function decodeBase64Url(segment) {
61
+ const base64 = segment
62
+ .replaceAll("-", "+")
63
+ .replaceAll("_", "/")
64
+ .padEnd(Math.ceil(segment.length / 4) * 4, "=");
65
+ const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
66
+ return new TextDecoder().decode(bytes);
67
+ }
68
+ /**
69
+ * Organisations d'un id_token de HGDC Account. À n'appeler que sur un jeton reçu directement
70
+ * du point de jeton d'Account par le serveur (champ `idToken` de la table `account` de
71
+ * better-auth) : sa signature n'est pas revérifiée ici (OpenID Connect Core 3.1.3.7, point 6).
72
+ * Lève une erreur si le jeton est illisible ou si un claim est invalide ; `[]` sans la portée.
73
+ */
74
+ export function organizationsFromIdToken(idToken) {
75
+ const payload = idToken.split(".")[1];
76
+ if (!payload)
77
+ throw new Error("id_token HGDC illisible");
78
+ const result = organizationsClaim.safeParse(JSON.parse(decodeBase64Url(payload)));
79
+ if (!result.success)
80
+ throw new Error(`Organisations HGDC invalides : ${z.prettifyError(result.error)}`);
81
+ return result.data.organizations ?? [];
82
+ }
83
+ /**
84
+ * Écart entre les organisations reçues d'Account et la copie du produit, pour une personne.
85
+ * Ne renvoie que ce qui change : une connexion sans changement n'écrit rien.
86
+ */
87
+ export function planOrganizationSync(claimed, current) {
88
+ const known = new Map(current.map((m) => [m.organizationId, m]));
89
+ const claimedIds = new Set(claimed.map((o) => o.id));
90
+ return {
91
+ upsert: claimed.filter((org) => {
92
+ const copy = known.get(org.id);
93
+ return !copy || copy.role !== org.role || copy.slug !== org.slug || copy.name !== org.name;
94
+ }),
95
+ remove: current.filter((m) => !claimedIds.has(m.organizationId)).map((m) => m.organizationId),
96
+ };
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/auth",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Connexion HGDC : bouton « Se connecter avec HGDC », config better-auth et gardes Convex.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://github.com/hugogebel-boop/hgdc-kit/tree/main/packages/auth#readme",