@ankhorage/expo-runtime 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @ankhorage/expo-runtime
2
2
 
3
+ ## 2.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b2e39b8: Classify Expo authentication browser results into neutral OAuth transport responses.
8
+
3
9
  ## 2.3.0
4
10
 
5
11
  ### Minor Changes
@@ -0,0 +1,4 @@
1
+ import type { AuthOAuthAuthorizationResponse } from '@ankhorage/contracts/auth';
2
+ export declare function resolveExpoOAuthBrowserResult(result: unknown): AuthOAuthAuthorizationResponse;
3
+ export declare function resolveExpoOAuthBrowserException(): AuthOAuthAuthorizationResponse;
4
+ //# sourceMappingURL=oauthBrowserTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthBrowserTransport.d.ts","sourceRoot":"","sources":["../src/oauthBrowserTransport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,2BAA2B,CAAC;AAEhF,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,OAAO,GAAG,8BAA8B,CAkB7F;AAED,wBAAgB,gCAAgC,IAAI,8BAA8B,CAQjF"}
@@ -0,0 +1,37 @@
1
+ export function resolveExpoOAuthBrowserResult(result) {
2
+ if (!isRecord(result))
3
+ return transportFailed('The Expo authentication browser returned no result.');
4
+ const type = Reflect.get(result, 'type');
5
+ if (type === 'success') {
6
+ const url = Reflect.get(result, 'url');
7
+ return typeof url === 'string' && url.trim().length > 0
8
+ ? { type: 'callback', url }
9
+ : transportFailed('The Expo authentication browser returned no callback URL.');
10
+ }
11
+ if (type === 'cancel') {
12
+ return { type: 'cancelled', reason: 'user_cancelled' };
13
+ }
14
+ if (type === 'dismiss') {
15
+ return { type: 'cancelled', reason: 'browser_dismissed' };
16
+ }
17
+ return transportFailed('The Expo authentication browser did not complete the OAuth redirect.');
18
+ }
19
+ export function resolveExpoOAuthBrowserException() {
20
+ return {
21
+ type: 'error',
22
+ error: {
23
+ code: 'browser_unavailable',
24
+ message: 'The Expo authentication browser could not be opened.',
25
+ },
26
+ };
27
+ }
28
+ function transportFailed(message) {
29
+ return {
30
+ type: 'error',
31
+ error: { code: 'transport_failed', message },
32
+ };
33
+ }
34
+ function isRecord(value) {
35
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
36
+ }
37
+ //# sourceMappingURL=oauthBrowserTransport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthBrowserTransport.js","sourceRoot":"","sources":["../src/oauthBrowserTransport.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,6BAA6B,CAAC,MAAe;IAC3D,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QACnB,OAAO,eAAe,CAAC,qDAAqD,CAAC,CAAC;IAEhF,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACvC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YACrD,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE;YAC3B,CAAC,CAAC,eAAe,CAAC,2DAA2D,CAAC,CAAC;IACnF,CAAC;IACD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IACzD,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC5D,CAAC;IACD,OAAO,eAAe,CAAC,sEAAsE,CAAC,CAAC;AACjG,CAAC;AAED,MAAM,UAAU,gCAAgC;IAC9C,OAAO;QACL,IAAI,EAAE,OAAO;QACb,KAAK,EAAE;YACL,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,sDAAsD;SAChE;KACF,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,OAAe;IACtC,OAAO;QACL,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE;KAC7C,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC","sourcesContent":["import type { AuthOAuthAuthorizationResponse } from '@ankhorage/contracts/auth';\n\nexport function resolveExpoOAuthBrowserResult(result: unknown): AuthOAuthAuthorizationResponse {\n if (!isRecord(result))\n return transportFailed('The Expo authentication browser returned no result.');\n\n const type = Reflect.get(result, 'type');\n if (type === 'success') {\n const url = Reflect.get(result, 'url');\n return typeof url === 'string' && url.trim().length > 0\n ? { type: 'callback', url }\n : transportFailed('The Expo authentication browser returned no callback URL.');\n }\n if (type === 'cancel') {\n return { type: 'cancelled', reason: 'user_cancelled' };\n }\n if (type === 'dismiss') {\n return { type: 'cancelled', reason: 'browser_dismissed' };\n }\n return transportFailed('The Expo authentication browser did not complete the OAuth redirect.');\n}\n\nexport function resolveExpoOAuthBrowserException(): AuthOAuthAuthorizationResponse {\n return {\n type: 'error',\n error: {\n code: 'browser_unavailable',\n message: 'The Expo authentication browser could not be opened.',\n },\n };\n}\n\nfunction transportFailed(message: string): AuthOAuthAuthorizationResponse {\n return {\n type: 'error',\n error: { code: 'transport_failed', message },\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/expo-runtime",
3
3
  "type": "module",
4
- "version": "2.3.0",
4
+ "version": "2.4.0",
5
5
  "description": "Declarative runtime integration for Expo apps: maps app capabilities to permissions, packages, config plugins, providers, and adapters",
6
6
  "homepage": "https://github.com/ankhorage/expo-runtime#readme",
7
7
  "bugs": {
@@ -48,6 +48,13 @@
48
48
  "import": "./dist/planning.js",
49
49
  "default": "./dist/planning.js"
50
50
  },
51
+ "./oauth-browser": {
52
+ "react-native": "./src/oauthBrowserTransport.ts",
53
+ "browser": "./src/oauthBrowserTransport.ts",
54
+ "types": "./src/oauthBrowserTransport.ts",
55
+ "import": "./dist/oauthBrowserTransport.js",
56
+ "default": "./dist/oauthBrowserTransport.js"
57
+ },
51
58
  "./media-picker": {
52
59
  "react-native": "./src/mediaPicker/index.ts",
53
60
  "browser": "./src/mediaPicker/index.ts",
@@ -0,0 +1,59 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import {
4
+ resolveExpoOAuthBrowserException,
5
+ resolveExpoOAuthBrowserResult,
6
+ } from './oauthBrowserTransport';
7
+
8
+ describe('Expo OAuth browser transport', () => {
9
+ test('maps a successful browser redirect to a callback response', () => {
10
+ expect(
11
+ resolveExpoOAuthBrowserResult({ type: 'success', url: 'ankh://auth/callback?code=abc' }),
12
+ ).toEqual({ type: 'callback', url: 'ankh://auth/callback?code=abc' });
13
+ });
14
+
15
+ test('maps user cancellation separately from browser dismissal', () => {
16
+ expect(resolveExpoOAuthBrowserResult({ type: 'cancel' })).toEqual({
17
+ type: 'cancelled',
18
+ reason: 'user_cancelled',
19
+ });
20
+ expect(resolveExpoOAuthBrowserResult({ type: 'dismiss' })).toEqual({
21
+ type: 'cancelled',
22
+ reason: 'browser_dismissed',
23
+ });
24
+ });
25
+
26
+ test.each(['locked', 'opened'])(
27
+ 'maps %s to a transport failure instead of cancellation',
28
+ (type) => {
29
+ expect(resolveExpoOAuthBrowserResult({ type })).toEqual({
30
+ type: 'error',
31
+ error: {
32
+ code: 'transport_failed',
33
+ message: 'The Expo authentication browser did not complete the OAuth redirect.',
34
+ },
35
+ });
36
+ },
37
+ );
38
+
39
+ test('rejects malformed success and unknown results as transport failures', () => {
40
+ expect(resolveExpoOAuthBrowserResult({ type: 'success' })).toMatchObject({
41
+ type: 'error',
42
+ error: { code: 'transport_failed' },
43
+ });
44
+ expect(resolveExpoOAuthBrowserResult(null)).toMatchObject({
45
+ type: 'error',
46
+ error: { code: 'transport_failed' },
47
+ });
48
+ });
49
+
50
+ test('maps a rejected browser invocation to browser unavailable', () => {
51
+ expect(resolveExpoOAuthBrowserException()).toEqual({
52
+ type: 'error',
53
+ error: {
54
+ code: 'browser_unavailable',
55
+ message: 'The Expo authentication browser could not be opened.',
56
+ },
57
+ });
58
+ });
59
+ });
@@ -0,0 +1,42 @@
1
+ import type { AuthOAuthAuthorizationResponse } from '@ankhorage/contracts/auth';
2
+
3
+ export function resolveExpoOAuthBrowserResult(result: unknown): AuthOAuthAuthorizationResponse {
4
+ if (!isRecord(result))
5
+ return transportFailed('The Expo authentication browser returned no result.');
6
+
7
+ const type = Reflect.get(result, 'type');
8
+ if (type === 'success') {
9
+ const url = Reflect.get(result, 'url');
10
+ return typeof url === 'string' && url.trim().length > 0
11
+ ? { type: 'callback', url }
12
+ : transportFailed('The Expo authentication browser returned no callback URL.');
13
+ }
14
+ if (type === 'cancel') {
15
+ return { type: 'cancelled', reason: 'user_cancelled' };
16
+ }
17
+ if (type === 'dismiss') {
18
+ return { type: 'cancelled', reason: 'browser_dismissed' };
19
+ }
20
+ return transportFailed('The Expo authentication browser did not complete the OAuth redirect.');
21
+ }
22
+
23
+ export function resolveExpoOAuthBrowserException(): AuthOAuthAuthorizationResponse {
24
+ return {
25
+ type: 'error',
26
+ error: {
27
+ code: 'browser_unavailable',
28
+ message: 'The Expo authentication browser could not be opened.',
29
+ },
30
+ };
31
+ }
32
+
33
+ function transportFailed(message: string): AuthOAuthAuthorizationResponse {
34
+ return {
35
+ type: 'error',
36
+ error: { code: 'transport_failed', message },
37
+ };
38
+ }
39
+
40
+ function isRecord(value: unknown): value is Record<string, unknown> {
41
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
42
+ }