@hgdc/auth 0.10.0 → 0.12.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 +28 -3
- package/dist/convex.d.ts +59 -5
- package/dist/convex.js +73 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hgdc/auth
|
|
2
2
|
|
|
3
|
-
Connexion d'un produit de la famille à Account (OIDC) : bouton « Se connecter avec
|
|
3
|
+
Connexion d'un produit de la famille à Account (OIDC) : bouton « Se connecter avec HGDC », configuration du client better-auth, proxy `/api/auth` pour TanStack Start et gardes Convex.
|
|
4
4
|
|
|
5
5
|
| Import | Contenu |
|
|
6
6
|
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -157,10 +157,10 @@ Une absence de session n'est jamais gardée. Déconnexion : recharger la page, o
|
|
|
157
157
|
## Garder une fonction Convex
|
|
158
158
|
|
|
159
159
|
```ts
|
|
160
|
-
import { requireOrgMember } from "@hgdc/auth/convex"
|
|
160
|
+
import { convexIssuers, requireOrgMember } from "@hgdc/auth/convex"
|
|
161
161
|
|
|
162
162
|
const { userId, role } = await requireOrgMember(ctx, organizationId, {
|
|
163
|
-
issuer: process.env
|
|
163
|
+
issuer: convexIssuers(process.env), // émetteurs des JWT du plugin convex
|
|
164
164
|
minRole: "admin",
|
|
165
165
|
load: async (ctx, { organizationId, userId }) => /* appartenance lue dans la base du produit */ null,
|
|
166
166
|
})
|
|
@@ -187,6 +187,31 @@ if (authErrorReason(error) === "insufficient_role") showUpgradeHint()
|
|
|
187
187
|
|
|
188
188
|
Les organisations arrivent par la portée `organizations` : claim `organizations: [{ id, slug, name, role }]` de l'id_token et de userinfo.
|
|
189
189
|
|
|
190
|
+
### Changer le domaine du déploiement Convex
|
|
191
|
+
|
|
192
|
+
Les jetons du plugin `convex` portent l'émetteur `CONVEX_SITE_URL`. Quand l'adresse du déploiement change (domaine personnalisé posé par `update_canonical_url`, migration vers un Convex auto-hébergé), les jetons déjà émis gardent l'ancienne pendant leur durée de vie (15 minutes) : Convex les refuse sur une connexion neuve (`NoAuthProvider`) et la garde répond `wrong_issuer` sur un WebSocket ouvert. La variable `CONVEX_PREVIOUS_SITE_URL` ouvre une fenêtre où les deux émetteurs sont acceptés :
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// convex/auth.config.ts
|
|
196
|
+
import { convexAuthProviders } from "@hgdc/auth/convex"
|
|
197
|
+
|
|
198
|
+
export default {
|
|
199
|
+
providers: convexAuthProviders(getAuthConfigProvider({ jwks: process.env.JWKS }), process.env),
|
|
200
|
+
} satisfies AuthConfig
|
|
201
|
+
|
|
202
|
+
// convex/auth.ts, dans les plugins : le plugin refuse deux fournisseurs « convex »
|
|
203
|
+
convex({ authConfig: currentAuthConfig(authConfig), jwks: process.env.JWKS })
|
|
204
|
+
|
|
205
|
+
// gardes
|
|
206
|
+
await requireIdentity(ctx, { issuer: convexIssuers(process.env) })
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
1. Poser `CONVEX_PREVIOUS_SITE_URL` à l'adresse actuelle (`npx convex env set`), puis `convex deploy`.
|
|
210
|
+
2. Changer l'adresse. Les nouveaux jetons portent le nouvel émetteur, les anciens restent acceptés.
|
|
211
|
+
3. Attendre 30 minutes (deux fois la durée d'un jeton), retirer la variable, redéployer.
|
|
212
|
+
|
|
213
|
+
Hors bascule, la variable est absente et rien ne change : `convexAuthProviders` rend le seul fournisseur, `convexIssuers` la seule `CONVEX_SITE_URL`. La liste ne sert qu'à donner deux adresses au même émetteur (même clé, mêmes utilisateurs), jamais à accepter un autre fournisseur.
|
|
214
|
+
|
|
190
215
|
## Synchroniser les organisations
|
|
191
216
|
|
|
192
217
|
Les organisations vivent dans Account ; le produit en garde une copie (espace par organisation, appartenances) que lit le chargeur de `requireOrgMember`. Le modèle recommandé, celui de Host :
|
package/dist/convex.d.ts
CHANGED
|
@@ -50,15 +50,69 @@ export declare function isAuthError(error: unknown): error is ConvexError<AuthEr
|
|
|
50
50
|
export declare function authErrorReason(error: unknown): AuthErrorReason | null;
|
|
51
51
|
/**
|
|
52
52
|
* Émetteur des jetons dont le `subject` désigne un utilisateur du produit. Avec le plugin
|
|
53
|
-
* `convex` de better-auth : `process.env.CONVEX_SITE_URL
|
|
54
|
-
* son émetteur : sans ce contrôle, un autre fournisseur
|
|
55
|
-
* le `subject` d'un membre et prendre ses droits.
|
|
56
|
-
*
|
|
53
|
+
* `convex` de better-auth : `process.env.CONVEX_SITE_URL`, ou `convexIssuers(process.env)`.
|
|
54
|
+
* Un `subject` n'est unique que chez son émetteur : sans ce contrôle, un autre fournisseur
|
|
55
|
+
* accepté par Convex pourrait présenter le `subject` d'un membre et prendre ses droits.
|
|
56
|
+
*
|
|
57
|
+
* Une liste ne sert qu'à donner plusieurs adresses au même émetteur, le temps d'un changement
|
|
58
|
+
* de domaine du déploiement : l'appartenance est cherchée par `subject`, qui ne distingue pas
|
|
59
|
+
* deux fournisseurs. Jamais deux bases d'utilisateurs différentes.
|
|
57
60
|
*/
|
|
58
61
|
export type IssuerOption = {
|
|
62
|
+
issuer: string | readonly string[];
|
|
63
|
+
};
|
|
64
|
+
/** Variables d'environnement lues par `convexIssuers` et `convexAuthProviders`. */
|
|
65
|
+
export type ConvexIssuerEnv = Readonly<Record<string, string | undefined>>;
|
|
66
|
+
/**
|
|
67
|
+
* Émetteurs des jetons du plugin `convex` de better-auth sur ce déploiement, à passer tels quels
|
|
68
|
+
* à `requireIdentity` et `requireOrgMember` : `CONVEX_SITE_URL`, plus `CONVEX_PREVIOUS_SITE_URL`
|
|
69
|
+
* quand elle est posée.
|
|
70
|
+
*
|
|
71
|
+
* `CONVEX_PREVIOUS_SITE_URL` ne vit que pendant la bascule d'un domaine : l'ancienne adresse
|
|
72
|
+
* `actions` du déploiement, pour que les jetons émis avant la bascule restent valables jusqu'à
|
|
73
|
+
* leur expiration (15 minutes). Elle se retire ensuite.
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* await requireIdentity(ctx, { issuer: convexIssuers(process.env) })
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function convexIssuers(env: ConvexIssuerEnv): string[];
|
|
80
|
+
/**
|
|
81
|
+
* Fournisseurs de `convex/auth.config.ts` : `provider` (celui de `getAuthConfigProvider`), plus
|
|
82
|
+
* sa copie pour `CONVEX_PREVIOUS_SITE_URL` quand elle est posée, avec la même clé. Sans elle,
|
|
83
|
+
* Convex refuse dès la bascule (`NoAuthProvider`) un jeton émis avant, sur une connexion neuve.
|
|
84
|
+
*
|
|
85
|
+
* La copie est gardée même si les deux adresses sont égales : elles le sont entre la pose de la
|
|
86
|
+
* variable et le changement de domaine, et seule `CONVEX_SITE_URL` suit alors le domaine.
|
|
87
|
+
* Le plugin `convex` de better-auth refuse deux fournisseurs `applicationID: "convex"` : il
|
|
88
|
+
* reçoit `currentAuthConfig(authConfig)`.
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* export default {
|
|
92
|
+
* providers: convexAuthProviders(getAuthConfigProvider({ jwks: process.env.JWKS }), process.env),
|
|
93
|
+
* } satisfies AuthConfig
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export declare function convexAuthProviders<P extends {
|
|
59
97
|
issuer: string;
|
|
98
|
+
}>(provider: P, env: ConvexIssuerEnv): P[];
|
|
99
|
+
/**
|
|
100
|
+
* Configuration à donner au plugin `convex` de better-auth : le seul fournisseur courant, le
|
|
101
|
+
* premier de `convexAuthProviders`.
|
|
102
|
+
*
|
|
103
|
+
* ```ts
|
|
104
|
+
* convex({ authConfig: currentAuthConfig(authConfig), jwks: process.env.JWKS })
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export declare function currentAuthConfig<P>(config: {
|
|
108
|
+
providers: readonly P[];
|
|
109
|
+
}): {
|
|
110
|
+
providers: P[];
|
|
60
111
|
};
|
|
61
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Identité de la personne connectée, émise par `issuer` (ou l'un d'eux), ou erreur
|
|
114
|
+
* `UNAUTHENTICATED`.
|
|
115
|
+
*/
|
|
62
116
|
export declare function requireIdentity(ctx: {
|
|
63
117
|
auth: Auth;
|
|
64
118
|
}, options: IssuerOption): Promise<UserIdentity>;
|
package/dist/convex.js
CHANGED
|
@@ -45,14 +45,84 @@ export function authErrorReason(error) {
|
|
|
45
45
|
function normalizeIssuer(value) {
|
|
46
46
|
return value.replace(/\/+$/, "");
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Émetteurs des jetons du plugin `convex` de better-auth sur ce déploiement, à passer tels quels
|
|
50
|
+
* à `requireIdentity` et `requireOrgMember` : `CONVEX_SITE_URL`, plus `CONVEX_PREVIOUS_SITE_URL`
|
|
51
|
+
* quand elle est posée.
|
|
52
|
+
*
|
|
53
|
+
* `CONVEX_PREVIOUS_SITE_URL` ne vit que pendant la bascule d'un domaine : l'ancienne adresse
|
|
54
|
+
* `actions` du déploiement, pour que les jetons émis avant la bascule restent valables jusqu'à
|
|
55
|
+
* leur expiration (15 minutes). Elle se retire ensuite.
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* await requireIdentity(ctx, { issuer: convexIssuers(process.env) })
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function convexIssuers(env) {
|
|
62
|
+
const issuers = [];
|
|
63
|
+
for (const value of [env.CONVEX_SITE_URL, env.CONVEX_PREVIOUS_SITE_URL]) {
|
|
64
|
+
const issuer = value ? normalizeIssuer(value.trim()) : "";
|
|
65
|
+
if (issuer && !issuers.includes(issuer))
|
|
66
|
+
issuers.push(issuer);
|
|
67
|
+
}
|
|
68
|
+
return issuers;
|
|
69
|
+
}
|
|
70
|
+
/** Lit une variable facultative dans `auth.config.ts`, où Convex lève sur une variable absente. */
|
|
71
|
+
function optionalEnv(env, name) {
|
|
72
|
+
try {
|
|
73
|
+
return env[name]?.trim() || undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Fournisseurs de `convex/auth.config.ts` : `provider` (celui de `getAuthConfigProvider`), plus
|
|
81
|
+
* sa copie pour `CONVEX_PREVIOUS_SITE_URL` quand elle est posée, avec la même clé. Sans elle,
|
|
82
|
+
* Convex refuse dès la bascule (`NoAuthProvider`) un jeton émis avant, sur une connexion neuve.
|
|
83
|
+
*
|
|
84
|
+
* La copie est gardée même si les deux adresses sont égales : elles le sont entre la pose de la
|
|
85
|
+
* variable et le changement de domaine, et seule `CONVEX_SITE_URL` suit alors le domaine.
|
|
86
|
+
* Le plugin `convex` de better-auth refuse deux fournisseurs `applicationID: "convex"` : il
|
|
87
|
+
* reçoit `currentAuthConfig(authConfig)`.
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* export default {
|
|
91
|
+
* providers: convexAuthProviders(getAuthConfigProvider({ jwks: process.env.JWKS }), process.env),
|
|
92
|
+
* } satisfies AuthConfig
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function convexAuthProviders(provider, env) {
|
|
96
|
+
// Convex refuse un push dont auth.config.ts lit une variable absente, et n'y énumère pas
|
|
97
|
+
// `process.env` (`in`, `Object.keys` ne voient rien) : seule une lecture directe, rattrapée, convient.
|
|
98
|
+
const previous = optionalEnv(env, "CONVEX_PREVIOUS_SITE_URL");
|
|
99
|
+
return previous ? [provider, { ...provider, issuer: normalizeIssuer(previous) }] : [provider];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Configuration à donner au plugin `convex` de better-auth : le seul fournisseur courant, le
|
|
103
|
+
* premier de `convexAuthProviders`.
|
|
104
|
+
*
|
|
105
|
+
* ```ts
|
|
106
|
+
* convex({ authConfig: currentAuthConfig(authConfig), jwks: process.env.JWKS })
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function currentAuthConfig(config) {
|
|
110
|
+
return { providers: config.providers.slice(0, 1) };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Identité de la personne connectée, émise par `issuer` (ou l'un d'eux), ou erreur
|
|
114
|
+
* `UNAUTHENTICATED`.
|
|
115
|
+
*/
|
|
49
116
|
export async function requireIdentity(ctx, options) {
|
|
50
117
|
const identity = await ctx.auth.getUserIdentity();
|
|
51
118
|
if (!identity)
|
|
52
119
|
throw authError("no_session");
|
|
53
|
-
|
|
120
|
+
const expected = (typeof options.issuer === "string" ? [options.issuer] : options.issuer)
|
|
121
|
+
.map(normalizeIssuer)
|
|
122
|
+
.filter((issuer) => issuer !== "");
|
|
123
|
+
if (expected.length === 0)
|
|
54
124
|
throw new Error("requireIdentity : émetteur attendu manquant (CONVEX_SITE_URL ?).");
|
|
55
|
-
if (normalizeIssuer(identity.issuer)
|
|
125
|
+
if (!expected.includes(normalizeIssuer(identity.issuer)))
|
|
56
126
|
throw authError("wrong_issuer");
|
|
57
127
|
return identity;
|
|
58
128
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export declare function HgdcMark({ className }: {
|
|
|
48
48
|
className?: string;
|
|
49
49
|
}): ReactElement;
|
|
50
50
|
/**
|
|
51
|
-
* Bouton « Se connecter avec
|
|
51
|
+
* Bouton « Se connecter avec HGDC ». Sans style propre : le produit le met en forme par
|
|
52
52
|
* `className`, l'état d'attente est exposé par `aria-busy` et `data-pending`.
|
|
53
53
|
*/
|
|
54
54
|
export declare function HgdcSignInButton({ authClient, callbackURL, errorCallbackURL, onError, label, disabled, ...rest }: HgdcSignInButtonProps): ReactElement;
|
package/dist/react.js
CHANGED
|
@@ -31,10 +31,10 @@ export function HgdcMark({ className }) {
|
|
|
31
31
|
return (_jsx("img", { src: `data:image/svg+xml,${encodeURIComponent(HGDC_MARK_SVG)}`, alt: "", "aria-hidden": "true", width: 16, height: 16, draggable: false, className: className }));
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Bouton « Se connecter avec
|
|
34
|
+
* Bouton « Se connecter avec HGDC ». Sans style propre : le produit le met en forme par
|
|
35
35
|
* `className`, l'état d'attente est exposé par `aria-busy` et `data-pending`.
|
|
36
36
|
*/
|
|
37
|
-
export function HgdcSignInButton({ authClient, callbackURL = "/", errorCallbackURL, onError, label = "Se connecter avec
|
|
37
|
+
export function HgdcSignInButton({ authClient, callbackURL = "/", errorCallbackURL, onError, label = "Se connecter avec HGDC", disabled, ...rest }) {
|
|
38
38
|
const [pending, setPending] = useState(false);
|
|
39
39
|
async function signIn() {
|
|
40
40
|
setPending(true);
|
package/dist/server.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type HgdcEntryOptions = {
|
|
|
33
33
|
/**
|
|
34
34
|
* Entrée commune `GET /auth/hgdc?next=<chemin>` (`HGDC_ENTRY_PATH`), à monter comme route
|
|
35
35
|
* serveur du produit. Le menu « Produits » de `@hgdc/ui` y envoie : on entre dans le produit
|
|
36
|
-
* sans passer par sa page d'accueil ni recliquer « Se connecter avec
|
|
36
|
+
* sans passer par sa page d'accueil ni recliquer « Se connecter avec HGDC ».
|
|
37
37
|
*
|
|
38
38
|
* - Session du produit ouverte : redirection vers `next`.
|
|
39
39
|
* - Sinon : la connexion HGDC part aussitôt (même appel que `HgdcSignInButton`, fait par le
|
package/dist/server.js
CHANGED
|
@@ -109,7 +109,7 @@ function authorizationUrl(body) {
|
|
|
109
109
|
/**
|
|
110
110
|
* Entrée commune `GET /auth/hgdc?next=<chemin>` (`HGDC_ENTRY_PATH`), à monter comme route
|
|
111
111
|
* serveur du produit. Le menu « Produits » de `@hgdc/ui` y envoie : on entre dans le produit
|
|
112
|
-
* sans passer par sa page d'accueil ni recliquer « Se connecter avec
|
|
112
|
+
* sans passer par sa page d'accueil ni recliquer « Se connecter avec HGDC ».
|
|
113
113
|
*
|
|
114
114
|
* - Session du produit ouverte : redirection vers `next`.
|
|
115
115
|
* - Sinon : la connexion HGDC part aussitôt (même appel que `HgdcSignInButton`, fait par le
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|