@growth-labs/auth 0.6.2 → 0.7.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 (62) hide show
  1. package/README.md +20 -0
  2. package/dist/routes/api/code-verify.d.ts.map +1 -1
  3. package/dist/routes/api/code-verify.js +1 -1
  4. package/dist/routes/api/code-verify.js.map +1 -1
  5. package/dist/routes/api/password-verify.d.ts.map +1 -1
  6. package/dist/routes/api/password-verify.js +1 -1
  7. package/dist/routes/api/password-verify.js.map +1 -1
  8. package/dist/routes/api/shared.d.ts +1 -1
  9. package/dist/routes/api/shared.d.ts.map +1 -1
  10. package/dist/routes/api/shared.js +25 -4
  11. package/dist/routes/api/shared.js.map +1 -1
  12. package/dist/routes/callback.d.ts.map +1 -1
  13. package/dist/routes/callback.js +4 -1
  14. package/dist/routes/callback.js.map +1 -1
  15. package/dist/routes/login.js.map +1 -1
  16. package/dist/utils/analytics.d.ts +2 -2
  17. package/dist/utils/analytics.d.ts.map +1 -1
  18. package/dist/utils/analytics.js +2 -1
  19. package/dist/utils/analytics.js.map +1 -1
  20. package/dist/utils/cookies.d.ts.map +1 -1
  21. package/dist/utils/cookies.js +1 -0
  22. package/dist/utils/cookies.js.map +1 -1
  23. package/package.json +8 -3
  24. package/src/client/login-page.ts +12 -0
  25. package/src/cloudflare-workers.d.ts +3 -0
  26. package/src/config-store.ts +70 -0
  27. package/src/index.ts +173 -0
  28. package/src/middleware/auth.ts +88 -0
  29. package/src/options.ts +75 -0
  30. package/src/renderers.ts +182 -0
  31. package/src/routes/api/code-request.ts +33 -0
  32. package/src/routes/api/code-verify.ts +35 -0
  33. package/src/routes/api/password-reset-confirm.ts +36 -0
  34. package/src/routes/api/password-reset-request.ts +32 -0
  35. package/src/routes/api/password-verify.ts +35 -0
  36. package/src/routes/api/shared.ts +130 -0
  37. package/src/routes/callback.ts +105 -0
  38. package/src/routes/flow.ts +53 -0
  39. package/src/routes/forgot-password.ts +31 -0
  40. package/src/routes/login.ts +130 -0
  41. package/src/routes/logout.ts +59 -0
  42. package/src/routes/password.ts +27 -0
  43. package/src/routes/reset-password.ts +33 -0
  44. package/src/routes/verify-code.ts +28 -0
  45. package/src/runtime-bridge.ts +15 -0
  46. package/src/runtime.ts +5 -0
  47. package/src/server.ts +185 -0
  48. package/src/types.ts +158 -0
  49. package/src/utils/analytics.ts +56 -0
  50. package/src/utils/client-secret.ts +21 -0
  51. package/src/utils/cookies.ts +134 -0
  52. package/src/utils/encoding.ts +5 -0
  53. package/src/utils/env-marker.ts +39 -0
  54. package/src/utils/identity-cookie.ts +138 -0
  55. package/src/utils/identity.ts +240 -0
  56. package/src/utils/index.ts +19 -0
  57. package/src/utils/issuer.ts +7 -0
  58. package/src/utils/openauth.ts +163 -0
  59. package/src/utils/pkce.ts +16 -0
  60. package/src/utils/state.ts +20 -0
  61. package/src/utils/tokens.ts +128 -0
  62. package/src/virtual.d.ts +5 -0
package/src/types.ts ADDED
@@ -0,0 +1,158 @@
1
+ export interface User {
2
+ id: string
3
+ email: string
4
+ name?: string | null
5
+ avatarUrl?: string | null
6
+ }
7
+
8
+ export type AuthenticatedUser = User
9
+ export type UnauthenticatedUser = null
10
+
11
+ export interface AuthRendererBrand {
12
+ title: string
13
+ brandName: string
14
+ logoUrl: string
15
+ }
16
+
17
+ export interface LoginRendererPayload {
18
+ providerUrls: {
19
+ google?: string
20
+ password?: string
21
+ emailCode?: string
22
+ }
23
+ errorCode?: 'auth_failed' | 'session_expired' | string
24
+ reset?: boolean
25
+ redirect: string
26
+ brand: AuthRendererBrand
27
+ }
28
+
29
+ export interface ForgotPasswordRendererPayload {
30
+ forgotPasswordUrl: string
31
+ passwordResetRequestUrl: string
32
+ errorCode?: string
33
+ sent?: boolean
34
+ loginUrl: string
35
+ brand: AuthRendererBrand
36
+ }
37
+
38
+ export interface ResetPasswordRendererPayload {
39
+ resetPasswordUrl: string
40
+ passwordResetConfirmUrl: string
41
+ errorCode?: string
42
+ token?: string
43
+ loginUrl: string
44
+ brand: AuthRendererBrand
45
+ }
46
+
47
+ export interface PasswordRendererPayload {
48
+ passwordVerifyUrl: string
49
+ forgotPasswordUrl: string
50
+ email?: string
51
+ errorCode?: string
52
+ redirect: string
53
+ loginUrl: string
54
+ brand: AuthRendererBrand
55
+ }
56
+
57
+ export interface VerifyCodeRendererPayload {
58
+ codeRequestUrl: string
59
+ codeVerifyUrl: string
60
+ email?: string
61
+ errorCode?: string
62
+ sent?: boolean
63
+ redirect: string
64
+ loginUrl: string
65
+ brand: AuthRendererBrand
66
+ }
67
+
68
+ export interface EmailCodeConfirmRendererPayload {
69
+ loginUrl: string
70
+ brand: AuthRendererBrand
71
+ }
72
+
73
+ export type AuthRendererResult = string | Response | Promise<string | Response>
74
+
75
+ export type LoginRenderer = (payload: LoginRendererPayload) => AuthRendererResult
76
+ export type ForgotPasswordRenderer = (payload: ForgotPasswordRendererPayload) => AuthRendererResult
77
+ export type ResetPasswordRenderer = (payload: ResetPasswordRendererPayload) => AuthRendererResult
78
+ export type PasswordRenderer = (payload: PasswordRendererPayload) => AuthRendererResult
79
+ export type VerifyCodeRenderer = (payload: VerifyCodeRendererPayload) => AuthRendererResult
80
+ export type EmailCodeConfirmRenderer = (
81
+ payload: EmailCodeConfirmRendererPayload,
82
+ ) => AuthRendererResult
83
+
84
+ /**
85
+ * A renderer registration for the `renderers` integration option.
86
+ *
87
+ * `module` is an ES module specifier resolvable by the consumer's bundler
88
+ * (path alias, package name, or relative path). `export` is the named
89
+ * export within that module that holds the renderer function. The Vite
90
+ * plugin emits a real static `import { <export> as <local> } from <module>`
91
+ * at the top of the virtual `virtual:growth-labs/auth/renderers` module,
92
+ * which makes the renderer fully bundleable and tree-shakeable in
93
+ * production. **Do not pass an inline async function** — that approach
94
+ * (via `Function.prototype.toString()`) crashed in production bundles in
95
+ * 0.3.6 because Vite's SSR transform rewrote dynamic imports inside the
96
+ * serialized body to `__vite_ssr_dynamic_import__()`, a helper that does
97
+ * not exist in production bundles. See `MIGRATION.md`.
98
+ */
99
+ export interface AuthRendererSpec {
100
+ module: string
101
+ export: string
102
+ }
103
+
104
+ export interface AuthRenderers {
105
+ login?: AuthRendererSpec
106
+ forgotPassword?: AuthRendererSpec
107
+ resetPassword?: AuthRendererSpec
108
+ password?: AuthRendererSpec
109
+ verifyCode?: AuthRendererSpec
110
+ emailCodeConfirm?: AuthRendererSpec
111
+ }
112
+
113
+ /**
114
+ * The shape of `virtual:growth-labs/auth/renderers` after the plugin
115
+ * resolves all registered specs into static ES imports. Internal — only
116
+ * the package's own routes import from this virtual module.
117
+ */
118
+ export interface ResolvedAuthRenderers {
119
+ login?: LoginRenderer
120
+ forgotPassword?: ForgotPasswordRenderer
121
+ resetPassword?: ResetPasswordRenderer
122
+ password?: PasswordRenderer
123
+ verifyCode?: VerifyCodeRenderer
124
+ emailCodeConfirm?: EmailCodeConfirmRenderer
125
+ }
126
+
127
+ export interface TokenRefreshResult {
128
+ accessToken: string
129
+ refreshToken: string
130
+ claims: {
131
+ sub: string
132
+ email: string
133
+ name?: string | null
134
+ image?: string
135
+ }
136
+ }
137
+
138
+ export interface TokenExchangeResult {
139
+ accessToken: string
140
+ refreshToken: string
141
+ isNewUser: boolean
142
+ provider: 'google' | 'password' | 'email-code' | 'unknown'
143
+ claims: {
144
+ sub: string
145
+ email: string
146
+ name?: string | null
147
+ image?: string
148
+ }
149
+ }
150
+
151
+ // Augment Astro's locals
152
+ declare global {
153
+ namespace App {
154
+ interface Locals {
155
+ user: User | null
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,56 @@
1
+ type TrackFn = (context: unknown, event: string, props?: Record<string, unknown>) => Promise<void>
2
+
3
+ let trackFn: TrackFn | null = null
4
+ let resolved = false
5
+ const ANALYTICS_UTILS_MODULE = '@growth-labs/analytics/utils'
6
+
7
+ async function importOptionalModule(modulePath: string): Promise<Record<string, unknown> | null> {
8
+ try {
9
+ return (await import(/* @vite-ignore */ modulePath)) as Record<string, unknown>
10
+ } catch {
11
+ return null
12
+ }
13
+ }
14
+
15
+ async function resolveAnalytics(): Promise<void> {
16
+ if (resolved) return
17
+ resolved = true
18
+ const mod = await importOptionalModule(ANALYTICS_UTILS_MODULE)
19
+ let candidate: unknown
20
+ try {
21
+ candidate = mod?.trackServerEvent
22
+ } catch {
23
+ candidate = undefined
24
+ }
25
+ if (typeof candidate === 'function') {
26
+ trackFn = candidate as TrackFn
27
+ }
28
+ }
29
+
30
+ export async function emitAuthEvent(
31
+ context: unknown,
32
+ eventName: string,
33
+ data: Record<string, unknown>,
34
+ ): Promise<void> {
35
+ await resolveAnalytics()
36
+ if (!trackFn) return
37
+ return trackFn(context, eventName, data)
38
+ }
39
+
40
+ export function analyticsAvailable(context: { locals?: Record<string, unknown> }): boolean {
41
+ void context
42
+ return true
43
+ }
44
+
45
+ export async function trackAuthEvent(
46
+ context: unknown,
47
+ eventName: string,
48
+ data: Record<string, unknown> = {},
49
+ ): Promise<void> {
50
+ return emitAuthEvent(context, eventName, data)
51
+ }
52
+
53
+ export function _resetAnalytics(): void {
54
+ trackFn = null
55
+ resolved = false
56
+ }
@@ -0,0 +1,21 @@
1
+ import { getRuntimeEnv } from '../runtime.js'
2
+
3
+ export function resolveClientSecret(clientSecret?: string): string | undefined {
4
+ if (!clientSecret) return undefined
5
+
6
+ const runtimeValue = getRuntimeEnv()[clientSecret]
7
+ if (typeof runtimeValue === 'string' && runtimeValue.length > 0) {
8
+ return runtimeValue
9
+ }
10
+
11
+ return clientSecret
12
+ }
13
+
14
+ export function createClientSecretBasicAuthHeader(clientId: string, clientSecret: string): string {
15
+ return `Basic ${btoa(`${formEncode(clientId)}:${formEncode(clientSecret)}`)}`
16
+ }
17
+
18
+ function formEncode(value: string): string {
19
+ const params = new URLSearchParams({ value })
20
+ return params.toString().slice('value='.length)
21
+ }
@@ -0,0 +1,134 @@
1
+ import type { ResolvedAuthOptions } from '../options.js'
2
+
3
+ export interface CookieAttributes {
4
+ path?: string
5
+ maxAge?: number
6
+ httpOnly?: boolean
7
+ secure?: boolean
8
+ sameSite?: 'strict' | 'lax' | 'none'
9
+ domain?: string
10
+ }
11
+
12
+ const IDENTITY_COOKIE_NAME = 'gl_id'
13
+ const IDENTITY_COOKIE_MAX_AGE = 2592000
14
+
15
+ export function getCookie(request: Request, name: string): string | null {
16
+ const header = request.headers.get('cookie')
17
+ if (!header) return null
18
+ const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
19
+ return match ? decodeURIComponent(match[1]) : null
20
+ }
21
+
22
+ export function setCookie(
23
+ response: Response,
24
+ name: string,
25
+ value: string,
26
+ attributes: CookieAttributes = {},
27
+ ): void {
28
+ const parts = [`${name}=${encodeURIComponent(value)}`]
29
+ if (attributes.path) parts.push(`Path=${attributes.path}`)
30
+ if (attributes.maxAge !== undefined) parts.push(`Max-Age=${attributes.maxAge}`)
31
+ if (attributes.httpOnly) parts.push('HttpOnly')
32
+ if (attributes.secure) parts.push('Secure')
33
+ if (attributes.sameSite) parts.push(`SameSite=${attributes.sameSite}`)
34
+ if (attributes.domain) parts.push(`Domain=${attributes.domain}`)
35
+ response.headers.append('Set-Cookie', parts.join('; '))
36
+ }
37
+
38
+ export function clearCookie(response: Response, name: string, domain?: string, path = '/'): void {
39
+ setCookie(response, name, '', {
40
+ path,
41
+ maxAge: 0,
42
+ ...(domain && { domain }),
43
+ })
44
+ }
45
+
46
+ export function setIdentityCookie(response: Response, value: string): void {
47
+ setCookie(response, IDENTITY_COOKIE_NAME, value, {
48
+ path: '/',
49
+ maxAge: IDENTITY_COOKIE_MAX_AGE,
50
+ httpOnly: true,
51
+ secure: true,
52
+ sameSite: 'lax',
53
+ })
54
+ }
55
+
56
+ export function clearIdentityCookie(response: Response): void {
57
+ setCookie(response, IDENTITY_COOKIE_NAME, '', {
58
+ path: '/',
59
+ maxAge: 0,
60
+ httpOnly: true,
61
+ secure: true,
62
+ sameSite: 'lax',
63
+ })
64
+ }
65
+
66
+ export function setTokenCookies(
67
+ response: Response,
68
+ options: ResolvedAuthOptions,
69
+ accessToken: string,
70
+ refreshToken: string,
71
+ ): void {
72
+ const base: CookieAttributes = {
73
+ path: '/',
74
+ httpOnly: true,
75
+ secure: true,
76
+ sameSite: 'lax',
77
+ ...(options.sessionCookieDomain && { domain: options.sessionCookieDomain }),
78
+ }
79
+ setCookie(response, `${options.cookiePrefix}_at`, accessToken, {
80
+ ...base,
81
+ maxAge: options.accessTokenMaxAge,
82
+ })
83
+ setCookie(response, `${options.cookiePrefix}_rt`, refreshToken, {
84
+ ...base,
85
+ maxAge: options.refreshTokenMaxAge,
86
+ })
87
+ }
88
+
89
+ export function clearTokenCookies(response: Response, options: ResolvedAuthOptions): void {
90
+ clearCookie(response, `${options.cookiePrefix}_at`, options.sessionCookieDomain)
91
+ clearCookie(response, `${options.cookiePrefix}_rt`, options.sessionCookieDomain)
92
+ }
93
+
94
+ export function clearAuthFlowCookies(response: Response, options: ResolvedAuthOptions): void {
95
+ clearCookie(response, `${options.cookiePrefix}_pkce`, undefined, options.callbackPath)
96
+ clearCookie(response, `${options.cookiePrefix}_pkce`)
97
+ clearCookie(response, `${options.cookiePrefix}_state`)
98
+ clearCookie(response, `${options.cookiePrefix}_redirect`)
99
+ clearCookie(response, `${options.cookiePrefix}_provider`)
100
+ clearCookie(response, `${options.cookiePrefix}_auth_path`)
101
+ }
102
+
103
+ export function setAuthFlowCookies(
104
+ response: Response,
105
+ options: ResolvedAuthOptions,
106
+ verifier: string,
107
+ state: string,
108
+ redirectPath: string,
109
+ provider?: string,
110
+ flowOptions: { pkcePath?: string; additionalPkcePaths?: string[] } = {},
111
+ ): void {
112
+ const baseAttrs: CookieAttributes = {
113
+ path: '/',
114
+ httpOnly: true,
115
+ secure: true,
116
+ sameSite: 'lax',
117
+ maxAge: 600,
118
+ }
119
+ const pkcePaths = new Set([
120
+ flowOptions.pkcePath ?? options.callbackPath,
121
+ ...(flowOptions.additionalPkcePaths ?? []),
122
+ ])
123
+ for (const path of pkcePaths) {
124
+ setCookie(response, `${options.cookiePrefix}_pkce`, verifier, {
125
+ ...baseAttrs,
126
+ path,
127
+ })
128
+ }
129
+ setCookie(response, `${options.cookiePrefix}_state`, state, baseAttrs)
130
+ setCookie(response, `${options.cookiePrefix}_redirect`, redirectPath, baseAttrs)
131
+ if (provider) {
132
+ setCookie(response, `${options.cookiePrefix}_provider`, provider, baseAttrs)
133
+ }
134
+ }
@@ -0,0 +1,5 @@
1
+ export function base64url(bytes: Uint8Array): string {
2
+ let binary = ''
3
+ for (const byte of bytes) binary += String.fromCharCode(byte)
4
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
5
+ }
@@ -0,0 +1,39 @@
1
+ const ENV_MARKER_PREFIX = 'env:'
2
+
3
+ /**
4
+ * Pure resolver: takes an `"env:VAR_NAME"` marker and an env bag, returns
5
+ * the bound value or throws when the binding is missing / empty. Plain
6
+ * strings pass through unchanged. Kept pure (no `cloudflare:workers`
7
+ * import) so `src/config-store.ts` — and therefore the package main entry
8
+ * — stays clear of the runtime-only specifier and the pure-Node import
9
+ * gate stays green.
10
+ *
11
+ * The runtime env is supplied at call sites (middleware/routes) which can
12
+ * pull from `cloudflare:workers` themselves, OR by `src/runtime-bridge.ts`
13
+ * which stashes it onto `globalThis.__GL_AUTH_RUNTIME_ENV__` for
14
+ * `getConfig()` to read implicitly.
15
+ */
16
+ export function resolveEnvMarker(
17
+ value: string | undefined,
18
+ env: Record<string, unknown>,
19
+ ): string | undefined {
20
+ if (typeof value !== 'string') return value
21
+ if (!value.startsWith(ENV_MARKER_PREFIX)) return value
22
+ const name = value.slice(ENV_MARKER_PREFIX.length)
23
+ if (!name) {
24
+ throw new Error(
25
+ '@growth-labs/auth: "env:" marker missing binding name (expected "env:VAR_NAME")',
26
+ )
27
+ }
28
+ const resolved = env[name]
29
+ if (typeof resolved !== 'string' || resolved.length === 0) {
30
+ throw new Error(
31
+ `@growth-labs/auth: env:${name} is not set in the Worker runtime env (declared as "env:${name}" in auth() options)`,
32
+ )
33
+ }
34
+ return resolved
35
+ }
36
+
37
+ export function isEnvMarker(value: unknown): boolean {
38
+ return typeof value === 'string' && value.startsWith(ENV_MARKER_PREFIX)
39
+ }
@@ -0,0 +1,138 @@
1
+ import { base64url } from './encoding.js'
2
+
3
+ const SECRET_BYTES = 32
4
+ const encoder = new TextEncoder()
5
+ const decoder = new TextDecoder()
6
+
7
+ export type IdentityCookieVerification = { valid: true; payload: string } | { valid: false }
8
+
9
+ export async function signIdentityCookie(payload: string, secret: string): Promise<string> {
10
+ if (!payload) {
11
+ throw new Error('identity cookie payload is required')
12
+ }
13
+
14
+ const payloadBytes = encoder.encode(payload)
15
+ const key = await importSecret(secret)
16
+ if (!key) {
17
+ throw new Error('GL_IDENTITY_SECRET must be a base64-encoded 32-byte secret')
18
+ }
19
+
20
+ const signature = new Uint8Array(
21
+ await crypto.subtle.sign('HMAC', key, toArrayBuffer(payloadBytes)),
22
+ )
23
+ return `${base64url(payloadBytes)}.${base64url(signature)}`
24
+ }
25
+
26
+ export async function createIdentityCookiePayload(
27
+ subject: string,
28
+ secret: string,
29
+ ): Promise<string> {
30
+ if (!subject || !secret) {
31
+ throw new Error('identity cookie subject and secret are required')
32
+ }
33
+
34
+ const digest = new Uint8Array(
35
+ await crypto.subtle.digest('SHA-256', encoder.encode(subject + secret)),
36
+ )
37
+ return [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('')
38
+ }
39
+
40
+ export async function verifyIdentityCookie(
41
+ cookieValue: string,
42
+ secret: string,
43
+ ): Promise<IdentityCookieVerification> {
44
+ if (!cookieValue || !secret) {
45
+ return { valid: false }
46
+ }
47
+
48
+ const parts = cookieValue.split('.')
49
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
50
+ return { valid: false }
51
+ }
52
+
53
+ const payloadBytes = decodeBase64Url(parts[0])
54
+ const signatureBytes = decodeBase64Url(parts[1])
55
+ const key = await importSecret(secret)
56
+ if (!payloadBytes || !signatureBytes || !key) {
57
+ return { valid: false }
58
+ }
59
+
60
+ const expected = new Uint8Array(
61
+ await crypto.subtle.sign('HMAC', key, toArrayBuffer(payloadBytes)),
62
+ )
63
+ if (!timingSafeEqual(expected, signatureBytes)) {
64
+ return { valid: false }
65
+ }
66
+
67
+ return {
68
+ valid: true,
69
+ payload: decoder.decode(payloadBytes),
70
+ }
71
+ }
72
+
73
+ export function timingSafeEqual(left: ArrayLike<number>, right: ArrayLike<number>): boolean {
74
+ const length = Math.max(left.length, right.length)
75
+ let diff = left.length ^ right.length
76
+
77
+ for (let index = 0; index < length; index += 1) {
78
+ const leftByte = index < left.length ? left[index] : 0
79
+ const rightByte = index < right.length ? right[index] : 0
80
+ diff |= leftByte ^ rightByte
81
+ }
82
+
83
+ return diff === 0
84
+ }
85
+
86
+ async function importSecret(secret: string): Promise<CryptoKey | null> {
87
+ const bytes = decodeBase64(secret)
88
+ if (!bytes || bytes.byteLength !== SECRET_BYTES) {
89
+ return null
90
+ }
91
+
92
+ return crypto.subtle.importKey(
93
+ 'raw',
94
+ toArrayBuffer(bytes),
95
+ { name: 'HMAC', hash: 'SHA-256' },
96
+ false,
97
+ ['sign'],
98
+ )
99
+ }
100
+
101
+ function decodeBase64(value: string): Uint8Array | null {
102
+ const normalized = value.trim().replace(/-/g, '+').replace(/_/g, '/')
103
+ if (!normalized) {
104
+ return null
105
+ }
106
+
107
+ try {
108
+ return binaryStringToBytes(atob(padBase64(normalized)))
109
+ } catch {
110
+ return null
111
+ }
112
+ }
113
+
114
+ function decodeBase64Url(value: string): Uint8Array | null {
115
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) {
116
+ return null
117
+ }
118
+
119
+ return decodeBase64(value)
120
+ }
121
+
122
+ function padBase64(value: string): string {
123
+ return value.padEnd(value.length + ((4 - (value.length % 4)) % 4), '=')
124
+ }
125
+
126
+ function binaryStringToBytes(value: string): Uint8Array {
127
+ const bytes = new Uint8Array(value.length)
128
+ for (let index = 0; index < value.length; index += 1) {
129
+ bytes[index] = value.charCodeAt(index)
130
+ }
131
+ return bytes
132
+ }
133
+
134
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
135
+ const buffer = new ArrayBuffer(bytes.byteLength)
136
+ new Uint8Array(buffer).set(bytes)
137
+ return buffer
138
+ }