@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.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/dist/host/layout/auth/resolveAuthLayoutPlan.d.ts +1 -1
  3. package/dist/host/layout/auth/resolveAuthLayoutPlan.d.ts.map +1 -1
  4. package/dist/host/layout/auth/resolveAuthLayoutPlan.js +24 -1
  5. package/dist/host/layout/auth/resolveAuthLayoutPlan.js.map +1 -1
  6. package/dist/host/layout/layoutGenerator.d.ts.map +1 -1
  7. package/dist/host/layout/layoutGenerator.js +45 -20
  8. package/dist/host/layout/layoutGenerator.js.map +1 -1
  9. package/dist/host/layout/templates/auth/adapter.js +1 -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 +80 -42
  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 +146 -217
  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 +296 -531
  36. package/dist/host/layout/templates/auth/screen.js.map +1 -1
  37. package/dist/host/layout/templates/auth/session.js +6 -6
  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 +9 -149
  47. package/dist/host/layout/templates/rootLayout.js.map +1 -1
  48. package/dist/host/orchestrator/generatedAppFiles.d.ts.map +1 -1
  49. package/dist/host/orchestrator/generatedAppFiles.js +6 -4
  50. package/dist/host/orchestrator/generatedAppFiles.js.map +1 -1
  51. package/dist/host/orchestrator/generatedRouteCleanup.js +1 -1
  52. package/dist/host/orchestrator/generatedRouteCleanup.js.map +1 -1
  53. package/dist/host/orchestrator/scaffolder.js +3 -3
  54. package/dist/host/orchestrator/scaffolder.js.map +1 -1
  55. package/dist/host/orchestrator/templates.d.ts.map +1 -1
  56. package/dist/host/orchestrator/templates.js +54 -52
  57. package/dist/host/orchestrator/templates.js.map +1 -1
  58. package/dist/projectDeployAssetPicker.d.ts +2 -1
  59. package/dist/projectDeployAssetPicker.d.ts.map +1 -1
  60. package/dist/projectDeployAssetPicker.js +27 -21
  61. package/dist/projectDeployAssetPicker.js.map +1 -1
  62. package/package.json +5 -4
@@ -1,13 +1,6 @@
1
1
  import { escapeStringLiteral } from '../../utils/escapeStringLiteral';
2
2
  export function getAuthOAuthRuntimeTs(args) {
3
- const callbackRoute = escapeStringLiteral(args.callbackRoute);
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 * 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
18
  import {
26
- authSessionStorage,
27
- getStoredAuthSession,
28
- } from './session';
19
+ completeOAuthCallback,
20
+ resolveOAuthCallbackUrl,
21
+ resolveOAuthRedirectUri,
22
+ } from './oauth-completion';
23
+ import { clearTransportAttempt, writeTransportAttempt } from './oauth-state';
29
24
 
30
- const OAUTH_CALLBACK_ROUTE = '${callbackRoute}';
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 StoredTransportAttempt {
50
- 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;
51
44
  }
52
45
 
53
- export type OAuthCallbackRouteParams = Readonly<
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
- ): Promise<GeneratedOAuthTransportOutcome> {
71
- const oauth = authAdapter.oauth;
83
+ ): { context: OAuthAuthorizationContext } | { outcome: GeneratedOAuthTransportOutcome } {
84
+ const { oauth } = authAdapter;
72
85
  if (!oauth) {
73
86
  return {
74
- status: 'error',
75
- message: 'OAuth is not available in this app configuration.',
76
- recoverable: true,
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
- status: 'error',
84
- message: 'This OAuth provider is not enabled.',
85
- recoverable: true,
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 = resolveOAuthRedirectUri();
107
+ return { context: { oauth, provider, redirectUri: resolveOAuthRedirectUri() } };
92
108
  } catch {
93
109
  return {
94
- status: 'error',
95
- message: 'The OAuth redirect URI could not be resolved in this environment.',
96
- recoverable: true,
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
- status: 'error',
105
- message: runtimeReadiness.message,
106
- recoverable: true,
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
- status: 'error',
120
- message: started.error.message,
121
- recoverable: started.error.recoverable,
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
- status: 'error',
134
- message: 'The OAuth authorization attempt could not be persisted.',
135
- recoverable: true,
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.data.attemptId,
142
- authorizationUrl: started.data.authorizationUrl,
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.data.authorizationUrl,
150
- started.data.redirectUri,
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.data.attemptId,
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
- const assign = location ? Reflect.get(location, 'assign') : undefined;
185
- if (typeof assign !== 'function') {
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
- Reflect.apply(assign, location, [args.authorizationUrl]);
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
- export function resolveOAuthCallbackUrl(params: OAuthCallbackRouteParams): string {
216
- const callbackUrl = new URL(resolveOAuthRedirectUri());
217
- for (const [name, value] of Object.entries(params)) {
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
- export async function completeOAuthCallback(
229
- callbackUrl: string,
230
- ): Promise<GeneratedOAuthTransportOutcome> {
231
- const oauth = authAdapter.oauth;
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 resolveOAuthRedirectUri(): string {
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,aAAa,GAAG,mBAAmB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACjD,MAAM,aAAa,GACjB,IAAI,CAAC,aAAa,CAAC,OAAO,KAAK,SAAS;QACtC,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC;IAC7D,MAAM,SAAS,GACb,IAAI,CAAC,aAAa,CAAC,GAAG,KAAK,SAAS;QAClC,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC;IAEzD,OAAO;;;;;;;;;;;;;;;;;;;gCAmBuB,aAAa;;oCAET,SAAS;;aAEhC,aAAa;SACjB,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiXjB,CAAC;AACF,CAAC"}
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