
Research
Security News
Malicious PyPI Package Exploits Deezer API for Coordinated Music Piracy
Socket researchers uncovered a malicious PyPI package exploiting Deezer’s API to enable coordinated music piracy through API abuse and C2 server control.
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 firebase firebase-admin firebase-functions zod
yarn add -D typescript ts-node
🎉 Since Fireschema v5, you no longer need to compile codes via custom transformer.
Zod Schema | Security Rules Output |
---|---|
z.any() | true |
z.unknown() | true |
z.undefined() | !("key" in data) |
z.null() | data.key == null |
z.boolean() | data.key is bool |
z.literal('a') | data.key == "a" |
z.string() | data.key is string |
z.string().min(5) | (data.key is string && data.key.size >= 5) |
z.string().min(5).max(20) | (data.key is string && data.key.size >= 5 && data.key.size <= 20) |
z.string().regex(/@example\.com$/) | (data.key is string && data.key.matches("@example\\.com$")) |
z.number() | data.key is number |
z.number().int() | data.key is int |
z.number().min(5) | (data.key is int && data.key >= 5) |
z.number().max(20) | (data.key is int && data.key <= 20) |
timestampType() | data.key is timestamp |
z.tuple([z.string(), z.number()]) | (data.key is list && data.key[0] is string && data.key[1] is number) |
z.string().array() | data.key is list |
z.string().array().min(5) | (data.key is list && data.key.size() >= 5) |
z.string().array().max(20) | (data.key is list && data.key.size() <= 20) |
z.string().optional() | (data.key is string || !("key" in data)) |
z.union([z.string(), z.null()]) | (data.key is string || data.key == null) |
The schema definition must be default exported.
import { Merge } from 'type-fest'
import { z } from 'zod'
import { DataModel, FirestoreModel, rules, timestampType } from 'fireschema'
export const UserType = z.object({
name: z.string(),
displayName: z.union([z.string(), z.null()]),
age: z.number().int(),
timestamp: timestampType(),
options: z.object({ a: z.boolean() }).optional(),
})
type User = z.infer<typeof UserType>
/* => {
name: string
displayName: string | null
age: number
timestamp: FTypes.Timestamp
options?: { a: boolean } | undefined
} */
type UserDecoded = Merge<User, { timestamp: Date }>
const UserModel = new DataModel({
schema: UserType,
decoder: (data: User): UserDecoded => ({
...data,
timestamp: data.timestamp.toDate(),
}),
})
const PostType = z.object({
authorUid: z.string(),
text: z.string(),
tags: z.object({ id: z.number().int(), name: z.string() }).array(),
})
const PostModel = new DataModel({
schema: PostType,
selectors: (q) => ({
byTag: (tag: string) => [
q.where('tags', 'array-contains', tag),
q.limit(20),
],
}),
})
export const firestoreModel = new FirestoreModel({
'function isAdmin()': `
return exists(${rules.basePath}/admins/$(request.auth.uid));
`,
'function requestUserIs(uid)': `
return request.auth.uid == uid;
`,
collectionGroups: {
'/posts/{postId}': {
allow: {
read: true,
},
},
},
'/users/{uid}': {
model: UserModel,
allow: {
read: true, // open access
write: rules.or('requestUserIs(uid)', 'isAdmin()'),
},
'/posts/{postId}': {
'function authorUidMatches()': `
return request.resource.data.authorUid == uid;
`,
model: PostModel,
allow: {
read: true,
write: rules.and('requestUserIs(uid)', 'authorUidMatches()'),
},
},
},
})
export default firestoreModel
yarn fireschema rules <path-to-schema>.ts
Environment variable
TS_NODE_PROJECT
is supported.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function __validator_meta__(data) {
return (
(request.method == "create" && data._createdAt == request.time && data._updatedAt == request.time)
|| (request.method == "update" && data._createdAt == resource.data._createdAt && data._updatedAt == request.time)
);
}
function __validator_keys__(data, keys) {
return data.keys().removeAll(['_createdAt', '_updatedAt']).hasOnly(keys);
}
function isAdmin() {
return exists(/databases/$(database)/documents/admins/$(request.auth.uid));
}
function requestUserIs(uid) {
return request.auth.uid == uid;
}
match /{path=**}/posts/{postId} {
allow read: if true;
}
match /users/{uid} {
function __validator_0__(data) {
return (__validator_meta__(data) && (
__validator_keys__(data, ['name', 'displayName', 'age', 'timestamp', 'options'])
&& data.name is string
&& (data.displayName is string || data.displayName == null)
&& data.age is int
&& data.timestamp is timestamp
&& (data.options.a is bool || !("options" in data))
));
}
allow read: if true;
allow write: if ((requestUserIs(uid) || isAdmin()) && __validator_0__(request.resource.data));
match /posts/{postId} {
function authorUidMatches() {
return request.resource.data.authorUid == uid;
}
function __validator_1__(data) {
return (__validator_meta__(data) && (
__validator_keys__(data, ['authorUid', 'text', 'tags'])
&& data.authorUid is string
&& data.text is string
&& data.tags is list
));
}
allow read: if true;
allow write: if ((requestUserIs(uid) && authorUidMatches()) && __validator_1__(request.resource.data));
}
}
}
}
The Firestore interface of Fireschema supports both Web SDK and Admin SDK.
import { initializeApp } from 'firebase/app' // or firebase-admin
import { initializeFirestore } from 'firebase/firestore'
import { TypedFirestoreWeb } from 'fireschema'
import { firestoreModel } from './1-1-schema.js'
const app = initializeApp({
// ...
})
const firestoreApp = initializeFirestore(app, {
ignoreUndefinedProperties: true,
})
/**
* Initialize TypedFirestore
*/
export const typedFirestore: TypedFirestoreWeb<typeof firestoreModel> =
new TypedFirestoreWeb(firestoreModel, firestoreApp)
/**
* Reference collections/documents and get snapshot
*/
const usersRef = typedFirestore.collection('users') // TypedCollectionRef instance
const userRef = usersRef.doc('userId') // TypedDocumentRef instance
const postsRef = userRef.collection('posts')
const postRef = postsRef.doc('123')
const techPostsQuery = postsRef.select.byTag('tech') // selector defined in schema
await userRef.get() // DocumentSnapshot<User>
await userRef.getData() // User
await postRef.get() // DocumentSnapshot<PostA | PostB>
await postsRef.get() // QuerySnapshot<PostA | PostB>
await postsRef.getData() // (PostA | PostB)[]
await techPostsQuery.get() // QuerySnapshot<PostA | PostB>
/**
* Get child collection of retrived document snapshot
*/
const snap = await usersRef.get()
const firstUserRef = snap.typedDocs[0]!.typedRef
await firstUserRef.collection('posts').get()
/**
* Reference parent collection/document
*/
const _postsRef = postRef.parentCollection()
const _userRef = postsRef.parentDocument()
/**
* Reference collections groups and get snapshot
*/
const postsGroup = typedFirestore.collectionGroup('posts')
const techPostsGroup = postsGroup.select.byTag('tech')
await postsGroup.get() // QuerySnapshot<PostA | PostB>
await techPostsGroup.get() // QuerySnapshot<PostA | PostB>
/**
* Write data
*/
await userRef.create(({ serverTimestamp }) => ({
name: 'test',
displayName: 'Test',
age: 20,
timestamp: serverTimestamp(),
options: { a: true },
}))
await userRef.setMerge({
age: 21,
})
await userRef.update({
age: 21,
})
await userRef.delete()
/**
* Transaction
*/
await typedFirestore.runTransaction(async (tt) => {
const snap = await tt.get(userRef)
tt.update(userRef, {
age: snap.data()!.age + 1,
})
})
import React, { Suspense } from 'react'
import { useTypedCollection, useTypedDoc } from 'fireschema/hooks'
import { typedFirestore } from './1-3-typed-firestore.js'
/**
* Get realtime updates of collection/query
*/
export const PostsComponent = () => {
const userRef = typedFirestore.collection('users').doc('user1')
const posts = useTypedCollection(userRef.collection('posts'))
const techPosts = useTypedCollection(userRef.collection('posts'), (select) =>
select.byTag('tech'),
)
return (
<Suspense fallback={'Loading...'}>
<ul>
{posts.data.map((post, i) => (
<li key={i}>{post.text}</li>
))}
</ul>
</Suspense>
)
}
/**
* Get realtime updates of document
*/
export const UserComponent = ({ id }: { id: string }) => {
const user = useTypedDoc(typedFirestore.collection('users').doc(id))
return (
<Suspense fallback={'Loading...'}>
<span>{user.data?.displayName}</span>
</Suspense>
)
}
import * as functions from 'firebase-functions'
import { z } from 'zod'
import { TypedFunctions } from 'fireschema/admin'
import { UserType, firestoreModel } from './1-1-schema.js'
/**
* Initialize TypedFunctions
*/
const timezone = 'Asia/Tokyo'
const typedFunctions = new TypedFunctions(firestoreModel, timezone)
const builder = functions.region('asia-northeast1')
/**
* functions/index.ts file
*/
export const UserJsonType = UserType.extend({ timestamp: z.string() })
export const callable = {
createUser: typedFunctions.callable({
schema: {
input: UserJsonType, // schema of request data (automatically validate on request)
output: z.object({ result: z.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: z.object({ text: z.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 { initializeApp } from 'firebase/app'
import { getFunctions } from 'firebase/functions'
import React from 'react'
import { TypedCaller } from 'fireschema'
type FunctionsModule = typeof import('./2-1-typed-functions.js')
const app = initializeApp({
// ...
})
const functionsApp = getFunctions(app, '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} />
}
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 34 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 researchers uncovered a malicious PyPI package exploiting Deezer’s API to enable coordinated music piracy through API abuse and C2 server control.
Research
The Socket Research Team discovered a malicious npm package, '@ton-wallet/create', stealing cryptocurrency wallet keys from developers and users in the TON ecosystem.
Security News
Newly introduced telemetry in devenv 1.4 sparked a backlash over privacy concerns, leading to the removal of its AI-powered feature after strong community pushback.