@ankhorage/studio 2.0.2 → 2.0.3
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/dist/host/layout/auth/resolveAuthLayoutPlan.d.ts +1 -1
- package/dist/host/layout/auth/resolveAuthLayoutPlan.d.ts.map +1 -1
- package/dist/host/layout/auth/resolveAuthLayoutPlan.js +24 -1
- package/dist/host/layout/auth/resolveAuthLayoutPlan.js.map +1 -1
- package/dist/host/layout/layoutGenerator.d.ts.map +1 -1
- package/dist/host/layout/layoutGenerator.js +38 -13
- package/dist/host/layout/layoutGenerator.js.map +1 -1
- package/dist/host/layout/templates/auth/callback.d.ts.map +1 -1
- package/dist/host/layout/templates/auth/callback.js +78 -37
- package/dist/host/layout/templates/auth/callback.js.map +1 -1
- package/dist/host/layout/templates/auth/form.d.ts +2 -0
- package/dist/host/layout/templates/auth/form.d.ts.map +1 -0
- package/dist/host/layout/templates/auth/form.js +238 -0
- package/dist/host/layout/templates/auth/form.js.map +1 -0
- package/dist/host/layout/templates/auth/navigation.d.ts +10 -0
- package/dist/host/layout/templates/auth/navigation.d.ts.map +1 -0
- package/dist/host/layout/templates/auth/navigation.js +183 -0
- package/dist/host/layout/templates/auth/navigation.js.map +1 -0
- package/dist/host/layout/templates/auth/oauth.d.ts.map +1 -1
- package/dist/host/layout/templates/auth/oauth.js +112 -196
- package/dist/host/layout/templates/auth/oauth.js.map +1 -1
- package/dist/host/layout/templates/auth/oauthCompletion.d.ts +6 -0
- package/dist/host/layout/templates/auth/oauthCompletion.d.ts.map +1 -0
- package/dist/host/layout/templates/auth/oauthCompletion.js +147 -0
- package/dist/host/layout/templates/auth/oauthCompletion.js.map +1 -0
- package/dist/host/layout/templates/auth/oauthState.d.ts +2 -0
- package/dist/host/layout/templates/auth/oauthState.d.ts.map +1 -0
- package/dist/host/layout/templates/auth/oauthState.js +45 -0
- package/dist/host/layout/templates/auth/oauthState.js.map +1 -0
- package/dist/host/layout/templates/auth/screen.d.ts +5 -2
- package/dist/host/layout/templates/auth/screen.d.ts.map +1 -1
- package/dist/host/layout/templates/auth/screen.js +295 -529
- package/dist/host/layout/templates/auth/screen.js.map +1 -1
- package/dist/host/layout/templates/auth/session.js +5 -5
- package/dist/host/layout/templates/auth/signOut.d.ts.map +1 -1
- package/dist/host/layout/templates/auth/signOut.js +10 -9
- package/dist/host/layout/templates/auth/signOut.js.map +1 -1
- package/dist/host/layout/templates/index.d.ts +5 -1
- package/dist/host/layout/templates/index.d.ts.map +1 -1
- package/dist/host/layout/templates/index.js +5 -1
- package/dist/host/layout/templates/index.js.map +1 -1
- package/dist/host/layout/templates/rootLayout.d.ts.map +1 -1
- package/dist/host/layout/templates/rootLayout.js +8 -147
- package/dist/host/layout/templates/rootLayout.js.map +1 -1
- package/dist/host/orchestrator/templates.d.ts.map +1 -1
- package/dist/host/orchestrator/templates.js +8 -11
- package/dist/host/orchestrator/templates.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { escapeStringLiteral } from '../../utils/escapeStringLiteral';
|
|
2
2
|
export function getAuthOAuthRuntimeTs(args) {
|
|
3
|
-
const callbackRoute = escapeStringLiteral(args.callbackRoute);
|
|
4
3
|
const providers = serializeOAuthProviders(args.providers);
|
|
5
|
-
const androidScheme = args.nativeSchemes.android === undefined
|
|
6
|
-
? 'undefined'
|
|
7
|
-
: `'${escapeStringLiteral(args.nativeSchemes.android)}'`;
|
|
8
|
-
const iosScheme = args.nativeSchemes.ios === undefined
|
|
9
|
-
? 'undefined'
|
|
10
|
-
: `'${escapeStringLiteral(args.nativeSchemes.ios)}'`;
|
|
11
4
|
return `import type {
|
|
12
5
|
AuthOAuthCompletionResult,
|
|
13
6
|
AuthOAuthTransportCancellationReason,
|
|
@@ -17,20 +10,19 @@ import {
|
|
|
17
10
|
resolveExpoOAuthBrowserResult,
|
|
18
11
|
} from '@ankhorage/expo-runtime/oauth-browser';
|
|
19
12
|
import { resolveExpoOAuthBrowserRuntimeReadiness } from '@ankhorage/expo-runtime/oauth-browser-runtime';
|
|
20
|
-
import
|
|
13
|
+
import type { OAuthProviderIconSpec } from '@ankhorage/zora';
|
|
21
14
|
import * as WebBrowser from 'expo-web-browser';
|
|
22
15
|
import { Platform } from 'react-native';
|
|
23
16
|
|
|
24
17
|
import { authAdapter } from './adapter';
|
|
25
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
completeOAuthCallback,
|
|
20
|
+
resolveOAuthCallbackUrl,
|
|
21
|
+
resolveOAuthRedirectUri,
|
|
22
|
+
} from './oauth-completion';
|
|
23
|
+
import { clearTransportAttempt, writeTransportAttempt } from './oauth-state';
|
|
26
24
|
|
|
27
|
-
const
|
|
28
|
-
const OAUTH_TRANSPORT_ATTEMPT_KEY = 'ankh.auth.oauth.transport';
|
|
29
|
-
const GENERATED_OAUTH_PROVIDERS = ${providers} as const;
|
|
30
|
-
const GENERATED_NATIVE_SCHEMES = {
|
|
31
|
-
android: ${androidScheme},
|
|
32
|
-
ios: ${iosScheme},
|
|
33
|
-
} as const;
|
|
25
|
+
const GENERATED_OAUTH_PROVIDERS: readonly GeneratedOAuthProvider[] = ${providers};
|
|
34
26
|
|
|
35
27
|
export const generatedOAuthProviderItems = GENERATED_OAUTH_PROVIDERS.map((provider) => ({
|
|
36
28
|
id: provider.id,
|
|
@@ -39,18 +31,36 @@ export const generatedOAuthProviderItems = GENERATED_OAUTH_PROVIDERS.map((provid
|
|
|
39
31
|
}));
|
|
40
32
|
|
|
41
33
|
export type GeneratedOAuthTransportOutcome =
|
|
42
|
-
| { status: 'authenticated' }
|
|
34
|
+
| { status: 'authenticated'; completion: 'fresh' | 'already-completed' }
|
|
43
35
|
| { status: 'cancelled'; message: string }
|
|
44
36
|
| { status: 'error'; message: string; recoverable: boolean };
|
|
45
37
|
|
|
46
|
-
interface
|
|
47
|
-
|
|
38
|
+
interface GeneratedOAuthProvider {
|
|
39
|
+
readonly id: 'google' | 'apple';
|
|
40
|
+
readonly label: string;
|
|
41
|
+
readonly scopes: readonly string[];
|
|
42
|
+
readonly queryParams: Readonly<Record<string, string>>;
|
|
43
|
+
readonly icon?: OAuthProviderIconSpec;
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
export
|
|
46
|
+
export { completeOAuthCallback, resolveOAuthCallbackUrl };
|
|
51
47
|
|
|
52
48
|
let activeAuthorization: Promise<GeneratedOAuthTransportOutcome> | null = null;
|
|
53
49
|
|
|
50
|
+
type GeneratedOAuthAdapter = NonNullable<(typeof authAdapter)['oauth']>;
|
|
51
|
+
|
|
52
|
+
interface OAuthAuthorizationContext {
|
|
53
|
+
oauth: GeneratedOAuthAdapter;
|
|
54
|
+
provider: GeneratedOAuthProvider;
|
|
55
|
+
redirectUri: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface StartedOAuthAuthorization {
|
|
59
|
+
attemptId: string;
|
|
60
|
+
authorizationUrl: string;
|
|
61
|
+
redirectUri: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
export function startOAuthAuthorization(
|
|
55
65
|
providerId: string,
|
|
56
66
|
): Promise<GeneratedOAuthTransportOutcome> {
|
|
@@ -61,86 +71,119 @@ export function startOAuthAuthorization(
|
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
async function runOAuthAuthorization(providerId: string): Promise<GeneratedOAuthTransportOutcome> {
|
|
64
|
-
const
|
|
74
|
+
const resolved = resolveAuthorizationContext(providerId);
|
|
75
|
+
if ('outcome' in resolved) return resolved.outcome;
|
|
76
|
+
const started = await startAuthorizationAttemptAsync(resolved.context);
|
|
77
|
+
if ('outcome' in started) return started.outcome;
|
|
78
|
+
return completeAuthorizationTransportAsync(resolved.context.oauth, started.authorization);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveAuthorizationContext(
|
|
82
|
+
providerId: string,
|
|
83
|
+
): { context: OAuthAuthorizationContext } | { outcome: GeneratedOAuthTransportOutcome } {
|
|
84
|
+
const { oauth } = authAdapter;
|
|
65
85
|
if (!oauth) {
|
|
66
86
|
return {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
outcome: {
|
|
88
|
+
status: 'error',
|
|
89
|
+
message: 'OAuth is not available in this app configuration.',
|
|
90
|
+
recoverable: true,
|
|
91
|
+
},
|
|
70
92
|
};
|
|
71
93
|
}
|
|
72
94
|
|
|
73
95
|
const provider = GENERATED_OAUTH_PROVIDERS.find((entry) => entry.id === providerId);
|
|
74
96
|
if (!provider) {
|
|
75
97
|
return {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
outcome: {
|
|
99
|
+
status: 'error',
|
|
100
|
+
message: 'This OAuth provider is not enabled.',
|
|
101
|
+
recoverable: true,
|
|
102
|
+
},
|
|
79
103
|
};
|
|
80
104
|
}
|
|
81
105
|
|
|
82
|
-
let redirectUri: string;
|
|
83
106
|
try {
|
|
84
|
-
redirectUri
|
|
107
|
+
return { context: { oauth, provider, redirectUri: resolveOAuthRedirectUri() } };
|
|
85
108
|
} catch {
|
|
86
109
|
return {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
outcome: {
|
|
111
|
+
status: 'error',
|
|
112
|
+
message: 'The OAuth redirect URI could not be resolved in this environment.',
|
|
113
|
+
recoverable: true,
|
|
114
|
+
},
|
|
90
115
|
};
|
|
91
116
|
}
|
|
117
|
+
}
|
|
92
118
|
|
|
119
|
+
async function startAuthorizationAttemptAsync(
|
|
120
|
+
context: OAuthAuthorizationContext,
|
|
121
|
+
): Promise<
|
|
122
|
+
{ authorization: StartedOAuthAuthorization } | { outcome: GeneratedOAuthTransportOutcome }
|
|
123
|
+
> {
|
|
93
124
|
if (Platform.OS !== 'web') {
|
|
94
125
|
const runtimeReadiness = resolveExpoOAuthBrowserRuntimeReadiness();
|
|
95
126
|
if (runtimeReadiness.status !== 'ready') {
|
|
96
127
|
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
128
|
+
outcome: {
|
|
129
|
+
status: 'error',
|
|
130
|
+
message: runtimeReadiness.message,
|
|
131
|
+
recoverable: true,
|
|
132
|
+
},
|
|
100
133
|
};
|
|
101
134
|
}
|
|
102
135
|
}
|
|
103
136
|
|
|
104
|
-
const started = await oauth.startAuthorization({
|
|
105
|
-
provider: provider.id,
|
|
106
|
-
redirectUri,
|
|
107
|
-
scopes: provider.scopes,
|
|
108
|
-
queryParams: provider.queryParams,
|
|
137
|
+
const started = await context.oauth.startAuthorization({
|
|
138
|
+
provider: context.provider.id,
|
|
139
|
+
redirectUri: context.redirectUri,
|
|
140
|
+
scopes: [...context.provider.scopes],
|
|
141
|
+
queryParams: { ...context.provider.queryParams },
|
|
109
142
|
});
|
|
110
143
|
if (!started.ok) {
|
|
111
144
|
return {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
outcome: {
|
|
146
|
+
status: 'error',
|
|
147
|
+
message: started.error.message,
|
|
148
|
+
recoverable: started.error.recoverable,
|
|
149
|
+
},
|
|
115
150
|
};
|
|
116
151
|
}
|
|
117
152
|
|
|
118
153
|
try {
|
|
119
|
-
await writeTransportAttempt({
|
|
120
|
-
attemptId: started.data.attemptId,
|
|
121
|
-
});
|
|
154
|
+
await writeTransportAttempt({ attemptId: started.data.attemptId });
|
|
122
155
|
} catch {
|
|
123
|
-
await cancelOAuthAttempt(started.data.attemptId, 'user_cancelled');
|
|
156
|
+
await cancelOAuthAttempt(context.oauth, started.data.attemptId, 'user_cancelled');
|
|
124
157
|
await clearTransportAttempt();
|
|
125
158
|
return {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
outcome: {
|
|
160
|
+
status: 'error',
|
|
161
|
+
message: 'The OAuth authorization attempt could not be persisted.',
|
|
162
|
+
recoverable: true,
|
|
163
|
+
},
|
|
129
164
|
};
|
|
130
165
|
}
|
|
131
166
|
|
|
167
|
+
return { authorization: started.data };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function completeAuthorizationTransportAsync(
|
|
171
|
+
oauth: GeneratedOAuthAdapter,
|
|
172
|
+
started: StartedOAuthAuthorization,
|
|
173
|
+
): Promise<GeneratedOAuthTransportOutcome> {
|
|
132
174
|
if (Platform.OS === 'web') {
|
|
133
175
|
return redirectWebAuthorization({
|
|
134
|
-
attemptId: started.
|
|
135
|
-
authorizationUrl: started.
|
|
176
|
+
attemptId: started.attemptId,
|
|
177
|
+
authorizationUrl: started.authorizationUrl,
|
|
178
|
+
oauth,
|
|
136
179
|
});
|
|
137
180
|
}
|
|
138
181
|
|
|
139
182
|
let browserResponse;
|
|
140
183
|
try {
|
|
141
184
|
const browserResult = await WebBrowser.openAuthSessionAsync(
|
|
142
|
-
started.
|
|
143
|
-
started.
|
|
185
|
+
started.authorizationUrl,
|
|
186
|
+
started.redirectUri,
|
|
144
187
|
);
|
|
145
188
|
browserResponse = resolveExpoOAuthBrowserResult(browserResult);
|
|
146
189
|
} catch {
|
|
@@ -154,7 +197,7 @@ async function runOAuthAuthorization(providerId: string): Promise<GeneratedOAuth
|
|
|
154
197
|
let completed: AuthOAuthCompletionResult;
|
|
155
198
|
try {
|
|
156
199
|
completed = await oauth.completeAuthorization({
|
|
157
|
-
attemptId: started.
|
|
200
|
+
attemptId: started.attemptId,
|
|
158
201
|
response: browserResponse,
|
|
159
202
|
});
|
|
160
203
|
} catch {
|
|
@@ -172,11 +215,11 @@ async function runOAuthAuthorization(providerId: string): Promise<GeneratedOAuth
|
|
|
172
215
|
async function redirectWebAuthorization(args: {
|
|
173
216
|
attemptId: string;
|
|
174
217
|
authorizationUrl: string;
|
|
218
|
+
oauth: GeneratedOAuthAdapter;
|
|
175
219
|
}): Promise<GeneratedOAuthTransportOutcome> {
|
|
176
220
|
const location = getBrowserLocation();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
await cancelOAuthAttempt(args.attemptId, 'browser_dismissed');
|
|
221
|
+
if (!hasBrowserLocationAssign(location)) {
|
|
222
|
+
await cancelOAuthAttempt(args.oauth, args.attemptId, 'browser_dismissed');
|
|
180
223
|
await clearTransportAttempt();
|
|
181
224
|
return {
|
|
182
225
|
status: 'error',
|
|
@@ -186,10 +229,10 @@ async function redirectWebAuthorization(args: {
|
|
|
186
229
|
}
|
|
187
230
|
|
|
188
231
|
try {
|
|
189
|
-
|
|
232
|
+
location.assign(args.authorizationUrl);
|
|
190
233
|
return waitForFullPageNavigation();
|
|
191
234
|
} catch {
|
|
192
|
-
await cancelOAuthAttempt(args.attemptId, 'browser_dismissed');
|
|
235
|
+
await cancelOAuthAttempt(args.oauth, args.attemptId, 'browser_dismissed');
|
|
193
236
|
await clearTransportAttempt();
|
|
194
237
|
return {
|
|
195
238
|
status: 'error',
|
|
@@ -205,112 +248,20 @@ function waitForFullPageNavigation(): Promise<never> {
|
|
|
205
248
|
});
|
|
206
249
|
}
|
|
207
250
|
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
if (name === '#') continue;
|
|
212
|
-
if (Array.isArray(value)) {
|
|
213
|
-
for (const item of value) callbackUrl.searchParams.append(name, item);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
if (typeof value === 'string') callbackUrl.searchParams.append(name, value);
|
|
217
|
-
}
|
|
218
|
-
return callbackUrl.toString();
|
|
251
|
+
function getBrowserLocation(): Readonly<Record<string, unknown>> | null {
|
|
252
|
+
const location: unknown = Reflect.get(globalThis, 'location');
|
|
253
|
+
return isRecord(location) ? location : null;
|
|
219
254
|
}
|
|
220
255
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
):
|
|
224
|
-
|
|
225
|
-
if (!oauth) {
|
|
226
|
-
return {
|
|
227
|
-
status: 'error',
|
|
228
|
-
message: 'OAuth is not available in this app configuration.',
|
|
229
|
-
recoverable: true,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const attempt = await readTransportAttempt();
|
|
234
|
-
if (!attempt) {
|
|
235
|
-
return {
|
|
236
|
-
status: 'error',
|
|
237
|
-
message: 'The OAuth authorization attempt was not found or has expired.',
|
|
238
|
-
recoverable: true,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
let completed: AuthOAuthCompletionResult;
|
|
243
|
-
try {
|
|
244
|
-
completed = await oauth.completeAuthorization({
|
|
245
|
-
attemptId: attempt.attemptId,
|
|
246
|
-
response: { type: 'callback', url: callbackUrl },
|
|
247
|
-
});
|
|
248
|
-
} catch {
|
|
249
|
-
await clearTransportAttempt();
|
|
250
|
-
return {
|
|
251
|
-
status: 'error',
|
|
252
|
-
message: 'The OAuth callback could not be completed.',
|
|
253
|
-
recoverable: true,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const storedSession = getStoredAuthSession();
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
!completed.ok &&
|
|
261
|
-
completed.status === 'error' &&
|
|
262
|
-
completed.error.code === 'callback_already_completed' &&
|
|
263
|
-
storedSession
|
|
264
|
-
) {
|
|
265
|
-
return { status: 'authenticated' };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const preservesCompletedAttempt =
|
|
269
|
-
completed.ok ||
|
|
270
|
-
(!completed.ok &&
|
|
271
|
-
completed.status === 'error' &&
|
|
272
|
-
completed.error.code === 'invalid_callback' &&
|
|
273
|
-
storedSession !== null);
|
|
274
|
-
if (!preservesCompletedAttempt) await clearTransportAttempt();
|
|
275
|
-
|
|
276
|
-
return toTransportOutcome(completed);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function resolveOAuthRedirectUri(): string {
|
|
280
|
-
let callbackPath = OAUTH_CALLBACK_ROUTE;
|
|
281
|
-
while (callbackPath.startsWith('/')) {
|
|
282
|
-
callbackPath = callbackPath.slice(1);
|
|
283
|
-
}
|
|
284
|
-
if (Platform.OS === 'web') {
|
|
285
|
-
const location = getBrowserLocation();
|
|
286
|
-
const origin = location ? Reflect.get(location, 'origin') : undefined;
|
|
287
|
-
if (typeof origin === 'string' && origin.length > 0) {
|
|
288
|
-
return new URL(\`/\${callbackPath}\`, origin).toString();
|
|
289
|
-
}
|
|
290
|
-
throw new Error('Web OAuth requires a canonical browser origin.');
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const nativeScheme =
|
|
294
|
-
Platform.OS === 'android'
|
|
295
|
-
? GENERATED_NATIVE_SCHEMES.android
|
|
296
|
-
: Platform.OS === 'ios'
|
|
297
|
-
? GENERATED_NATIVE_SCHEMES.ios
|
|
298
|
-
: undefined;
|
|
299
|
-
if (!nativeScheme) {
|
|
300
|
-
throw new Error('Native OAuth requires a configured application scheme.');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return Linking.createURL(callbackPath, { scheme: nativeScheme });
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function getBrowserLocation(): object | null {
|
|
307
|
-
const location = Reflect.get(globalThis, 'location');
|
|
308
|
-
return typeof location === 'object' && location !== null ? location : null;
|
|
256
|
+
function hasBrowserLocationAssign(
|
|
257
|
+
location: Readonly<Record<string, unknown>> | null,
|
|
258
|
+
): location is Readonly<Record<string, unknown>> & { assign: (url: string) => void } {
|
|
259
|
+
return location !== null && typeof location.assign === 'function';
|
|
309
260
|
}
|
|
310
261
|
|
|
311
262
|
function toTransportOutcome(result: AuthOAuthCompletionResult): GeneratedOAuthTransportOutcome {
|
|
312
263
|
if (result.ok) {
|
|
313
|
-
return { status: 'authenticated' };
|
|
264
|
+
return { status: 'authenticated', completion: 'fresh' };
|
|
314
265
|
}
|
|
315
266
|
if (result.status === 'cancelled') {
|
|
316
267
|
return {
|
|
@@ -329,11 +280,10 @@ function toTransportOutcome(result: AuthOAuthCompletionResult): GeneratedOAuthTr
|
|
|
329
280
|
}
|
|
330
281
|
|
|
331
282
|
async function cancelOAuthAttempt(
|
|
283
|
+
oauth: GeneratedOAuthAdapter,
|
|
332
284
|
attemptId: string,
|
|
333
285
|
reason: AuthOAuthTransportCancellationReason,
|
|
334
286
|
): Promise<void> {
|
|
335
|
-
const oauth = authAdapter.oauth;
|
|
336
|
-
if (!oauth) return;
|
|
337
287
|
try {
|
|
338
288
|
await oauth.completeAuthorization({
|
|
339
289
|
attemptId,
|
|
@@ -344,40 +294,6 @@ async function cancelOAuthAttempt(
|
|
|
344
294
|
}
|
|
345
295
|
}
|
|
346
296
|
|
|
347
|
-
async function writeTransportAttempt(attempt: StoredTransportAttempt): Promise<void> {
|
|
348
|
-
await authSessionStorage.setItem(OAUTH_TRANSPORT_ATTEMPT_KEY, JSON.stringify(attempt));
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async function readTransportAttempt(): Promise<StoredTransportAttempt | null> {
|
|
352
|
-
const raw = await authSessionStorage.getItem(OAUTH_TRANSPORT_ATTEMPT_KEY);
|
|
353
|
-
if (!raw) return null;
|
|
354
|
-
try {
|
|
355
|
-
const value: unknown = JSON.parse(raw);
|
|
356
|
-
if (isRecord(value)) {
|
|
357
|
-
const attemptId = Reflect.get(value, 'attemptId');
|
|
358
|
-
if (typeof attemptId === 'string' && attemptId.trim().length > 0) {
|
|
359
|
-
return { attemptId };
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
} catch {
|
|
363
|
-
// Invalid transport state is cleaned below without exposing its contents.
|
|
364
|
-
}
|
|
365
|
-
await clearTransportAttempt();
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async function clearTransportAttempt(): Promise<void> {
|
|
370
|
-
await safeRemoveTransportItem(OAUTH_TRANSPORT_ATTEMPT_KEY);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function safeRemoveTransportItem(key: string): Promise<void> {
|
|
374
|
-
try {
|
|
375
|
-
await authSessionStorage.removeItem(key);
|
|
376
|
-
} catch {
|
|
377
|
-
// Cleanup failures are intentionally not surfaced with persisted state.
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
297
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
382
298
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
383
299
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../../../../src/host/layout/templates/auth/oauth.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAMtE,MAAM,UAAU,qBAAqB,CAAC,IAAkC;IACtE,MAAM,
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../../../../src/host/layout/templates/auth/oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ExpoRuntimeNativeSchemeMap } from '@ankhorage/expo-runtime/planning';
|
|
2
|
+
export declare function getAuthOAuthCompletionTs(args: {
|
|
3
|
+
callbackRoute: string;
|
|
4
|
+
nativeSchemes: ExpoRuntimeNativeSchemeMap;
|
|
5
|
+
}): string;
|
|
6
|
+
//# sourceMappingURL=oauthCompletion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauthCompletion.d.ts","sourceRoot":"","sources":["../../../../../src/host/layout/templates/auth/oauthCompletion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAInF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,0BAA0B,CAAC;CAC3C,UA8IA"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { escapeStringLiteral } from '../../utils/escapeStringLiteral';
|
|
2
|
+
export function getAuthOAuthCompletionTs(args) {
|
|
3
|
+
const callbackRoute = escapeStringLiteral(args.callbackRoute);
|
|
4
|
+
const androidScheme = serializeOptionalScheme(args.nativeSchemes.android);
|
|
5
|
+
const iosScheme = serializeOptionalScheme(args.nativeSchemes.ios);
|
|
6
|
+
return `import type { AuthOAuthCompletionResult } from '@ankhorage/contracts/auth';
|
|
7
|
+
import * as Linking from 'expo-linking';
|
|
8
|
+
import { Platform } from 'react-native';
|
|
9
|
+
|
|
10
|
+
import { authAdapter } from './adapter';
|
|
11
|
+
import type { GeneratedOAuthTransportOutcome } from './oauth';
|
|
12
|
+
import { clearTransportAttempt, readTransportAttempt } from './oauth-state';
|
|
13
|
+
import { getStoredAuthSession } from './session';
|
|
14
|
+
|
|
15
|
+
const OAUTH_CALLBACK_ROUTE = '${callbackRoute}';
|
|
16
|
+
const GENERATED_NATIVE_SCHEMES = {
|
|
17
|
+
android: ${androidScheme},
|
|
18
|
+
ios: ${iosScheme},
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export type OAuthCallbackRouteParams = Readonly<Record<string, string | string[] | undefined>>;
|
|
22
|
+
|
|
23
|
+
export function resolveOAuthCallbackUrl(params: OAuthCallbackRouteParams): string {
|
|
24
|
+
const callbackUrl = new URL(resolveOAuthRedirectUri());
|
|
25
|
+
for (const [name, value] of Object.entries(params)) {
|
|
26
|
+
if (name === '#') continue;
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
for (const item of value) callbackUrl.searchParams.append(name, item);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'string') callbackUrl.searchParams.append(name, value);
|
|
32
|
+
}
|
|
33
|
+
return callbackUrl.toString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function completeOAuthCallback(
|
|
37
|
+
callbackUrl: string,
|
|
38
|
+
): Promise<GeneratedOAuthTransportOutcome> {
|
|
39
|
+
const { oauth } = authAdapter;
|
|
40
|
+
if (!oauth) return createUnavailableOAuthOutcome();
|
|
41
|
+
const attempt = await readTransportAttempt();
|
|
42
|
+
if (!attempt) {
|
|
43
|
+
return {
|
|
44
|
+
status: 'error',
|
|
45
|
+
message: 'The OAuth authorization attempt was not found or has expired.',
|
|
46
|
+
recoverable: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let completed: AuthOAuthCompletionResult;
|
|
51
|
+
try {
|
|
52
|
+
completed = await oauth.completeAuthorization({
|
|
53
|
+
attemptId: attempt.attemptId,
|
|
54
|
+
response: { type: 'callback', url: callbackUrl },
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
await clearTransportAttempt();
|
|
58
|
+
return {
|
|
59
|
+
status: 'error',
|
|
60
|
+
message: 'The OAuth callback could not be completed.',
|
|
61
|
+
recoverable: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return resolveCompletedOAuthOutcome(completed);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveOAuthRedirectUri(): string {
|
|
68
|
+
let callbackPath = OAUTH_CALLBACK_ROUTE;
|
|
69
|
+
while (callbackPath.startsWith('/')) callbackPath = callbackPath.slice(1);
|
|
70
|
+
if (Platform.OS === 'web') return resolveWebOAuthRedirectUri(callbackPath);
|
|
71
|
+
const nativeScheme =
|
|
72
|
+
Platform.OS === 'android'
|
|
73
|
+
? GENERATED_NATIVE_SCHEMES.android
|
|
74
|
+
: Platform.OS === 'ios'
|
|
75
|
+
? GENERATED_NATIVE_SCHEMES.ios
|
|
76
|
+
: undefined;
|
|
77
|
+
if (!nativeScheme) throw new Error('Native OAuth requires a configured application scheme.');
|
|
78
|
+
return Linking.createURL(callbackPath, { scheme: nativeScheme });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createUnavailableOAuthOutcome(): GeneratedOAuthTransportOutcome {
|
|
82
|
+
return {
|
|
83
|
+
status: 'error',
|
|
84
|
+
message: 'OAuth is not available in this app configuration.',
|
|
85
|
+
recoverable: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getBrowserLocation(): Readonly<Record<string, unknown>> | null {
|
|
90
|
+
const location: unknown = Reflect.get(globalThis, 'location');
|
|
91
|
+
return isRecord(location) ? location : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
95
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function resolveCompletedOAuthOutcome(
|
|
99
|
+
completed: AuthOAuthCompletionResult,
|
|
100
|
+
): Promise<GeneratedOAuthTransportOutcome> {
|
|
101
|
+
const hasStoredSession = getStoredAuthSession() !== null;
|
|
102
|
+
if (
|
|
103
|
+
!completed.ok &&
|
|
104
|
+
completed.status === 'error' &&
|
|
105
|
+
completed.error.code === 'callback_already_completed' &&
|
|
106
|
+
hasStoredSession
|
|
107
|
+
) {
|
|
108
|
+
return { status: 'authenticated', completion: 'already-completed' };
|
|
109
|
+
}
|
|
110
|
+
if (completed.ok) return toTransportOutcome(completed);
|
|
111
|
+
const preservesAttempt =
|
|
112
|
+
completed.status === 'error' && completed.error.code === 'invalid_callback' && hasStoredSession;
|
|
113
|
+
if (!preservesAttempt) await clearTransportAttempt();
|
|
114
|
+
return toTransportOutcome(completed);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveWebOAuthRedirectUri(callbackPath: string): string {
|
|
118
|
+
const origin = getBrowserLocation()?.origin;
|
|
119
|
+
if (typeof origin !== 'string' || origin.length === 0) {
|
|
120
|
+
throw new Error('Web OAuth requires a canonical browser origin.');
|
|
121
|
+
}
|
|
122
|
+
return new URL(\`/\${callbackPath}\`, origin).toString();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function toTransportOutcome(result: AuthOAuthCompletionResult): GeneratedOAuthTransportOutcome {
|
|
126
|
+
if (result.ok) return { status: 'authenticated', completion: 'fresh' };
|
|
127
|
+
if (result.status === 'cancelled') {
|
|
128
|
+
return {
|
|
129
|
+
status: 'cancelled',
|
|
130
|
+
message:
|
|
131
|
+
result.reason === 'provider_denied'
|
|
132
|
+
? 'Authorization was declined by the provider.'
|
|
133
|
+
: 'Authorization was cancelled.',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
status: 'error',
|
|
138
|
+
message: result.error.message,
|
|
139
|
+
recoverable: result.error.recoverable,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
function serializeOptionalScheme(scheme) {
|
|
145
|
+
return scheme === undefined ? 'undefined' : `'${escapeStringLiteral(scheme)}'`;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=oauthCompletion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauthCompletion.js","sourceRoot":"","sources":["../../../../../src/host/layout/templates/auth/oauthCompletion.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAEtE,MAAM,UAAU,wBAAwB,CAAC,IAGxC;IACC,MAAM,aAAa,GAAG,mBAAmB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC9D,MAAM,aAAa,GAAG,uBAAuB,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC1E,MAAM,SAAS,GAAG,uBAAuB,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IAElE,OAAO;;;;;;;;;gCASuB,aAAa;;aAEhC,aAAa;SACjB,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4HjB,CAAC;AACF,CAAC;AAED,SAAS,uBAAuB,CAAC,MAA0B;IACzD,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,mBAAmB,CAAC,MAAM,CAAC,GAAG,CAAC;AACjF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauthState.d.ts","sourceRoot":"","sources":["../../../../../src/host/layout/templates/auth/oauthState.ts"],"names":[],"mappings":"AAAA,wBAAgB,mBAAmB,WA2ClC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function getAuthOAuthStateTs() {
|
|
2
|
+
return `import { authSessionStorage } from './session';
|
|
3
|
+
|
|
4
|
+
const OAUTH_TRANSPORT_ATTEMPT_KEY = 'ankh.auth.oauth.transport';
|
|
5
|
+
|
|
6
|
+
interface StoredTransportAttempt {
|
|
7
|
+
attemptId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function clearTransportAttempt(): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
await authSessionStorage.removeItem(OAUTH_TRANSPORT_ATTEMPT_KEY);
|
|
13
|
+
} catch {
|
|
14
|
+
// Cleanup failures are intentionally not surfaced with persisted state.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function readTransportAttempt(): Promise<StoredTransportAttempt | null> {
|
|
19
|
+
const raw = await authSessionStorage.getItem(OAUTH_TRANSPORT_ATTEMPT_KEY);
|
|
20
|
+
if (!raw) return null;
|
|
21
|
+
try {
|
|
22
|
+
const value: unknown = JSON.parse(raw);
|
|
23
|
+
if (isRecord(value)) {
|
|
24
|
+
const attemptId: unknown = Reflect.get(value, 'attemptId');
|
|
25
|
+
if (typeof attemptId === 'string' && attemptId.trim().length > 0) {
|
|
26
|
+
return { attemptId };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Invalid transport state is cleaned below without exposing its contents.
|
|
31
|
+
}
|
|
32
|
+
await clearTransportAttempt();
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function writeTransportAttempt(attempt: StoredTransportAttempt): Promise<void> {
|
|
37
|
+
await authSessionStorage.setItem(OAUTH_TRANSPORT_ATTEMPT_KEY, JSON.stringify(attempt));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=oauthState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauthState.js","sourceRoot":"","sources":["../../../../../src/host/layout/templates/auth/oauthState.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCR,CAAC;AACF,CAAC"}
|