@atproto/oauth-client-expo 0.0.1

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 (81) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE.txt +7 -0
  3. package/README.md +140 -0
  4. package/android/.editorconfig +2 -0
  5. package/android/build.gradle +47 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/expo/modules/atprotooauthclient/Crypto.kt +69 -0
  8. package/android/src/main/java/expo/modules/atprotooauthclient/ExpoAtprotoOAuthClientModule.kt +41 -0
  9. package/android/src/main/java/expo/modules/atprotooauthclient/Jose.kt +116 -0
  10. package/android/src/main/java/expo/modules/atprotooauthclient/Records.kt +61 -0
  11. package/dist/ExpoAtprotoOAuthClientModule.d.ts +22 -0
  12. package/dist/ExpoAtprotoOAuthClientModule.d.ts.map +1 -0
  13. package/dist/ExpoAtprotoOAuthClientModule.js +3 -0
  14. package/dist/ExpoAtprotoOAuthClientModule.js.map +1 -0
  15. package/dist/ExpoAtprotoOAuthClientModule.types.d.ts +2 -0
  16. package/dist/ExpoAtprotoOAuthClientModule.types.d.ts.map +1 -0
  17. package/dist/ExpoAtprotoOAuthClientModule.types.js +2 -0
  18. package/dist/ExpoAtprotoOAuthClientModule.types.js.map +1 -0
  19. package/dist/expo-oauth-client-interface.d.ts +6 -0
  20. package/dist/expo-oauth-client-interface.d.ts.map +1 -0
  21. package/dist/expo-oauth-client-interface.js +2 -0
  22. package/dist/expo-oauth-client-interface.js.map +1 -0
  23. package/dist/expo-oauth-client-options.d.ts +9 -0
  24. package/dist/expo-oauth-client-options.d.ts.map +1 -0
  25. package/dist/expo-oauth-client-options.js +2 -0
  26. package/dist/expo-oauth-client-options.js.map +1 -0
  27. package/dist/expo-oauth-client.native.d.ts +13 -0
  28. package/dist/expo-oauth-client.native.d.ts.map +1 -0
  29. package/dist/expo-oauth-client.native.js +130 -0
  30. package/dist/expo-oauth-client.native.js.map +1 -0
  31. package/dist/expo-oauth-client.web.d.ts +9 -0
  32. package/dist/expo-oauth-client.web.d.ts.map +1 -0
  33. package/dist/expo-oauth-client.web.js +24 -0
  34. package/dist/expo-oauth-client.web.js.map +1 -0
  35. package/dist/index.d.ts +4 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +2 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/polyfill.d.ts +5 -0
  40. package/dist/polyfill.d.ts.map +1 -0
  41. package/dist/polyfill.js +5 -0
  42. package/dist/polyfill.js.map +1 -0
  43. package/dist/utils/expo-key.d.ts +11 -0
  44. package/dist/utils/expo-key.d.ts.map +1 -0
  45. package/dist/utils/expo-key.js +29 -0
  46. package/dist/utils/expo-key.js.map +1 -0
  47. package/dist/utils/mmkv-simple-store-ttl.d.ts +24 -0
  48. package/dist/utils/mmkv-simple-store-ttl.d.ts.map +1 -0
  49. package/dist/utils/mmkv-simple-store-ttl.js +62 -0
  50. package/dist/utils/mmkv-simple-store-ttl.js.map +1 -0
  51. package/dist/utils/mmkv-simple-store.d.ts +18 -0
  52. package/dist/utils/mmkv-simple-store.d.ts.map +1 -0
  53. package/dist/utils/mmkv-simple-store.js +31 -0
  54. package/dist/utils/mmkv-simple-store.js.map +1 -0
  55. package/dist/utils/stores.d.ts +24 -0
  56. package/dist/utils/stores.d.ts.map +1 -0
  57. package/dist/utils/stores.js +99 -0
  58. package/dist/utils/stores.js.map +1 -0
  59. package/expo-module.config.json +9 -0
  60. package/ios/Crypto.swift +83 -0
  61. package/ios/ExpoAtprotoOAuthClient.podspec +31 -0
  62. package/ios/ExpoAtprotoOAuthClientModule.swift +45 -0
  63. package/ios/Jose.swift +137 -0
  64. package/ios/Records.swift +58 -0
  65. package/package.json +52 -0
  66. package/src/ExpoAtprotoOAuthClientModule.ts +33 -0
  67. package/src/ExpoAtprotoOAuthClientModule.types.ts +2 -0
  68. package/src/expo-oauth-client-interface.ts +10 -0
  69. package/src/expo-oauth-client-options.ts +27 -0
  70. package/src/expo-oauth-client.d.ts +6 -0
  71. package/src/expo-oauth-client.native.ts +111 -0
  72. package/src/expo-oauth-client.web.ts +42 -0
  73. package/src/index.ts +4 -0
  74. package/src/polyfill.ts +4 -0
  75. package/src/utils/expo-key.ts +50 -0
  76. package/src/utils/mmkv-simple-store-ttl.ts +90 -0
  77. package/src/utils/mmkv-simple-store.ts +48 -0
  78. package/src/utils/stores.ts +115 -0
  79. package/tsconfig.build.json +8 -0
  80. package/tsconfig.build.tsbuildinfo +1 -0
  81. package/tsconfig.json +4 -0
@@ -0,0 +1,83 @@
1
+ import Foundation
2
+ import CryptoKit
3
+ import JOSESwift
4
+
5
+ class CryptoUtil: NSObject {
6
+ static func digest(data: Data) -> Data {
7
+ let hash = SHA256.hash(data: data)
8
+ return Data(hash)
9
+ }
10
+
11
+ public static func getRandomValues(byteLength: Int) -> Data {
12
+ let bytes = (0..<byteLength).map { _ in UInt8.random(in: UInt8.min...UInt8.max) }
13
+ return Data(bytes)
14
+ }
15
+
16
+ static func generateJwk() -> EncodedJWK {
17
+ let kid = UUID().uuidString
18
+
19
+ let privKey = P256.Signing.PrivateKey()
20
+ let pubKey = privKey.publicKey
21
+
22
+ let x = pubKey.x963Representation[1..<33].base64URLEncodedString()
23
+ let y = pubKey.x963Representation[33...].base64URLEncodedString()
24
+ let d = privKey.rawRepresentation.base64URLEncodedString()
25
+
26
+ let jwk = EncodedJWK()
27
+ jwk.kty = "EC"
28
+ jwk.crv = "P-256"
29
+ jwk.kid = kid
30
+ jwk.x = x
31
+ jwk.y = y
32
+ jwk.d = d
33
+ jwk.alg = "ES256"
34
+
35
+ return jwk
36
+ }
37
+
38
+ static func decodeJwk(x: String, y: String, d: String) throws -> SecKey {
39
+ func base64UrlDecode(_ string: String) -> Data? {
40
+ var base64 = string
41
+ .replacingOccurrences(of: "-", with: "+")
42
+ .replacingOccurrences(of: "_", with: "/")
43
+
44
+ let remainder = base64.count % 4
45
+ if remainder > 0 {
46
+ base64 += String(repeating: "=", count: 4 - remainder)
47
+ }
48
+
49
+ return Data(base64Encoded: base64)
50
+ }
51
+
52
+ guard let xData = base64UrlDecode(x),
53
+ let yData = base64UrlDecode(y),
54
+ let dData = base64UrlDecode(d) else {
55
+ throw ExpoAtprotoOAuthClientError.invalidJwk
56
+ }
57
+
58
+ var keyData = Data()
59
+ keyData.append(0x04)
60
+ keyData.append(xData)
61
+ keyData.append(yData)
62
+ keyData.append(dData)
63
+
64
+ let attributes: [String: Any] = [
65
+ kSecAttrKeyType as String: kSecAttrKeyTypeEC,
66
+ kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
67
+ kSecAttrKeySizeInBits as String: 256
68
+ ]
69
+
70
+ var error: Unmanaged<CFError>?
71
+
72
+ let key = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error)
73
+ if error != nil {
74
+ throw error!.takeUnretainedValue()
75
+ }
76
+
77
+ guard let key = key else {
78
+ throw ExpoAtprotoOAuthClientError.invalidJwk
79
+ }
80
+
81
+ return key
82
+ }
83
+ }
@@ -0,0 +1,31 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ExpoAtprotoOAuthClient'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.authors = package['authors']
13
+ s.homepage = package['homepage']
14
+ s.platforms = {
15
+ :ios => '15.1',
16
+ :tvos => '15.1'
17
+ }
18
+ s.swift_version = '5.4'
19
+ s.source = { git: 'https://github.com/bluesky-social/atproto' }
20
+ s.static_framework = true
21
+
22
+ s.dependency 'ExpoModulesCore'
23
+ s.dependency 'JOSESwift', '~> 2.3'
24
+
25
+ # Swift/Objective-C compatibility
26
+ s.pod_target_xcconfig = {
27
+ 'DEFINES_MODULE' => 'YES',
28
+ }
29
+
30
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
31
+ end
@@ -0,0 +1,45 @@
1
+ import ExpoModulesCore
2
+
3
+ enum ExpoAtprotoOAuthClientError: Error {
4
+ case unsupportedAlgorithm(String)
5
+ case invalidJwk
6
+ case invalidHeader(String)
7
+ case invalidPayload(String)
8
+ case nullSigner
9
+ }
10
+
11
+ public class ExpoAtprotoOAuthClientModule: Module {
12
+ public func definition() -> ModuleDefinition {
13
+ Name("ExpoAtprotoOAuthClient")
14
+
15
+ AsyncFunction("digest") { (data: Data, algo: String) throws -> Data in
16
+ if algo != "sha256" {
17
+ throw ExpoAtprotoOAuthClientError.unsupportedAlgorithm(algo)
18
+ }
19
+ return CryptoUtil.digest(data: data)
20
+ }
21
+
22
+ AsyncFunction("getRandomValues") { (byteLength: Int) -> Data in
23
+ return CryptoUtil.getRandomValues(byteLength: byteLength)
24
+ }
25
+
26
+ AsyncFunction("generatePrivateJwk") { (algo: String) throws -> EncodedJWK in
27
+ if algo != "ES256" {
28
+ throw ExpoAtprotoOAuthClientError.unsupportedAlgorithm(algo)
29
+ }
30
+ return CryptoUtil.generateJwk()
31
+ }
32
+
33
+ AsyncFunction("createJwt") { (header: String, payload: String, jwk: EncodedJWK) throws -> String in
34
+ let jwk = try CryptoUtil.decodeJwk(x: jwk.x, y: jwk.y, d: jwk.d)
35
+ let jwt = try JoseUtil.createJwt(header: header, payload: payload, jwk: jwk)
36
+ return jwt
37
+ }
38
+
39
+ AsyncFunction("verifyJwt") { (token: String, jwk: EncodedJWK, options: VerifyOptions) throws -> VerifyResult in
40
+ let jwk = try CryptoUtil.decodeJwk(x: jwk.x, y: jwk.y, d: jwk.d)
41
+ let res = try JoseUtil.verifyJwt(token: token, jwk: jwk, options: options)
42
+ return res
43
+ }
44
+ }
45
+ }
package/ios/Jose.swift ADDED
@@ -0,0 +1,137 @@
1
+ import JOSESwift
2
+
3
+ class JoseUtil: NSObject {
4
+ private static func headerStringToHeader(_ headerString: String) -> JWSHeader? {
5
+ guard let headerData = headerString.data(using: .utf8) else {
6
+ return nil
7
+ }
8
+ return JWSHeader(headerData)
9
+ }
10
+
11
+ private static func payloadStringToPayload(_ payloadString: String) -> Payload? {
12
+ guard let payloadData = payloadString.data(using: .utf8) else {
13
+ return nil
14
+ }
15
+ return Payload(payloadData)
16
+ }
17
+
18
+ static func createJwt(header: String, payload: String, jwk: SecKey) throws -> String {
19
+ guard let header = headerStringToHeader(header) else {
20
+ throw ExpoAtprotoOAuthClientError.invalidHeader("could not parse header string")
21
+ }
22
+
23
+ guard let payload = payloadStringToPayload(payload) else {
24
+ throw ExpoAtprotoOAuthClientError.invalidPayload("could not parse payload string")
25
+ }
26
+
27
+ let signer = Signer(signingAlgorithm: .ES256, key: jwk)
28
+
29
+ guard let signer = signer else {
30
+ throw ExpoAtprotoOAuthClientError.nullSigner
31
+ }
32
+
33
+ let jws = try JWS(header: header, payload: payload, signer: signer)
34
+
35
+ return jws.compactSerializedString
36
+ }
37
+
38
+ static func verifyJwt(token: String, jwk: SecKey, options: VerifyOptions) throws -> VerifyResult {
39
+ guard let jws = try? JWS(compactSerialization: token),
40
+ let verifier = Verifier(verifyingAlgorithm: .ES256, key: jwk),
41
+ let validation = try? jws.validate(using: verifier)
42
+ else {
43
+ throw ExpoAtprotoOAuthClientError.invalidJwk
44
+ }
45
+
46
+ let header = validation.header
47
+ let payload = String(data: validation.payload.data(), encoding: .utf8)
48
+ guard let payload = payload else {
49
+ throw ExpoAtprotoOAuthClientError.invalidPayload("unable to parse payload")
50
+ }
51
+
52
+ var protectedHeader: [String: Any] = [:]
53
+ protectedHeader["alg"] = "ES256"
54
+ if header.jku != nil {
55
+ protectedHeader["jku"] = header.jku?.absoluteString
56
+ }
57
+ if header.kid != nil {
58
+ protectedHeader["kid"] = header.kid
59
+ }
60
+ if header.typ != nil {
61
+ protectedHeader["typ"] = header.typ
62
+ }
63
+ if header.cty != nil {
64
+ protectedHeader["cty"] = header.cty
65
+ }
66
+ if header.crit != nil {
67
+ protectedHeader["crit"] = header.crit
68
+ }
69
+
70
+ if let typ = options.typ {
71
+ if header.typ != typ {
72
+ throw ExpoAtprotoOAuthClientError.invalidPayload("typ mismatch")
73
+ }
74
+ }
75
+
76
+ let claims = try JSONSerialization.jsonObject(with: validation.payload.data(), options: []) as? [String: Any]
77
+
78
+ if let requiredClaims = options.requiredClaims {
79
+ try requiredClaims.forEach { c in
80
+ if claims?[c] == nil {
81
+ throw ExpoAtprotoOAuthClientError.invalidPayload("required claim \(c) missing")
82
+ }
83
+ }
84
+ }
85
+
86
+ if let audience = options.audience {
87
+ if claims?["aud"] as? String != audience {
88
+ throw ExpoAtprotoOAuthClientError.invalidPayload("audience mismatch")
89
+ }
90
+ }
91
+
92
+ if let subject = options.subject {
93
+ if claims?["sub"] as? String != subject {
94
+ throw ExpoAtprotoOAuthClientError.invalidPayload("subject mismatch")
95
+ }
96
+ }
97
+
98
+ if let checkTolerance = options.clockTolerance {
99
+ let now = Date()
100
+ let expiryDate: Date
101
+ if let expiryString = claims?["exp"] as? String {
102
+ let formatter = ISO8601DateFormatter()
103
+ expiryDate = formatter.date(from: expiryString)!
104
+ } else {
105
+ throw ExpoAtprotoOAuthClientError.invalidPayload("expiry missing")
106
+ }
107
+ if expiryDate < now - checkTolerance {
108
+ throw ExpoAtprotoOAuthClientError.invalidPayload("token expired")
109
+ }
110
+ }
111
+
112
+ if let maxTokenAge = options.maxTokenAge {
113
+ let now = Date()
114
+ if let expiryString = claims?["exp"] as? String {
115
+ let formatter = ISO8601DateFormatter()
116
+ let expiryDate = formatter.date(from: expiryString)!
117
+ if expiryDate < now - maxTokenAge {
118
+ throw ExpoAtprotoOAuthClientError.invalidPayload("token expired")
119
+ }
120
+ } else {
121
+ throw ExpoAtprotoOAuthClientError.invalidPayload("expiry missing")
122
+ }
123
+ }
124
+
125
+ if let issuer = options.issuer {
126
+ if claims?["iss"] as? String != issuer {
127
+ throw ExpoAtprotoOAuthClientError.invalidPayload("issuer mismatch")
128
+ }
129
+ }
130
+
131
+ let res = VerifyResult()
132
+ res.payload = payload
133
+ res.protectedHeader = protectedHeader
134
+
135
+ return res
136
+ }
137
+ }
@@ -0,0 +1,58 @@
1
+ import ExpoModulesCore
2
+
3
+ struct EncodedJWK: Record {
4
+ @Field
5
+ var kty: String
6
+
7
+ @Field
8
+ var crv: String
9
+
10
+ @Field
11
+ var kid: String
12
+
13
+ @Field
14
+ var x: String
15
+
16
+ @Field
17
+ var y: String
18
+
19
+ @Field
20
+ var d: String
21
+
22
+ @Field
23
+ var alg: String
24
+ }
25
+
26
+ struct VerifyOptions: Record {
27
+ @Field
28
+ var audience: String?
29
+
30
+ @Field
31
+ var clockTolerance: Double?
32
+
33
+ @Field
34
+ var issuer: String?
35
+
36
+ @Field
37
+ var maxTokenAge: Double?
38
+
39
+ @Field
40
+ var subject: String?
41
+
42
+ @Field
43
+ var typ: String?
44
+
45
+ @Field
46
+ var currentDate: Date?
47
+
48
+ @Field
49
+ var requiredClaims: [String]?
50
+ }
51
+
52
+ struct VerifyResult: Record {
53
+ @Field
54
+ var payload: String
55
+
56
+ @Field
57
+ var protectedHeader: [String: Any]
58
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@atproto/oauth-client-expo",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "description": "ATPROTO OAuth client for Expo applications",
6
+ "authors": [
7
+ "Hailey <me@haileyok.com> (https://github.com/haileyok)",
8
+ "Matthieu Sieben <me@matthieusieben.com> (https://github.com/matthieusieben)"
9
+ ],
10
+ "keywords": [
11
+ "atproto",
12
+ "oauth",
13
+ "client",
14
+ "react-native",
15
+ "expo"
16
+ ],
17
+ "bugs": {
18
+ "url": "https://github.com/bluesky-social/atproto/issues"
19
+ },
20
+ "homepage": "https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-expo#readme",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/bluesky-social/atproto",
24
+ "directory": "packages/oauth/oauth-client-expo"
25
+ },
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "abortcontroller-polyfill": "^1.7.8",
36
+ "core-js": "^3",
37
+ "event-target-polyfill": "^0.0.4",
38
+ "expo-web-browser": "^15.0.8",
39
+ "react-native-mmkv": "^3.3.3",
40
+ "react-native-url-polyfill": "^3.0.0",
41
+ "@atproto-labs/simple-store": "0.3.0",
42
+ "@atproto/oauth-client": "0.5.7",
43
+ "@atproto/oauth-client-browser": "0.3.33"
44
+ },
45
+ "peerDependencies": {
46
+ "expo": "*",
47
+ "react-native": "*"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc --build tsconfig.build.json"
51
+ }
52
+ }
@@ -0,0 +1,33 @@
1
+ import { NativeModule, requireNativeModule } from 'expo'
2
+ import { SignedJwt, VerifyOptions, VerifyResult } from '@atproto/oauth-client'
3
+ import { ExpoAtprotoOAuthClientModuleEvents } from './ExpoAtprotoOAuthClientModule.types'
4
+
5
+ export type NativeJwk = {
6
+ kty: 'EC'
7
+ crv: 'P-256'
8
+ kid: string
9
+ x: string
10
+ y: string
11
+ d: string
12
+ alg: 'ES256'
13
+ }
14
+
15
+ declare class ExpoAtprotoOAuthClientModule extends NativeModule<ExpoAtprotoOAuthClientModuleEvents> {
16
+ digest(data: Uint8Array, algo: string): Promise<Uint8Array>
17
+
18
+ getRandomValues(byteLength: number): Promise<Uint8Array>
19
+
20
+ generatePrivateJwk(algorithm: string): Promise<NativeJwk>
21
+
22
+ createJwt(header: string, payload: string, jwk: NativeJwk): Promise<SignedJwt>
23
+
24
+ verifyJwt<C extends string = never>(
25
+ token: SignedJwt,
26
+ jwk: NativeJwk,
27
+ options: VerifyOptions<C>,
28
+ ): Promise<VerifyResult<C>>
29
+ }
30
+
31
+ export default requireNativeModule<ExpoAtprotoOAuthClientModule>(
32
+ 'ExpoAtprotoOAuthClient',
33
+ )
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line @typescript-eslint/ban-types
2
+ export type ExpoAtprotoOAuthClientModuleEvents = {}
@@ -0,0 +1,10 @@
1
+ import type {
2
+ AuthorizeOptions,
3
+ OAuthClient,
4
+ OAuthSession,
5
+ } from '@atproto/oauth-client'
6
+
7
+ export interface ExpoOAuthClientInterface extends OAuthClient, Disposable {
8
+ signIn(input: string, options?: AuthorizeOptions): Promise<OAuthSession>
9
+ handleCallback(): Promise<null | OAuthSession>
10
+ }
@@ -0,0 +1,27 @@
1
+ import type {
2
+ OAuthClientMetadataInput,
3
+ OAuthClientOptions,
4
+ OAuthResponseMode,
5
+ } from '@atproto/oauth-client'
6
+
7
+ export type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>
8
+
9
+ export type ExpoOAuthClientOptions = Simplify<
10
+ {
11
+ clientMetadata: Readonly<OAuthClientMetadataInput>
12
+ responseMode?: Exclude<OAuthResponseMode, 'form_post'>
13
+ } & Omit<
14
+ OAuthClientOptions,
15
+ | 'clientMetadata'
16
+ | 'responseMode'
17
+ | 'keyset'
18
+ | 'runtimeImplementation'
19
+ | 'sessionStore'
20
+ | 'stateStore'
21
+ | 'didCache'
22
+ | 'handleCache'
23
+ | 'dpopNonceCache'
24
+ | 'authorizationServerMetadataCache'
25
+ | 'protectedResourceMetadataCache'
26
+ >
27
+ >
@@ -0,0 +1,6 @@
1
+ import { ExpoOAuthClientInterface } from './expo-oauth-client-interface'
2
+ import { ExpoOAuthClientOptions } from './expo-oauth-client-options'
3
+
4
+ export declare class ExpoOAuthClient implements ExpoOAuthClientInterface {
5
+ constructor(options: ExpoOAuthClientOptions)
6
+ }
@@ -0,0 +1,111 @@
1
+ import './polyfill'
2
+
3
+ import { openAuthSessionAsync } from 'expo-web-browser'
4
+ import {
5
+ AuthorizeOptions,
6
+ OAuthClient,
7
+ OAuthSession,
8
+ } from '@atproto/oauth-client'
9
+ import { default as NativeModule } from './ExpoAtprotoOAuthClientModule'
10
+ import { ExpoOAuthClientInterface } from './expo-oauth-client-interface'
11
+ import { ExpoOAuthClientOptions } from './expo-oauth-client-options'
12
+ import { ExpoKey } from './utils/expo-key'
13
+ import {
14
+ AuthorizationServerMetadataCache,
15
+ DidCache,
16
+ DpopNonceCache,
17
+ HandleCache,
18
+ ProtectedResourceMetadataCache,
19
+ SessionStore,
20
+ StateStore,
21
+ } from './utils/stores'
22
+
23
+ export const CUSTOM_URI_SCHEME_REGEX = /^(?:[^.]+(?:\.[^.]+)+):\/(?:[^/].*)?$/
24
+ const isCustomUriScheme = (uri: string) => CUSTOM_URI_SCHEME_REGEX.test(uri)
25
+
26
+ export class ExpoOAuthClient
27
+ extends OAuthClient
28
+ implements ExpoOAuthClientInterface
29
+ {
30
+ readonly #disposables: DisposableStack
31
+
32
+ constructor(options: ExpoOAuthClientOptions) {
33
+ using stack = new DisposableStack()
34
+
35
+ super({
36
+ ...options,
37
+ responseMode: options.responseMode ?? 'query',
38
+ keyset: undefined,
39
+ runtimeImplementation: {
40
+ createKey: async (algs) => ExpoKey.generate(algs),
41
+ digest: async (bytes, { name }) => NativeModule.digest(bytes, name),
42
+ getRandomValues: async (length) => NativeModule.getRandomValues(length),
43
+ },
44
+ sessionStore: stack.use(new SessionStore()),
45
+ stateStore: stack.use(new StateStore()),
46
+ didCache: stack.use(new DidCache()),
47
+ handleCache: stack.use(new HandleCache()),
48
+ dpopNonceCache: stack.use(new DpopNonceCache()),
49
+ authorizationServerMetadataCache: stack.use(
50
+ new AuthorizationServerMetadataCache(),
51
+ ),
52
+ protectedResourceMetadataCache: stack.use(
53
+ new ProtectedResourceMetadataCache(),
54
+ ),
55
+ })
56
+
57
+ stack.defer(() => super[Symbol.dispose]?.())
58
+
59
+ this.#disposables = stack.move()
60
+ }
61
+
62
+ async handleCallback(): Promise<null | OAuthSession> {
63
+ return null
64
+ }
65
+
66
+ async signIn(
67
+ input: string,
68
+ options?: AuthorizeOptions,
69
+ ): Promise<OAuthSession> {
70
+ const redirectUri =
71
+ options?.redirect_uri ??
72
+ this.clientMetadata.redirect_uris.find(isCustomUriScheme)
73
+
74
+ if (!redirectUri) {
75
+ throw new TypeError(
76
+ 'A redirect URI with a custom scheme is required for Expo OAuth.',
77
+ )
78
+ }
79
+
80
+ const url = await this.authorize(input, {
81
+ ...options,
82
+ redirect_uri: redirectUri,
83
+ display: options?.display ?? 'touch',
84
+ })
85
+
86
+ console.debug('openAuthSessionAsync', { url, redirectUri })
87
+
88
+ const result = await openAuthSessionAsync(url.toString(), redirectUri)
89
+
90
+ console.debug('AUTH SESSION RESULT', result)
91
+
92
+ if (result.type === 'success') {
93
+ const callbackUrl = new URL(result.url)
94
+ const params =
95
+ this.responseMode === 'fragment'
96
+ ? new URLSearchParams(callbackUrl.hash.slice(1))
97
+ : callbackUrl.searchParams
98
+
99
+ const { session } = await this.callback(params, {
100
+ redirect_uri: redirectUri,
101
+ })
102
+ return session
103
+ } else {
104
+ throw new Error(`Authentication cancelled: ${result.type}`)
105
+ }
106
+ }
107
+
108
+ [Symbol.dispose]() {
109
+ this.#disposables.dispose()
110
+ }
111
+ }
@@ -0,0 +1,42 @@
1
+ import {
2
+ AuthorizeOptions,
3
+ BrowserOAuthClient,
4
+ OAuthSession,
5
+ } from '@atproto/oauth-client-browser'
6
+ import { ExpoOAuthClientInterface } from './expo-oauth-client-interface'
7
+ import { ExpoOAuthClientOptions } from './expo-oauth-client-options'
8
+
9
+ export class ExpoOAuthClient
10
+ extends BrowserOAuthClient
11
+ implements ExpoOAuthClientInterface
12
+ {
13
+ constructor({
14
+ clientMetadata,
15
+ responseMode = 'fragment',
16
+ ...options
17
+ }: ExpoOAuthClientOptions) {
18
+ super({ ...options, clientMetadata, responseMode })
19
+ }
20
+
21
+ override async signIn(
22
+ input: string,
23
+ options?: AuthorizeOptions,
24
+ ): Promise<OAuthSession> {
25
+ // Force popup mode
26
+ return this.signInPopup(input, {
27
+ ...options,
28
+ display: options?.display ?? 'touch',
29
+ })
30
+ }
31
+
32
+ async handleCallback(): Promise<null | OAuthSession> {
33
+ const params = this.readCallbackParams()
34
+ if (!params) return null
35
+
36
+ const url = this.findRedirectUrl()
37
+ if (!url) return null
38
+
39
+ const { session } = await this.initCallback(params, url)
40
+ return session
41
+ }
42
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export type { ExpoOAuthClientInterface } from './expo-oauth-client-interface'
2
+ export type { ExpoOAuthClientOptions } from './expo-oauth-client-options'
3
+
4
+ export { ExpoOAuthClient } from './expo-oauth-client'
@@ -0,0 +1,4 @@
1
+ import 'core-js/proposals/explicit-resource-management'
2
+ import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
3
+ import 'event-target-polyfill'
4
+ import 'react-native-url-polyfill/auto'