@ankhorage/contracts 2.1.0 → 3.0.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/CHANGELOG.md +6 -0
- package/dist/auth.d.ts +77 -14
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +37 -0
- package/dist/auth.js.map +1 -1
- package/package.json +1 -1
- package/src/auth-oauth.test.ts +189 -44
- package/src/auth.ts +132 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @ankhorage/contracts
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- d46f70b: Replace the optional URL-only OAuth adapter methods with one canonical provider-neutral OAuth capability that requires authorization start and callback completion, models transport cancellation and failures explicitly, and resolves successful OAuth sign-in to the existing `AuthSession` contract.
|
|
8
|
+
|
|
3
9
|
## 2.1.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/dist/auth.d.ts
CHANGED
|
@@ -42,7 +42,6 @@ export interface AuthOAuthProviderConfig {
|
|
|
42
42
|
label?: string;
|
|
43
43
|
enabled?: boolean;
|
|
44
44
|
scopes?: string[];
|
|
45
|
-
redirectTo?: string;
|
|
46
45
|
queryParams?: Record<string, string>;
|
|
47
46
|
icon?: IconSpec;
|
|
48
47
|
/** Logical server-side secret reference; raw credentials must never be stored here. */
|
|
@@ -120,20 +119,83 @@ export interface VerifyOtpInput {
|
|
|
120
119
|
redirectTo?: string;
|
|
121
120
|
metadata?: Record<string, unknown>;
|
|
122
121
|
}
|
|
123
|
-
export
|
|
122
|
+
export declare const AUTH_OAUTH_ERROR_STAGES: readonly ["start", "transport", "callback", "exchange", "session", "profile"];
|
|
123
|
+
export type AuthOAuthErrorStage = (typeof AUTH_OAUTH_ERROR_STAGES)[number];
|
|
124
|
+
export declare const AUTH_OAUTH_ERROR_CODES: readonly ["oauth_unavailable", "provider_disabled", "provider_misconfigured", "invalid_redirect_uri", "authorization_failed", "authorization_attempt_not_found", "invalid_callback", "state_mismatch", "pkce_mismatch", "callback_already_completed", "code_exchange_failed", "network_error", "session_persistence_failed", "profile_creation_failed", "provider_error"];
|
|
125
|
+
export type AuthOAuthErrorCode = (typeof AUTH_OAUTH_ERROR_CODES)[number];
|
|
126
|
+
export interface AuthOAuthError extends AuthAdapterError {
|
|
127
|
+
code: AuthOAuthErrorCode;
|
|
128
|
+
stage: AuthOAuthErrorStage;
|
|
129
|
+
provider?: AuthOAuthProviderId;
|
|
130
|
+
recoverable: boolean;
|
|
131
|
+
}
|
|
132
|
+
export declare const AUTH_OAUTH_TRANSPORT_CANCELLATION_REASONS: readonly ["user_cancelled", "browser_dismissed"];
|
|
133
|
+
export type AuthOAuthTransportCancellationReason = (typeof AUTH_OAUTH_TRANSPORT_CANCELLATION_REASONS)[number];
|
|
134
|
+
export declare const AUTH_OAUTH_CANCELLATION_REASONS: readonly ["user_cancelled", "browser_dismissed", "provider_denied"];
|
|
135
|
+
export type AuthOAuthCancellationReason = (typeof AUTH_OAUTH_CANCELLATION_REASONS)[number];
|
|
136
|
+
export declare const AUTH_OAUTH_TRANSPORT_ERROR_CODES: readonly ["browser_unavailable", "transport_failed"];
|
|
137
|
+
export type AuthOAuthTransportErrorCode = (typeof AUTH_OAUTH_TRANSPORT_ERROR_CODES)[number];
|
|
138
|
+
export interface AuthOAuthTransportError {
|
|
139
|
+
code: AuthOAuthTransportErrorCode;
|
|
140
|
+
message: string;
|
|
141
|
+
cause?: unknown;
|
|
142
|
+
}
|
|
143
|
+
export interface StartOAuthAuthorizationInput {
|
|
124
144
|
provider: AuthOAuthProviderId;
|
|
125
|
-
|
|
126
|
-
scopes?: string[];
|
|
127
|
-
queryParams?: Record<string, string
|
|
128
|
-
metadata?: Record<string, unknown>;
|
|
145
|
+
redirectUri: string;
|
|
146
|
+
scopes?: readonly string[];
|
|
147
|
+
queryParams?: Readonly<Record<string, string>>;
|
|
129
148
|
}
|
|
130
|
-
export interface
|
|
149
|
+
export interface AuthOAuthAuthorizationRequest {
|
|
150
|
+
attemptId: string;
|
|
131
151
|
provider: AuthOAuthProviderId;
|
|
132
|
-
|
|
152
|
+
authorizationUrl: string;
|
|
153
|
+
redirectUri: string;
|
|
133
154
|
}
|
|
134
|
-
export
|
|
155
|
+
export type AuthOAuthStartResult = {
|
|
156
|
+
ok: true;
|
|
157
|
+
data: AuthOAuthAuthorizationRequest;
|
|
158
|
+
} | {
|
|
159
|
+
ok: false;
|
|
160
|
+
error: AuthOAuthError;
|
|
161
|
+
};
|
|
162
|
+
export type AuthOAuthAuthorizationResponse = {
|
|
163
|
+
type: 'callback';
|
|
135
164
|
url: string;
|
|
136
|
-
|
|
165
|
+
} | {
|
|
166
|
+
type: 'cancelled';
|
|
167
|
+
reason: AuthOAuthTransportCancellationReason;
|
|
168
|
+
} | {
|
|
169
|
+
type: 'error';
|
|
170
|
+
error: AuthOAuthTransportError;
|
|
171
|
+
};
|
|
172
|
+
export interface CompleteOAuthAuthorizationInput {
|
|
173
|
+
attemptId: string;
|
|
174
|
+
response: AuthOAuthAuthorizationResponse;
|
|
175
|
+
}
|
|
176
|
+
export type AuthOAuthCompletionResult = {
|
|
177
|
+
ok: true;
|
|
178
|
+
status: 'authenticated';
|
|
179
|
+
provider: AuthOAuthProviderId;
|
|
180
|
+
session: AuthSession;
|
|
181
|
+
} | {
|
|
182
|
+
ok: false;
|
|
183
|
+
status: 'cancelled';
|
|
184
|
+
provider: AuthOAuthProviderId;
|
|
185
|
+
reason: AuthOAuthCancellationReason;
|
|
186
|
+
} | {
|
|
187
|
+
ok: false;
|
|
188
|
+
status: 'error';
|
|
189
|
+
error: AuthOAuthError;
|
|
190
|
+
};
|
|
191
|
+
export interface AuthOAuthCapabilities {
|
|
192
|
+
/** Enabled providers for which start and callback completion are operational. */
|
|
193
|
+
providers: readonly [AuthOAuthProviderId, ...AuthOAuthProviderId[]];
|
|
194
|
+
}
|
|
195
|
+
export interface AuthOAuthAdapter {
|
|
196
|
+
readonly capabilities: AuthOAuthCapabilities;
|
|
197
|
+
startAuthorization(input: StartOAuthAuthorizationInput): Promise<AuthOAuthStartResult>;
|
|
198
|
+
completeAuthorization(input: CompleteOAuthAuthorizationInput): Promise<AuthOAuthCompletionResult>;
|
|
137
199
|
}
|
|
138
200
|
export interface AuthAdapterCapabilities {
|
|
139
201
|
signInIdentifiers: AuthIdentifierKind[];
|
|
@@ -141,11 +203,14 @@ export interface AuthAdapterCapabilities {
|
|
|
141
203
|
supportsPasswordReset: boolean;
|
|
142
204
|
supportsOtp: boolean;
|
|
143
205
|
supportsSessionRefresh: boolean;
|
|
144
|
-
supportsOAuth?: boolean;
|
|
145
|
-
oauthProviders?: AuthOAuthProviderId[];
|
|
146
206
|
}
|
|
147
207
|
export interface AuthAdapter {
|
|
148
208
|
readonly capabilities?: AuthAdapterCapabilities;
|
|
209
|
+
/**
|
|
210
|
+
* Presence is the canonical OAuth capability signal. When present, both authorization start and
|
|
211
|
+
* callback completion are mandatory and operational for every advertised provider.
|
|
212
|
+
*/
|
|
213
|
+
readonly oauth?: AuthOAuthAdapter;
|
|
149
214
|
signIn(input: SignInInput): Promise<AuthResult<AuthSession>>;
|
|
150
215
|
signUp(input: SignUpInput): Promise<AuthResult<AuthSession | AuthUser>>;
|
|
151
216
|
signOut(input?: SignOutInput): Promise<AuthResult>;
|
|
@@ -153,7 +218,5 @@ export interface AuthAdapter {
|
|
|
153
218
|
refreshSession?(): Promise<AuthResult<AuthSession | null>>;
|
|
154
219
|
requestPasswordReset?(input: PasswordResetInput): Promise<AuthResult>;
|
|
155
220
|
verifyOtp?(input: VerifyOtpInput): Promise<AuthResult<AuthSession>>;
|
|
156
|
-
signInWithOAuth?(input: SignInWithOAuthInput): Promise<AuthResult<AuthOAuthRedirect>>;
|
|
157
|
-
completeOAuthSignIn?(input: CompleteOAuthSignInInput): Promise<AuthResult<AuthSession>>;
|
|
158
221
|
}
|
|
159
222
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,eAAO,MAAM,qBAAqB,yCAA0C,CAAC;AAC7E,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,eAAO,MAAM,mBAAmB,6FAMtB,CAAC;AACX,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AACxE,MAAM,MAAM,eAAe,GAAG,oBAAoB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAEnE,eAAO,MAAM,uBAAuB,8MAoB1B,CAAC;AACX,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAChF,MAAM,MAAM,mBAAmB,GAAG,wBAAwB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAE3E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,iBAAiB;;;;;;;CAOK,CAAC;AAEpC,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,cAAc,GAAG,cAAc,CAErE;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,eAAe,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,eAAe,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,mBAAmB,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.js
CHANGED
|
@@ -38,4 +38,41 @@ export const DEFAULT_AUTH_FLOW = {
|
|
|
38
38
|
export function resolveAuthFlow(flow) {
|
|
39
39
|
return { ...(flow ?? DEFAULT_AUTH_FLOW) };
|
|
40
40
|
}
|
|
41
|
+
export const AUTH_OAUTH_ERROR_STAGES = [
|
|
42
|
+
'start',
|
|
43
|
+
'transport',
|
|
44
|
+
'callback',
|
|
45
|
+
'exchange',
|
|
46
|
+
'session',
|
|
47
|
+
'profile',
|
|
48
|
+
];
|
|
49
|
+
export const AUTH_OAUTH_ERROR_CODES = [
|
|
50
|
+
'oauth_unavailable',
|
|
51
|
+
'provider_disabled',
|
|
52
|
+
'provider_misconfigured',
|
|
53
|
+
'invalid_redirect_uri',
|
|
54
|
+
'authorization_failed',
|
|
55
|
+
'authorization_attempt_not_found',
|
|
56
|
+
'invalid_callback',
|
|
57
|
+
'state_mismatch',
|
|
58
|
+
'pkce_mismatch',
|
|
59
|
+
'callback_already_completed',
|
|
60
|
+
'code_exchange_failed',
|
|
61
|
+
'network_error',
|
|
62
|
+
'session_persistence_failed',
|
|
63
|
+
'profile_creation_failed',
|
|
64
|
+
'provider_error',
|
|
65
|
+
];
|
|
66
|
+
export const AUTH_OAUTH_TRANSPORT_CANCELLATION_REASONS = [
|
|
67
|
+
'user_cancelled',
|
|
68
|
+
'browser_dismissed',
|
|
69
|
+
];
|
|
70
|
+
export const AUTH_OAUTH_CANCELLATION_REASONS = [
|
|
71
|
+
...AUTH_OAUTH_TRANSPORT_CANCELLATION_REASONS,
|
|
72
|
+
'provider_denied',
|
|
73
|
+
];
|
|
74
|
+
export const AUTH_OAUTH_TRANSPORT_ERROR_CODES = [
|
|
75
|
+
'browser_unavailable',
|
|
76
|
+
'transport_failed',
|
|
77
|
+
];
|
|
41
78
|
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAU,CAAC;AAG7E,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,GAAG,qBAAqB;IACxB,UAAU;IACV,aAAa;IACb,WAAW;IACX,UAAU;CACF,CAAC;AAIX,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,OAAO;IACP,OAAO;IACP,WAAW;IACX,SAAS;IACT,UAAU;IACV,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,UAAU;IACV,UAAU;IACV,QAAQ;IACR,OAAO;IACP,SAAS;IACT,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,MAAM;CACE,CAAC;AAmBX,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,WAAW,EAAE,SAAS;IACtB,WAAW,EAAE,SAAS;IACtB,YAAY,EAAE,UAAU;IACxB,mBAAmB,EAAE,iBAAiB;IACtC,eAAe,EAAE,GAAG;IACpB,iBAAiB,EAAE,SAAS;CACK,CAAC;AAEpC,MAAM,UAAU,eAAe,CAAC,IAAqB;IACnD,OAAO,EAAE,GAAG,CAAC,IAAI,IAAI,iBAAiB,CAAC,EAAE,CAAC;AAC5C,CAAC","sourcesContent":["import type { SecretRef } from './secrets';\nimport type { IconSpec } from './types';\n\nexport const AUTH_IDENTIFIER_KINDS = ['email', 'phone', 'username'] as const;\nexport type AuthIdentifierKind = (typeof AUTH_IDENTIFIER_KINDS)[number];\n\nexport const AUTH_SIGN_UP_FIELDS = [\n ...AUTH_IDENTIFIER_KINDS,\n 'password',\n 'displayName',\n 'firstName',\n 'lastName',\n] as const;\nexport type KnownAuthSignUpField = (typeof AUTH_SIGN_UP_FIELDS)[number];\nexport type AuthSignUpField = KnownAuthSignUpField | (string & {});\n\nexport const AUTH_OAUTH_PROVIDER_IDS = [\n 'apple',\n 'azure',\n 'bitbucket',\n 'discord',\n 'facebook',\n 'figma',\n 'github',\n 'gitlab',\n 'google',\n 'kakao',\n 'keycloak',\n 'linkedin',\n 'notion',\n 'slack',\n 'spotify',\n 'twitch',\n 'twitter',\n 'workos',\n 'zoom',\n] as const;\nexport type KnownAuthOAuthProviderId = (typeof AUTH_OAUTH_PROVIDER_IDS)[number];\nexport type AuthOAuthProviderId = KnownAuthOAuthProviderId | (string & {});\n\nexport interface AuthIdentifier {\n kind: AuthIdentifierKind;\n value: string;\n}\n\nexport interface AuthFlowConfig {\n signInRoute: string;\n signUpRoute?: string;\n signOutRoute?: string;\n forgotPasswordRoute?: string;\n otpRoute?: string;\n postSignInRoute: string;\n unauthorizedRoute?: string;\n}\n\nexport const DEFAULT_AUTH_FLOW = {\n signInRoute: 'sign-in',\n signUpRoute: 'sign-up',\n signOutRoute: 'sign-out',\n forgotPasswordRoute: 'forgot-password',\n postSignInRoute: '/',\n unauthorizedRoute: 'sign-in',\n} as const satisfies AuthFlowConfig;\n\nexport function resolveAuthFlow(flow?: AuthFlowConfig): AuthFlowConfig {\n return { ...(flow ?? DEFAULT_AUTH_FLOW) };\n}\n\nexport interface AuthSignInConfig {\n identifiers: AuthIdentifierKind[];\n}\n\nexport interface AuthSignUpConfig {\n requiredFields: AuthSignUpField[];\n optionalFields?: AuthSignUpField[];\n}\n\nexport interface AuthOAuthProviderConfig {\n id: AuthOAuthProviderId;\n label?: string;\n enabled?: boolean;\n scopes?: string[];\n
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAU,CAAC;AAG7E,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,GAAG,qBAAqB;IACxB,UAAU;IACV,aAAa;IACb,WAAW;IACX,UAAU;CACF,CAAC;AAIX,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,OAAO;IACP,OAAO;IACP,WAAW;IACX,SAAS;IACT,UAAU;IACV,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,UAAU;IACV,UAAU;IACV,QAAQ;IACR,OAAO;IACP,SAAS;IACT,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,MAAM;CACE,CAAC;AAmBX,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,WAAW,EAAE,SAAS;IACtB,WAAW,EAAE,SAAS;IACtB,YAAY,EAAE,UAAU;IACxB,mBAAmB,EAAE,iBAAiB;IACtC,eAAe,EAAE,GAAG;IACpB,iBAAiB,EAAE,SAAS;CACK,CAAC;AAEpC,MAAM,UAAU,eAAe,CAAC,IAAqB;IACnD,OAAO,EAAE,GAAG,CAAC,IAAI,IAAI,iBAAiB,CAAC,EAAE,CAAC;AAC5C,CAAC;AA2GD,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,OAAO;IACP,WAAW;IACX,UAAU;IACV,UAAU;IACV,SAAS;IACT,SAAS;CACD,CAAC;AAGX,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,mBAAmB;IACnB,mBAAmB;IACnB,wBAAwB;IACxB,sBAAsB;IACtB,sBAAsB;IACtB,iCAAiC;IACjC,kBAAkB;IAClB,gBAAgB;IAChB,eAAe;IACf,4BAA4B;IAC5B,sBAAsB;IACtB,eAAe;IACf,4BAA4B;IAC5B,yBAAyB;IACzB,gBAAgB;CACR,CAAC;AAUX,MAAM,CAAC,MAAM,yCAAyC,GAAG;IACvD,gBAAgB;IAChB,mBAAmB;CACX,CAAC;AAIX,MAAM,CAAC,MAAM,+BAA+B,GAAG;IAC7C,GAAG,yCAAyC;IAC5C,iBAAiB;CACT,CAAC;AAGX,MAAM,CAAC,MAAM,gCAAgC,GAAG;IAC9C,qBAAqB;IACrB,kBAAkB;CACV,CAAC","sourcesContent":["import type { SecretRef } from './secrets';\nimport type { IconSpec } from './types';\n\nexport const AUTH_IDENTIFIER_KINDS = ['email', 'phone', 'username'] as const;\nexport type AuthIdentifierKind = (typeof AUTH_IDENTIFIER_KINDS)[number];\n\nexport const AUTH_SIGN_UP_FIELDS = [\n ...AUTH_IDENTIFIER_KINDS,\n 'password',\n 'displayName',\n 'firstName',\n 'lastName',\n] as const;\nexport type KnownAuthSignUpField = (typeof AUTH_SIGN_UP_FIELDS)[number];\nexport type AuthSignUpField = KnownAuthSignUpField | (string & {});\n\nexport const AUTH_OAUTH_PROVIDER_IDS = [\n 'apple',\n 'azure',\n 'bitbucket',\n 'discord',\n 'facebook',\n 'figma',\n 'github',\n 'gitlab',\n 'google',\n 'kakao',\n 'keycloak',\n 'linkedin',\n 'notion',\n 'slack',\n 'spotify',\n 'twitch',\n 'twitter',\n 'workos',\n 'zoom',\n] as const;\nexport type KnownAuthOAuthProviderId = (typeof AUTH_OAUTH_PROVIDER_IDS)[number];\nexport type AuthOAuthProviderId = KnownAuthOAuthProviderId | (string & {});\n\nexport interface AuthIdentifier {\n kind: AuthIdentifierKind;\n value: string;\n}\n\nexport interface AuthFlowConfig {\n signInRoute: string;\n signUpRoute?: string;\n signOutRoute?: string;\n forgotPasswordRoute?: string;\n otpRoute?: string;\n postSignInRoute: string;\n unauthorizedRoute?: string;\n}\n\nexport const DEFAULT_AUTH_FLOW = {\n signInRoute: 'sign-in',\n signUpRoute: 'sign-up',\n signOutRoute: 'sign-out',\n forgotPasswordRoute: 'forgot-password',\n postSignInRoute: '/',\n unauthorizedRoute: 'sign-in',\n} as const satisfies AuthFlowConfig;\n\nexport function resolveAuthFlow(flow?: AuthFlowConfig): AuthFlowConfig {\n return { ...(flow ?? DEFAULT_AUTH_FLOW) };\n}\n\nexport interface AuthSignInConfig {\n identifiers: AuthIdentifierKind[];\n}\n\nexport interface AuthSignUpConfig {\n requiredFields: AuthSignUpField[];\n optionalFields?: AuthSignUpField[];\n}\n\nexport interface AuthOAuthProviderConfig {\n id: AuthOAuthProviderId;\n label?: string;\n enabled?: boolean;\n scopes?: string[];\n queryParams?: Record<string, string>;\n icon?: IconSpec;\n /** Logical server-side secret reference; raw credentials must never be stored here. */\n credentialsRef?: SecretRef;\n}\n\nexport interface AuthOAuthConfig {\n enabled: boolean;\n callbackRoute: string;\n providers: AuthOAuthProviderConfig[];\n}\n\nexport interface AuthProviderConfig {\n provider: string;\n signIn: AuthSignInConfig;\n signUp?: AuthSignUpConfig;\n oauth?: AuthOAuthConfig;\n passwordReset?: {\n enabled: boolean;\n };\n otp?: {\n enabled: boolean;\n };\n}\n\nexport interface AuthUser {\n id: string;\n email?: string;\n phone?: string;\n username?: string;\n displayName?: string;\n avatarUrl?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface AuthSession {\n accessToken: string;\n refreshToken?: string;\n expiresAt?: number;\n tokenType?: string;\n user: AuthUser;\n}\n\nexport interface AuthAdapterError {\n code: string;\n message: string;\n cause?: unknown;\n}\n\nexport type AuthResult<TData = void> =\n | {\n ok: true;\n data?: TData;\n }\n | {\n ok: false;\n error: AuthAdapterError;\n };\n\nexport interface SignInInput {\n identifier: AuthIdentifier;\n password?: string;\n otp?: string;\n redirectTo?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface SignUpInput {\n identifier: AuthIdentifier;\n password?: string;\n profile?: Record<string, unknown>;\n redirectTo?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface SignOutInput {\n allDevices?: boolean;\n}\n\nexport interface PasswordResetInput {\n identifier: AuthIdentifier;\n redirectTo?: string;\n}\n\nexport interface VerifyOtpInput {\n identifier: AuthIdentifier;\n token: string;\n redirectTo?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport const AUTH_OAUTH_ERROR_STAGES = [\n 'start',\n 'transport',\n 'callback',\n 'exchange',\n 'session',\n 'profile',\n] as const;\nexport type AuthOAuthErrorStage = (typeof AUTH_OAUTH_ERROR_STAGES)[number];\n\nexport const AUTH_OAUTH_ERROR_CODES = [\n 'oauth_unavailable',\n 'provider_disabled',\n 'provider_misconfigured',\n 'invalid_redirect_uri',\n 'authorization_failed',\n 'authorization_attempt_not_found',\n 'invalid_callback',\n 'state_mismatch',\n 'pkce_mismatch',\n 'callback_already_completed',\n 'code_exchange_failed',\n 'network_error',\n 'session_persistence_failed',\n 'profile_creation_failed',\n 'provider_error',\n] as const;\nexport type AuthOAuthErrorCode = (typeof AUTH_OAUTH_ERROR_CODES)[number];\n\nexport interface AuthOAuthError extends AuthAdapterError {\n code: AuthOAuthErrorCode;\n stage: AuthOAuthErrorStage;\n provider?: AuthOAuthProviderId;\n recoverable: boolean;\n}\n\nexport const AUTH_OAUTH_TRANSPORT_CANCELLATION_REASONS = [\n 'user_cancelled',\n 'browser_dismissed',\n] as const;\nexport type AuthOAuthTransportCancellationReason =\n (typeof AUTH_OAUTH_TRANSPORT_CANCELLATION_REASONS)[number];\n\nexport const AUTH_OAUTH_CANCELLATION_REASONS = [\n ...AUTH_OAUTH_TRANSPORT_CANCELLATION_REASONS,\n 'provider_denied',\n] as const;\nexport type AuthOAuthCancellationReason = (typeof AUTH_OAUTH_CANCELLATION_REASONS)[number];\n\nexport const AUTH_OAUTH_TRANSPORT_ERROR_CODES = [\n 'browser_unavailable',\n 'transport_failed',\n] as const;\nexport type AuthOAuthTransportErrorCode = (typeof AUTH_OAUTH_TRANSPORT_ERROR_CODES)[number];\n\nexport interface AuthOAuthTransportError {\n code: AuthOAuthTransportErrorCode;\n message: string;\n cause?: unknown;\n}\n\nexport interface StartOAuthAuthorizationInput {\n provider: AuthOAuthProviderId;\n redirectUri: string;\n scopes?: readonly string[];\n queryParams?: Readonly<Record<string, string>>;\n}\n\nexport interface AuthOAuthAuthorizationRequest {\n attemptId: string;\n provider: AuthOAuthProviderId;\n authorizationUrl: string;\n redirectUri: string;\n}\n\nexport type AuthOAuthStartResult =\n | {\n ok: true;\n data: AuthOAuthAuthorizationRequest;\n }\n | {\n ok: false;\n error: AuthOAuthError;\n };\n\nexport type AuthOAuthAuthorizationResponse =\n | {\n type: 'callback';\n url: string;\n }\n | {\n type: 'cancelled';\n reason: AuthOAuthTransportCancellationReason;\n }\n | {\n type: 'error';\n error: AuthOAuthTransportError;\n };\n\nexport interface CompleteOAuthAuthorizationInput {\n attemptId: string;\n response: AuthOAuthAuthorizationResponse;\n}\n\nexport type AuthOAuthCompletionResult =\n | {\n ok: true;\n status: 'authenticated';\n provider: AuthOAuthProviderId;\n session: AuthSession;\n }\n | {\n ok: false;\n status: 'cancelled';\n provider: AuthOAuthProviderId;\n reason: AuthOAuthCancellationReason;\n }\n | {\n ok: false;\n status: 'error';\n error: AuthOAuthError;\n };\n\nexport interface AuthOAuthCapabilities {\n /** Enabled providers for which start and callback completion are operational. */\n providers: readonly [AuthOAuthProviderId, ...AuthOAuthProviderId[]];\n}\n\nexport interface AuthOAuthAdapter {\n readonly capabilities: AuthOAuthCapabilities;\n\n startAuthorization(input: StartOAuthAuthorizationInput): Promise<AuthOAuthStartResult>;\n completeAuthorization(input: CompleteOAuthAuthorizationInput): Promise<AuthOAuthCompletionResult>;\n}\n\nexport interface AuthAdapterCapabilities {\n signInIdentifiers: AuthIdentifierKind[];\n supportsSignUp: boolean;\n supportsPasswordReset: boolean;\n supportsOtp: boolean;\n supportsSessionRefresh: boolean;\n}\n\nexport interface AuthAdapter {\n readonly capabilities?: AuthAdapterCapabilities;\n /**\n * Presence is the canonical OAuth capability signal. When present, both authorization start and\n * callback completion are mandatory and operational for every advertised provider.\n */\n readonly oauth?: AuthOAuthAdapter;\n\n signIn(input: SignInInput): Promise<AuthResult<AuthSession>>;\n signUp(input: SignUpInput): Promise<AuthResult<AuthSession | AuthUser>>;\n signOut(input?: SignOutInput): Promise<AuthResult>;\n\n getSession(): Promise<AuthResult<AuthSession | null>>;\n refreshSession?(): Promise<AuthResult<AuthSession | null>>;\n\n requestPasswordReset?(input: PasswordResetInput): Promise<AuthResult>;\n verifyOtp?(input: VerifyOtpInput): Promise<AuthResult<AuthSession>>;\n}\n"]}
|
package/package.json
CHANGED
package/src/auth-oauth.test.ts
CHANGED
|
@@ -1,12 +1,58 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
1
5
|
import { describe, expect, it } from 'bun:test';
|
|
2
6
|
|
|
3
7
|
import {
|
|
8
|
+
AUTH_OAUTH_CANCELLATION_REASONS,
|
|
9
|
+
AUTH_OAUTH_ERROR_CODES,
|
|
4
10
|
AUTH_OAUTH_PROVIDER_IDS,
|
|
5
11
|
type AuthAdapter,
|
|
6
12
|
type AuthOAuthConfig,
|
|
13
|
+
type AuthSession,
|
|
7
14
|
type AuthSpec,
|
|
8
15
|
} from './index';
|
|
9
16
|
|
|
17
|
+
const session: AuthSession = {
|
|
18
|
+
accessToken: 'access-token',
|
|
19
|
+
refreshToken: 'refresh-token',
|
|
20
|
+
user: {
|
|
21
|
+
id: 'user-1',
|
|
22
|
+
email: 'person@example.com',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function createBaseAdapter(): Omit<AuthAdapter, 'oauth'> {
|
|
27
|
+
return {
|
|
28
|
+
capabilities: {
|
|
29
|
+
signInIdentifiers: ['email'],
|
|
30
|
+
supportsSignUp: true,
|
|
31
|
+
supportsPasswordReset: true,
|
|
32
|
+
supportsOtp: true,
|
|
33
|
+
supportsSessionRefresh: true,
|
|
34
|
+
},
|
|
35
|
+
signIn() {
|
|
36
|
+
return Promise.resolve({
|
|
37
|
+
ok: false,
|
|
38
|
+
error: { code: 'unsupported', message: 'Password sign-in is not implemented.' },
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
signUp() {
|
|
42
|
+
return Promise.resolve({
|
|
43
|
+
ok: false,
|
|
44
|
+
error: { code: 'unsupported', message: 'Sign-up is not implemented.' },
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
signOut() {
|
|
48
|
+
return Promise.resolve({ ok: true });
|
|
49
|
+
},
|
|
50
|
+
getSession() {
|
|
51
|
+
return Promise.resolve({ ok: true, data: null });
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
10
56
|
describe('OAuth auth contracts', () => {
|
|
11
57
|
it('accepts provider-neutral OAuth provider config on auth specs', () => {
|
|
12
58
|
const oauth: AuthOAuthConfig = {
|
|
@@ -27,7 +73,6 @@ describe('OAuth auth contracts', () => {
|
|
|
27
73
|
id: 'custom-sso',
|
|
28
74
|
label: 'Custom SSO',
|
|
29
75
|
enabled: false,
|
|
30
|
-
redirectTo: '/auth/custom/callback',
|
|
31
76
|
queryParams: {
|
|
32
77
|
prompt: 'select_account',
|
|
33
78
|
},
|
|
@@ -38,7 +83,6 @@ describe('OAuth auth contracts', () => {
|
|
|
38
83
|
const auth: AuthSpec = {
|
|
39
84
|
scope: 'global',
|
|
40
85
|
provider: 'supabase',
|
|
41
|
-
authorization: { kind: 'RBAC', engine: 'native' },
|
|
42
86
|
oauth,
|
|
43
87
|
};
|
|
44
88
|
|
|
@@ -48,56 +92,157 @@ describe('OAuth auth contracts', () => {
|
|
|
48
92
|
expect(auth.oauth?.providers[1]?.enabled).toBe(false);
|
|
49
93
|
});
|
|
50
94
|
|
|
51
|
-
it('
|
|
95
|
+
it('advertises OAuth only through one complete start and completion capability', async () => {
|
|
52
96
|
const adapter: AuthAdapter = {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
...createBaseAdapter(),
|
|
98
|
+
oauth: {
|
|
99
|
+
capabilities: {
|
|
100
|
+
providers: ['google', 'custom-sso'],
|
|
101
|
+
},
|
|
102
|
+
startAuthorization(input) {
|
|
103
|
+
return Promise.resolve({
|
|
104
|
+
ok: true,
|
|
105
|
+
data: {
|
|
106
|
+
attemptId: 'oauth-attempt-1',
|
|
107
|
+
provider: input.provider,
|
|
108
|
+
authorizationUrl: `https://auth.example.com/oauth/${input.provider}`,
|
|
109
|
+
redirectUri: input.redirectUri,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
completeAuthorization(input) {
|
|
114
|
+
if (input.response.type === 'cancelled') {
|
|
115
|
+
return Promise.resolve({
|
|
116
|
+
ok: false,
|
|
117
|
+
status: 'cancelled',
|
|
118
|
+
provider: 'google',
|
|
119
|
+
reason: input.response.reason,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (input.response.type === 'error') {
|
|
124
|
+
return Promise.resolve({
|
|
125
|
+
ok: false,
|
|
126
|
+
status: 'error',
|
|
127
|
+
error: {
|
|
128
|
+
code: 'authorization_failed',
|
|
129
|
+
message: input.response.error.message,
|
|
130
|
+
stage: 'transport',
|
|
131
|
+
provider: 'google',
|
|
132
|
+
recoverable: true,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return Promise.resolve({
|
|
138
|
+
ok: true,
|
|
139
|
+
status: 'authenticated',
|
|
140
|
+
provider: 'google',
|
|
141
|
+
session,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
76
144
|
},
|
|
77
|
-
|
|
78
|
-
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const started = await adapter.oauth?.startAuthorization({
|
|
148
|
+
provider: 'google',
|
|
149
|
+
redirectUri: 'ankh-app://auth/callback',
|
|
150
|
+
scopes: ['openid', 'email', 'profile'],
|
|
151
|
+
queryParams: { prompt: 'select_account' },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(adapter.oauth?.capabilities.providers).toEqual(['google', 'custom-sso']);
|
|
155
|
+
expect(started?.ok).toBe(true);
|
|
156
|
+
expect(started?.ok === true ? started.data.attemptId : undefined).toBe('oauth-attempt-1');
|
|
157
|
+
expect(started?.ok === true ? started.data.redirectUri : undefined).toBe(
|
|
158
|
+
'ankh-app://auth/callback',
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const completed = await adapter.oauth?.completeAuthorization({
|
|
162
|
+
attemptId: 'oauth-attempt-1',
|
|
163
|
+
response: {
|
|
164
|
+
type: 'callback',
|
|
165
|
+
url: 'ankh-app://auth/callback?code=opaque-code&state=opaque-state',
|
|
79
166
|
},
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(completed?.status).toBe('authenticated');
|
|
170
|
+
expect(completed?.ok === true ? completed.session : undefined).toEqual(session);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('models user cancellation separately from OAuth failures', async () => {
|
|
174
|
+
const adapter: AuthAdapter = {
|
|
175
|
+
...createBaseAdapter(),
|
|
176
|
+
oauth: {
|
|
177
|
+
capabilities: { providers: ['google'] },
|
|
178
|
+
startAuthorization(input) {
|
|
179
|
+
return Promise.resolve({
|
|
180
|
+
ok: true,
|
|
181
|
+
data: {
|
|
182
|
+
attemptId: 'oauth-attempt-2',
|
|
183
|
+
provider: input.provider,
|
|
184
|
+
authorizationUrl: 'https://auth.example.com/oauth/google',
|
|
185
|
+
redirectUri: input.redirectUri,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
completeAuthorization(input) {
|
|
190
|
+
if (input.response.type === 'cancelled') {
|
|
191
|
+
return Promise.resolve({
|
|
192
|
+
ok: false,
|
|
193
|
+
status: 'cancelled',
|
|
194
|
+
provider: 'google',
|
|
195
|
+
reason: input.response.reason,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return Promise.resolve({
|
|
200
|
+
ok: false,
|
|
201
|
+
status: 'error',
|
|
202
|
+
error: {
|
|
203
|
+
code: 'invalid_callback',
|
|
204
|
+
message: 'The OAuth callback is invalid.',
|
|
205
|
+
stage: 'callback',
|
|
206
|
+
provider: 'google',
|
|
207
|
+
recoverable: true,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
},
|
|
88
211
|
},
|
|
89
212
|
};
|
|
90
213
|
|
|
91
|
-
const
|
|
214
|
+
const cancelled = await adapter.oauth?.completeAuthorization({
|
|
215
|
+
attemptId: 'oauth-attempt-2',
|
|
216
|
+
response: { type: 'cancelled', reason: 'browser_dismissed' },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(AUTH_OAUTH_CANCELLATION_REASONS).toContain('provider_denied');
|
|
220
|
+
expect(AUTH_OAUTH_ERROR_CODES).toContain('pkce_mismatch');
|
|
221
|
+
expect(cancelled).toEqual({
|
|
222
|
+
ok: false,
|
|
223
|
+
status: 'cancelled',
|
|
92
224
|
provider: 'google',
|
|
93
|
-
|
|
94
|
-
scopes: ['openid', 'email', 'profile'],
|
|
225
|
+
reason: 'browser_dismissed',
|
|
95
226
|
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('removes the superseded URL-only OAuth adapter surface', () => {
|
|
230
|
+
const source = readFileSync(
|
|
231
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), 'auth.ts'),
|
|
232
|
+
'utf8',
|
|
233
|
+
);
|
|
234
|
+
const removedSymbols = [
|
|
235
|
+
'SignInWith' + 'OAuthInput',
|
|
236
|
+
'AuthOAuth' + 'Redirect',
|
|
237
|
+
'CompleteOAuth' + 'SignInInput',
|
|
238
|
+
'signInWith' + 'OAuth',
|
|
239
|
+
'completeOAuth' + 'SignIn',
|
|
240
|
+
'supports' + 'OAuth',
|
|
241
|
+
'oauth' + 'Providers',
|
|
242
|
+
];
|
|
96
243
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
expect(result?.ok === true ? result.data?.provider : undefined).toBe('google');
|
|
101
|
-
expect(result?.ok === true ? result.data?.url : undefined).toContain('/oauth/google');
|
|
244
|
+
for (const symbol of removedSymbols) {
|
|
245
|
+
expect(source).not.toContain(symbol);
|
|
246
|
+
}
|
|
102
247
|
});
|
|
103
248
|
});
|
package/src/auth.ts
CHANGED
|
@@ -80,7 +80,6 @@ export interface AuthOAuthProviderConfig {
|
|
|
80
80
|
label?: string;
|
|
81
81
|
enabled?: boolean;
|
|
82
82
|
scopes?: string[];
|
|
83
|
-
redirectTo?: string;
|
|
84
83
|
queryParams?: Record<string, string>;
|
|
85
84
|
icon?: IconSpec;
|
|
86
85
|
/** Logical server-side secret reference; raw credentials must never be stored here. */
|
|
@@ -172,22 +171,139 @@ export interface VerifyOtpInput {
|
|
|
172
171
|
metadata?: Record<string, unknown>;
|
|
173
172
|
}
|
|
174
173
|
|
|
175
|
-
export
|
|
174
|
+
export const AUTH_OAUTH_ERROR_STAGES = [
|
|
175
|
+
'start',
|
|
176
|
+
'transport',
|
|
177
|
+
'callback',
|
|
178
|
+
'exchange',
|
|
179
|
+
'session',
|
|
180
|
+
'profile',
|
|
181
|
+
] as const;
|
|
182
|
+
export type AuthOAuthErrorStage = (typeof AUTH_OAUTH_ERROR_STAGES)[number];
|
|
183
|
+
|
|
184
|
+
export const AUTH_OAUTH_ERROR_CODES = [
|
|
185
|
+
'oauth_unavailable',
|
|
186
|
+
'provider_disabled',
|
|
187
|
+
'provider_misconfigured',
|
|
188
|
+
'invalid_redirect_uri',
|
|
189
|
+
'authorization_failed',
|
|
190
|
+
'authorization_attempt_not_found',
|
|
191
|
+
'invalid_callback',
|
|
192
|
+
'state_mismatch',
|
|
193
|
+
'pkce_mismatch',
|
|
194
|
+
'callback_already_completed',
|
|
195
|
+
'code_exchange_failed',
|
|
196
|
+
'network_error',
|
|
197
|
+
'session_persistence_failed',
|
|
198
|
+
'profile_creation_failed',
|
|
199
|
+
'provider_error',
|
|
200
|
+
] as const;
|
|
201
|
+
export type AuthOAuthErrorCode = (typeof AUTH_OAUTH_ERROR_CODES)[number];
|
|
202
|
+
|
|
203
|
+
export interface AuthOAuthError extends AuthAdapterError {
|
|
204
|
+
code: AuthOAuthErrorCode;
|
|
205
|
+
stage: AuthOAuthErrorStage;
|
|
206
|
+
provider?: AuthOAuthProviderId;
|
|
207
|
+
recoverable: boolean;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const AUTH_OAUTH_TRANSPORT_CANCELLATION_REASONS = [
|
|
211
|
+
'user_cancelled',
|
|
212
|
+
'browser_dismissed',
|
|
213
|
+
] as const;
|
|
214
|
+
export type AuthOAuthTransportCancellationReason =
|
|
215
|
+
(typeof AUTH_OAUTH_TRANSPORT_CANCELLATION_REASONS)[number];
|
|
216
|
+
|
|
217
|
+
export const AUTH_OAUTH_CANCELLATION_REASONS = [
|
|
218
|
+
...AUTH_OAUTH_TRANSPORT_CANCELLATION_REASONS,
|
|
219
|
+
'provider_denied',
|
|
220
|
+
] as const;
|
|
221
|
+
export type AuthOAuthCancellationReason = (typeof AUTH_OAUTH_CANCELLATION_REASONS)[number];
|
|
222
|
+
|
|
223
|
+
export const AUTH_OAUTH_TRANSPORT_ERROR_CODES = [
|
|
224
|
+
'browser_unavailable',
|
|
225
|
+
'transport_failed',
|
|
226
|
+
] as const;
|
|
227
|
+
export type AuthOAuthTransportErrorCode = (typeof AUTH_OAUTH_TRANSPORT_ERROR_CODES)[number];
|
|
228
|
+
|
|
229
|
+
export interface AuthOAuthTransportError {
|
|
230
|
+
code: AuthOAuthTransportErrorCode;
|
|
231
|
+
message: string;
|
|
232
|
+
cause?: unknown;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface StartOAuthAuthorizationInput {
|
|
176
236
|
provider: AuthOAuthProviderId;
|
|
177
|
-
|
|
178
|
-
scopes?: string[];
|
|
179
|
-
queryParams?: Record<string, string
|
|
180
|
-
metadata?: Record<string, unknown>;
|
|
237
|
+
redirectUri: string;
|
|
238
|
+
scopes?: readonly string[];
|
|
239
|
+
queryParams?: Readonly<Record<string, string>>;
|
|
181
240
|
}
|
|
182
241
|
|
|
183
|
-
export interface
|
|
242
|
+
export interface AuthOAuthAuthorizationRequest {
|
|
243
|
+
attemptId: string;
|
|
184
244
|
provider: AuthOAuthProviderId;
|
|
185
|
-
|
|
245
|
+
authorizationUrl: string;
|
|
246
|
+
redirectUri: string;
|
|
186
247
|
}
|
|
187
248
|
|
|
188
|
-
export
|
|
189
|
-
|
|
190
|
-
|
|
249
|
+
export type AuthOAuthStartResult =
|
|
250
|
+
| {
|
|
251
|
+
ok: true;
|
|
252
|
+
data: AuthOAuthAuthorizationRequest;
|
|
253
|
+
}
|
|
254
|
+
| {
|
|
255
|
+
ok: false;
|
|
256
|
+
error: AuthOAuthError;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export type AuthOAuthAuthorizationResponse =
|
|
260
|
+
| {
|
|
261
|
+
type: 'callback';
|
|
262
|
+
url: string;
|
|
263
|
+
}
|
|
264
|
+
| {
|
|
265
|
+
type: 'cancelled';
|
|
266
|
+
reason: AuthOAuthTransportCancellationReason;
|
|
267
|
+
}
|
|
268
|
+
| {
|
|
269
|
+
type: 'error';
|
|
270
|
+
error: AuthOAuthTransportError;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export interface CompleteOAuthAuthorizationInput {
|
|
274
|
+
attemptId: string;
|
|
275
|
+
response: AuthOAuthAuthorizationResponse;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export type AuthOAuthCompletionResult =
|
|
279
|
+
| {
|
|
280
|
+
ok: true;
|
|
281
|
+
status: 'authenticated';
|
|
282
|
+
provider: AuthOAuthProviderId;
|
|
283
|
+
session: AuthSession;
|
|
284
|
+
}
|
|
285
|
+
| {
|
|
286
|
+
ok: false;
|
|
287
|
+
status: 'cancelled';
|
|
288
|
+
provider: AuthOAuthProviderId;
|
|
289
|
+
reason: AuthOAuthCancellationReason;
|
|
290
|
+
}
|
|
291
|
+
| {
|
|
292
|
+
ok: false;
|
|
293
|
+
status: 'error';
|
|
294
|
+
error: AuthOAuthError;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export interface AuthOAuthCapabilities {
|
|
298
|
+
/** Enabled providers for which start and callback completion are operational. */
|
|
299
|
+
providers: readonly [AuthOAuthProviderId, ...AuthOAuthProviderId[]];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface AuthOAuthAdapter {
|
|
303
|
+
readonly capabilities: AuthOAuthCapabilities;
|
|
304
|
+
|
|
305
|
+
startAuthorization(input: StartOAuthAuthorizationInput): Promise<AuthOAuthStartResult>;
|
|
306
|
+
completeAuthorization(input: CompleteOAuthAuthorizationInput): Promise<AuthOAuthCompletionResult>;
|
|
191
307
|
}
|
|
192
308
|
|
|
193
309
|
export interface AuthAdapterCapabilities {
|
|
@@ -196,12 +312,15 @@ export interface AuthAdapterCapabilities {
|
|
|
196
312
|
supportsPasswordReset: boolean;
|
|
197
313
|
supportsOtp: boolean;
|
|
198
314
|
supportsSessionRefresh: boolean;
|
|
199
|
-
supportsOAuth?: boolean;
|
|
200
|
-
oauthProviders?: AuthOAuthProviderId[];
|
|
201
315
|
}
|
|
202
316
|
|
|
203
317
|
export interface AuthAdapter {
|
|
204
318
|
readonly capabilities?: AuthAdapterCapabilities;
|
|
319
|
+
/**
|
|
320
|
+
* Presence is the canonical OAuth capability signal. When present, both authorization start and
|
|
321
|
+
* callback completion are mandatory and operational for every advertised provider.
|
|
322
|
+
*/
|
|
323
|
+
readonly oauth?: AuthOAuthAdapter;
|
|
205
324
|
|
|
206
325
|
signIn(input: SignInInput): Promise<AuthResult<AuthSession>>;
|
|
207
326
|
signUp(input: SignUpInput): Promise<AuthResult<AuthSession | AuthUser>>;
|
|
@@ -212,7 +331,4 @@ export interface AuthAdapter {
|
|
|
212
331
|
|
|
213
332
|
requestPasswordReset?(input: PasswordResetInput): Promise<AuthResult>;
|
|
214
333
|
verifyOtp?(input: VerifyOtpInput): Promise<AuthResult<AuthSession>>;
|
|
215
|
-
|
|
216
|
-
signInWithOAuth?(input: SignInWithOAuthInput): Promise<AuthResult<AuthOAuthRedirect>>;
|
|
217
|
-
completeOAuthSignIn?(input: CompleteOAuthSignInInput): Promise<AuthResult<AuthSession>>;
|
|
218
334
|
}
|