@hgdc/auth 0.11.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 CHANGED
@@ -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.CONVEX_SITE_URL!, // émetteur des JWT du plugin convex
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`. Un `subject` n'est unique que chez
54
- * son émetteur : sans ce contrôle, un autre fournisseur accepté par Convex pourrait présenter
55
- * le `subject` d'un membre et prendre ses droits. Un seul émetteur : l'appartenance est
56
- * cherchée par `subject`, qui ne distingue pas deux fournisseurs.
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
- /** Identité de la personne connectée, émise par `issuer`, ou erreur `UNAUTHENTICATED`. */
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
- /** Identité de la personne connectée, émise par `issuer`, ou erreur `UNAUTHENTICATED`. */
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
- if (!options.issuer)
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) !== normalizeIssuer(options.issuer))
125
+ if (!expected.includes(normalizeIssuer(identity.issuer)))
56
126
  throw authError("wrong_issuer");
57
127
  return identity;
58
128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/auth",
3
- "version": "0.11.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",