handshake-auth 0.1.0 → 0.2.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.
Files changed (54) hide show
  1. package/ReadMe.md +230 -16
  2. package/dist/index.d.ts +18 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/middleware/express.d.ts +67 -0
  7. package/dist/middleware/express.d.ts.map +1 -1
  8. package/dist/middleware/express.js +69 -0
  9. package/dist/middleware/express.js.map +1 -1
  10. package/dist/middleware/index.d.ts +2 -2
  11. package/dist/middleware/index.d.ts.map +1 -1
  12. package/dist/middleware/index.js +1 -1
  13. package/dist/middleware/index.js.map +1 -1
  14. package/dist/strategies/discord.d.ts +99 -0
  15. package/dist/strategies/discord.d.ts.map +1 -0
  16. package/dist/strategies/discord.js +85 -0
  17. package/dist/strategies/discord.js.map +1 -0
  18. package/dist/strategies/github.d.ts +112 -0
  19. package/dist/strategies/github.d.ts.map +1 -0
  20. package/dist/strategies/github.js +110 -0
  21. package/dist/strategies/github.js.map +1 -0
  22. package/dist/strategies/google.d.ts +91 -0
  23. package/dist/strategies/google.d.ts.map +1 -0
  24. package/dist/strategies/google.js +77 -0
  25. package/dist/strategies/google.js.map +1 -0
  26. package/dist/strategies/index.d.ts +16 -0
  27. package/dist/strategies/index.d.ts.map +1 -1
  28. package/dist/strategies/index.js +10 -0
  29. package/dist/strategies/index.js.map +1 -1
  30. package/dist/strategies/magic-link.d.ts +141 -0
  31. package/dist/strategies/magic-link.d.ts.map +1 -0
  32. package/dist/strategies/magic-link.js +186 -0
  33. package/dist/strategies/magic-link.js.map +1 -0
  34. package/dist/strategies/microsoft.d.ts +127 -0
  35. package/dist/strategies/microsoft.d.ts.map +1 -0
  36. package/dist/strategies/microsoft.js +98 -0
  37. package/dist/strategies/microsoft.js.map +1 -0
  38. package/dist/strategies/oauth-base.d.ts +162 -0
  39. package/dist/strategies/oauth-base.d.ts.map +1 -0
  40. package/dist/strategies/oauth-base.js +243 -0
  41. package/dist/strategies/oauth-base.js.map +1 -0
  42. package/dist/strategies/password.d.ts +69 -6
  43. package/dist/strategies/password.d.ts.map +1 -1
  44. package/dist/strategies/password.js +73 -24
  45. package/dist/strategies/password.js.map +1 -1
  46. package/dist/strategies/twitter-x.d.ts +130 -0
  47. package/dist/strategies/twitter-x.d.ts.map +1 -0
  48. package/dist/strategies/twitter-x.js +275 -0
  49. package/dist/strategies/twitter-x.js.map +1 -0
  50. package/dist/strategies/username-password.d.ts +38 -0
  51. package/dist/strategies/username-password.d.ts.map +1 -0
  52. package/dist/strategies/username-password.js +61 -0
  53. package/dist/strategies/username-password.js.map +1 -0
  54. package/package.json +2 -2
@@ -0,0 +1,141 @@
1
+ import type { AuthResult, Strategy, HandshakeCallbacks } from '../types.js';
2
+ /**
3
+ * Configuration options for the Magic Link strategy.
4
+ */
5
+ export interface MagicLinkOptions {
6
+ /**
7
+ * Base URL for building the callback URL (e.g., 'http://localhost:3000').
8
+ * Required for constructing the full magic link URL.
9
+ */
10
+ baseUrl: string;
11
+ /**
12
+ * Path for the magic link callback route.
13
+ * @default '/auth/magic/callback'
14
+ */
15
+ callbackPath?: string;
16
+ /**
17
+ * How many minutes until the token expires.
18
+ * @default 15
19
+ */
20
+ tokenExpiryMinutes?: number;
21
+ /**
22
+ * Callback to send the magic link to the user.
23
+ * You handle email delivery (e.g., via handshake-email, Postmark, Resend, etc.)
24
+ *
25
+ * @param email - The user's email address
26
+ * @param token - The generated token
27
+ * @param url - The full callback URL with token
28
+ */
29
+ sendMagicLink: (email: string, token: string, url: string) => Promise<void>;
30
+ }
31
+ /**
32
+ * Result from the send phase of magic link authentication.
33
+ * Returned when initiating a magic link flow.
34
+ */
35
+ export interface MagicLinkSendResult {
36
+ sent: true;
37
+ email: string;
38
+ }
39
+ /**
40
+ * Magic Link authentication strategy for passwordless authentication.
41
+ *
42
+ * This strategy uses the Inversion of Control pattern:
43
+ * - Library handles token generation and flow logic
44
+ * - You provide callbacks for storage and email delivery
45
+ *
46
+ * The strategy has two phases:
47
+ * 1. **Send phase** (`magic:send`): Generate token, store it, and send email
48
+ * 2. **Verify phase** (`magic:verify`): Verify token and return account
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const hs = new Handshake({
53
+ * findAccount: async (email) => db.accounts.findByEmail(email),
54
+ * storeMagicToken: async (email, token, expiresAt) => {
55
+ * await db.magicTokens.create({ email, token, expiresAt });
56
+ * },
57
+ * verifyMagicToken: async (token) => {
58
+ * const record = await db.magicTokens.findOne({ token });
59
+ * if (!record || record.expiresAt < new Date()) return null;
60
+ * await db.magicTokens.delete({ token }); // One-time use
61
+ * return { email: record.email };
62
+ * },
63
+ * });
64
+ *
65
+ * hs.use(new MagicLinkStrategy({
66
+ * baseUrl: 'http://localhost:3000',
67
+ * sendMagicLink: async (email, token, url) => {
68
+ * await emailService.send(email, 'Your magic link', url);
69
+ * },
70
+ * }));
71
+ * ```
72
+ */
73
+ export declare class MagicLinkStrategy<TAccount> implements Strategy<TAccount> {
74
+ readonly name = "magic";
75
+ private readonly baseUrl;
76
+ private readonly callbackPath;
77
+ private readonly tokenExpiryMinutes;
78
+ private readonly sendMagicLink;
79
+ constructor(options: MagicLinkOptions);
80
+ /**
81
+ * Authenticate using the magic link strategy.
82
+ *
83
+ * This method handles two sub-strategies:
84
+ * - `magic:send` - Initiate magic link flow (send email)
85
+ * - `magic:verify` - Verify token and return account
86
+ *
87
+ * @param callbacks - Handshake callbacks
88
+ * @param args - [phase, ...phaseArgs] where phase is 'send' or 'verify'
89
+ */
90
+ authenticate(callbacks: HandshakeCallbacks<TAccount>, ...args: unknown[]): Promise<AuthResult<TAccount>>;
91
+ /**
92
+ * Handle the send phase: generate token, store it, and send email.
93
+ */
94
+ private handleSend;
95
+ /**
96
+ * Handle the verify phase: verify token and return account.
97
+ */
98
+ private handleVerify;
99
+ /**
100
+ * Build the full callback URL with the token.
101
+ */
102
+ private buildCallbackUrl;
103
+ }
104
+ /**
105
+ * Alias strategies for the two magic link phases.
106
+ * These allow using `hs.authenticate('magic:send', email)` syntax.
107
+ */
108
+ export declare class MagicLinkSendStrategy<TAccount> implements Strategy<TAccount> {
109
+ private readonly magicLinkStrategy;
110
+ readonly name = "magic:send";
111
+ constructor(magicLinkStrategy: MagicLinkStrategy<TAccount>);
112
+ authenticate(callbacks: HandshakeCallbacks<TAccount>, ...args: unknown[]): Promise<AuthResult<TAccount>>;
113
+ }
114
+ export declare class MagicLinkVerifyStrategy<TAccount> implements Strategy<TAccount> {
115
+ private readonly magicLinkStrategy;
116
+ readonly name = "magic:verify";
117
+ constructor(magicLinkStrategy: MagicLinkStrategy<TAccount>);
118
+ authenticate(callbacks: HandshakeCallbacks<TAccount>, ...args: unknown[]): Promise<AuthResult<TAccount>>;
119
+ }
120
+ /**
121
+ * Helper function to register all magic link strategies at once.
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * import { Handshake, useMagicLink } from 'handshake-auth';
126
+ *
127
+ * const hs = new Handshake({...});
128
+ * useMagicLink(hs, {
129
+ * baseUrl: 'http://localhost:3000',
130
+ * sendMagicLink: async (email, token, url) => {...},
131
+ * });
132
+ *
133
+ * // Now you can use:
134
+ * await hs.authenticate('magic:send', email);
135
+ * await hs.authenticate('magic:verify', token);
136
+ * ```
137
+ */
138
+ export declare function useMagicLink<TAccount>(hs: {
139
+ use: (strategy: Strategy<TAccount>) => unknown;
140
+ }, options: MagicLinkOptions): MagicLinkStrategy<TAccount>;
141
+ //# sourceMappingURL=magic-link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magic-link.d.ts","sourceRoot":"","sources":["../../src/strategies/magic-link.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE5E;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;;;;;;OAOG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7E;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,iBAAiB,CAAC,QAAQ,CAAE,YAAW,QAAQ,CAAC,QAAQ,CAAC;IACpE,QAAQ,CAAC,IAAI,WAAW;IAExB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoC;gBAEtD,OAAO,EAAE,gBAAgB;IAOrC;;;;;;;;;OASG;IACG,YAAY,CAChB,SAAS,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EACvC,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAehC;;OAEG;YACW,UAAU;IAqCxB;;OAEG;YACW,YAAY;IAgC1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAKzB;AAED;;;GAGG;AACH,qBAAa,qBAAqB,CAAC,QAAQ,CAAE,YAAW,QAAQ,CAAC,QAAQ,CAAC;IAG5D,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAF9C,QAAQ,CAAC,IAAI,gBAAgB;gBAEA,iBAAiB,EAAE,iBAAiB,CAAC,QAAQ,CAAC;IAErE,YAAY,CAChB,SAAS,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EACvC,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;CAGjC;AAED,qBAAa,uBAAuB,CAAC,QAAQ,CAAE,YAAW,QAAQ,CAAC,QAAQ,CAAC;IAG9D,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAF9C,QAAQ,CAAC,IAAI,kBAAkB;gBAEF,iBAAiB,EAAE,iBAAiB,CAAC,QAAQ,CAAC;IAErE,YAAY,CAChB,SAAS,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EACvC,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;CAGjC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EACnC,EAAE,EAAE;IAAE,GAAG,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAA;CAAE,EACtD,OAAO,EAAE,gBAAgB,GACxB,iBAAiB,CAAC,QAAQ,CAAC,CAM7B"}
@@ -0,0 +1,186 @@
1
+ import { randomUUID } from 'crypto';
2
+ /**
3
+ * Magic Link authentication strategy for passwordless authentication.
4
+ *
5
+ * This strategy uses the Inversion of Control pattern:
6
+ * - Library handles token generation and flow logic
7
+ * - You provide callbacks for storage and email delivery
8
+ *
9
+ * The strategy has two phases:
10
+ * 1. **Send phase** (`magic:send`): Generate token, store it, and send email
11
+ * 2. **Verify phase** (`magic:verify`): Verify token and return account
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const hs = new Handshake({
16
+ * findAccount: async (email) => db.accounts.findByEmail(email),
17
+ * storeMagicToken: async (email, token, expiresAt) => {
18
+ * await db.magicTokens.create({ email, token, expiresAt });
19
+ * },
20
+ * verifyMagicToken: async (token) => {
21
+ * const record = await db.magicTokens.findOne({ token });
22
+ * if (!record || record.expiresAt < new Date()) return null;
23
+ * await db.magicTokens.delete({ token }); // One-time use
24
+ * return { email: record.email };
25
+ * },
26
+ * });
27
+ *
28
+ * hs.use(new MagicLinkStrategy({
29
+ * baseUrl: 'http://localhost:3000',
30
+ * sendMagicLink: async (email, token, url) => {
31
+ * await emailService.send(email, 'Your magic link', url);
32
+ * },
33
+ * }));
34
+ * ```
35
+ */
36
+ export class MagicLinkStrategy {
37
+ name = 'magic';
38
+ baseUrl;
39
+ callbackPath;
40
+ tokenExpiryMinutes;
41
+ sendMagicLink;
42
+ constructor(options) {
43
+ this.baseUrl = options.baseUrl;
44
+ this.callbackPath = options.callbackPath ?? '/auth/magic/callback';
45
+ this.tokenExpiryMinutes = options.tokenExpiryMinutes ?? 15;
46
+ this.sendMagicLink = options.sendMagicLink;
47
+ }
48
+ /**
49
+ * Authenticate using the magic link strategy.
50
+ *
51
+ * This method handles two sub-strategies:
52
+ * - `magic:send` - Initiate magic link flow (send email)
53
+ * - `magic:verify` - Verify token and return account
54
+ *
55
+ * @param callbacks - Handshake callbacks
56
+ * @param args - [phase, ...phaseArgs] where phase is 'send' or 'verify'
57
+ */
58
+ async authenticate(callbacks, ...args) {
59
+ const [phase, ...phaseArgs] = args;
60
+ if (phase === 'send') {
61
+ return this.handleSend(callbacks, phaseArgs[0]);
62
+ }
63
+ else if (phase === 'verify') {
64
+ return this.handleVerify(callbacks, phaseArgs[0]);
65
+ }
66
+ else {
67
+ return {
68
+ account: null,
69
+ error: `Invalid magic link phase: ${phase}. Use 'send' or 'verify'`,
70
+ };
71
+ }
72
+ }
73
+ /**
74
+ * Handle the send phase: generate token, store it, and send email.
75
+ */
76
+ async handleSend(callbacks, email) {
77
+ if (!email) {
78
+ return { account: null, error: 'Email is required' };
79
+ }
80
+ if (!callbacks.storeMagicToken) {
81
+ return {
82
+ account: null,
83
+ error: 'storeMagicToken callback is required for magic link strategy',
84
+ };
85
+ }
86
+ // Generate token and expiry
87
+ const token = randomUUID();
88
+ const expiresAt = new Date(Date.now() + this.tokenExpiryMinutes * 60 * 1000);
89
+ // Build callback URL
90
+ const url = this.buildCallbackUrl(token);
91
+ // Store the token (user's responsibility)
92
+ await callbacks.storeMagicToken(email, token, expiresAt);
93
+ // Send the email (user's responsibility)
94
+ await this.sendMagicLink(email, token, url);
95
+ // Return special result for send phase
96
+ // We return account: null but no error to indicate success
97
+ // The caller should check for the absence of error
98
+ return {
99
+ account: null,
100
+ // No error means success - email was sent
101
+ };
102
+ }
103
+ /**
104
+ * Handle the verify phase: verify token and return account.
105
+ */
106
+ async handleVerify(callbacks, token) {
107
+ if (!token) {
108
+ return { account: null, error: 'Token is required' };
109
+ }
110
+ if (!callbacks.verifyMagicToken) {
111
+ return {
112
+ account: null,
113
+ error: 'verifyMagicToken callback is required for magic link strategy',
114
+ };
115
+ }
116
+ // Verify the token (user's responsibility to check expiry and delete)
117
+ const result = await callbacks.verifyMagicToken(token);
118
+ if (!result) {
119
+ return { account: null, error: 'Invalid or expired token' };
120
+ }
121
+ // Find the account by email
122
+ const account = await callbacks.findAccount(result.email);
123
+ if (!account) {
124
+ return { account: null, error: 'Account not found' };
125
+ }
126
+ return { account, strategy: this.name };
127
+ }
128
+ /**
129
+ * Build the full callback URL with the token.
130
+ */
131
+ buildCallbackUrl(token) {
132
+ const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
133
+ const path = this.callbackPath.startsWith('/') ? this.callbackPath : `/${this.callbackPath}`;
134
+ return `${base}${path}?token=${encodeURIComponent(token)}`;
135
+ }
136
+ }
137
+ /**
138
+ * Alias strategies for the two magic link phases.
139
+ * These allow using `hs.authenticate('magic:send', email)` syntax.
140
+ */
141
+ export class MagicLinkSendStrategy {
142
+ magicLinkStrategy;
143
+ name = 'magic:send';
144
+ constructor(magicLinkStrategy) {
145
+ this.magicLinkStrategy = magicLinkStrategy;
146
+ }
147
+ async authenticate(callbacks, ...args) {
148
+ return this.magicLinkStrategy.authenticate(callbacks, 'send', ...args);
149
+ }
150
+ }
151
+ export class MagicLinkVerifyStrategy {
152
+ magicLinkStrategy;
153
+ name = 'magic:verify';
154
+ constructor(magicLinkStrategy) {
155
+ this.magicLinkStrategy = magicLinkStrategy;
156
+ }
157
+ async authenticate(callbacks, ...args) {
158
+ return this.magicLinkStrategy.authenticate(callbacks, 'verify', ...args);
159
+ }
160
+ }
161
+ /**
162
+ * Helper function to register all magic link strategies at once.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * import { Handshake, useMagicLink } from 'handshake-auth';
167
+ *
168
+ * const hs = new Handshake({...});
169
+ * useMagicLink(hs, {
170
+ * baseUrl: 'http://localhost:3000',
171
+ * sendMagicLink: async (email, token, url) => {...},
172
+ * });
173
+ *
174
+ * // Now you can use:
175
+ * await hs.authenticate('magic:send', email);
176
+ * await hs.authenticate('magic:verify', token);
177
+ * ```
178
+ */
179
+ export function useMagicLink(hs, options) {
180
+ const strategy = new MagicLinkStrategy(options);
181
+ hs.use(strategy);
182
+ hs.use(new MagicLinkSendStrategy(strategy));
183
+ hs.use(new MagicLinkVerifyStrategy(strategy));
184
+ return strategy;
185
+ }
186
+ //# sourceMappingURL=magic-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magic-link.js","sourceRoot":"","sources":["../../src/strategies/magic-link.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ import type { OAuthProfile } from '../types.js';
2
+ import { OAuthStrategy } from './oauth-base.js';
3
+ /**
4
+ * Microsoft user profile from the Microsoft Graph /me endpoint.
5
+ */
6
+ export interface MicrosoftProfile {
7
+ id: string;
8
+ displayName?: string;
9
+ givenName?: string;
10
+ surname?: string;
11
+ mail?: string;
12
+ userPrincipalName?: string;
13
+ jobTitle?: string;
14
+ officeLocation?: string;
15
+ preferredLanguage?: string;
16
+ businessPhones?: string[];
17
+ mobilePhone?: string;
18
+ }
19
+ /**
20
+ * Microsoft tenant options.
21
+ * - 'common': Any Microsoft account (personal or work/school)
22
+ * - 'organizations': Work/school accounts only
23
+ * - 'consumers': Personal Microsoft accounts only
24
+ * - Specific tenant ID: Accounts from that tenant only
25
+ */
26
+ export type MicrosoftTenant = 'common' | 'organizations' | 'consumers' | string;
27
+ /**
28
+ * Configuration options for Microsoft OAuth strategy.
29
+ */
30
+ export interface MicrosoftStrategyOptions {
31
+ /**
32
+ * Microsoft OAuth client ID (Application ID)
33
+ */
34
+ clientId: string;
35
+ /**
36
+ * Microsoft OAuth client secret
37
+ */
38
+ clientSecret: string;
39
+ /**
40
+ * Your callback URL (must match Azure Portal configuration)
41
+ */
42
+ redirectUri: string;
43
+ /**
44
+ * Azure AD tenant
45
+ * - 'common': Any Microsoft account (default)
46
+ * - 'organizations': Work/school accounts only
47
+ * - 'consumers': Personal Microsoft accounts only
48
+ * - Specific tenant ID: Accounts from that tenant only
49
+ * @default 'common'
50
+ */
51
+ tenant?: MicrosoftTenant;
52
+ /**
53
+ * OAuth scopes to request
54
+ * @default ['openid', 'email', 'profile', 'User.Read']
55
+ */
56
+ scopes?: string[];
57
+ }
58
+ /**
59
+ * Microsoft OAuth2 authentication strategy using Azure AD / Microsoft Identity Platform.
60
+ *
61
+ * Supports multiple tenant configurations:
62
+ * - `common`: Accept any Microsoft account
63
+ * - `organizations`: Only work/school accounts
64
+ * - `consumers`: Only personal Microsoft accounts
65
+ * - Specific tenant ID: Only accounts from that organization
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const hs = new Handshake({
70
+ * findAccount: async (email) => db.accounts.findByEmail(email),
71
+ * findOrCreateFromOAuth: async (provider, profile) => {
72
+ * let account = await db.accounts.findByProviderId(provider, profile.id);
73
+ * if (!account) {
74
+ * account = await db.accounts.create({
75
+ * email: profile.email,
76
+ * name: profile.name,
77
+ * providerId: profile.id,
78
+ * provider,
79
+ * });
80
+ * }
81
+ * return account;
82
+ * },
83
+ * });
84
+ *
85
+ * // Accept any Microsoft account
86
+ * hs.use(new MicrosoftStrategy({
87
+ * clientId: process.env.MICROSOFT_CLIENT_ID!,
88
+ * clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
89
+ * redirectUri: 'http://localhost:3000/auth/microsoft/callback',
90
+ * tenant: 'common',
91
+ * }));
92
+ *
93
+ * // Or restrict to a specific organization
94
+ * hs.use(new MicrosoftStrategy({
95
+ * clientId: process.env.MICROSOFT_CLIENT_ID!,
96
+ * clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
97
+ * redirectUri: 'http://localhost:3000/auth/microsoft/callback',
98
+ * tenant: 'your-tenant-id',
99
+ * }));
100
+ *
101
+ * // Routes
102
+ * app.get('/auth/microsoft', async (req, res) => {
103
+ * const result = await hs.authenticate('microsoft', req, res, 'redirect');
104
+ * if ('redirectUrl' in result) {
105
+ * res.redirect(result.redirectUrl);
106
+ * }
107
+ * });
108
+ *
109
+ * app.get('/auth/microsoft/callback', async (req, res) => {
110
+ * const result = await hs.authenticate('microsoft', req, res, 'callback');
111
+ * if (result.account) {
112
+ * login(req, result.account);
113
+ * res.redirect('/dashboard');
114
+ * } else {
115
+ * res.status(401).send(result.error);
116
+ * }
117
+ * });
118
+ * ```
119
+ */
120
+ export declare class MicrosoftStrategy<TAccount> extends OAuthStrategy<TAccount> {
121
+ constructor(options: MicrosoftStrategyOptions);
122
+ /**
123
+ * Map Microsoft profile to standard OAuthProfile.
124
+ */
125
+ protected mapProfile(data: unknown, accessToken: string): OAuthProfile;
126
+ }
127
+ //# sourceMappingURL=microsoft.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"microsoft.d.ts","sourceRoot":"","sources":["../../src/strategies/microsoft.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,eAAe,GAAG,WAAW,GAAG,MAAM,CAAC;AAEhF;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IAEzB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,qBAAa,iBAAiB,CAAC,QAAQ,CAAE,SAAQ,aAAa,CAAC,QAAQ,CAAC;gBAC1D,OAAO,EAAE,wBAAwB;IAgB7C;;OAEG;IAEH,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,YAAY;CAgBvE"}
@@ -0,0 +1,98 @@
1
+ import { OAuthStrategy } from './oauth-base.js';
2
+ /**
3
+ * Microsoft OAuth2 authentication strategy using Azure AD / Microsoft Identity Platform.
4
+ *
5
+ * Supports multiple tenant configurations:
6
+ * - `common`: Accept any Microsoft account
7
+ * - `organizations`: Only work/school accounts
8
+ * - `consumers`: Only personal Microsoft accounts
9
+ * - Specific tenant ID: Only accounts from that organization
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const hs = new Handshake({
14
+ * findAccount: async (email) => db.accounts.findByEmail(email),
15
+ * findOrCreateFromOAuth: async (provider, profile) => {
16
+ * let account = await db.accounts.findByProviderId(provider, profile.id);
17
+ * if (!account) {
18
+ * account = await db.accounts.create({
19
+ * email: profile.email,
20
+ * name: profile.name,
21
+ * providerId: profile.id,
22
+ * provider,
23
+ * });
24
+ * }
25
+ * return account;
26
+ * },
27
+ * });
28
+ *
29
+ * // Accept any Microsoft account
30
+ * hs.use(new MicrosoftStrategy({
31
+ * clientId: process.env.MICROSOFT_CLIENT_ID!,
32
+ * clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
33
+ * redirectUri: 'http://localhost:3000/auth/microsoft/callback',
34
+ * tenant: 'common',
35
+ * }));
36
+ *
37
+ * // Or restrict to a specific organization
38
+ * hs.use(new MicrosoftStrategy({
39
+ * clientId: process.env.MICROSOFT_CLIENT_ID!,
40
+ * clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
41
+ * redirectUri: 'http://localhost:3000/auth/microsoft/callback',
42
+ * tenant: 'your-tenant-id',
43
+ * }));
44
+ *
45
+ * // Routes
46
+ * app.get('/auth/microsoft', async (req, res) => {
47
+ * const result = await hs.authenticate('microsoft', req, res, 'redirect');
48
+ * if ('redirectUrl' in result) {
49
+ * res.redirect(result.redirectUrl);
50
+ * }
51
+ * });
52
+ *
53
+ * app.get('/auth/microsoft/callback', async (req, res) => {
54
+ * const result = await hs.authenticate('microsoft', req, res, 'callback');
55
+ * if (result.account) {
56
+ * login(req, result.account);
57
+ * res.redirect('/dashboard');
58
+ * } else {
59
+ * res.status(401).send(result.error);
60
+ * }
61
+ * });
62
+ * ```
63
+ */
64
+ export class MicrosoftStrategy extends OAuthStrategy {
65
+ constructor(options) {
66
+ const tenant = options.tenant ?? 'common';
67
+ super({
68
+ name: 'microsoft',
69
+ clientId: options.clientId,
70
+ clientSecret: options.clientSecret,
71
+ redirectUri: options.redirectUri,
72
+ authorizeUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
73
+ tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
74
+ userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
75
+ scopes: options.scopes ?? ['openid', 'email', 'profile', 'User.Read'],
76
+ stateCookieName: 'microsoft_oauth_state',
77
+ });
78
+ }
79
+ /**
80
+ * Map Microsoft profile to standard OAuthProfile.
81
+ */
82
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
83
+ mapProfile(data, accessToken) {
84
+ const profile = data;
85
+ // Microsoft may provide email in 'mail' or 'userPrincipalName'
86
+ // userPrincipalName is often an email for work/school accounts
87
+ const email = profile.mail ??
88
+ (profile.userPrincipalName?.includes('@') ? profile.userPrincipalName : null);
89
+ return {
90
+ id: profile.id,
91
+ email: email ?? null,
92
+ name: profile.displayName ?? null,
93
+ picture: null, // Microsoft Graph requires separate API call for photo
94
+ raw: profile,
95
+ };
96
+ }
97
+ }
98
+ //# sourceMappingURL=microsoft.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"microsoft.js","sourceRoot":"","sources":["../../src/strategies/microsoft.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAgEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,MAAM,OAAO,iBAA4B,SAAQ,aAAuB;IACtE,YAAY,OAAiC;QAC3C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,QAAQ,CAAC;QAE1C,KAAK,CAAC;YACJ,IAAI,EAAE,WAAW;YACjB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,YAAY,EAAE,qCAAqC,MAAM,wBAAwB;YACjF,QAAQ,EAAE,qCAAqC,MAAM,oBAAoB;YACzE,WAAW,EAAE,qCAAqC;YAClD,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC;YACrE,eAAe,EAAE,uBAAuB;SACzC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACH,6DAA6D;IACnD,UAAU,CAAC,IAAa,EAAE,WAAmB;QACrD,MAAM,OAAO,GAAG,IAAwB,CAAC;QAEzC,+DAA+D;QAC/D,+DAA+D;QAC/D,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI;YACxB,CAAC,OAAO,CAAC,iBAAiB,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAEhF,OAAO;YACL,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,KAAK,EAAE,KAAK,IAAI,IAAI;YACpB,IAAI,EAAE,OAAO,CAAC,WAAW,IAAI,IAAI;YACjC,OAAO,EAAE,IAAI,EAAE,uDAAuD;YACtE,GAAG,EAAE,OAA6C;SACnD,CAAC;IACJ,CAAC;CACF"}