@ankhorage/expo-runtime 2.3.0 → 2.5.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,17 @@
1
1
  # @ankhorage/expo-runtime
2
2
 
3
+ ## 2.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b6541d8: Add runtime readiness detection that rejects Expo Go for brokered native OAuth and requires a custom development or standalone build with the configured app scheme.
8
+
9
+ ## 2.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - b2e39b8: Classify Expo authentication browser results into neutral OAuth transport responses.
14
+
3
15
  ## 2.3.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # EXPO-RUNTIME
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v2.2.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v2.4.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
7
7
 
8
8
  Declarative runtime integration for Expo apps: maps app capabilities to permissions, packages, config plugins, providers, and adapters
9
9
 
@@ -0,0 +1,4 @@
1
+ import { type ExpoOAuthBrowserRuntimeReadiness } from './oauthBrowserRuntimeCore';
2
+ export type { ExpoOAuthBrowserRuntimeReadiness } from './oauthBrowserRuntimeCore';
3
+ export declare function resolveExpoOAuthBrowserRuntimeReadiness(): ExpoOAuthBrowserRuntimeReadiness;
4
+ //# sourceMappingURL=oauthBrowserRuntime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthBrowserRuntime.d.ts","sourceRoot":"","sources":["../src/oauthBrowserRuntime.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,gCAAgC,EAEtC,MAAM,2BAA2B,CAAC;AAEnC,YAAY,EAAE,gCAAgC,EAAE,MAAM,2BAA2B,CAAC;AAElF,wBAAgB,uCAAuC,IAAI,gCAAgC,CAE1F"}
@@ -0,0 +1,6 @@
1
+ import Constants from 'expo-constants';
2
+ import { resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig, } from './oauthBrowserRuntimeCore';
3
+ export function resolveExpoOAuthBrowserRuntimeReadiness() {
4
+ return resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig(Constants.expoGoConfig);
5
+ }
6
+ //# sourceMappingURL=oauthBrowserRuntime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthBrowserRuntime.js","sourceRoot":"","sources":["../src/oauthBrowserRuntime.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,gBAAgB,CAAC;AAEvC,OAAO,EAEL,uDAAuD,GACxD,MAAM,2BAA2B,CAAC;AAInC,MAAM,UAAU,uCAAuC;IACrD,OAAO,uDAAuD,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;AACzF,CAAC","sourcesContent":["import Constants from 'expo-constants';\n\nimport {\n type ExpoOAuthBrowserRuntimeReadiness,\n resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig,\n} from './oauthBrowserRuntimeCore';\n\nexport type { ExpoOAuthBrowserRuntimeReadiness } from './oauthBrowserRuntimeCore';\n\nexport function resolveExpoOAuthBrowserRuntimeReadiness(): ExpoOAuthBrowserRuntimeReadiness {\n return resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig(Constants.expoGoConfig);\n}\n"]}
@@ -0,0 +1,9 @@
1
+ export type ExpoOAuthBrowserRuntimeReadiness = {
2
+ readonly status: 'ready';
3
+ } | {
4
+ readonly status: 'unsupported';
5
+ readonly reason: 'expo-go';
6
+ readonly message: string;
7
+ };
8
+ export declare function resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig(expoGoConfig: unknown): ExpoOAuthBrowserRuntimeReadiness;
9
+ //# sourceMappingURL=oauthBrowserRuntimeCore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthBrowserRuntimeCore.d.ts","sourceRoot":"","sources":["../src/oauthBrowserRuntimeCore.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gCAAgC,GACxC;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC5B;IACE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEN,wBAAgB,uDAAuD,CACrE,YAAY,EAAE,OAAO,GACpB,gCAAgC,CAWlC"}
@@ -0,0 +1,11 @@
1
+ export function resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig(expoGoConfig) {
2
+ if (expoGoConfig === null) {
3
+ return { status: 'ready' };
4
+ }
5
+ return {
6
+ status: 'unsupported',
7
+ reason: 'expo-go',
8
+ message: 'Brokered OAuth requires a development or standalone build with the configured app scheme; Expo Go cannot provide a stable OAuth callback scheme.',
9
+ };
10
+ }
11
+ //# sourceMappingURL=oauthBrowserRuntimeCore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthBrowserRuntimeCore.js","sourceRoot":"","sources":["../src/oauthBrowserRuntimeCore.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,uDAAuD,CACrE,YAAqB;IAErB,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAC7B,CAAC;IAED,OAAO;QACL,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,SAAS;QACjB,OAAO,EACL,kJAAkJ;KACrJ,CAAC;AACJ,CAAC","sourcesContent":["export type ExpoOAuthBrowserRuntimeReadiness =\n | { readonly status: 'ready' }\n | {\n readonly status: 'unsupported';\n readonly reason: 'expo-go';\n readonly message: string;\n };\n\nexport function resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig(\n expoGoConfig: unknown,\n): ExpoOAuthBrowserRuntimeReadiness {\n if (expoGoConfig === null) {\n return { status: 'ready' };\n }\n\n return {\n status: 'unsupported',\n reason: 'expo-go',\n message:\n 'Brokered OAuth requires a development or standalone build with the configured app scheme; Expo Go cannot provide a stable OAuth callback scheme.',\n };\n}\n"]}
@@ -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.5.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,20 @@
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
+ },
58
+ "./oauth-browser-runtime": {
59
+ "react-native": "./src/oauthBrowserRuntime.ts",
60
+ "browser": "./src/oauthBrowserRuntime.ts",
61
+ "types": "./src/oauthBrowserRuntime.ts",
62
+ "import": "./dist/oauthBrowserRuntime.js",
63
+ "default": "./dist/oauthBrowserRuntime.js"
64
+ },
51
65
  "./media-picker": {
52
66
  "react-native": "./src/mediaPicker/index.ts",
53
67
  "browser": "./src/mediaPicker/index.ts",
@@ -85,6 +99,7 @@
85
99
  "@ankhorage/zora": "^2.8.7",
86
100
  "expo": "~54.0.34",
87
101
  "expo-camera": "~17.0.10",
102
+ "expo-constants": "~18.0.13",
88
103
  "expo-document-picker": "~14.0.8",
89
104
  "expo-file-system": "~19.0.23",
90
105
  "expo-image-picker": "~17.0.11",
@@ -92,6 +107,9 @@
92
107
  "react-native": "0.81.5"
93
108
  },
94
109
  "peerDependenciesMeta": {
110
+ "expo-constants": {
111
+ "optional": true
112
+ },
95
113
  "expo-document-picker": {
96
114
  "optional": true
97
115
  },
@@ -112,6 +130,7 @@
112
130
  "@types/react": "^19.2.14",
113
131
  "expo": "~54.0.34",
114
132
  "expo-camera": "~17.0.10",
133
+ "expo-constants": "~18.0.13",
115
134
  "expo-document-picker": "~14.0.8",
116
135
  "expo-file-system": "~19.0.23",
117
136
  "expo-image-picker": "~17.0.11",
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig } from './oauthBrowserRuntimeCore';
4
+
5
+ describe('Expo OAuth browser runtime readiness', () => {
6
+ test('allows a custom native application build', () => {
7
+ expect(resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig(null)).toEqual({
8
+ status: 'ready',
9
+ });
10
+ });
11
+
12
+ test('rejects Expo Go with an actionable development-build requirement', () => {
13
+ expect(
14
+ resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig({ debuggerHost: '127.0.0.1:8081' }),
15
+ ).toEqual({
16
+ status: 'unsupported',
17
+ reason: 'expo-go',
18
+ message:
19
+ 'Brokered OAuth requires a development or standalone build with the configured app scheme; Expo Go cannot provide a stable OAuth callback scheme.',
20
+ });
21
+ });
22
+
23
+ test('does not serialize Expo Go configuration into readiness output', () => {
24
+ const readiness = resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig({
25
+ secretLikeValue: 'do-not-echo',
26
+ });
27
+
28
+ expect(JSON.stringify(readiness)).not.toContain('do-not-echo');
29
+ });
30
+ });
@@ -0,0 +1,12 @@
1
+ import Constants from 'expo-constants';
2
+
3
+ import {
4
+ type ExpoOAuthBrowserRuntimeReadiness,
5
+ resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig,
6
+ } from './oauthBrowserRuntimeCore';
7
+
8
+ export type { ExpoOAuthBrowserRuntimeReadiness } from './oauthBrowserRuntimeCore';
9
+
10
+ export function resolveExpoOAuthBrowserRuntimeReadiness(): ExpoOAuthBrowserRuntimeReadiness {
11
+ return resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig(Constants.expoGoConfig);
12
+ }
@@ -0,0 +1,22 @@
1
+ export type ExpoOAuthBrowserRuntimeReadiness =
2
+ | { readonly status: 'ready' }
3
+ | {
4
+ readonly status: 'unsupported';
5
+ readonly reason: 'expo-go';
6
+ readonly message: string;
7
+ };
8
+
9
+ export function resolveExpoOAuthBrowserRuntimeReadinessFromExpoGoConfig(
10
+ expoGoConfig: unknown,
11
+ ): ExpoOAuthBrowserRuntimeReadiness {
12
+ if (expoGoConfig === null) {
13
+ return { status: 'ready' };
14
+ }
15
+
16
+ return {
17
+ status: 'unsupported',
18
+ reason: 'expo-go',
19
+ message:
20
+ 'Brokered OAuth requires a development or standalone build with the configured app scheme; Expo Go cannot provide a stable OAuth callback scheme.',
21
+ };
22
+ }
@@ -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
+ }