sveltekit-firebase-helpers 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.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # sveltekit-firebase-helpers
2
+
3
+ Helpers for using Firebase with SvelteKit. Because I always end up with the same code on every SvelteKit + Firebase project I have ...
4
+
5
+ ## Firebase Auth Gotchas
6
+
7
+ When using Firebase Auth, the protocol of the auth service needs to match that of your app. What are the implications of this?
8
+
9
+ Well, if you're using the _live_ Firebase Auth service you need to use `https` even when running in local dev mode which can be achieved via the `vite-plugin-mkcert` package. You will also need to make sure that your app follows the [Best practices for using signInWithRedirect on browsers that block third-party storage access](https://firebase.google.com/docs/auth/web/redirect-best-practices) which, spoiler, you should do via Option 3 of that article (and what this package helps with). You _could_ use `signInWithPopup` but you really shouldn't as it comes with its own problems.
10
+
11
+ If you're using the Auth Emulator you have two options. It runs on `http` only so will work fine if you _don't_ include the `vite-plugin-mkcert` package in your vite config (but silently fail if you do). If you _do_ need `https` locally for other reasons you can use [Caddy](https://caddyserver.com/) as a reverse proxy to provide the secure access to it. Personally, I prefer to toggle the vite config so I don't have to install and start another service which keeps the project neat and self-contained.
12
+
13
+ Finally, how do you sync the client-side auth state to your server? If you only use firebase-hosted services you don't need to, but if you have code on your server that needs to know the identity of the user, such as a shopping cart checkout, it will be important. How do you do this?
14
+
15
+ You _could_ check for the ID Token changing on the client, and send a fetch request to your own server to mirror the state using a cookie, but there are subtle issues with this - the ID Token is automatically refreshed when calling firebase services but the one in the cookie may end up out-of-date if you're calling your own server. You also then have state in two different places, potentially out-of-sync, which is not ideal.
16
+
17
+ A better option is to implement [Session management with service workers](https://firebase.google.com/docs/auth/web/service-worker-sessions) where the existing client-side auth state is used to automatically add an Authorization bearer token to each server request - it will even handle an initial page load if you need the auth state for SSR. Adding this auth token is something else this package can help with.
18
+
19
+ It also handles some edge cases you may otherwise struggle with. Picture this - in response to a form POST action you want to update the custom claims for a user, maybe to indicate which service plan they are now on or that they have some new permission. You might _expect_ to do the form post, have the `invalidateAll()` method called (which Svelte's `use:enhance` form action does for you by default) and see your freshly loaded data based on the claim you set during the server action. Except it won't. The Firebase client-side lib syncs the auth state of the main thread with the service-worker but it does it via polling so there can be a half-second or so where it doesn't have the latest auth token, or any auth token at all if you have just loaded a page from a sign-in redirect. Again, this is something this package can help with by providing it's own temporary auth-token-syncing for those situations just to ensure the state seen by the service-worker is completely up-to-date.
20
+
21
+ Oh, and one other issue - SvelteKit is often very opinionated but sometimes those opinions are a little blinkered. You can't, for instance, use _dynamic_ .env variables in your service-worker code, only _static_ ones which just doesn't cut-it for serious work - you should be able to deploy the same compiled code / docker image to different environments and have it work, having to compile a specific version for each one is tedious and error prone. Anyway, we built something in to handle this as well to save you the work of figuring it out.
22
+
23
+ ## Usage
24
+
25
+ How to use the features of the package. Of course, you need to install it using the package manager of your choice (which should be `pnpm`):
26
+
27
+ pnpm i svelte-firebase-helpers
28
+
29
+ ### Server Hooks
30
+
31
+ The server-side helpers are added as handlers in your app [server hooks](https://svelte.dev/docs/kit/hooks#Server-hooks). You can add them separately (using `sequence` if you have multiple handlers) or use a single call to add them all. Let's go through each one individually first:
32
+
33
+ #### Auth Handle
34
+
35
+ To decode any http `Authorization` header added by the service-worker use `createAuthHandle` passing in the `firebase-admin/auth` instance it needs to decode the firebase ID token:
36
+
37
+ ```ts
38
+ import { createAuthHandle } from "svelte-firebase-helpers";
39
+ import { auth } from './firebase.server'
40
+
41
+ export const handle = createAuthHandle(auth)
42
+ ```
43
+
44
+ Any requests with an `Authorication Bearer [token]` header will now be decoded and added as a `locals.user` property.
45
+
46
+ #### Options Handle
47
+
48
+ The service-worker needs access to the firebase config, but SveleKit conspires to make that difficult to import. To get around this we provide our own server-endpoint that the service-worker can request it from.
49
+
50
+ ```ts
51
+ import { createOptionsHandle } from "svelte-firebase-helpers";
52
+ import { options } from './firebase'
53
+
54
+ export const handle = createOptionsHandle(options)
55
+ ```
56
+
57
+ Example `options.ts`:
58
+
59
+ ```ts
60
+ import { dev } from '$app/environment'
61
+ import {
62
+ PUBLIC_FIREBASE_API_KEY,
63
+ PUBLIC_FIREBASE_AUTH_DOMAIN,
64
+ PUBLIC_FIREBASE_DATABASE_URL,
65
+ PUBLIC_FIREBASE_PROJECT_ID,
66
+ PUBLIC_FIREBASE_STORAGE_BUCKET,
67
+ PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
68
+ PUBLIC_FIREBASE_APP_ID,
69
+ PUBLIC_FIREBASE_MEASUREMENT_ID
70
+ } from '$env/static/public'
71
+
72
+ export const options = {
73
+ apiKey: PUBLIC_FIREBASE_API_KEY,
74
+ authDomain: dev ? 'localhost:5173' : PUBLIC_FIREBASE_AUTH_DOMAIN,
75
+ databaseURL: PUBLIC_FIREBASE_DATABASE_URL,
76
+ projectId: PUBLIC_FIREBASE_PROJECT_ID,
77
+ storageBucket: PUBLIC_FIREBASE_STORAGE_BUCKET,
78
+ messagingSenderId: PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
79
+ appId: PUBLIC_FIREBASE_APP_ID,
80
+ measurementId: PUBLIC_FIREBASE_MEASUREMENT_ID
81
+ }
82
+ ```
83
+
84
+ You can test the handle is working correctly by requesting the `/__/firebase/init.json` endpoint in your app. You should see the client-side Firebase Options config returned (which is perfectly safe and normal to send to the client for the client-side Firebase libraries to work).
85
+
86
+ #### Proxy Handle
87
+
88
+ To proxy auth requests so you can use `signInWithRedirect` on browsers that block 3rd party cookies (now all of them) use `createAuthHandle` passing in the `application-id.firebaseapp.com` domain name from you Firebase app config, e.g. `captaincodeman-experiment.firebaseapp.com`.
89
+
90
+ ```ts
91
+ import { createOptionsHandle } from "svelte-firebase-helpers";
92
+ import { env } from '$env/dynamic/public'
93
+
94
+ const auth_domain = env.PUBLIC_FIREBASE_AUTH_DOMAIN
95
+
96
+ export const handle = createProxyHandle(auth_domain)
97
+ ```
98
+
99
+ Any requests to `/__/auth/...` will be proxied to the `auth_domain` configured, effectively making your app serve the firebase auth endpoints itself to get around the 3rd party cookie restrictions.
100
+
101
+ #### Combined Handle
102
+
103
+ An alternative is to use a single combined `createHandle` function that will add each individual handle if the property it needs is included.
104
+
105
+ So pass the `auth` instance (`firebase-admin/auth`) and it will add the Auth Handle. Pass the `options` object and it will add the Options Handle. Pass the `auth_domain` and the Proxy Handle will be added.
106
+
107
+ ```ts
108
+ import { env } from '$env/dynamic/public'
109
+ import { createHandle } from 'svelte-firebase-helpers'
110
+ import { options } from './routes/firebase'
111
+ import { auth } from './routes/firebase-server'
112
+
113
+ const auth_domain = env.PUBLIC_FIREBASE_AUTH_DOMAIN
114
+
115
+ export const handle = createHandle({ auth, options, auth_domain })
116
+ ```
117
+
118
+ ### Service Worker
119
+
120
+ To automatically add the `Authentication Bearer [idToken]` http header to each server request, use the `addFirebaseAuth` method in your service worker. If running against the Firebase Auth Emulator pass that as a separate `auth_emulator` parameter. The service-worker code will automatically request and use the `/__/firebase/init.json` endpoint provided by the Options Handle above so it needs to be added to the Server Hooks.
121
+
122
+ ```ts
123
+ /// <reference types="@sveltejs/kit" />
124
+ /// <reference no-default-lib="true"/>
125
+ /// <reference lib="esnext" />
126
+ /// <reference lib="webworker" />
127
+
128
+ import { addFirebaseAuth } from 'svelte-firebase-helpers'
129
+
130
+ addFirebaseAuth({
131
+ auth_emulator: 'http://localhost:9099' // if using the Firebase Auth Emulator
132
+ })
133
+ ```
134
+
135
+ NOTE: when using a service-worker for firebase state, you _must_ implement [custom auth initialization](https://firebase.google.com/docs/auth/web/custom-dependencies#when_to_use_custom_initialization) specifically using `indexedDBLocalPersistence` for persistence. This is handled in the `addFirebaseAuth` implementation but for your own firebase client you can initialize it using:
136
+
137
+ ```ts
138
+ import {
139
+ browserPopupRedirectResolver,
140
+ getAuth,
141
+ indexedDBLocalPersistence,
142
+ initializeAuth,
143
+ } from 'firebase/auth'
144
+ import { app } from './app'
145
+ import { browser } from '$app/environment'
146
+
147
+ // SSR friendly auth initialization
148
+ export const auth = browser
149
+ ? initializeAuth(app, {
150
+ persistence: [indexedDBLocalPersistence],
151
+ popupRedirectResolver: browserPopupRedirectResolver,
152
+ })
153
+ : getAuth(app)
154
+ ```
155
+
156
+ The `browser` check is to avoid an error if using SSR.
157
+
158
+ One additional advantage of this is that your client-side auth dependencies are reduced so your app should start faster (you can also avoid loading the `popupRedirectResolver` for initialization and pass it to methods like `signInWithRedirect` when called for even greater savings).
@@ -0,0 +1,34 @@
1
+ import { Auth } from 'firebase/auth';
2
+ import { Handle } from '@sveltejs/kit';
3
+ import { Auth as Auth$1, DecodedIdToken } from 'firebase-admin/auth';
4
+ import { FirebaseOptions } from 'firebase/app';
5
+
6
+ declare function syncAuthToken(auth: Auth): Promise<void>;
7
+
8
+ declare function createAuthHandle(auth: Auth$1): Handle;
9
+
10
+ declare function createOptionsHandle(options: FirebaseOptions): Handle;
11
+
12
+ declare function createProxyHandle(auth_domain: string, init?: HeadersInit): Handle;
13
+
14
+ declare function createHandle(config: {
15
+ auth?: Auth$1;
16
+ options?: FirebaseOptions;
17
+ auth_domain?: string;
18
+ init?: HeadersInit;
19
+ }): Handle;
20
+
21
+ declare global {
22
+ namespace App {
23
+ interface Locals {
24
+ user: DecodedIdToken | null;
25
+ }
26
+ }
27
+ }
28
+
29
+ /// <reference no-default-lib="true"/>
30
+ declare function addFirebaseAuth(config: {
31
+ auth_emulator?: string;
32
+ }): void;
33
+
34
+ export { addFirebaseAuth, createAuthHandle, createHandle, createOptionsHandle, createProxyHandle, syncAuthToken };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{getIdToken as k}from"firebase/auth";async function C(t){if(t.currentUser){let o=await k(t.currentUser,!0),{serviceWorker:n}=navigator,{controller:r}=n;r&&await new Promise(c=>{n.addEventListener("message",()=>c(),{once:!0}),r.postMessage({type:"useToken",useToken:o})})}}function y(t){return async({event:n,resolve:r})=>{let{locals:c,request:e}=n,s=e.headers.get("Authorization")?.split("Bearer ")[1];if(s)try{c.user=await t.verifyIdToken(s)}catch(a){console.error(a)}return r(n)}}var g=globalThis.process?.env?.NODE_ENV,_=g&&!g.toLowerCase().startsWith("prod");function w(t,o){let n=JSON.stringify(t),r=new Headers(o?.headers);return r.has("content-length")||r.set("content-length",b.encode(n).byteLength.toString()),r.has("content-type")||r.set("content-type","application/json"),new Response(n,{...o,headers:r})}var b=new TextEncoder;function H(t){return async({event:n,resolve:r})=>{let{request:c,url:e}=n;return c.method==="GET"&&e.pathname.startsWith("/__/firebase/init.json")?(console.log("handle options"),w(t)):r(n)}}function T(t,o){return async({event:r,resolve:c})=>{let{request:e,url:i}=r;if(e.method==="GET"&&i.pathname.startsWith("/__/auth/")){console.log("handle proxy",t),i.host=t,i.port="443";let s=await fetch(i,{headers:{"Accept-Encoding":"identity"}}),a=await s.text(),u=new Headers({...o,"Cache-Control":s.headers.get("Cache-Control"),"Content-Type":s.headers.get("Content-Type"),Vary:"accept-encoding"});return new Response(a,{headers:u})}return c(r)}}function l(...t){let o=t.length;return o?({event:n,resolve:r})=>{return c(0,n,{});function c(e,i,s){let a=t[e];return a({event:i,resolve:(u,f)=>{let p=async({html:d,done:x})=>(f?.transformPageChunk&&(d=await f.transformPageChunk({html:d,done:x})??""),s?.transformPageChunk&&(d=await s.transformPageChunk({html:d,done:x})??""),d),h=s?.filterSerializedResponseHeaders??f?.filterSerializedResponseHeaders,m=s?.preload??f?.preload;return e<o-1?c(e+1,u,{transformPageChunk:p,filterSerializedResponseHeaders:h,preload:m}):r(u,{transformPageChunk:p,filterSerializedResponseHeaders:h,preload:m})}})}}:({event:n,resolve:r})=>r(n)}function ye(t){let o=[];return t.auth&&o.push(y(t.auth)),t.options&&o.push(H(t.options)),t.auth_domain&&o.push(T(t.auth_domain,t.init)),l(...o)}import{initializeApp as E}from"firebase/app";import{getIdToken as R,initializeAuth as S,connectAuthEmulator as j,indexedDBLocalPersistence as O}from"firebase/auth";function He(t){let o=new Promise(async(e,i)=>{let a=await(await fetch("/__/firebase/init.json")).json(),u=E(a),f=S(u,{persistence:[O]});t.auth_emulator&&j(f,t.auth_emulator),await f.authStateReady(),e(f)}),n;self.addEventListener("message",e=>{function i(){self.clients.matchAll({}).then(function(s){s&&s.length&&s[0].postMessage({ack:!0})})}if(e.data)switch(e.data.type){case"useToken":n=e.data.useToken,i(),setTimeout(()=>n=void 0,5e3);break}});async function r(){let e=await o;if(e.currentUser)try{return await R(e.currentUser)}catch{return null}else return null}async function c(e){try{if(e.method!=="GET")if(e.headers.get("Content-Type")?.indexOf("json")!==-1){let i=await e.json();return JSON.stringify(i)}else return e.text()}catch{}}self.addEventListener("fetch",e=>{if(new URL(e.request.url).origin!==location.origin)return;async function s(){let a=e.request,u=n??await r();if(u){let f=new Headers(a.headers);f.append("Authorization","Bearer "+u);let p=await c(a);try{a=new Request(a.url,{method:a.method,headers:f,mode:"same-origin",credentials:a.credentials,cache:a.cache,redirect:a.redirect,referrer:a.referrer,body:p})}catch{}}return fetch(a)}e.respondWith(s())})}export{He as addFirebaseAuth,y as createAuthHandle,ye as createHandle,H as createOptionsHandle,T as createProxyHandle,C as syncAuthToken};
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "sveltekit-firebase-helpers",
3
+ "description": "Helpers for using Firebase with SvelteKit",
4
+ "version": "0.0.1",
5
+ "files": [
6
+ "dist",
7
+ "!dist/**/*.test.*",
8
+ "!dist/**/*.spec.*"
9
+ ],
10
+ "sideEffects": [
11
+ "**/*.css"
12
+ ],
13
+ "svelte": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "svelte": "./dist/index.js"
20
+ }
21
+ },
22
+ "peerDependencies": {
23
+ "firebase": "^11.0.0",
24
+ "firebase-admin": "^13.0.0",
25
+ "svelte": "^5.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@sveltejs/adapter-auto": "^6.0.0",
29
+ "@sveltejs/kit": "^2.16.0",
30
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
31
+ "esm-env": "^1.2.2",
32
+ "firebase": "^11.9.1",
33
+ "firebase-admin": "^13.4.0",
34
+ "prettier": "^3.4.2",
35
+ "prettier-plugin-svelte": "^3.3.3",
36
+ "publint": "^0.3.2",
37
+ "svelte": "^5.0.0",
38
+ "svelte-check": "^4.0.0",
39
+ "tsup": "^8.5.0",
40
+ "typescript": "^5.0.0",
41
+ "vite": "^6.2.6",
42
+ "vite-plugin-devtools-json": "^0.2.0",
43
+ "vite-plugin-mkcert": "^1.17.8"
44
+ },
45
+ "keywords": [
46
+ "svelte"
47
+ ],
48
+ "scripts": {
49
+ "dev": "vite dev",
50
+ "build": "vite build && npm run prepack",
51
+ "preview": "vite preview",
52
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
53
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
54
+ "format": "prettier --write .",
55
+ "lint": "prettier --check .",
56
+ "emulators:start": "firebase emulators:start --only auth"
57
+ }
58
+ }