alepha 0.19.4 → 0.19.5

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.
Files changed (104) hide show
  1. package/dist/api/audits/index.d.ts +8 -8
  2. package/dist/api/issues/index.d.ts +810 -0
  3. package/dist/api/issues/index.d.ts.map +1 -0
  4. package/dist/api/issues/index.js +447 -0
  5. package/dist/api/issues/index.js.map +1 -0
  6. package/dist/api/keys/index.d.ts +5 -5
  7. package/dist/api/users/index.d.ts +6 -0
  8. package/dist/api/users/index.d.ts.map +1 -1
  9. package/dist/api/users/index.js +10 -3
  10. package/dist/api/users/index.js.map +1 -1
  11. package/dist/api/workflows/index.d.ts +3 -3
  12. package/dist/captcha/index.d.ts +142 -0
  13. package/dist/captcha/index.d.ts.map +1 -0
  14. package/dist/captcha/index.js +177 -0
  15. package/dist/captcha/index.js.map +1 -0
  16. package/dist/cli/core/index.d.ts +82 -2
  17. package/dist/cli/core/index.d.ts.map +1 -1
  18. package/dist/cli/core/index.js +90 -6
  19. package/dist/cli/core/index.js.map +1 -1
  20. package/dist/cli/platform/index.d.ts +84 -10
  21. package/dist/cli/platform/index.d.ts.map +1 -1
  22. package/dist/cli/platform/index.js +92 -4
  23. package/dist/cli/platform/index.js.map +1 -1
  24. package/dist/cli/vendor/index.d.ts +30 -3
  25. package/dist/cli/vendor/index.d.ts.map +1 -1
  26. package/dist/cli/vendor/index.js +98 -21
  27. package/dist/cli/vendor/index.js.map +1 -1
  28. package/dist/command/index.d.ts.map +1 -1
  29. package/dist/command/index.js +2 -3
  30. package/dist/command/index.js.map +1 -1
  31. package/dist/orm/core/index.bun.js +6 -6
  32. package/dist/orm/core/index.bun.js.map +1 -1
  33. package/dist/orm/core/index.d.ts.map +1 -1
  34. package/dist/orm/core/index.js +6 -6
  35. package/dist/orm/core/index.js.map +1 -1
  36. package/dist/react/i18n/index.d.ts +1 -0
  37. package/dist/react/i18n/index.d.ts.map +1 -1
  38. package/dist/react/i18n/index.js +8 -4
  39. package/dist/react/i18n/index.js.map +1 -1
  40. package/dist/security/index.d.ts.map +1 -1
  41. package/dist/security/index.js.map +1 -1
  42. package/dist/server/auth/index.d.ts +145 -2
  43. package/dist/server/auth/index.d.ts.map +1 -1
  44. package/dist/server/auth/index.js +364 -63
  45. package/dist/server/auth/index.js.map +1 -1
  46. package/dist/server/cookies/index.d.ts.map +1 -1
  47. package/dist/server/cookies/index.js.map +1 -1
  48. package/dist/websocket/index.d.ts.map +1 -1
  49. package/dist/websocket/index.js.map +1 -1
  50. package/package.json +11 -1
  51. package/src/api/issues/__tests__/IssueService.spec.ts +263 -0
  52. package/src/api/issues/controllers/AdminIssueController.ts +149 -0
  53. package/src/api/issues/controllers/IssueController.ts +44 -0
  54. package/src/api/issues/entities/issues.ts +49 -0
  55. package/src/api/issues/index.ts +53 -0
  56. package/src/api/issues/schemas/createIssueSchema.ts +13 -0
  57. package/src/api/issues/schemas/issueConfigAtom.ts +13 -0
  58. package/src/api/issues/schemas/issueQuerySchema.ts +18 -0
  59. package/src/api/issues/schemas/issueResourceSchema.ts +6 -0
  60. package/src/api/issues/schemas/myIssueQuerySchema.ts +10 -0
  61. package/src/api/issues/schemas/updateIssueSchema.ts +13 -0
  62. package/src/api/issues/services/IssueService.ts +264 -0
  63. package/src/api/users/primitives/$realm.ts +24 -0
  64. package/src/api/users/services/CredentialService.ts +6 -3
  65. package/src/api/users/services/RegistrationService.ts +15 -5
  66. package/src/api/users/services/SessionService.ts +2 -0
  67. package/src/captcha/__tests__/MemoryCaptchaProvider.spec.ts +74 -0
  68. package/src/captcha/index.ts +33 -0
  69. package/src/captcha/providers/CaptchaProvider.ts +17 -0
  70. package/src/captcha/providers/MemoryCaptchaProvider.ts +65 -0
  71. package/src/captcha/providers/TurnstileCaptchaProvider.ts +125 -0
  72. package/src/cli/core/atoms/buildOptions.ts +57 -0
  73. package/src/cli/core/commands/build.ts +2 -0
  74. package/src/cli/core/providers/ViteDevServerProvider.ts +1 -1
  75. package/src/cli/core/services/ViteUtils.ts +5 -2
  76. package/src/cli/core/tasks/BuildClientTask.ts +3 -1
  77. package/src/cli/core/tasks/BuildCloudflareTask.ts +4 -0
  78. package/src/cli/core/tasks/BuildPwaTask.ts +81 -0
  79. package/src/cli/platform/adapters/CloudflareAdapter.ts +24 -0
  80. package/src/cli/platform/atoms/platformOptions.ts +19 -3
  81. package/src/cli/platform/hooks/PlatformHook.ts +51 -0
  82. package/src/cli/platform/index.ts +1 -0
  83. package/src/cli/platform/services/CloudflareApi.ts +22 -1
  84. package/src/cli/platform/services/PlatformOrchestrator.ts +67 -2
  85. package/src/cli/vendor/__tests__/VendorService.spec.ts +40 -1
  86. package/src/cli/vendor/commands/VendorCommand.ts +41 -38
  87. package/src/cli/vendor/services/VendorService.ts +108 -4
  88. package/src/command/__tests__/CliProvider.spec.ts +45 -0
  89. package/src/command/providers/CliProvider.ts +3 -4
  90. package/src/orm/core/services/Repository.ts +20 -6
  91. package/src/react/i18n/__tests__/I18nProvider.spec.ts +83 -0
  92. package/src/react/i18n/providers/I18nProvider.ts +12 -10
  93. package/src/security/primitives/$issuer.ts +3 -1
  94. package/src/server/auth/index.ts +7 -0
  95. package/src/server/auth/primitives/$auth.ts +37 -3
  96. package/src/server/auth/primitives/$authApple.ts +114 -4
  97. package/src/server/auth/primitives/$authFacebook.ts +98 -0
  98. package/src/server/auth/primitives/$authFranceConnect.ts +105 -0
  99. package/src/server/auth/primitives/$authGithub.ts +22 -16
  100. package/src/server/auth/primitives/$authMicrosoft.ts +88 -0
  101. package/src/server/auth/providers/ServerAuthProvider.ts +197 -72
  102. package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -0
  103. package/src/server/core/__tests__/ServerRouterProvider-errorHandler.spec.ts +1 -1
  104. package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -1
@@ -1328,11 +1328,19 @@ var AuthPrimitive = class extends Primitive {
1328
1328
  /**
1329
1329
  * Extracts user information from the access token.
1330
1330
  * This is used to create a user account from the access token.
1331
+ *
1332
+ * `externalProfile` carries extra profile fields that cannot be derived from the
1333
+ * ID token or userinfo endpoint — e.g. Apple's `user` form field that is only
1334
+ * delivered once, on first authorization. ID token / userinfo fields take
1335
+ * precedence; externalProfile only fills gaps.
1331
1336
  */
1332
- async user(tokens) {
1337
+ async user(tokens, externalProfile) {
1333
1338
  try {
1334
1339
  if ("oauth" in this.options) {
1335
- const profile = await this.options.oauth.userinfo(tokens);
1340
+ const profile = {
1341
+ ...externalProfile,
1342
+ ...await this.options.oauth.userinfo(tokens)
1343
+ };
1336
1344
  if (this.options.oauth.account) return this.options.oauth.account({
1337
1345
  ...tokens,
1338
1346
  user: profile
@@ -1340,7 +1348,10 @@ var AuthPrimitive = class extends Primitive {
1340
1348
  return this.securityProvider.createUserFromPayload(profile);
1341
1349
  }
1342
1350
  if ("oidc" in this.options) {
1343
- const payload = this.getUserFromIdToken(tokens.id_token || "");
1351
+ const payload = {
1352
+ ...externalProfile,
1353
+ ...this.getUserFromIdToken(tokens.id_token || "")
1354
+ };
1344
1355
  if (this.options.oidc.account) return this.options.oidc.account({
1345
1356
  ...tokens,
1346
1357
  user: payload
@@ -1426,6 +1437,14 @@ var ServerAuthProvider = class {
1426
1437
  serverCookiesProvider = $inject(ServerCookiesProvider);
1427
1438
  dateTimeProvider = $inject(DateTimeProvider);
1428
1439
  serverLinksProvider = $inject(ServerLinksProvider);
1440
+ /**
1441
+ * Validates that a redirect URI is a safe relative path.
1442
+ * Prevents open redirect attacks by rejecting absolute URLs and protocol-relative URLs.
1443
+ */
1444
+ validateRedirectUri(uri) {
1445
+ if (!uri.startsWith("/") || uri.startsWith("//")) return "/";
1446
+ return uri;
1447
+ }
1429
1448
  get identities() {
1430
1449
  return this.alepha.primitives($auth).filter((auth) => !auth.options.disabled);
1431
1450
  }
@@ -1433,6 +1452,7 @@ var ServerAuthProvider = class {
1433
1452
  name: "authorizationCode",
1434
1453
  ttl: [15, "minutes"],
1435
1454
  httpOnly: true,
1455
+ encrypt: true,
1436
1456
  schema: t.object({
1437
1457
  provider: t.text(),
1438
1458
  realm: t.optional(t.text()),
@@ -1494,7 +1514,7 @@ var ServerAuthProvider = class {
1494
1514
  const tokens = this.getTokens(cookies);
1495
1515
  if (tokens) {
1496
1516
  const provider = this.provider(tokens);
1497
- if (!("realm" in provider.options)) {
1517
+ if (!("issuer" in provider.options)) {
1498
1518
  const user = await provider.user(tokens);
1499
1519
  return {
1500
1520
  api: await this.serverLinksProvider.getUserApiLinks({
@@ -1619,10 +1639,12 @@ var ServerAuthProvider = class {
1619
1639
  };
1620
1640
  if (oidc) parameters.nonce = randomState();
1621
1641
  if (scope) parameters.scope = scope;
1642
+ if (oidc && oidc.responseMode) parameters.response_mode = oidc.responseMode;
1643
+ if (oidc && oidc.authorizationParameters) Object.assign(parameters, oidc.authorizationParameters);
1622
1644
  this.authorizationCode.set({
1623
1645
  state,
1624
1646
  nonce: parameters.nonce,
1625
- redirectUri: query.redirect_uri ?? "/",
1647
+ redirectUri: this.validateRedirectUri(query.redirect_uri ?? "/"),
1626
1648
  loginUri,
1627
1649
  provider: query.provider,
1628
1650
  realm: query.realm
@@ -1638,9 +1660,11 @@ var ServerAuthProvider = class {
1638
1660
  code_challenge_method: "S256"
1639
1661
  };
1640
1662
  if (scope) parameters.scope = scope;
1663
+ if (oidc && oidc.responseMode) parameters.response_mode = oidc.responseMode;
1664
+ if (oidc && oidc.authorizationParameters) Object.assign(parameters, oidc.authorizationParameters);
1641
1665
  this.authorizationCode.set({
1642
1666
  codeVerifier,
1643
- redirectUri: query.redirect_uri ?? "/",
1667
+ redirectUri: this.validateRedirectUri(query.redirect_uri ?? "/"),
1644
1668
  loginUri,
1645
1669
  provider: query.provider,
1646
1670
  realm: query.realm
@@ -1649,55 +1673,103 @@ var ServerAuthProvider = class {
1649
1673
  }
1650
1674
  });
1651
1675
  /**
1676
+ * Extracts provider-specific extra profile fields delivered via the
1677
+ * authorization callback form body rather than the ID token or userinfo
1678
+ * endpoint. Currently handles Apple Sign In's `user` field, which is sent
1679
+ * only on the user's first authorization and contains their name.
1680
+ */
1681
+ async extractFormPostProfile(req) {
1682
+ try {
1683
+ const userField = (await req.formData()).get("user");
1684
+ if (typeof userField !== "string") return;
1685
+ const parsed = JSON.parse(userField);
1686
+ const profile = {};
1687
+ if (parsed.name?.firstName) profile.given_name = parsed.name.firstName;
1688
+ if (parsed.name?.lastName) profile.family_name = parsed.name.lastName;
1689
+ if (parsed.name?.firstName || parsed.name?.lastName) profile.name = [parsed.name?.firstName, parsed.name?.lastName].filter(Boolean).join(" ");
1690
+ if (parsed.email) profile.email = parsed.email;
1691
+ return Object.keys(profile).length > 0 ? profile : void 0;
1692
+ } catch (e) {
1693
+ this.log.warn("Failed to parse form_post profile from callback body", e);
1694
+ return;
1695
+ }
1696
+ }
1697
+ /**
1698
+ * Shared callback logic for both GET and POST OAuth2/OIDC callbacks.
1699
+ * For form_post response mode (e.g. Apple Sign In), the raw Request object
1700
+ * is passed so openid-client can read the authorization code from the POST body.
1701
+ */
1702
+ async handleCallback(url, reply, cookies, raw) {
1703
+ const authorizationCode = this.authorizationCode.get({ cookies });
1704
+ if (!authorizationCode) throw new BadRequestError("Missing code verifier");
1705
+ const provider = this.provider(authorizationCode);
1706
+ const oauth = await provider.getOAuth();
1707
+ if (!oauth) throw new SecurityError(`Auth provider '${provider.name}' does not support OAuth2`);
1708
+ const redirectUri = authorizationCode.redirectUri ?? "/";
1709
+ const loginUri = authorizationCode.loginUri;
1710
+ let currentUrl = url;
1711
+ let externalProfile;
1712
+ if (raw?.web?.req && raw.web.req.method === "POST") {
1713
+ const cloned = raw.web.req.clone();
1714
+ currentUrl = raw.web.req;
1715
+ externalProfile = await this.extractFormPostProfile(cloned);
1716
+ }
1717
+ const externalTokens = await authorizationCodeGrant(oauth, currentUrl, {
1718
+ pkceCodeVerifier: authorizationCode.codeVerifier,
1719
+ expectedState: authorizationCode.state,
1720
+ expectedNonce: authorizationCode.nonce
1721
+ }).then((tokens) => ({
1722
+ issued_at: this.dateTimeProvider.now().unix(),
1723
+ provider: provider.name,
1724
+ ...tokens
1725
+ })).catch((e) => {
1726
+ this.log.error("Failed to get access token", e);
1727
+ throw new SecurityError("Failed to get access token", { cause: e });
1728
+ });
1729
+ this.authorizationCode.del({ cookies });
1730
+ const issuer = provider.issuer;
1731
+ if (!issuer) {
1732
+ this.setTokens(externalTokens, cookies);
1733
+ reply.redirect(redirectUri, 302);
1734
+ return;
1735
+ }
1736
+ let user;
1737
+ try {
1738
+ user = await provider.user(externalTokens, externalProfile);
1739
+ } catch (e) {
1740
+ this.log.warn("OAuth2 account linking failed", e);
1741
+ const errorUrl = new URL(loginUri || redirectUri, url.origin);
1742
+ errorUrl.searchParams.set("error", e instanceof BadRequestError ? e.message : "Authentication failed");
1743
+ reply.redirect(errorUrl.pathname + errorUrl.search, 302);
1744
+ return;
1745
+ }
1746
+ const tokens = await issuer.createToken(user);
1747
+ this.setTokens({
1748
+ ...tokens,
1749
+ issued_at: this.dateTimeProvider.now().unix(),
1750
+ provider: provider.name
1751
+ }, cookies);
1752
+ reply.redirect(redirectUri, 302);
1753
+ }
1754
+ /**
1652
1755
  * Callback for OAuth2/OIDC providers.
1653
1756
  * It handles the authorization code flow and retrieves the access token.
1654
1757
  */
1655
1758
  callback = $route({
1656
1759
  path: alephaServerAuthRoutes.callback,
1657
1760
  handler: async ({ url, reply, cookies }) => {
1658
- const authorizationCode = this.authorizationCode.get({ cookies });
1659
- if (!authorizationCode) throw new BadRequestError("Missing code verifier");
1660
- const provider = this.provider(authorizationCode);
1661
- const oauth = await provider.getOAuth();
1662
- if (!oauth) throw new SecurityError(`Auth provider '${provider.name}' does not support OAuth2`);
1663
- const redirectUri = authorizationCode.redirectUri ?? "/";
1664
- const loginUri = authorizationCode.loginUri;
1665
- const externalTokens = await authorizationCodeGrant(oauth, url, {
1666
- pkceCodeVerifier: authorizationCode.codeVerifier,
1667
- expectedState: authorizationCode.state,
1668
- expectedNonce: authorizationCode.nonce
1669
- }).then((tokens) => ({
1670
- issued_at: this.dateTimeProvider.now().unix(),
1671
- provider: provider.name,
1672
- ...tokens
1673
- })).catch((e) => {
1674
- this.log.error("Failed to get access token", e);
1675
- throw new SecurityError("Failed to get access token", { cause: e });
1676
- });
1677
- this.authorizationCode.del({ cookies });
1678
- const issuer = provider.issuer;
1679
- if (!issuer) {
1680
- this.setTokens(externalTokens, cookies);
1681
- reply.redirect(redirectUri, 302);
1682
- return;
1683
- }
1684
- let user;
1685
- try {
1686
- user = await provider.user(externalTokens);
1687
- } catch (e) {
1688
- this.log.warn("OAuth2 account linking failed", e);
1689
- const errorUrl = new URL(loginUri || redirectUri, url.origin);
1690
- errorUrl.searchParams.set("error", e instanceof BadRequestError ? e.message : "Authentication failed");
1691
- reply.redirect(errorUrl.pathname + errorUrl.search, 302);
1692
- return;
1693
- }
1694
- const tokens = await issuer.createToken(user);
1695
- this.setTokens({
1696
- ...tokens,
1697
- issued_at: this.dateTimeProvider.now().unix(),
1698
- provider: provider.name
1699
- }, cookies);
1700
- reply.redirect(redirectUri, 302);
1761
+ await this.handleCallback(url, reply, cookies);
1762
+ }
1763
+ });
1764
+ /**
1765
+ * POST callback for OAuth2/OIDC providers using form_post response mode.
1766
+ * Apple Sign In sends the authorization code via POST body instead of URL query parameters.
1767
+ */
1768
+ callbackPost = $route({
1769
+ path: alephaServerAuthRoutes.callback,
1770
+ method: "POST",
1771
+ handler: async ({ url, reply, cookies, raw }) => {
1772
+ await this.handleCallback(url, reply, cookies, raw);
1701
1773
  }
1702
1774
  });
1703
1775
  /**
@@ -1708,7 +1780,7 @@ var ServerAuthProvider = class {
1708
1780
  method: "POST",
1709
1781
  schema: { query: t.object({ post_logout_redirect_uri: t.optional(t.text()) }) },
1710
1782
  handler: async ({ query, reply, cookies }) => {
1711
- const redirect = query.post_logout_redirect_uri ?? "/";
1783
+ const redirect = this.validateRedirectUri(query.post_logout_redirect_uri ?? "/");
1712
1784
  const tokens = this.getTokens(cookies);
1713
1785
  if (!tokens) {
1714
1786
  reply.redirect(redirect, 302);
@@ -1812,7 +1884,7 @@ var ServerAuthProvider = class {
1812
1884
  }
1813
1885
  extractAccessToken(tokens) {
1814
1886
  const idp = this.provider(tokens.provider);
1815
- if ("oidc" in idp.options && !("realm" in idp.options) && idp.options.oidc?.useIdToken) return tokens.id_token;
1887
+ if ("oidc" in idp.options && !("issuer" in idp.options) && idp.options.oidc?.useIdToken) return tokens.id_token;
1816
1888
  return tokens.access_token;
1817
1889
  }
1818
1890
  async refreshTokens(tokens) {
@@ -1851,6 +1923,84 @@ const authenticationProviderSchema = t.object({
1851
1923
  ], { description: "Type of the authentication provider." })
1852
1924
  }, { title: "AuthenticationProvider" });
1853
1925
  //#endregion
1926
+ //#region ../../src/server/auth/primitives/$authApple.ts
1927
+ /**
1928
+ * Already configured Apple authentication primitive.
1929
+ *
1930
+ * Uses OpenID Connect (OIDC) to authenticate users via their Apple accounts.
1931
+ * Upon successful authentication, it links the Apple account to a user session.
1932
+ *
1933
+ * Apple-specific behavior:
1934
+ * - `response_mode=form_post` (required by Apple when requesting `email`/`name`).
1935
+ * - Scope: `name email` (Apple does not support the standard `profile` scope).
1936
+ * - The user's name is only provided on the first authorization, as a `user`
1937
+ * form field on the POST callback. The framework extracts it and injects
1938
+ * `given_name` / `family_name` / `name` into the profile before linking.
1939
+ * Subsequent logins only return `sub` and `email` in the ID token.
1940
+ * - `email_verified` and `is_private_email` are normalized from Apple's
1941
+ * string ("true"/"false") representation to booleans.
1942
+ *
1943
+ * Client secret:
1944
+ * Apple requires the client secret to be a signed ES256 JWT generated from
1945
+ * your Apple private key, team ID, and key ID. This JWT is valid for up to 6
1946
+ * months; you must rotate it before expiration. Generate it out of band and
1947
+ * set it via `APPLE_CLIENT_SECRET`.
1948
+ *
1949
+ * See: https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
1950
+ *
1951
+ * Environment Variables:
1952
+ * - `APPLE_CLIENT_ID`: The Service ID obtained from the Apple Developer Console.
1953
+ * - `APPLE_CLIENT_SECRET`: The signed ES256 JWT client secret generated from your
1954
+ * Apple private key.
1955
+ */
1956
+ const $authApple = (realm, options = {}) => {
1957
+ const { alepha } = $context();
1958
+ const env = alepha.parseEnv(t.object({
1959
+ APPLE_CLIENT_ID: t.optional(t.text({ description: "The Service ID obtained from the Apple Developer Console." })),
1960
+ APPLE_CLIENT_SECRET: t.optional(t.text({ description: "The signed ES256 JWT client secret generated from your Apple private key." }))
1961
+ }));
1962
+ const disabled = !env.APPLE_CLIENT_ID || !env.APPLE_CLIENT_SECRET;
1963
+ const name = "apple";
1964
+ const userAccount = options.account ?? (realm.link ? realm.link(name) : void 0);
1965
+ if (!userAccount) throw new AlephaError("Authentication requires a link function in the realm primitive.");
1966
+ const account = async (opts) => {
1967
+ return userAccount(normalizeApplePayload(opts));
1968
+ };
1969
+ return $auth({
1970
+ issuer: realm,
1971
+ name,
1972
+ oidc: {
1973
+ issuer: "https://appleid.apple.com",
1974
+ clientId: env.APPLE_CLIENT_ID,
1975
+ clientSecret: env.APPLE_CLIENT_SECRET,
1976
+ scope: "name email",
1977
+ responseMode: "form_post",
1978
+ ...options,
1979
+ account
1980
+ },
1981
+ disabled
1982
+ });
1983
+ };
1984
+ /**
1985
+ * Normalize Apple-specific profile quirks before handing off to the
1986
+ * user-provided link function.
1987
+ *
1988
+ * Why: Apple's ID token non-conformities — `email_verified` and
1989
+ * `is_private_email` are delivered as the strings "true"/"false" rather than
1990
+ * booleans. Normalize so downstream code can rely on standard OIDC shapes.
1991
+ */
1992
+ const normalizeApplePayload = (opts) => {
1993
+ const user = { ...opts.user };
1994
+ for (const key of ["email_verified", "is_private_email"]) {
1995
+ const raw = user[key];
1996
+ if (typeof raw === "string") user[key] = raw === "true";
1997
+ }
1998
+ return {
1999
+ ...opts,
2000
+ user
2001
+ };
2002
+ };
2003
+ //#endregion
1854
2004
  //#region ../../src/server/auth/primitives/$authCredentials.ts
1855
2005
  /**
1856
2006
  * Already configured Credentials authentication primitive.
@@ -1868,6 +2018,108 @@ const $authCredentials = (realm, options = {}) => {
1868
2018
  });
1869
2019
  };
1870
2020
  //#endregion
2021
+ //#region ../../src/server/auth/primitives/$authFacebook.ts
2022
+ /**
2023
+ * Already configured Facebook authentication primitive.
2024
+ *
2025
+ * Uses OAuth2 to authenticate users via their Facebook accounts.
2026
+ * Upon successful authentication, it links the Facebook account to a user session.
2027
+ *
2028
+ * Environment Variables:
2029
+ * - `FACEBOOK_CLIENT_ID`: The App ID obtained from the Meta Developer Console.
2030
+ * - `FACEBOOK_CLIENT_SECRET`: The App Secret obtained from the Meta Developer Console.
2031
+ */
2032
+ const $authFacebook = (realm, options = {}) => {
2033
+ const { alepha } = $context();
2034
+ const env = alepha.parseEnv(t.object({
2035
+ FACEBOOK_CLIENT_ID: t.optional(t.text({ description: "The App ID obtained from the Meta Developer Console." })),
2036
+ FACEBOOK_CLIENT_SECRET: t.optional(t.text({ description: "The App Secret obtained from the Meta Developer Console." }))
2037
+ }));
2038
+ const disabled = !env.FACEBOOK_CLIENT_ID || !env.FACEBOOK_CLIENT_SECRET;
2039
+ const name = "facebook";
2040
+ const account = options.account ?? (realm.link ? realm.link(name) : void 0);
2041
+ if (!account) throw new AlephaError("Authentication requires a link function in the realm primitive.");
2042
+ return $auth({
2043
+ issuer: realm,
2044
+ name,
2045
+ oauth: {
2046
+ clientId: env.FACEBOOK_CLIENT_ID,
2047
+ clientSecret: env.FACEBOOK_CLIENT_SECRET,
2048
+ authorization: "https://www.facebook.com/v25.0/dialog/oauth",
2049
+ token: "https://graph.facebook.com/v25.0/oauth/access_token",
2050
+ scope: "email",
2051
+ userinfo: async (tokens) => {
2052
+ const res = await fetch("https://graph.facebook.com/v25.0/me?fields=id,name,email,picture.width(200).height(200)", { headers: { Authorization: `Bearer ${tokens.access_token}` } }).then((res) => res.json());
2053
+ const user = { sub: res.id };
2054
+ if (res.email) user.email = res.email;
2055
+ if (res.name) user.name = res.name.trim();
2056
+ if (res.picture?.data?.url) user.picture = res.picture.data.url;
2057
+ return user;
2058
+ },
2059
+ ...options,
2060
+ account
2061
+ },
2062
+ disabled
2063
+ });
2064
+ };
2065
+ //#endregion
2066
+ //#region ../../src/server/auth/primitives/$authFranceConnect.ts
2067
+ /**
2068
+ * Creates an authentication provider primitive for France Connect.
2069
+ *
2070
+ * Uses OpenID Connect (OIDC) to authenticate users via France Connect,
2071
+ * the French government's identity federation system. It provides verified
2072
+ * identity data (name, email, birthdate) sourced directly from government
2073
+ * databases.
2074
+ *
2075
+ * **France Connect-specific behaviour**:
2076
+ * - Scopes use individual claim names (`given_name`, `family_name`) rather
2077
+ * than the standard grouped `profile` scope.
2078
+ * - The `acr_values=eidas1` authorization parameter is mandatory and is
2079
+ * included automatically.
2080
+ * - Logout is mandatory in France Connect integrations. Store the `id_token`
2081
+ * returned at login and pass it to the logout endpoint when the session ends.
2082
+ *
2083
+ * **Environment Variables** (obtain from partenaires.franceconnect.gouv.fr):
2084
+ * - `FRANCECONNECT_CLIENT_ID` — OAuth 2.0 client ID for your France Connect service provider.
2085
+ * - `FRANCECONNECT_CLIENT_SECRET` — OAuth 2.0 client secret for your France Connect service provider.
2086
+ *
2087
+ * @example
2088
+ * ```ts
2089
+ * class AuthProviders {
2090
+ * franceconnect = $authFranceConnect(this.userRealm);
2091
+ * }
2092
+ * ```
2093
+ */
2094
+ const $authFranceConnect = (realm, options = {}) => {
2095
+ const { alepha } = $context();
2096
+ const env = alepha.parseEnv(t.object({
2097
+ FRANCECONNECT_CLIENT_ID: t.optional(t.text({ description: "The OAuth 2.0 client ID for your France Connect service provider, obtained from partenaires.franceconnect.gouv.fr." })),
2098
+ FRANCECONNECT_CLIENT_SECRET: t.optional(t.text({ description: "The OAuth 2.0 client secret for your France Connect service provider, obtained from partenaires.franceconnect.gouv.fr." }))
2099
+ }));
2100
+ const disabled = !env.FRANCECONNECT_CLIENT_ID || !env.FRANCECONNECT_CLIENT_SECRET;
2101
+ const name = "franceconnect";
2102
+ const account = options.account ?? (realm.link ? realm.link(name) : void 0);
2103
+ if (!account) throw new AlephaError("Authentication requires a link function in the realm primitive.");
2104
+ return $auth({
2105
+ issuer: realm,
2106
+ name,
2107
+ oidc: {
2108
+ issuer: "https://oidc.franceconnect.gouv.fr/api/v2",
2109
+ clientId: env.FRANCECONNECT_CLIENT_ID,
2110
+ clientSecret: env.FRANCECONNECT_CLIENT_SECRET,
2111
+ scope: "openid given_name family_name email",
2112
+ ...options,
2113
+ authorizationParameters: {
2114
+ acr_values: "eidas1",
2115
+ ...options.authorizationParameters
2116
+ },
2117
+ account
2118
+ },
2119
+ disabled
2120
+ });
2121
+ };
2122
+ //#endregion
1871
2123
  //#region ../../src/server/auth/primitives/$authGithub.ts
1872
2124
  /**
1873
2125
  * Already configured GitHub authentication primitive.
@@ -1900,23 +2152,20 @@ const $authGithub = (realm, options = {}) => {
1900
2152
  scope: "read:user user:email",
1901
2153
  userinfo: async (tokens) => {
1902
2154
  const BASE_URL = "https://api.github.com";
1903
- const res = await fetch(`${BASE_URL}/user`, { headers: {
2155
+ const headers = {
1904
2156
  Authorization: `Bearer ${tokens.access_token}`,
1905
2157
  "User-Agent": "Alepha"
1906
- } }).then((res) => res.json());
2158
+ };
2159
+ const res = await fetch(`${BASE_URL}/user`, { headers }).then((res) => res.json());
1907
2160
  const user = { sub: res.id.toString() };
1908
2161
  if (res.email) user.email = res.email;
1909
2162
  if (res.name) user.name = res.name.trim();
1910
2163
  if (res.avatar_url) user.picture = res.avatar_url;
1911
- if (!user.email) {
1912
- const res = await fetch(`${BASE_URL}/user/emails`, { headers: {
1913
- Authorization: `Bearer ${tokens.access_token}`,
1914
- "User-Agent": "Alepha"
1915
- } });
1916
- if (res.ok) {
1917
- const emails = await res.json();
1918
- user.email = (emails.find((e) => e.primary) ?? emails[0]).email;
1919
- }
2164
+ const emailsRes = await fetch(`${BASE_URL}/user/emails`, { headers });
2165
+ if (emailsRes.ok) {
2166
+ const emails = await emailsRes.json();
2167
+ if (!user.email) user.email = (emails.find((e) => e.primary) ?? emails[0])?.email;
2168
+ if (user.email) user.email_verified = emails.find((e) => e.email === user.email)?.verified ?? false;
1920
2169
  }
1921
2170
  return user;
1922
2171
  },
@@ -1962,6 +2211,55 @@ const $authGoogle = (realm, options = {}) => {
1962
2211
  });
1963
2212
  };
1964
2213
  //#endregion
2214
+ //#region ../../src/server/auth/primitives/$authMicrosoft.ts
2215
+ /**
2216
+ * Already configured Microsoft Entra ID (Azure AD) authentication primitive.
2217
+ *
2218
+ * Uses OpenID Connect (OIDC) to authenticate users via their Microsoft accounts.
2219
+ * Supports personal Microsoft accounts, work/school (Azure AD) accounts, and
2220
+ * multi-tenant applications.
2221
+ *
2222
+ * The tenant ID defaults to `"common"`, which allows all Microsoft account types
2223
+ * (personal, work, school). To restrict to a specific Azure AD tenant, set
2224
+ * `MICROSOFT_TENANT_ID` to your tenant's GUID or domain.
2225
+ *
2226
+ * **Note on multi-tenant issuer validation**: Microsoft's OIDC discovery document
2227
+ * for the `common` endpoint returns `{tenantid}` as a literal placeholder in the
2228
+ * `issuer` field. This is expected behavior for multi-tenant endpoints. The
2229
+ * openid-client library handles this during token validation automatically.
2230
+ *
2231
+ * Environment Variables:
2232
+ * - `MICROSOFT_CLIENT_ID`: The application (client) ID from the Azure Portal.
2233
+ * - `MICROSOFT_CLIENT_SECRET`: The client secret value from the Azure Portal.
2234
+ * - `MICROSOFT_TENANT_ID`: (Optional) Azure AD tenant ID or `"common"` for
2235
+ * multi-tenant. Defaults to `"common"`.
2236
+ */
2237
+ const $authMicrosoft = (realm, options = {}) => {
2238
+ const { alepha } = $context();
2239
+ const env = alepha.parseEnv(t.object({
2240
+ MICROSOFT_CLIENT_ID: t.optional(t.text({ description: "The application (client) ID obtained from the Azure Portal." })),
2241
+ MICROSOFT_CLIENT_SECRET: t.optional(t.text({ description: "The client secret value obtained from the Azure Portal." })),
2242
+ MICROSOFT_TENANT_ID: t.optional(t.text({ description: "The Azure AD tenant ID or 'common' for multi-tenant. Defaults to 'common'." }))
2243
+ }));
2244
+ const disabled = !env.MICROSOFT_CLIENT_ID || !env.MICROSOFT_CLIENT_SECRET;
2245
+ const tenantId = env.MICROSOFT_TENANT_ID ?? "common";
2246
+ const name = "microsoft";
2247
+ const account = options.account ?? (realm.link ? realm.link(name) : void 0);
2248
+ if (!account) throw new AlephaError("Authentication requires a link function in the realm primitive.");
2249
+ return $auth({
2250
+ issuer: realm,
2251
+ name,
2252
+ oidc: {
2253
+ issuer: `https://login.microsoftonline.com/${tenantId}/v2.0`,
2254
+ clientId: env.MICROSOFT_CLIENT_ID,
2255
+ clientSecret: env.MICROSOFT_CLIENT_SECRET,
2256
+ ...options,
2257
+ account
2258
+ },
2259
+ disabled
2260
+ });
2261
+ };
2262
+ //#endregion
1965
2263
  //#region ../../src/server/auth/index.ts
1966
2264
  /**
1967
2265
  * OAuth2/OIDC authentication with social login providers.
@@ -1972,6 +2270,9 @@ const $authGoogle = (realm, options = {}) => {
1972
2270
  * - Google OAuth integration
1973
2271
  * - GitHub OAuth integration
1974
2272
  * - Apple OAuth integration
2273
+ * - Facebook OAuth integration
2274
+ * - Microsoft Entra ID (Azure AD) integration
2275
+ * - France Connect integration
1975
2276
  * - Cookie-based, SSR-friendly authentication
1976
2277
  * - Token management and refresh
1977
2278
  *
@@ -1983,6 +2284,6 @@ const AlephaServerAuth = $module({
1983
2284
  services: [AlephaServerCookies, ServerAuthProvider]
1984
2285
  });
1985
2286
  //#endregion
1986
- export { $auth, $authCredentials, $authGithub, $authGoogle, AlephaServerAuth, AuthPrimitive, ServerAuthProvider, alephaServerAuthRoutes, authenticationProviderSchema, tokenResponseSchema, tokensSchema, userinfoResponseSchema };
2287
+ export { $auth, $authApple, $authCredentials, $authFacebook, $authFranceConnect, $authGithub, $authGoogle, $authMicrosoft, AlephaServerAuth, AuthPrimitive, ServerAuthProvider, alephaServerAuthRoutes, authenticationProviderSchema, tokenResponseSchema, tokensSchema, userinfoResponseSchema };
1987
2288
 
1988
2289
  //# sourceMappingURL=index.js.map