@ankhorage/studio 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/host/layout/auth/resolveAuthLayoutPlan.d.ts +1 -1
  2. package/dist/host/layout/auth/resolveAuthLayoutPlan.d.ts.map +1 -1
  3. package/dist/host/layout/auth/resolveAuthLayoutPlan.js +24 -1
  4. package/dist/host/layout/auth/resolveAuthLayoutPlan.js.map +1 -1
  5. package/dist/host/layout/layoutGenerator.d.ts.map +1 -1
  6. package/dist/host/layout/layoutGenerator.js +38 -13
  7. package/dist/host/layout/layoutGenerator.js.map +1 -1
  8. package/dist/host/layout/templates/auth/adapter.d.ts.map +1 -1
  9. package/dist/host/layout/templates/auth/adapter.js +4 -1
  10. package/dist/host/layout/templates/auth/adapter.js.map +1 -1
  11. package/dist/host/layout/templates/auth/callback.d.ts.map +1 -1
  12. package/dist/host/layout/templates/auth/callback.js +78 -37
  13. package/dist/host/layout/templates/auth/callback.js.map +1 -1
  14. package/dist/host/layout/templates/auth/form.d.ts +2 -0
  15. package/dist/host/layout/templates/auth/form.d.ts.map +1 -0
  16. package/dist/host/layout/templates/auth/form.js +238 -0
  17. package/dist/host/layout/templates/auth/form.js.map +1 -0
  18. package/dist/host/layout/templates/auth/navigation.d.ts +10 -0
  19. package/dist/host/layout/templates/auth/navigation.d.ts.map +1 -0
  20. package/dist/host/layout/templates/auth/navigation.js +183 -0
  21. package/dist/host/layout/templates/auth/navigation.js.map +1 -0
  22. package/dist/host/layout/templates/auth/oauth.d.ts.map +1 -1
  23. package/dist/host/layout/templates/auth/oauth.js +112 -196
  24. package/dist/host/layout/templates/auth/oauth.js.map +1 -1
  25. package/dist/host/layout/templates/auth/oauthCompletion.d.ts +6 -0
  26. package/dist/host/layout/templates/auth/oauthCompletion.d.ts.map +1 -0
  27. package/dist/host/layout/templates/auth/oauthCompletion.js +147 -0
  28. package/dist/host/layout/templates/auth/oauthCompletion.js.map +1 -0
  29. package/dist/host/layout/templates/auth/oauthState.d.ts +2 -0
  30. package/dist/host/layout/templates/auth/oauthState.d.ts.map +1 -0
  31. package/dist/host/layout/templates/auth/oauthState.js +45 -0
  32. package/dist/host/layout/templates/auth/oauthState.js.map +1 -0
  33. package/dist/host/layout/templates/auth/screen.d.ts +5 -2
  34. package/dist/host/layout/templates/auth/screen.d.ts.map +1 -1
  35. package/dist/host/layout/templates/auth/screen.js +295 -529
  36. package/dist/host/layout/templates/auth/screen.js.map +1 -1
  37. package/dist/host/layout/templates/auth/session.js +5 -5
  38. package/dist/host/layout/templates/auth/signOut.d.ts.map +1 -1
  39. package/dist/host/layout/templates/auth/signOut.js +10 -9
  40. package/dist/host/layout/templates/auth/signOut.js.map +1 -1
  41. package/dist/host/layout/templates/index.d.ts +5 -1
  42. package/dist/host/layout/templates/index.d.ts.map +1 -1
  43. package/dist/host/layout/templates/index.js +5 -1
  44. package/dist/host/layout/templates/index.js.map +1 -1
  45. package/dist/host/layout/templates/rootLayout.d.ts.map +1 -1
  46. package/dist/host/layout/templates/rootLayout.js +8 -147
  47. package/dist/host/layout/templates/rootLayout.js.map +1 -1
  48. package/dist/host/orchestrator/templates.d.ts.map +1 -1
  49. package/dist/host/orchestrator/templates.js +11 -13
  50. package/dist/host/orchestrator/templates.js.map +1 -1
  51. package/package.json +3 -3
@@ -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 * as Linking from 'expo-linking';
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 { authSessionStorage, getStoredAuthSession } from './session';
18
+ import {
19
+ completeOAuthCallback,
20
+ resolveOAuthCallbackUrl,
21
+ resolveOAuthRedirectUri,
22
+ } from './oauth-completion';
23
+ import { clearTransportAttempt, writeTransportAttempt } from './oauth-state';
26
24
 
27
- const OAUTH_CALLBACK_ROUTE = '${callbackRoute}';
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 StoredTransportAttempt {
47
- attemptId: string;
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 type OAuthCallbackRouteParams = Readonly<Record<string, string | string[] | undefined>>;
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 oauth = authAdapter.oauth;
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
- status: 'error',
68
- message: 'OAuth is not available in this app configuration.',
69
- recoverable: true,
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
- status: 'error',
77
- message: 'This OAuth provider is not enabled.',
78
- recoverable: true,
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 = resolveOAuthRedirectUri();
107
+ return { context: { oauth, provider, redirectUri: resolveOAuthRedirectUri() } };
85
108
  } catch {
86
109
  return {
87
- status: 'error',
88
- message: 'The OAuth redirect URI could not be resolved in this environment.',
89
- recoverable: true,
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
- status: 'error',
98
- message: runtimeReadiness.message,
99
- recoverable: true,
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
- status: 'error',
113
- message: started.error.message,
114
- recoverable: started.error.recoverable,
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
- status: 'error',
127
- message: 'The OAuth authorization attempt could not be persisted.',
128
- recoverable: true,
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.data.attemptId,
135
- authorizationUrl: started.data.authorizationUrl,
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.data.authorizationUrl,
143
- started.data.redirectUri,
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.data.attemptId,
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
- const assign = location ? Reflect.get(location, 'assign') : undefined;
178
- if (typeof assign !== 'function') {
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
- Reflect.apply(assign, location, [args.authorizationUrl]);
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
- export function resolveOAuthCallbackUrl(params: OAuthCallbackRouteParams): string {
209
- const callbackUrl = new URL(resolveOAuthRedirectUri());
210
- for (const [name, value] of Object.entries(params)) {
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
- export async function completeOAuthCallback(
222
- callbackUrl: string,
223
- ): Promise<GeneratedOAuthTransportOutcome> {
224
- const oauth = authAdapter.oauth;
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":"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"}
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,2 @@
1
+ export declare function getAuthOAuthStateTs(): string;
2
+ //# sourceMappingURL=oauthState.d.ts.map
@@ -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"}