@octane-xplat/auth 0.9.0 → 0.11.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.
Files changed (76) hide show
  1. package/README.md +115 -16
  2. package/dist/native/auth/src/base64.js +17 -0
  3. package/dist/native/auth/src/hosted.js +252 -0
  4. package/dist/native/auth/src/sha256.js +141 -0
  5. package/dist/native/index.js +6 -5
  6. package/dist/native/octane-client-build.json +1 -1
  7. package/dist/native/platform/src/a11y.js +1 -0
  8. package/dist/native/platform/src/app-info.js +1 -0
  9. package/dist/native/platform/src/auth-session.js +138 -0
  10. package/dist/native/platform/src/breakpoints.tsrx.js +5 -0
  11. package/dist/native/platform/src/clipboard.js +1 -0
  12. package/dist/native/platform/src/connectivity.js +1 -0
  13. package/dist/native/platform/src/deep-links.js +1 -0
  14. package/dist/native/platform/src/device.js +3 -0
  15. package/dist/native/platform/src/index.js +17 -0
  16. package/dist/native/platform/src/lifecycle.tsrx.js +7 -0
  17. package/dist/native/platform/src/locale.js +3 -0
  18. package/dist/native/platform/src/open-url.js +1 -0
  19. package/dist/native/platform/src/random.js +38 -0
  20. package/dist/native/platform/src/safe-area.tsrx.js +4 -0
  21. package/dist/native/platform/src/screen.tsrx.js +4 -0
  22. package/dist/native/platform/src/storage.js +1 -0
  23. package/dist/native/platform/src/system-bars.js +1 -0
  24. package/dist/native/secure-storage/src/index.js +2 -0
  25. package/dist/native/secure-storage/src/secure-storage.js +17 -0
  26. package/dist/web/auth/src/base64.js +17 -0
  27. package/dist/web/auth/src/hosted.js +251 -0
  28. package/dist/web/auth/src/sha256.js +141 -0
  29. package/dist/web/index.js +6 -5
  30. package/dist/web/octane-client-build.json +1 -1
  31. package/dist/web/platform/src/app-info.web.js +3 -0
  32. package/dist/web/platform/src/auth-session.web.js +10 -0
  33. package/dist/web/platform/src/breakpoints.web.tsrx.js +7 -0
  34. package/dist/web/platform/src/clipboard.web.js +3 -0
  35. package/dist/web/platform/src/deep-links.web.js +3 -0
  36. package/dist/web/platform/src/device.web.js +2 -0
  37. package/dist/web/platform/src/host-bridge.web.js +18 -0
  38. package/dist/web/platform/src/host-protocol.js +73 -0
  39. package/dist/web/platform/src/host-runtime.web.js +76 -0
  40. package/dist/web/platform/src/index.web.js +13 -0
  41. package/dist/web/platform/src/lifecycle.web.tsrx.js +3 -0
  42. package/dist/web/platform/src/locale.web.js +10 -0
  43. package/dist/web/platform/src/random.web.js +21 -0
  44. package/dist/web/platform/src/safe-area.web.tsrx.js +3 -0
  45. package/dist/web/platform/src/screen.web.tsrx.js +3 -0
  46. package/dist/web/platform/src/webauthn.web.js +2 -0
  47. package/dist/web/secure-storage/src/secure-storage.web.js +21 -0
  48. package/package.json +11 -5
  49. package/src/AppleSignInButton.macos.tsrx +28 -30
  50. package/src/GoogleSignInButton.macos.tsrx +18 -4
  51. package/src/apple-button.macos.ts +75 -0
  52. package/src/apple.macos.ts +87 -130
  53. package/src/base64.ts +25 -0
  54. package/src/google.macos.ts +81 -14
  55. package/src/hosted.ts +392 -0
  56. package/src/index.macos.ts +17 -0
  57. package/src/index.ts +17 -0
  58. package/src/index.web.ts +17 -0
  59. package/src/sha256.ts +118 -0
  60. package/src/types.ts +216 -4
  61. package/types/generated/.tsrx-typegen-manifest.json +3 -0
  62. package/types/generated/base64.d.ts +1 -0
  63. package/types/generated/hosted.d.ts +6 -0
  64. package/types/generated/index.d.ts +2 -1
  65. package/types/generated/index.web.d.ts +2 -1
  66. package/types/generated/sha256.d.ts +4 -0
  67. package/types/generated/types.d.ts +200 -4
  68. package/types/index.macos.d.ts +40 -0
  69. /package/dist/native/{AppleSignInButton.tsrx.js → auth/src/AppleSignInButton.tsrx.js} +0 -0
  70. /package/dist/native/{GoogleSignInButton.tsrx.js → auth/src/GoogleSignInButton.tsrx.js} +0 -0
  71. /package/dist/native/{apple.js → auth/src/apple.js} +0 -0
  72. /package/dist/native/{google.js → auth/src/google.js} +0 -0
  73. /package/dist/web/{AppleSignInButton.web.tsrx.js → auth/src/AppleSignInButton.web.tsrx.js} +0 -0
  74. /package/dist/web/{GoogleSignInButton.web.tsrx.js → auth/src/GoogleSignInButton.web.tsrx.js} +0 -0
  75. /package/dist/web/{apple.web.js → auth/src/apple.web.js} +0 -0
  76. /package/dist/web/{google.web.js → auth/src/google.web.js} +0 -0
@@ -3,6 +3,7 @@
3
3
  // available since macOS 10.15). The `com.apple.developer.applesignin`
4
4
  // entitlement must sit on the packaged app for the sheet to present;
5
5
  // hosts without it still load the framework and surface the OS error.
6
+ import { sha256Hex } from './sha256'
6
7
  import type {
7
8
  AppleAuth,
8
9
  AppleAuthConfig,
@@ -41,92 +42,11 @@ const SCOPE_MAP: Record<string, () => any> = {
41
42
  function isCancel(error: unknown): boolean {
42
43
  // ASAuthorizationError.canceled = 1001 (NSError.code).
43
44
  return (
44
- (error as any)?.code === 1001 ||
45
- /cancel/i.test(String((error as any)?.localizedDescription ?? error))
45
+ (error as any)?.domain === 'com.apple.AuthenticationServices.AuthorizationError' &&
46
+ Number((error as any)?.code) === 1001
46
47
  )
47
48
  }
48
49
 
49
- function utf8Bytes(input: string): Uint8Array {
50
- const data = NSString.stringWithString(input).dataUsingEncoding(4 /* NSUTF8StringEncoding */)
51
- return new Uint8Array(interop.bufferFromData(data))
52
- }
53
-
54
- // FIPS 180-4 — the request nonce must be the hex SHA-256 of the app's nonce
55
- // (same transform the iOS plugin applies via CommonCrypto, which has no
56
- // node-api metadata). Kept local to stay dependency-free.
57
- function sha256Hex(input: string): string {
58
- const K = new Uint32Array([
59
- 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
60
- 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
61
- 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
62
- 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
63
- 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
64
- 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
65
- 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
66
- 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
67
- ])
68
-
69
- const H = new Uint32Array([
70
- 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
71
- ])
72
-
73
- const bytes = utf8Bytes(input)
74
- const bitLen = bytes.length * 8
75
- const padded = new Uint8Array((bytes.length + 9 + 63) & ~63)
76
- padded.set(bytes)
77
- padded[bytes.length] = 0x80
78
- const dv = new DataView(padded.buffer)
79
- dv.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000))
80
- dv.setUint32(padded.length - 4, bitLen >>> 0)
81
- const w = new Uint32Array(64)
82
- for (let block = 0; block < padded.length; block += 64) {
83
- for (let t = 0; t < 16; t++) {
84
- w[t] = dv.getUint32(block + t * 4)
85
- }
86
-
87
- for (let t = 16; t < 64; t++) {
88
- const s0 =
89
- ((w[t - 15] >>> 7) | (w[t - 15] << 25)) ^
90
- ((w[t - 15] >>> 18) | (w[t - 15] << 14)) ^
91
- (w[t - 15] >>> 3)
92
-
93
- const s1 =
94
- ((w[t - 2] >>> 17) | (w[t - 2] << 15)) ^
95
- ((w[t - 2] >>> 19) | (w[t - 2] << 13)) ^
96
- (w[t - 2] >>> 10)
97
-
98
- w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0
99
- }
100
-
101
- let [a, b, c, d, e, f, g, h] = H
102
- for (let t = 0; t < 64; t++) {
103
- const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7))
104
- const t1 = (h + S1 + ((e & f) ^ (~e & g)) + K[t] + w[t]) >>> 0
105
- const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10))
106
- const t2 = (S0 + ((a & b) ^ (a & c) ^ (b & c))) >>> 0
107
- h = g
108
- g = f
109
- f = e
110
- e = (d + t1) >>> 0
111
- d = c
112
- c = b
113
- b = a
114
- a = (t1 + t2) >>> 0
115
- }
116
-
117
- H[0] = (H[0] + a) >>> 0
118
- H[1] = (H[1] + b) >>> 0
119
- H[2] = (H[2] + c) >>> 0
120
- H[3] = (H[3] + d) >>> 0
121
- H[4] = (H[4] + e) >>> 0
122
- H[5] = (H[5] + f) >>> 0
123
- H[6] = (H[6] + g) >>> 0
124
- H[7] = (H[7] + h) >>> 0
125
- }
126
-
127
- return [...H].map((x) => x.toString(16).padStart(8, '0')).join('')
128
- }
129
-
130
50
  function toCredential(credential: any): AuthCredential {
131
51
  const tokenData = credential.valueForKey('identityToken')
132
52
  const codeData = credential.valueForKey('authorizationCode')
@@ -189,15 +109,24 @@ function ensureClasses() {
189
109
 
190
110
  AppleAuthDelegate = NSObject.extend(
191
111
  {
192
- authorizationControllerDidCompleteWithAuthorization(_controller: any, authorization: any) {
193
- const finish = activeFinish
194
- activeFinish = null
195
- finish?.({ status: 'success', credential: toCredential(authorization.credential) })
112
+ authorizationControllerDidCompleteWithAuthorization(
113
+ this: any,
114
+ _controller: any,
115
+ authorization: any,
116
+ ) {
117
+ try {
118
+ const credential = toCredential(authorization.credential)
119
+ if (!credential.user.id || !credential.idToken) {
120
+ throw new Error('Sign in with Apple returned an incomplete credential')
121
+ }
122
+
123
+ this.finish?.({ status: 'success', credential })
124
+ } catch (error) {
125
+ this.finish?.({ status: 'error', message: String((error as any)?.message ?? error) })
126
+ }
196
127
  },
197
- authorizationControllerDidCompleteWithError(_controller: any, error: any) {
198
- const finish = activeFinish
199
- activeFinish = null
200
- finish?.(
128
+ authorizationControllerDidCompleteWithError(this: any, _controller: any, error: any) {
129
+ this.finish?.(
201
130
  isCancel(error)
202
131
  ? { status: 'cancelled' }
203
132
  : { status: 'error', message: String((error as any)?.localizedDescription ?? error) },
@@ -243,7 +172,7 @@ function ensureClasses() {
243
172
  let activeController: any = null
244
173
  let activeDelegate: any = null
245
174
  let activeAnchor: any = null
246
- let activeFinish: ((result: SignInResult) => void) | null = null
175
+ let active = false
247
176
 
248
177
  export const appleAuth: AppleAuth = {
249
178
  get supported() {
@@ -257,37 +186,59 @@ export const appleAuth: AppleAuth = {
257
186
  return { status: 'error', message: 'AuthenticationServices is unavailable on this host' }
258
187
  }
259
188
 
260
- if (activeController) {
189
+ if (active) {
261
190
  return { status: 'error', message: 'a Sign in with Apple flow is already active' }
262
191
  }
263
192
 
193
+ active = true
264
194
  return new Promise<SignInResult>((resolve) => {
195
+ let settled = false
265
196
  const finish = (result: SignInResult) => {
197
+ if (settled) {
198
+ return
199
+ }
200
+
201
+ settled = true
202
+ active = false
266
203
  activeController = null
267
204
  activeDelegate = null
268
205
  activeAnchor = null
269
- activeFinish = null
270
206
  resolve(result)
271
207
  }
272
208
 
273
- ensureClasses()
274
- activeDelegate = AppleAuthDelegate.new()
275
- activeFinish = finish
276
- const provider = ASAuthorizationAppleIDProvider.new()
277
- const request = provider.createRequest()
278
- if (options?.scopes?.length) {
279
- request.requestedScopes = options.scopes.map((s) => SCOPE_MAP[s]?.()).filter(Boolean)
280
- }
281
-
282
- if (options?.nonce !== undefined) {
283
- request.nonce = sha256Hex(options.nonce)
209
+ try {
210
+ const app = NSApplication.sharedApplication
211
+ if (!(app.keyWindow ?? app.mainWindow ?? app.windows?.firstObject)) {
212
+ throw new Error('Sign in with Apple requires an AppKit presentation window')
213
+ }
214
+
215
+ ensureClasses()
216
+ activeDelegate = AppleAuthDelegate.new()
217
+ activeDelegate.finish = finish
218
+ const provider = ASAuthorizationAppleIDProvider.new()
219
+ const request = provider.createRequest()
220
+ if (options?.scopes?.length) {
221
+ request.requestedScopes = options.scopes.map((s) => SCOPE_MAP[s]?.()).filter(Boolean)
222
+ }
223
+
224
+ if (options?.nonce !== undefined) {
225
+ request.nonce = sha256Hex(options.nonce)
226
+ }
227
+
228
+ activeAnchor = AppleAuthAnchor.new()
229
+ activeController = ASAuthorizationController.alloc().initWithAuthorizationRequests([
230
+ request,
231
+ ])
232
+
233
+ activeController.delegate = activeDelegate
234
+ activeController.presentationContextProvider = activeAnchor
235
+ activeController.performRequests()
236
+ } catch (error) {
237
+ finish({
238
+ status: 'error',
239
+ message: String((error as any)?.localizedDescription ?? (error as any)?.message ?? error),
240
+ })
284
241
  }
285
-
286
- activeAnchor = AppleAuthAnchor.new()
287
- activeController = ASAuthorizationController.alloc().initWithAuthorizationRequests([request])
288
- activeController.delegate = activeDelegate
289
- activeController.presentationContextProvider = activeAnchor
290
- activeController.performRequests()
291
242
  })
292
243
  },
293
244
  async getCredentialState(userId: string): Promise<AppleCredentialState> {
@@ -296,25 +247,31 @@ export const appleAuth: AppleAuth = {
296
247
  }
297
248
 
298
249
  return new Promise<AppleCredentialState>((resolve) => {
299
- ASAuthorizationAppleIDProvider.new().getCredentialStateForUserIDCompletion(
300
- userId,
301
- (state: number, error: any) => {
302
- if (error) {
303
- resolve('unknown')
304
- return
305
- }
306
-
307
- resolve(
308
- state === 1
309
- ? 'authorized'
310
- : state === 0
311
- ? 'revoked'
312
- : state === 3
313
- ? 'transferred'
314
- : 'notFound',
315
- )
316
- },
317
- )
250
+ try {
251
+ ASAuthorizationAppleIDProvider.new().getCredentialStateForUserIDCompletion(
252
+ userId,
253
+ (state: number, error: any) => {
254
+ if (error) {
255
+ resolve('unknown')
256
+ return
257
+ }
258
+
259
+ resolve(
260
+ state === 1
261
+ ? 'authorized'
262
+ : state === 0
263
+ ? 'revoked'
264
+ : state === 3
265
+ ? 'transferred'
266
+ : state === 2
267
+ ? 'notFound'
268
+ : 'unknown',
269
+ )
270
+ },
271
+ )
272
+ } catch {
273
+ resolve('unknown')
274
+ }
318
275
  })
319
276
  },
320
277
  }
package/src/base64.ts ADDED
@@ -0,0 +1,25 @@
1
+ // Unpadded base64url (RFC 4648 §5) — the encoding PKCE and hosted-auth
2
+ // wire formats use. `btoa` is not declared on every target's typecheck,
3
+ // so encode by hand.
4
+
5
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
6
+
7
+ export function bytesToBase64Url(bytes: Uint8Array): string {
8
+ let out = ''
9
+ for (let i = 0; i < bytes.length; i += 3) {
10
+ const a = bytes[i]
11
+ const b = i + 1 < bytes.length ? bytes[i + 1] : 0
12
+ const c = i + 2 < bytes.length ? bytes[i + 2] : 0
13
+ out += ALPHABET[a >> 2]
14
+ out += ALPHABET[((a & 0x03) << 4) | (b >> 4)]
15
+ if (i + 1 < bytes.length) {
16
+ out += ALPHABET[((b & 0x0f) << 2) | (c >> 6)]
17
+ }
18
+
19
+ if (i + 2 < bytes.length) {
20
+ out += ALPHABET[c & 0x3f]
21
+ }
22
+ }
23
+
24
+ return out
25
+ }
@@ -1,18 +1,85 @@
1
- // Google Sign-In — macOS leaf. The AppKit host has no NativeScript plugin
2
- // runtime; apps that need Google sign-in on macOS run the hosted ceremony
3
- // through `@octane-xplat/platform`'s `authSession` instead.
4
- import type { GoogleAuth, SignInResult } from './types'
5
-
6
- const UNSUPPORTED: SignInResult = {
7
- status: 'error',
8
- message: 'Google Sign-In is unsupported by the AppKit host — use a hosted authSession flow',
9
- }
1
+ // Google credentials on AppKit come from the app's hosted OAuth backend.
2
+ // The shared methods/result stay identical to the SDK leaves.
3
+ import { authSession } from '@octane-xplat/platform'
4
+ import type { GoogleAuth, GoogleAuthConfig, SignInResult } from './types'
5
+
6
+ let configuration: GoogleAuthConfig | undefined
7
+ let active = false
10
8
 
11
9
  export const googleAuth: GoogleAuth = {
12
- supported: false,
13
- async configure() {},
14
- async signIn(): Promise<SignInResult> {
15
- return UNSUPPORTED
10
+ get supported() {
11
+ return authSession.supported
12
+ },
13
+ async configure(config) {
14
+ configuration = config ? { ...config, scopes: config.scopes?.slice() } : undefined
15
+ },
16
+ async signIn(options = {}): Promise<SignInResult> {
17
+ const config = configuration
18
+ const flow = config?.hostedFlow
19
+ const session = authSession.impl
20
+ if (!session) {
21
+ return { status: 'error', message: 'AuthenticationServices is unavailable on this host' }
22
+ }
23
+
24
+ if (!flow) {
25
+ return {
26
+ status: 'error',
27
+ message: 'Google Sign-In on macOS requires configure({ hostedFlow })',
28
+ }
29
+ }
30
+
31
+ if (active) {
32
+ return { status: 'error', message: 'a Google Sign-In flow is already active' }
33
+ }
34
+
35
+ active = true
36
+ try {
37
+ const request = await flow.createRequest({
38
+ ...options,
39
+ clientId: config?.clientId,
40
+ serverClientId: config?.serverClientId,
41
+ scopes: config?.scopes?.slice(),
42
+ hostedDomain: config?.hostedDomain,
43
+ })
44
+
45
+ // Backend URLs must use TLS; the backend owns Google registration,
46
+ // state/nonce binding and single-use callback-code redemption.
47
+ if (!/^https:\/\//i.test(request.url)) {
48
+ throw new Error('Google hosted sign-in requires an HTTPS URL')
49
+ }
50
+
51
+ const result = await session.open(request.url, {
52
+ callbackScheme: request.callbackScheme,
53
+ prefersEphemeralSession: true,
54
+ })
55
+
56
+ if (result.type === 'cancel') {
57
+ return { status: 'cancelled' }
58
+ }
59
+
60
+ if (result.type === 'error') {
61
+ return { status: 'error', message: result.message }
62
+ }
63
+
64
+ const credential = await flow.complete(result.url)
65
+ if (
66
+ credential?.provider !== 'google' ||
67
+ typeof credential.user?.id !== 'string' ||
68
+ !credential.user.id ||
69
+ !Array.isArray(credential.scopes) ||
70
+ credential.scopes.some((scope) => typeof scope !== 'string')
71
+ ) {
72
+ throw new Error('Google hosted sign-in returned an invalid credential')
73
+ }
74
+
75
+ return { status: 'success', credential }
76
+ } catch (error) {
77
+ return { status: 'error', message: String((error as any)?.message ?? error) }
78
+ } finally {
79
+ active = false
80
+ }
81
+ },
82
+ async signOut() {
83
+ await configuration?.hostedFlow?.signOut?.()
16
84
  },
17
- async signOut() {},
18
85
  }