@ankhorage/studio 2.0.1 → 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/README.md +1 -1
- 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 +45 -20
- package/dist/host/layout/layoutGenerator.js.map +1 -1
- package/dist/host/layout/templates/auth/adapter.js +1 -1
- package/dist/host/layout/templates/auth/adapter.js.map +1 -1
- package/dist/host/layout/templates/auth/callback.d.ts.map +1 -1
- package/dist/host/layout/templates/auth/callback.js +80 -42
- 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 +146 -217
- 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 +296 -531
- package/dist/host/layout/templates/auth/screen.js.map +1 -1
- package/dist/host/layout/templates/auth/session.js +6 -6
- 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 +9 -149
- package/dist/host/layout/templates/rootLayout.js.map +1 -1
- package/dist/host/orchestrator/generatedAppFiles.d.ts.map +1 -1
- package/dist/host/orchestrator/generatedAppFiles.js +6 -4
- package/dist/host/orchestrator/generatedAppFiles.js.map +1 -1
- package/dist/host/orchestrator/generatedRouteCleanup.js +1 -1
- package/dist/host/orchestrator/generatedRouteCleanup.js.map +1 -1
- package/dist/host/orchestrator/scaffolder.js +3 -3
- package/dist/host/orchestrator/scaffolder.js.map +1 -1
- package/dist/host/orchestrator/templates.d.ts.map +1 -1
- package/dist/host/orchestrator/templates.js +54 -52
- package/dist/host/orchestrator/templates.js.map +1 -1
- package/dist/projectDeployAssetPicker.d.ts +2 -1
- package/dist/projectDeployAssetPicker.d.ts.map +1 -1
- package/dist/projectDeployAssetPicker.js +27 -21
- package/dist/projectDeployAssetPicker.js.map +1 -1
- package/package.json +5 -4
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { escapeStringLiteral } from '../../utils/escapeStringLiteral';
|
|
2
2
|
export function getAuthOAuthRuntimeTs(args) {
|
|
3
|
-
const
|
|
4
|
-
const providers = JSON.stringify(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)}'`;
|
|
3
|
+
const providers = serializeOAuthProviders(args.providers);
|
|
11
4
|
return `import type {
|
|
12
5
|
AuthOAuthCompletionResult,
|
|
13
6
|
AuthOAuthTransportCancellationReason,
|
|
@@ -17,23 +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
18
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
completeOAuthCallback,
|
|
20
|
+
resolveOAuthCallbackUrl,
|
|
21
|
+
resolveOAuthRedirectUri,
|
|
22
|
+
} from './oauth-completion';
|
|
23
|
+
import { clearTransportAttempt, writeTransportAttempt } from './oauth-state';
|
|
29
24
|
|
|
30
|
-
const
|
|
31
|
-
const OAUTH_TRANSPORT_ATTEMPT_KEY = 'ankh.auth.oauth.transport';
|
|
32
|
-
const GENERATED_OAUTH_PROVIDERS = ${providers} as const;
|
|
33
|
-
const GENERATED_NATIVE_SCHEMES = {
|
|
34
|
-
android: ${androidScheme},
|
|
35
|
-
ios: ${iosScheme},
|
|
36
|
-
} as const;
|
|
25
|
+
const GENERATED_OAUTH_PROVIDERS: readonly GeneratedOAuthProvider[] = ${providers};
|
|
37
26
|
|
|
38
27
|
export const generatedOAuthProviderItems = GENERATED_OAUTH_PROVIDERS.map((provider) => ({
|
|
39
28
|
id: provider.id,
|
|
@@ -42,20 +31,36 @@ export const generatedOAuthProviderItems = GENERATED_OAUTH_PROVIDERS.map((provid
|
|
|
42
31
|
}));
|
|
43
32
|
|
|
44
33
|
export type GeneratedOAuthTransportOutcome =
|
|
45
|
-
| { status: 'authenticated' }
|
|
34
|
+
| { status: 'authenticated'; completion: 'fresh' | 'already-completed' }
|
|
46
35
|
| { status: 'cancelled'; message: string }
|
|
47
36
|
| { status: 'error'; message: string; recoverable: boolean };
|
|
48
37
|
|
|
49
|
-
interface
|
|
50
|
-
|
|
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;
|
|
51
44
|
}
|
|
52
45
|
|
|
53
|
-
export
|
|
54
|
-
Record<string, string | string[] | undefined>
|
|
55
|
-
>;
|
|
46
|
+
export { completeOAuthCallback, resolveOAuthCallbackUrl };
|
|
56
47
|
|
|
57
48
|
let activeAuthorization: Promise<GeneratedOAuthTransportOutcome> | null = null;
|
|
58
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
|
+
|
|
59
64
|
export function startOAuthAuthorization(
|
|
60
65
|
providerId: string,
|
|
61
66
|
): Promise<GeneratedOAuthTransportOutcome> {
|
|
@@ -65,89 +70,120 @@ export function startOAuthAuthorization(
|
|
|
65
70
|
return activeAuthorization;
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
async function runOAuthAuthorization(
|
|
73
|
+
async function runOAuthAuthorization(providerId: string): Promise<GeneratedOAuthTransportOutcome> {
|
|
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(
|
|
69
82
|
providerId: string,
|
|
70
|
-
):
|
|
71
|
-
const oauth = authAdapter
|
|
83
|
+
): { context: OAuthAuthorizationContext } | { outcome: GeneratedOAuthTransportOutcome } {
|
|
84
|
+
const { oauth } = authAdapter;
|
|
72
85
|
if (!oauth) {
|
|
73
86
|
return {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
outcome: {
|
|
88
|
+
status: 'error',
|
|
89
|
+
message: 'OAuth is not available in this app configuration.',
|
|
90
|
+
recoverable: true,
|
|
91
|
+
},
|
|
77
92
|
};
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
const provider = GENERATED_OAUTH_PROVIDERS.find((entry) => entry.id === providerId);
|
|
81
96
|
if (!provider) {
|
|
82
97
|
return {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
outcome: {
|
|
99
|
+
status: 'error',
|
|
100
|
+
message: 'This OAuth provider is not enabled.',
|
|
101
|
+
recoverable: true,
|
|
102
|
+
},
|
|
86
103
|
};
|
|
87
104
|
}
|
|
88
105
|
|
|
89
|
-
let redirectUri: string;
|
|
90
106
|
try {
|
|
91
|
-
redirectUri
|
|
107
|
+
return { context: { oauth, provider, redirectUri: resolveOAuthRedirectUri() } };
|
|
92
108
|
} catch {
|
|
93
109
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
outcome: {
|
|
111
|
+
status: 'error',
|
|
112
|
+
message: 'The OAuth redirect URI could not be resolved in this environment.',
|
|
113
|
+
recoverable: true,
|
|
114
|
+
},
|
|
97
115
|
};
|
|
98
116
|
}
|
|
117
|
+
}
|
|
99
118
|
|
|
119
|
+
async function startAuthorizationAttemptAsync(
|
|
120
|
+
context: OAuthAuthorizationContext,
|
|
121
|
+
): Promise<
|
|
122
|
+
{ authorization: StartedOAuthAuthorization } | { outcome: GeneratedOAuthTransportOutcome }
|
|
123
|
+
> {
|
|
100
124
|
if (Platform.OS !== 'web') {
|
|
101
125
|
const runtimeReadiness = resolveExpoOAuthBrowserRuntimeReadiness();
|
|
102
126
|
if (runtimeReadiness.status !== 'ready') {
|
|
103
127
|
return {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
outcome: {
|
|
129
|
+
status: 'error',
|
|
130
|
+
message: runtimeReadiness.message,
|
|
131
|
+
recoverable: true,
|
|
132
|
+
},
|
|
107
133
|
};
|
|
108
134
|
}
|
|
109
135
|
}
|
|
110
136
|
|
|
111
|
-
const started = await oauth.startAuthorization({
|
|
112
|
-
provider: provider.id,
|
|
113
|
-
redirectUri,
|
|
114
|
-
scopes: provider.scopes,
|
|
115
|
-
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 },
|
|
116
142
|
});
|
|
117
143
|
if (!started.ok) {
|
|
118
144
|
return {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
outcome: {
|
|
146
|
+
status: 'error',
|
|
147
|
+
message: started.error.message,
|
|
148
|
+
recoverable: started.error.recoverable,
|
|
149
|
+
},
|
|
122
150
|
};
|
|
123
151
|
}
|
|
124
152
|
|
|
125
153
|
try {
|
|
126
|
-
await writeTransportAttempt({
|
|
127
|
-
attemptId: started.data.attemptId,
|
|
128
|
-
});
|
|
154
|
+
await writeTransportAttempt({ attemptId: started.data.attemptId });
|
|
129
155
|
} catch {
|
|
130
|
-
await cancelOAuthAttempt(started.data.attemptId, 'user_cancelled');
|
|
156
|
+
await cancelOAuthAttempt(context.oauth, started.data.attemptId, 'user_cancelled');
|
|
131
157
|
await clearTransportAttempt();
|
|
132
158
|
return {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
outcome: {
|
|
160
|
+
status: 'error',
|
|
161
|
+
message: 'The OAuth authorization attempt could not be persisted.',
|
|
162
|
+
recoverable: true,
|
|
163
|
+
},
|
|
136
164
|
};
|
|
137
165
|
}
|
|
138
166
|
|
|
167
|
+
return { authorization: started.data };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function completeAuthorizationTransportAsync(
|
|
171
|
+
oauth: GeneratedOAuthAdapter,
|
|
172
|
+
started: StartedOAuthAuthorization,
|
|
173
|
+
): Promise<GeneratedOAuthTransportOutcome> {
|
|
139
174
|
if (Platform.OS === 'web') {
|
|
140
175
|
return redirectWebAuthorization({
|
|
141
|
-
attemptId: started.
|
|
142
|
-
authorizationUrl: started.
|
|
176
|
+
attemptId: started.attemptId,
|
|
177
|
+
authorizationUrl: started.authorizationUrl,
|
|
178
|
+
oauth,
|
|
143
179
|
});
|
|
144
180
|
}
|
|
145
181
|
|
|
146
182
|
let browserResponse;
|
|
147
183
|
try {
|
|
148
184
|
const browserResult = await WebBrowser.openAuthSessionAsync(
|
|
149
|
-
started.
|
|
150
|
-
started.
|
|
185
|
+
started.authorizationUrl,
|
|
186
|
+
started.redirectUri,
|
|
151
187
|
);
|
|
152
188
|
browserResponse = resolveExpoOAuthBrowserResult(browserResult);
|
|
153
189
|
} catch {
|
|
@@ -161,7 +197,7 @@ async function runOAuthAuthorization(
|
|
|
161
197
|
let completed: AuthOAuthCompletionResult;
|
|
162
198
|
try {
|
|
163
199
|
completed = await oauth.completeAuthorization({
|
|
164
|
-
attemptId: started.
|
|
200
|
+
attemptId: started.attemptId,
|
|
165
201
|
response: browserResponse,
|
|
166
202
|
});
|
|
167
203
|
} catch {
|
|
@@ -179,11 +215,11 @@ async function runOAuthAuthorization(
|
|
|
179
215
|
async function redirectWebAuthorization(args: {
|
|
180
216
|
attemptId: string;
|
|
181
217
|
authorizationUrl: string;
|
|
218
|
+
oauth: GeneratedOAuthAdapter;
|
|
182
219
|
}): Promise<GeneratedOAuthTransportOutcome> {
|
|
183
220
|
const location = getBrowserLocation();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
await cancelOAuthAttempt(args.attemptId, 'browser_dismissed');
|
|
221
|
+
if (!hasBrowserLocationAssign(location)) {
|
|
222
|
+
await cancelOAuthAttempt(args.oauth, args.attemptId, 'browser_dismissed');
|
|
187
223
|
await clearTransportAttempt();
|
|
188
224
|
return {
|
|
189
225
|
status: 'error',
|
|
@@ -193,10 +229,10 @@ async function redirectWebAuthorization(args: {
|
|
|
193
229
|
}
|
|
194
230
|
|
|
195
231
|
try {
|
|
196
|
-
|
|
232
|
+
location.assign(args.authorizationUrl);
|
|
197
233
|
return waitForFullPageNavigation();
|
|
198
234
|
} catch {
|
|
199
|
-
await cancelOAuthAttempt(args.attemptId, 'browser_dismissed');
|
|
235
|
+
await cancelOAuthAttempt(args.oauth, args.attemptId, 'browser_dismissed');
|
|
200
236
|
await clearTransportAttempt();
|
|
201
237
|
return {
|
|
202
238
|
status: 'error',
|
|
@@ -212,125 +248,20 @@ function waitForFullPageNavigation(): Promise<never> {
|
|
|
212
248
|
});
|
|
213
249
|
}
|
|
214
250
|
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
if (name === '#') continue;
|
|
219
|
-
if (Array.isArray(value)) {
|
|
220
|
-
for (const item of value) callbackUrl.searchParams.append(name, item);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (typeof value === 'string') callbackUrl.searchParams.append(name, value);
|
|
224
|
-
}
|
|
225
|
-
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;
|
|
226
254
|
}
|
|
227
255
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
):
|
|
231
|
-
|
|
232
|
-
if (!oauth) {
|
|
233
|
-
return {
|
|
234
|
-
status: 'error',
|
|
235
|
-
message: 'OAuth is not available in this app configuration.',
|
|
236
|
-
recoverable: true,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const attempt = await readTransportAttempt();
|
|
241
|
-
if (!attempt) {
|
|
242
|
-
if (getStoredAuthSession() && isCanonicalOAuthCallback(callbackUrl)) {
|
|
243
|
-
return { status: 'authenticated' };
|
|
244
|
-
}
|
|
245
|
-
return {
|
|
246
|
-
status: 'error',
|
|
247
|
-
message: 'The OAuth authorization attempt was not found or has expired.',
|
|
248
|
-
recoverable: true,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
let completed: AuthOAuthCompletionResult;
|
|
253
|
-
try {
|
|
254
|
-
completed = await oauth.completeAuthorization({
|
|
255
|
-
attemptId: attempt.attemptId,
|
|
256
|
-
response: { type: 'callback', url: callbackUrl },
|
|
257
|
-
});
|
|
258
|
-
} catch {
|
|
259
|
-
await clearTransportAttempt();
|
|
260
|
-
return {
|
|
261
|
-
status: 'error',
|
|
262
|
-
message: 'The OAuth callback could not be completed.',
|
|
263
|
-
recoverable: true,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
await clearTransportAttempt();
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
!completed.ok &&
|
|
270
|
-
completed.status === 'error' &&
|
|
271
|
-
completed.error.code === 'callback_already_completed' &&
|
|
272
|
-
getStoredAuthSession()
|
|
273
|
-
) {
|
|
274
|
-
return { status: 'authenticated' };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return toTransportOutcome(completed);
|
|
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';
|
|
278
260
|
}
|
|
279
261
|
|
|
280
|
-
function
|
|
281
|
-
let callbackPath = OAUTH_CALLBACK_ROUTE;
|
|
282
|
-
while (callbackPath.startsWith('/')) {
|
|
283
|
-
callbackPath = callbackPath.slice(1);
|
|
284
|
-
}
|
|
285
|
-
if (Platform.OS === 'web') {
|
|
286
|
-
const location = getBrowserLocation();
|
|
287
|
-
const origin = location ? Reflect.get(location, 'origin') : undefined;
|
|
288
|
-
if (typeof origin === 'string' && origin.length > 0) {
|
|
289
|
-
return new URL(\`/\${callbackPath}\`, origin).toString();
|
|
290
|
-
}
|
|
291
|
-
throw new Error('Web OAuth requires a canonical browser origin.');
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const nativeScheme =
|
|
295
|
-
Platform.OS === 'android'
|
|
296
|
-
? GENERATED_NATIVE_SCHEMES.android
|
|
297
|
-
: Platform.OS === 'ios'
|
|
298
|
-
? GENERATED_NATIVE_SCHEMES.ios
|
|
299
|
-
: undefined;
|
|
300
|
-
if (!nativeScheme) {
|
|
301
|
-
throw new Error('Native OAuth requires a configured application scheme.');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return Linking.createURL(callbackPath, { scheme: nativeScheme });
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function isCanonicalOAuthCallback(callbackUrl: string): boolean {
|
|
308
|
-
try {
|
|
309
|
-
const delivered = new URL(callbackUrl);
|
|
310
|
-
const expected = new URL(resolveOAuthRedirectUri());
|
|
311
|
-
return (
|
|
312
|
-
delivered.protocol === expected.protocol &&
|
|
313
|
-
delivered.username === expected.username &&
|
|
314
|
-
delivered.password === expected.password &&
|
|
315
|
-
delivered.host === expected.host &&
|
|
316
|
-
delivered.pathname === expected.pathname &&
|
|
317
|
-
delivered.hash.length === 0
|
|
318
|
-
);
|
|
319
|
-
} catch {
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function getBrowserLocation(): object | null {
|
|
325
|
-
const location = Reflect.get(globalThis, 'location');
|
|
326
|
-
return typeof location === 'object' && location !== null ? location : null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function toTransportOutcome(
|
|
330
|
-
result: AuthOAuthCompletionResult,
|
|
331
|
-
): GeneratedOAuthTransportOutcome {
|
|
262
|
+
function toTransportOutcome(result: AuthOAuthCompletionResult): GeneratedOAuthTransportOutcome {
|
|
332
263
|
if (result.ok) {
|
|
333
|
-
return { status: 'authenticated' };
|
|
264
|
+
return { status: 'authenticated', completion: 'fresh' };
|
|
334
265
|
}
|
|
335
266
|
if (result.status === 'cancelled') {
|
|
336
267
|
return {
|
|
@@ -349,11 +280,10 @@ function toTransportOutcome(
|
|
|
349
280
|
}
|
|
350
281
|
|
|
351
282
|
async function cancelOAuthAttempt(
|
|
283
|
+
oauth: GeneratedOAuthAdapter,
|
|
352
284
|
attemptId: string,
|
|
353
285
|
reason: AuthOAuthTransportCancellationReason,
|
|
354
286
|
): Promise<void> {
|
|
355
|
-
const oauth = authAdapter.oauth;
|
|
356
|
-
if (!oauth) return;
|
|
357
287
|
try {
|
|
358
288
|
await oauth.completeAuthorization({
|
|
359
289
|
attemptId,
|
|
@@ -364,43 +294,42 @@ async function cancelOAuthAttempt(
|
|
|
364
294
|
}
|
|
365
295
|
}
|
|
366
296
|
|
|
367
|
-
async function writeTransportAttempt(attempt: StoredTransportAttempt): Promise<void> {
|
|
368
|
-
await authSessionStorage.setItem(OAUTH_TRANSPORT_ATTEMPT_KEY, JSON.stringify(attempt));
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async function readTransportAttempt(): Promise<StoredTransportAttempt | null> {
|
|
372
|
-
const raw = await authSessionStorage.getItem(OAUTH_TRANSPORT_ATTEMPT_KEY);
|
|
373
|
-
if (!raw) return null;
|
|
374
|
-
try {
|
|
375
|
-
const value: unknown = JSON.parse(raw);
|
|
376
|
-
if (isRecord(value)) {
|
|
377
|
-
const attemptId = Reflect.get(value, 'attemptId');
|
|
378
|
-
if (typeof attemptId === 'string' && attemptId.trim().length > 0) {
|
|
379
|
-
return { attemptId };
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
} catch {
|
|
383
|
-
// Invalid transport state is cleaned below without exposing its contents.
|
|
384
|
-
}
|
|
385
|
-
await clearTransportAttempt();
|
|
386
|
-
return null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
async function clearTransportAttempt(): Promise<void> {
|
|
390
|
-
await safeRemoveTransportItem(OAUTH_TRANSPORT_ATTEMPT_KEY);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function safeRemoveTransportItem(key: string): Promise<void> {
|
|
394
|
-
try {
|
|
395
|
-
await authSessionStorage.removeItem(key);
|
|
396
|
-
} catch {
|
|
397
|
-
// Cleanup failures are intentionally not surfaced with persisted state.
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
297
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
402
298
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
403
299
|
}
|
|
404
300
|
`;
|
|
405
301
|
}
|
|
302
|
+
function serializeOAuthProviders(providers) {
|
|
303
|
+
if (providers.length === 0)
|
|
304
|
+
return '[]';
|
|
305
|
+
const entries = providers.map((provider) => {
|
|
306
|
+
const scopes = provider.scopes.map((scope) => `'${escapeStringLiteral(scope)}'`).join(', ');
|
|
307
|
+
const queryParams = Object.entries(provider.queryParams)
|
|
308
|
+
.flatMap(([name, value]) => value === undefined
|
|
309
|
+
? []
|
|
310
|
+
: [`${serializeObjectPropertyName(name)}: '${escapeStringLiteral(value)}'`])
|
|
311
|
+
.join(', ');
|
|
312
|
+
const serializedQueryParams = queryParams.length > 0 ? `{ ${queryParams} }` : '{}';
|
|
313
|
+
const icon = provider.icon ? `,\n icon: ${serializeOAuthIcon(provider.icon)}` : '';
|
|
314
|
+
return ` {
|
|
315
|
+
id: '${escapeStringLiteral(provider.id)}',
|
|
316
|
+
label: '${escapeStringLiteral(provider.label)}',
|
|
317
|
+
scopes: [${scopes}],
|
|
318
|
+
queryParams: ${serializedQueryParams}${icon},
|
|
319
|
+
}`;
|
|
320
|
+
});
|
|
321
|
+
return `[\n${entries.join(',\n')},\n]`;
|
|
322
|
+
}
|
|
323
|
+
function serializeOAuthIcon(icon) {
|
|
324
|
+
const properties = Object.entries(icon).flatMap(([name, value]) => {
|
|
325
|
+
if (value === undefined)
|
|
326
|
+
return [];
|
|
327
|
+
const serialized = typeof value === 'number' ? String(value) : `'${escapeStringLiteral(value)}'`;
|
|
328
|
+
return [`${name}: ${serialized}`];
|
|
329
|
+
});
|
|
330
|
+
return `{ ${properties.join(', ')} }`;
|
|
331
|
+
}
|
|
332
|
+
function serializeObjectPropertyName(name) {
|
|
333
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(name) ? name : `'${escapeStringLiteral(name)}'`;
|
|
334
|
+
}
|
|
406
335
|
//# sourceMappingURL=oauth.js.map
|
|
@@ -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"}
|