Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
fireschema
Advanced tools
- **Strong type safety for Firestore** - Automatically provide type information to _nested documents_ without unsafe type assertions, from the simple schema. Also support data decoding. - **Security rules generation** - Generate firestore.rules file inclu
Strongly typed Firestore framework for TypeScript
yarn add fireschema
yarn add -D typescript ts-node
To generate Firestore security rules or embedding type validation code into Callable Function, you need to compile codes via Fireschema's custom transformer that retrives type information from TypeScript AST.
Currently official TypeScript package doesn't support custom transformers, and you need to use ttypescript that wraps TypeScript compiler.
Add following options to your tsconfig.json
,
{
"compilerOptions": {
"plugins": [
{
"transform": "fireschema/transformer"
}
]
}
}
and replace the commands.
before | after | |
---|---|---|
typescript | tsc | ttsc (ttypescript's compile command) |
ts-node | ts-node | ts-node --compiler ttypescript |
ttsc
andts-node
supports specifyingtsconfig.json
by using environment variableTS_NODE_PROJECT
.
If you use ts-jest, add following options to jest config.
module.exports = {
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json',
compiler: 'ttypescript',
},
},
}
Do not use following variable names except importing from fireschema.
$collectionSchema
__$__
users/{uid}
- User
users/{uid}/posts/{postId}
- PostA | PostB
The schema definition must be named exported as firestoreSchema
.
import { Merge } from 'type-fest'
import {
$allow,
$collectionGroups,
$collectionSchema,
$docLabel,
$functions,
$or,
$schema,
createFirestoreSchema,
FTypes,
} from 'fireschema'
// user
export type User = {
name: string
displayName: string | null
age: number
timestamp: FTypes.Timestamp
options: { a: boolean } | undefined
}
export type UserDecoded = Merge<User, { timestamp: Date }>
const UserSchema = $collectionSchema<User, UserDecoded>()({
decoder: (data) => ({
...data,
timestamp: data.timestamp.toDate(),
}),
})
// post
type PostA = {
type: 'a'
tags: { id: number; name: string }[]
text: string
}
type PostB = {
type: 'b'
tags: { id: number; name: string }[]
texts: string[]
}
const PostSchema = $collectionSchema<PostA | PostB>()({
selectors: (q) => ({
byTag: (tag: string) => q.where('tags', 'array-contains', tag),
}),
})
export const firestoreSchema = createFirestoreSchema({
[$functions]: {
// whether /admins/<uid> exists
['isAdmin()']: `
return exists(/databases/$(database)/documents/admins/$(request.auth.uid));
`,
// whether uid matches
['matchesUser(uid)']: `
return request.auth.uid == uid;
`,
},
[$collectionGroups]: {
posts: {
[$docLabel]: 'postId',
[$schema]: PostSchema,
[$allow]: {
read: true,
},
},
},
// /users/{uid}
users: {
[$docLabel]: 'uid', // {uid}
[$schema]: UserSchema, // collectionSchema
[$allow]: {
// access control
read: true, // all user
write: $or(['matchesUser(uid)', 'isAdmin()']), // only users matching {uid} or admins
},
// /users/{uid}/posts/{postId}
posts: {
[$docLabel]: 'postId',
[$schema]: PostSchema,
[$allow]: {
read: true,
write: 'matchesUser(uid)',
},
},
},
})
yarn fireschema <path-to-schema>.ts
Environment variable
TS_NODE_PROJECT
is supported similarly tottsc
andts-node
.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isAdmin() {
return exists(/databases/$(database)/documents/admins/$(request.auth.uid));
}
function matchesUser(uid) {
return request.auth.uid == uid;
}
match /{path=**}/posts/{postId} {
allow read: if true;
}
match /users/{uid} {
function __validator_0__(data) {
return (
(!("_createdAt" in data) || data._createdAt == request.time)
&& (!("_updatedAt" in data) || data._updatedAt == request.time)
&& data.name is string
&& (data.displayName == null || data.displayName is string)
&& (data.age is int || data.age is float)
&& data.timestamp is timestamp
&& (!("options" in data) || data.options.a is bool)
);
}
allow read: if true;
allow write: if ((matchesUser(uid) || isAdmin()) && __validator_0__(request.resource.data));
match /posts/{postId} {
function __validator_1__(data) {
return ((
(!("_createdAt" in data) || data._createdAt == request.time)
&& (!("_updatedAt" in data) || data._updatedAt == request.time)
&& data.type == "a"
&& (data.tags.size() == 0 || ((data.tags[0].id is int || data.tags[0].id is float) && data.tags[0].name is string))
&& data.text is string
) || (
(!("_createdAt" in data) || data._createdAt == request.time)
&& (!("_updatedAt" in data) || data._updatedAt == request.time)
&& data.type == "b"
&& (data.tags.size() == 0 || ((data.tags[0].id is int || data.tags[0].id is float) && data.tags[0].name is string))
&& (data.texts.size() == 0 || data.texts[0] is string)
));
}
allow read: if true;
allow write: if (matchesUser(uid) && __validator_1__(request.resource.data));
}
}
}
}
The Firestore interface of Fireschema supports both Web SDK and Admin SDK.
import firebase from 'firebase/app' // or firebase-admin
import { TypedFirestore } from 'fireschema'
import { firestoreSchema } from './1-1-schema'
const app: firebase.app.App = firebase.initializeApp({
// ...
})
const firestoreApp = app.firestore()
firestoreApp.settings({ ignoreUndefinedProperties: true })
/**
* Initialize TypedFirestore
*/
export const typedFirestore: TypedFirestore<
typeof firestoreSchema,
firebase.firestore.Firestore
> = new TypedFirestore(firestoreSchema, firebase.firestore, firestoreApp)
/**
* Reference collections/documents and get snapshot
*/
const users = typedFirestore.collection('users') // TypedCollectionRef instance
const user = users.doc('userId') // TypedDocumentRef instance
const posts = user.collection('posts')
const post = posts.doc('123')
const techPosts = user.collectionQuery(
'posts',
(q) => q.byTag('tech'), // selector defined in schema
)
!(async () => {
await user.get() // DocumentSnapshot<User>
await post.get() // DocumentSnapshot<PostA | PostB>
await posts.get() // QuerySnapshot<PostA | PostB>
await techPosts.get() // QuerySnapshot<PostA | PostB>
})
/**
* Get child collection of retrived document snapshot
*/
!(async () => {
const snap = await users.get()
const firstUserRef = snap.docs[0]!.ref
await typedFirestore.wrapDocument(firstUserRef).collection('posts').get()
})
/**
* Reference parent collection/document
*/
const _posts = post.parentCollection()
const _user = posts.parentDocument()
/**
* Reference collections groups and get snapshot
*/
const postsGroup = typedFirestore.collectionGroup(
'posts', // collection name: passed to original collectionGroup method
'users.posts', // to get schema options
)
const techPostsGroup = typedFirestore.collectionGroupQuery(
'posts',
'users.posts',
(q) => q.byTag('tech'),
)
!(async () => {
await postsGroup.get() // QuerySnapshot<PostA | PostB>
await techPostsGroup.get() // QuerySnapshot<PostA | PostB>
})
/**
* Write data
*/
!(async () => {
await user.create({
name: 'test',
displayName: 'Test',
age: 20,
timestamp: typedFirestore.firestoreStatic.FieldValue.serverTimestamp(),
options: { a: true },
})
await user.setMerge({
age: 21,
})
await user.update({
age: 21,
})
await user.delete()
})
/**
* Transaction
*/
!(async () => {
await typedFirestore.runTransaction(async (tt) => {
const snap = await tt.get(user)
tt.update(user, {
age: snap.data()!.age + 1,
})
})
})
import React from 'react'
import { useTypedDocument, useTypedQuery } from 'fireschema/hooks'
import { typedFirestore } from './1-3-typed-firestore'
/**
* Get realtime updates of collection/query
*/
export const UsersComponent = () => {
const users = useTypedQuery(typedFirestore.collection('users'))
if (!users.data) {
return <span>{'Loading...'}</span>
}
return (
<ul>
{users.data.map((user, i) => (
<li key={i}>{user.displayName}</li>
))}
</ul>
)
}
/**
* Get realtime updates of document
*/
export const UserComponent = ({ id }: { id: string }) => {
const user = useTypedDocument(typedFirestore.collection('users').doc(id))
if (!user.data) {
return <span>{'Loading...'}</span>
}
return <span>{user.data.displayName}</span>
}
import { firestore } from 'firebase-admin'
import * as functions from 'firebase-functions'
import { Merge } from 'type-fest'
import { $jsonSchema, TypedFunctions } from 'fireschema'
import { firestoreSchema, User } from './1-1-schema'
/**
* Initialize TypedFunctions
*/
const timezone = 'Asia/Tokyo'
const typedFunctions = new TypedFunctions(
firestoreSchema,
firestore,
functions,
timezone,
)
const builder = functions.region('asia-northeast1')
/**
* functions/index.ts file
*/
export type UserJson = Merge<User, { timestamp: string }>
export const callable = {
createUser: typedFunctions.callable({
schema: [
$jsonSchema<UserJson>(), // schema of request data (automatically validate on request)
$jsonSchema<{ result: boolean }>(), // schema of response data
],
builder,
handler: async (data, context) => {
console.log(data) // UserJson
return { result: true }
},
}),
}
export const firestoreTrigger = {
onUserCreate: typedFunctions.firestoreTrigger.onCreate({
builder,
path: 'users/{uid}',
handler: async (decodedData, snap, context) => {
console.log(decodedData) // UserDecoded (provided based on path string)
console.log(snap) // QueryDocumentSnapshot<User>
},
}),
}
export const http = {
getKeys: typedFunctions.http({
builder,
handler: (req, resp) => {
if (req.method !== 'POST') {
resp.status(400).send()
return
}
resp.json(Object.keys(req.body))
},
}),
}
export const topic = {
publishMessage: typedFunctions.topic('publish_message', {
schema: $jsonSchema<{ text: string }>(),
builder,
handler: async (data) => {
data // { text: string }
},
}),
}
export const schedule = {
cron: typedFunctions.schedule({
builder,
schedule: '0 0 * * *',
handler: async (context) => {
console.log(context.timestamp)
},
}),
}
Automatically provide types to request/response data based on passed functions module type.
import firebase from 'firebase/app'
import React from 'react'
import { TypedCaller } from 'fireschema'
type FunctionsModule = typeof import('./2-1-typed-functions')
const app: firebase.app.App = firebase.initializeApp({
// ...
})
const functionsApp = app.functions('asia-northeast1')
export const typedCaller = new TypedCaller<FunctionsModule>(functionsApp)
const Component = () => {
const createUser = async () => {
const result = await typedCaller.call('createUser', {
name: 'test',
displayName: 'Test',
age: 20,
timestamp: new Date().toISOString(),
options: { a: true },
})
if (result.error) {
console.error(result.error)
return
}
console.log(result.data)
}
return <button onClick={createUser}></button>
}
FAQs
- **Strong type safety for Firestore** - Automatically provide type information to _nested documents_ without unsafe type assertions, from the simple schema. Also support data decoding. - **Security rules generation** - Generate firestore.rules file inclu
The npm package fireschema receives a total of 50 weekly downloads. As such, fireschema popularity was classified as not popular.
We found that fireschema demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.