@stoker-platform/cli 0.5.12
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/LICENSE.md +102 -0
- package/init-files/.##gitignore## +102 -0
- package/init-files/.devcontainer/devcontainer.json +14 -0
- package/init-files/.env/.env +70 -0
- package/init-files/.eslintrc.cjs +35 -0
- package/init-files/.firebaserc +6 -0
- package/init-files/.prettierignore +86 -0
- package/init-files/.prettierrc +5 -0
- package/init-files/bin/build.js +221 -0
- package/init-files/bin/shim.js +159 -0
- package/init-files/extensions/firestore-send-email.env +7 -0
- package/init-files/external.package.json +4 -0
- package/init-files/firebase-rules/database.rules.json +9 -0
- package/init-files/firebase-rules/firestore.custom.rules +19 -0
- package/init-files/firebase-rules/firestore.indexes.json +0 -0
- package/init-files/firebase-rules/firestore.rules +0 -0
- package/init-files/firebase-rules/storage.rules +0 -0
- package/init-files/firebase.hosting.json +122 -0
- package/init-files/firebase.json +52 -0
- package/init-files/functions/.##gitignore## +14 -0
- package/init-files/functions/.eslintrc.cjs +28 -0
- package/init-files/functions/package.json +46 -0
- package/init-files/functions/prompts/chat.prompt +17 -0
- package/init-files/functions/src/index.ts +457 -0
- package/init-files/functions/tsconfig.dev.json +3 -0
- package/init-files/functions/tsconfig.json +17 -0
- package/init-files/icons/logo-large.png +0 -0
- package/init-files/icons/logo-small.png +0 -0
- package/init-files/ops.js +25 -0
- package/init-files/package.json +53 -0
- package/init-files/project-data.json +5 -0
- package/init-files/remoteconfig.template.json +1 -0
- package/init-files/src/collections/Inbox.ts +444 -0
- package/init-files/src/collections/Outbox.ts +270 -0
- package/init-files/src/collections/Settings.ts +44 -0
- package/init-files/src/collections/Users.ts +138 -0
- package/init-files/src/main.ts +245 -0
- package/init-files/src/utils.ts +3 -0
- package/init-files/src/vite-env.d.ts +1 -0
- package/init-files/test/test.ts +5 -0
- package/init-files/tsconfig.json +23 -0
- package/init-files/vitest.config.ts +9 -0
- package/lib/package.json +45 -0
- package/lib/src/data/exportToBigQuery.js +41 -0
- package/lib/src/data/seedData.js +347 -0
- package/lib/src/deploy/applySchema.js +43 -0
- package/lib/src/deploy/cloud-functions/getFunctionsData.js +18 -0
- package/lib/src/deploy/deployProject.js +116 -0
- package/lib/src/deploy/firestore-export/exportFirestoreData.js +29 -0
- package/lib/src/deploy/firestore-ttl/deployTTLs.js +127 -0
- package/lib/src/deploy/live-update/liveUpdate.js +22 -0
- package/lib/src/deploy/maintenance/activateMaintenanceMode.js +9 -0
- package/lib/src/deploy/maintenance/disableMaintenanceMode.js +9 -0
- package/lib/src/deploy/maintenance/setDeploymentStatus.js +22 -0
- package/lib/src/deploy/rules-indexes/generateFirestoreIndexes.js +23 -0
- package/lib/src/deploy/rules-indexes/generateFirestoreRules.js +35 -0
- package/lib/src/deploy/rules-indexes/generateStorageRules.js +23 -0
- package/lib/src/deploy/schema/generateSchema.js +184 -0
- package/lib/src/deploy/schema/persistSchema.js +14 -0
- package/lib/src/lint/lintSchema.js +1491 -0
- package/lib/src/lint/securityReport.js +223 -0
- package/lib/src/main.js +460 -0
- package/lib/src/migration/firestore/migrateFirestore.js +8 -0
- package/lib/src/migration/firestore/operations/deleteField.js +58 -0
- package/lib/src/migration/migrateAll.js +30 -0
- package/lib/src/ops/auditDenormalized.js +124 -0
- package/lib/src/ops/auditPermissions.js +92 -0
- package/lib/src/ops/auditRelations.js +186 -0
- package/lib/src/ops/explainPreloadQueries.js +65 -0
- package/lib/src/ops/getUser.js +10 -0
- package/lib/src/ops/getUserPermissions.js +19 -0
- package/lib/src/ops/getUserRecord.js +20 -0
- package/lib/src/ops/listProjects.js +8 -0
- package/lib/src/ops/setUserCollection.js +14 -0
- package/lib/src/ops/setUserDocument.js +11 -0
- package/lib/src/ops/setUserRole.js +14 -0
- package/lib/src/project/addProject.js +935 -0
- package/lib/src/project/addRecord.js +9 -0
- package/lib/src/project/addRecordPrompt.js +205 -0
- package/lib/src/project/addTenant.js +59 -0
- package/lib/src/project/buildWebApp.js +10 -0
- package/lib/src/project/customDomain.js +157 -0
- package/lib/src/project/deleteProject.js +51 -0
- package/lib/src/project/deleteRecord.js +11 -0
- package/lib/src/project/deleteTenant.js +49 -0
- package/lib/src/project/getOne.js +25 -0
- package/lib/src/project/getSome.js +28 -0
- package/lib/src/project/initProject.js +16 -0
- package/lib/src/project/prepareEmulatorData.js +125 -0
- package/lib/src/project/setProject.js +13 -0
- package/lib/src/project/startEmulators.js +30 -0
- package/lib/src/project/updateRecord.js +9 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { generateSchema } from "../deploy/schema/generateSchema.js";
|
|
2
|
+
import { getAccessFields, getField, getDependencyIndexFields, isDependencyField, isRelationField, getRoleGroups, getFieldCustomization, } from "@stoker-platform/utils";
|
|
3
|
+
import { statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
/* eslint-disable security/detect-object-injection */
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
export const securityReport = async () => {
|
|
9
|
+
const writeRuleReads = {};
|
|
10
|
+
const roles = {};
|
|
11
|
+
let isError = false;
|
|
12
|
+
const schema = await generateSchema();
|
|
13
|
+
const path = join(process.cwd(), "lib", "main.js");
|
|
14
|
+
const url = pathToFileURL(path).href;
|
|
15
|
+
const globalConfigFile = await import(url);
|
|
16
|
+
const globalConfig = globalConfigFile.default("node");
|
|
17
|
+
for (const [collectionName, collectionSchema] of Object.entries(schema.collections)) {
|
|
18
|
+
const { auth, fields, access } = collectionSchema;
|
|
19
|
+
const { serverWriteOnly } = access;
|
|
20
|
+
if (serverWriteOnly)
|
|
21
|
+
continue;
|
|
22
|
+
writeRuleReads[collectionName] = {};
|
|
23
|
+
writeRuleReads[collectionName].main = new Set();
|
|
24
|
+
writeRuleReads[collectionName].batch = new Set();
|
|
25
|
+
writeRuleReads[collectionName].main.add("User Document Lookup");
|
|
26
|
+
writeRuleReads[collectionName].main.add("Latest Deploy Document");
|
|
27
|
+
writeRuleReads[collectionName].main.add("Maintenance Mode Document");
|
|
28
|
+
writeRuleReads[collectionName].batch.add("User Document Lookup");
|
|
29
|
+
writeRuleReads[collectionName].batch.add("Latest Deploy Document");
|
|
30
|
+
writeRuleReads[collectionName].batch.add("Maintenance Mode Document");
|
|
31
|
+
const roleGroups = getRoleGroups(collectionSchema, schema);
|
|
32
|
+
roleGroups.forEach(() => {
|
|
33
|
+
writeRuleReads[collectionName].batch.add("Main Document");
|
|
34
|
+
});
|
|
35
|
+
if (auth) {
|
|
36
|
+
writeRuleReads[collectionName].main.add("Document Lock Lookup");
|
|
37
|
+
writeRuleReads[collectionName].batch.add("Document Lock Lookup");
|
|
38
|
+
}
|
|
39
|
+
for (const field of fields) {
|
|
40
|
+
if (isRelationField(field)) {
|
|
41
|
+
if (field.enforceHierarchy) {
|
|
42
|
+
writeRuleReads[collectionName].batch.add(`${field.name} ${field.enforceHierarchy.field} Hierarchy Document`);
|
|
43
|
+
writeRuleReads[collectionName].main.add(`${field.name} ${field.enforceHierarchy.field} Hierarchy Document`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if ("unique" in field && field.unique) {
|
|
47
|
+
writeRuleReads[collectionName].batch.add("Main Document");
|
|
48
|
+
writeRuleReads[collectionName].batch.add(`${field.name} Main Unique Document Exists`);
|
|
49
|
+
writeRuleReads[collectionName].batch.add(`${field.name} Main Unique Document Get`);
|
|
50
|
+
writeRuleReads[collectionName].main.add(`${field.name} Main Unique Document Exists`);
|
|
51
|
+
writeRuleReads[collectionName].main.add(`${field.name} Main Unique Document Get`);
|
|
52
|
+
}
|
|
53
|
+
if (isDependencyField(field, collectionSchema, schema)) {
|
|
54
|
+
writeRuleReads[collectionName].batch.add("Main Document");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const role of globalConfig.roles) {
|
|
59
|
+
roles[role] = {};
|
|
60
|
+
for (const [collectionName, collectionSchema] of Object.entries(schema.collections)) {
|
|
61
|
+
const { fields, access } = collectionSchema;
|
|
62
|
+
if (access.operations.assignable === true ||
|
|
63
|
+
(typeof access.operations.assignable === "object" && access.operations.assignable.includes(role)) ||
|
|
64
|
+
access.operations.read?.includes(role)) {
|
|
65
|
+
for (const field of fields) {
|
|
66
|
+
if (field.access)
|
|
67
|
+
continue;
|
|
68
|
+
const path = join(process.cwd(), "lib", "collections", `${collectionName}.js`);
|
|
69
|
+
const url = pathToFileURL(path).href;
|
|
70
|
+
const customizationFile = await import(url);
|
|
71
|
+
const customization = customizationFile.default("node");
|
|
72
|
+
if (isRelationField(field)) {
|
|
73
|
+
const relationCollection = schema.collections[field.collection];
|
|
74
|
+
const relationAccess = relationCollection.access;
|
|
75
|
+
if (relationAccess.operations.assignable === true ||
|
|
76
|
+
(typeof relationAccess.operations.assignable === "object" &&
|
|
77
|
+
relationAccess.operations.assignable.includes(role)) ||
|
|
78
|
+
!relationAccess.operations.read?.includes(role)) {
|
|
79
|
+
if (field.dependencyFields) {
|
|
80
|
+
for (const dependencyField of field.dependencyFields) {
|
|
81
|
+
if (dependencyField.roles.includes(role)) {
|
|
82
|
+
roles[role][field.collection] ||= {};
|
|
83
|
+
if (!roles[role][field.collection][dependencyField.field]) {
|
|
84
|
+
roles[role][field.collection][dependencyField.field] = new Set();
|
|
85
|
+
}
|
|
86
|
+
roles[role][field.collection][dependencyField.field].add(`${collectionName}- Dependency`);
|
|
87
|
+
const dependencyCollection = schema.collections[field.collection];
|
|
88
|
+
const dependencyFieldSchema = getField(dependencyCollection.fields, dependencyField.field);
|
|
89
|
+
const indexFields = getDependencyIndexFields(dependencyFieldSchema, dependencyCollection, schema);
|
|
90
|
+
for (const indexField of indexFields) {
|
|
91
|
+
const fieldCustomization = getFieldCustomization(indexField, customization);
|
|
92
|
+
roles[role][field.collection] ||= {};
|
|
93
|
+
if (dependencyCollection.access.serverReadOnly?.includes(role) &&
|
|
94
|
+
fieldCustomization?.custom?.serverAccess?.read !== undefined) {
|
|
95
|
+
if (!roles[role][field.collection][indexField.name]) {
|
|
96
|
+
roles[role][field.collection][indexField.name] = new Set();
|
|
97
|
+
}
|
|
98
|
+
roles[role][field.collection][indexField.name].add(`${collectionName}- Dependency- Index- Check Server Read Function`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
if (!roles[role][field.collection][indexField.name]) {
|
|
102
|
+
roles[role][field.collection][indexField.name] = new Set();
|
|
103
|
+
}
|
|
104
|
+
roles[role][field.collection][indexField.name].add(`${collectionName}- Dependency- Index`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (field.includeFields) {
|
|
111
|
+
for (const includeField of field.includeFields) {
|
|
112
|
+
roles[role][field.collection] ||= {};
|
|
113
|
+
if (!roles[role][field.collection][includeField]) {
|
|
114
|
+
roles[role][field.collection][includeField] = new Set();
|
|
115
|
+
}
|
|
116
|
+
if (field.preserve) {
|
|
117
|
+
roles[role][field.collection][includeField].add(`${collectionName} "${field.name}" Relation- Include- Preserved`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
roles[role][field.collection][includeField].add(`${collectionName} "${field.name}" Relation- Include`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
roles[role][field.collection] ||= {};
|
|
125
|
+
if (!roles[role][field.collection][`${collectionName} "${field.name}" Relation${field.preserve ? "- Preserved" : ""}`]) {
|
|
126
|
+
roles[role][field.collection][`${collectionName} "${field.name}" Relation${field.preserve ? "- Preserved" : ""}`] = new Set();
|
|
127
|
+
}
|
|
128
|
+
const restrictCreate = field.restrictCreate === true ||
|
|
129
|
+
(typeof field.restrictCreate === "object" && field.restrictCreate.includes(role));
|
|
130
|
+
const restrictUpdate = field.restrictUpdate === true ||
|
|
131
|
+
(typeof field.restrictUpdate === "object" && field.restrictUpdate.includes(role));
|
|
132
|
+
if (access.operations.assignable === true ||
|
|
133
|
+
(typeof access.operations.assignable === "object" &&
|
|
134
|
+
access.operations.assignable.includes(role)) ||
|
|
135
|
+
access.operations.create?.includes(role) ||
|
|
136
|
+
access.operations.update?.includes(role)) {
|
|
137
|
+
if (restrictCreate && !restrictUpdate) {
|
|
138
|
+
roles[role][field.collection][`${collectionName} "${field.name}" Relation${field.preserve ? "- Preserved" : ""}`].add("Read & Update");
|
|
139
|
+
}
|
|
140
|
+
else if (restrictUpdate && !restrictCreate) {
|
|
141
|
+
roles[role][field.collection][`${collectionName} "${field.name}" Relation${field.preserve ? "- Preserved" : ""}`].add("Read & Create");
|
|
142
|
+
}
|
|
143
|
+
else if (!restrictCreate && !restrictUpdate) {
|
|
144
|
+
roles[role][field.collection][`${collectionName} "${field.name}" Relation${field.preserve ? "- Preserved" : ""}`].add("Read & Create & Update");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
roles[role][field.collection][`${collectionName} "${field.name}" Relation${field.preserve ? "- Preserved" : ""}`].add("Read Only");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
console.log("Security Rule Write Reads:\n");
|
|
157
|
+
for (const [collection, reads] of Object.entries(writeRuleReads)) {
|
|
158
|
+
console.log(`${collection}:\n`);
|
|
159
|
+
const main = Array.from(reads.main).length;
|
|
160
|
+
if (main >= 8) {
|
|
161
|
+
console.log(`[WARN] Main: ${main}`);
|
|
162
|
+
}
|
|
163
|
+
else if (main >= 10) {
|
|
164
|
+
console.log(`[ERROR] Main: ${main}`);
|
|
165
|
+
isError = true;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.log(`Main: ${main}`);
|
|
169
|
+
}
|
|
170
|
+
const batch = Array.from(reads.batch).length;
|
|
171
|
+
if (batch >= 16) {
|
|
172
|
+
console.log(`[WARN] Batch: ${Array.from(reads.batch).length}`);
|
|
173
|
+
}
|
|
174
|
+
else if (batch >= 20) {
|
|
175
|
+
console.log(`[ERROR] Batch: ${batch}`);
|
|
176
|
+
isError = true;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.log(`Batch: ${batch}`);
|
|
180
|
+
}
|
|
181
|
+
console.log("\n");
|
|
182
|
+
}
|
|
183
|
+
console.log("\n\nPossible Excess Permissions:\n");
|
|
184
|
+
for (const [role, collections] of Object.entries(roles)) {
|
|
185
|
+
console.log(`${role.toUpperCase()}\n`);
|
|
186
|
+
for (const [collection, fields] of Object.entries(collections)) {
|
|
187
|
+
console.log(`${collection}:\n${Object.entries(fields)
|
|
188
|
+
.map(([field, collections]) => `${field} (${Array.from(collections).join(", ")})`)
|
|
189
|
+
.join("\n")}`);
|
|
190
|
+
console.log("\n");
|
|
191
|
+
}
|
|
192
|
+
console.log("\n");
|
|
193
|
+
}
|
|
194
|
+
for (const collectionSchema of Object.values(schema.collections)) {
|
|
195
|
+
const accessFields = [
|
|
196
|
+
...new Set(schema.config.roles.map((role) => getAccessFields(collectionSchema, role)).flat()),
|
|
197
|
+
];
|
|
198
|
+
for (const accessField of accessFields) {
|
|
199
|
+
if (!accessField.restrictUpdate && accessField.name !== "id" && accessField.name !== "Created_By") {
|
|
200
|
+
console.error(`Field ${accessField.name} in ${collectionSchema.labels.collection} is used to control access but does not have restrictUpdate set.`);
|
|
201
|
+
isError = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const filePath = join(process.cwd(), "firebase-rules", "firestore.rules");
|
|
206
|
+
const fileStats = statSync(filePath);
|
|
207
|
+
const rulesetSize = fileStats.size / 1024;
|
|
208
|
+
if (rulesetSize > 256) {
|
|
209
|
+
console.error(`[ERROR] Size of ruleset: ${rulesetSize.toFixed(2)} KB - Exceeds Maximum of 256 KB.`);
|
|
210
|
+
isError = true;
|
|
211
|
+
}
|
|
212
|
+
else if (rulesetSize > 200) {
|
|
213
|
+
console.warn(`[WARN] Size of ruleset: ${rulesetSize.toFixed(2)} KB.`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
console.log(`Size of ruleset: ${rulesetSize.toFixed(2)} KB.`);
|
|
217
|
+
}
|
|
218
|
+
if (isError) {
|
|
219
|
+
throw new Error("Security errors found in schema.");
|
|
220
|
+
}
|
|
221
|
+
console.log("\nNo security errors found in schema.");
|
|
222
|
+
process.exit();
|
|
223
|
+
};
|