@donotdev/cli 0.0.14 → 0.0.16
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/dependencies-matrix.json +372 -88
- package/dist/bin/commands/agent-setup.js +7 -1
- package/dist/bin/commands/build.js +141 -44
- package/dist/bin/commands/bump.js +81 -41
- package/dist/bin/commands/cacheout.js +37 -9
- package/dist/bin/commands/create-app.js +276 -121
- package/dist/bin/commands/create-project.js +506 -217
- package/dist/bin/commands/deploy.js +1785 -694
- package/dist/bin/commands/dev.js +177 -43
- package/dist/bin/commands/doctor.d.ts +6 -0
- package/dist/bin/commands/doctor.d.ts.map +1 -0
- package/dist/bin/commands/{lint.js → doctor.js} +1215 -156
- package/dist/bin/commands/doctor.js.map +1 -0
- package/dist/bin/commands/emu.js +451 -104
- package/dist/bin/commands/format.js +37 -9
- package/dist/bin/commands/make-admin.js +77499 -11
- package/dist/bin/commands/preview.js +181 -43
- package/dist/bin/commands/setup.d.ts +6 -0
- package/dist/bin/commands/setup.d.ts.map +1 -0
- package/dist/bin/commands/setup.js +11733 -0
- package/dist/bin/commands/setup.js.map +1 -0
- package/dist/bin/commands/supabase-setup.d.ts +6 -0
- package/dist/bin/commands/supabase-setup.d.ts.map +1 -0
- package/dist/bin/commands/supabase-setup.js +7 -0
- package/dist/bin/commands/supabase-setup.js.map +1 -0
- package/dist/bin/commands/sync-secrets.js +211 -34
- package/dist/bin/commands/type-check.d.ts +14 -0
- package/dist/bin/commands/type-check.d.ts.map +1 -0
- package/dist/bin/commands/type-check.js +2049 -0
- package/dist/bin/commands/type-check.js.map +1 -0
- package/dist/bin/commands/wai.js +3 -1
- package/dist/bin/dndev.js +73 -52
- package/dist/bin/donotdev.js +54 -45
- package/dist/index.js +4212 -3050
- package/package.json +3 -3
- package/templates/app-demo/src/App.tsx.example +1 -0
- package/templates/app-demo/src/pages/FullPage.tsx.example +2 -2
- package/templates/app-demo/src/pages/components/DemoLayout.tsx.example +2 -2
- package/templates/app-demo/src/themes.css.example +5 -12
- package/templates/app-expo/.env.example +44 -0
- package/templates/app-expo/.expo/README.md.example +5 -0
- package/templates/app-expo/.gitignore.example +36 -0
- package/templates/app-expo/README.md.example +58 -0
- package/templates/app-expo/app/.gitkeep +2 -0
- package/templates/app-expo/app/_layout.tsx.example +41 -0
- package/templates/app-expo/app/form.tsx.example +52 -0
- package/templates/app-expo/app/index.tsx.example +89 -0
- package/templates/app-expo/app/list.tsx.example +32 -0
- package/templates/app-expo/app/profile.tsx.example +76 -0
- package/templates/app-expo/app/signin.tsx.example +53 -0
- package/templates/app-expo/app.json.example +39 -0
- package/templates/app-expo/assets/adaptive-icon.png +0 -0
- package/templates/app-expo/assets/favicon.png +0 -0
- package/templates/app-expo/assets/icon.png +0 -0
- package/templates/app-expo/assets/splash.png +0 -0
- package/templates/app-expo/babel.config.js.example +10 -0
- package/templates/app-expo/eas.json.example +20 -0
- package/templates/app-expo/expo-env.d.ts.example +4 -0
- package/templates/app-expo/metro.config.js.example +20 -0
- package/templates/app-expo/service-account-key.json.example +12 -0
- package/templates/app-expo/src/config/app.ts.example +46 -0
- package/templates/app-expo/src/config/providers.ts.example +7 -0
- package/templates/app-expo/tsconfig.json.example +19 -0
- package/templates/app-next/.env.example +4 -33
- package/templates/app-next/src/app/ClientLayout.tsx.example +2 -0
- package/templates/app-next/src/app/layout.tsx.example +7 -6
- package/templates/app-next/src/config/providers.ts.example +7 -0
- package/templates/app-next/src/globals.css.example +2 -11
- package/templates/app-next/src/pages/HomePage.tsx.example +1 -1
- package/templates/app-next/src/themes.css.example +10 -13
- package/templates/app-vite/.env.example +3 -32
- package/templates/app-vite/index.html.example +2 -24
- package/templates/app-vite/src/App.tsx.example +2 -0
- package/templates/app-vite/src/config/providers.ts.example +7 -0
- package/templates/app-vite/src/globals.css.example +2 -12
- package/templates/app-vite/src/pages/FormPageExample.tsx.example +1 -2
- package/templates/app-vite/src/pages/HomePage.tsx.example +2 -2
- package/templates/app-vite/src/themes.css.example +109 -79
- package/templates/app-vite/vercel.json.example +11 -0
- package/templates/functions-firebase/README.md.example +1 -1
- package/templates/functions-firebase/build.mjs.example +2 -72
- package/templates/functions-firebase/functions-firebase/.env.example.example +24 -26
- package/templates/functions-firebase/functions-firebase/README.md.example +1 -1
- package/templates/functions-firebase/functions-firebase/build.mjs.example +2 -72
- package/templates/functions-firebase/functions-firebase/tsconfig.json.example +1 -1
- package/templates/functions-firebase/functions.config.js.example +1 -1
- package/templates/functions-supabase/supabase/config.toml.example +59 -0
- package/templates/functions-supabase/supabase/functions/.env.example +13 -0
- package/templates/functions-supabase/supabase/functions/cancel-subscription/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/change-plan/index.ts.example +11 -0
- package/templates/functions-supabase/supabase/functions/create-checkout-session/index.ts.example +11 -0
- package/templates/functions-supabase/supabase/functions/create-customer-portal/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/crud/index.ts.example +16 -0
- package/templates/functions-supabase/supabase/functions/delete-account/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/deno.json.example +8 -0
- package/templates/functions-supabase/supabase/functions/get-custom-claims/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/get-user-auth-status/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/refresh-subscription-status/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/remove-custom-claims/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/set-custom-claims/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/migrations/20250101000000_idempotency.sql +24 -0
- package/templates/functions-supabase/supabase/migrations/20250101000001_rate_limits.sql +22 -0
- package/templates/functions-supabase/supabase/migrations/20250101000002_cleanup_jobs.sql +28 -0
- package/templates/functions-supabase/supabase/migrations/20250101000003_operation_metrics.sql +28 -0
- package/templates/functions-vercel/functions-vercel/tsconfig.json.example +1 -1
- package/templates/functions-vercel/functions-vercel/vercel.json.example +1 -1
- package/templates/functions-vercel/vercel.json.example +1 -1
- package/templates/github/github/workflows/firebase-deploy.yml.example +1 -1
- package/templates/github/workflows/firebase-deploy.yml.example +1 -1
- package/templates/overlay-firebase/env.fragment.example +34 -0
- package/templates/overlay-firebase/env.fragment.expo.example +34 -0
- package/templates/overlay-firebase/env.fragment.nextjs.example +34 -0
- package/templates/overlay-firebase/src/config/providers.expo.ts.example +49 -0
- package/templates/overlay-firebase/src/config/providers.ts.example +23 -0
- package/templates/overlay-supabase/env.fragment.example +12 -0
- package/templates/overlay-supabase/env.fragment.expo.example +12 -0
- package/templates/overlay-supabase/env.fragment.nextjs.example +12 -0
- package/templates/overlay-supabase/src/config/providers.expo.ts.example +35 -0
- package/templates/overlay-supabase/src/config/providers.ts.example +33 -0
- package/templates/overlay-supabase/vercel.headers.example +23 -0
- package/templates/overlay-supabase/vercel.json.example +22 -0
- package/templates/overlay-vercel/env.fragment.example +34 -0
- package/templates/overlay-vercel/env.fragment.nextjs.example +34 -0
- package/templates/overlay-vercel/src/config/providers.ts.example +24 -0
- package/templates/root-consumer/.claude/agents/architect.md.example +2 -310
- package/templates/root-consumer/.claude/agents/builder.md.example +2 -326
- package/templates/root-consumer/.claude/agents/coder.md.example +2 -83
- package/templates/root-consumer/.claude/agents/extractor.md.example +2 -231
- package/templates/root-consumer/.claude/agents/polisher.md.example +2 -132
- package/templates/root-consumer/.claude/agents/prompt-engineer.md.example +2 -81
- package/templates/root-consumer/.claude/commands/grill.md.example +30 -0
- package/templates/root-consumer/.claude/commands/techdebt.md.example +28 -0
- package/templates/root-consumer/.clinerules.example +1 -0
- package/templates/root-consumer/.cursor/rules/no-docs.mdc.example +15 -0
- package/templates/root-consumer/.cursorrules.example +1 -0
- package/templates/root-consumer/.github/copilot-instructions.md.example +1 -0
- package/templates/root-consumer/.windsurfrules.example +1 -0
- package/templates/root-consumer/AI.md.example +44 -123
- package/templates/root-consumer/CLAUDE.md.example +1 -134
- package/templates/root-consumer/CONVENTIONS.md.example +1 -0
- package/templates/root-consumer/GEMINI.md.example +1 -0
- package/templates/root-consumer/firebase.json.example +1 -1
- package/templates/root-consumer/guides/dndev/AGENT_START_HERE.md.example +22 -2
- package/templates/root-consumer/guides/dndev/COMPONENTS_ADV.md.example +0 -18
- package/templates/root-consumer/guides/dndev/COMPONENTS_UI.md.example +1 -1
- package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +101 -32
- package/templates/root-consumer/guides/dndev/INDEX.md.example +4 -2
- package/templates/root-consumer/guides/dndev/SETUP_APP_CONFIG.md.example +3 -3
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +241 -12
- package/templates/root-consumer/guides/dndev/SETUP_FIREBASE.md.example +13 -7
- package/templates/root-consumer/guides/dndev/SETUP_OAUTH_PROVIDERS.md.example +60 -0
- package/templates/root-consumer/guides/dndev/SETUP_SOC2.md.example +234 -0
- package/templates/root-consumer/guides/dndev/SETUP_STRIPE.md.example +62 -0
- package/templates/root-consumer/guides/dndev/SETUP_SUPABASE.md.example +124 -0
- package/templates/root-consumer/guides/dndev/SETUP_THEMES.md.example +6 -2
- package/templates/root-consumer/guides/dndev/SETUP_VERCEL.md.example +176 -0
- package/templates/root-consumer/guides/dndev/USE_ROUTING.md.example +5 -9
- package/templates/root-consumer/guides/dndev/essences_reference.css.example +174 -0
- package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +7 -8
- package/templates/root-consumer/guides/wai-way/agents/builder.md.example +10 -0
- package/templates/root-consumer/guides/wai-way/agents/extractor.md.example +25 -5
- package/templates/root-consumer/guides/wai-way/agents/polisher.md.example +13 -2
- package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +2 -2
- package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +55 -15
- package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +15 -4
- package/templates/root-consumer/guides/wai-way/spec_template.md.example +7 -6
- package/dist/bin/commands/lint.d.ts +0 -11
- package/dist/bin/commands/lint.d.ts.map +0 -1
- package/dist/bin/commands/lint.js.map +0 -1
- package/dist/bin/commands/staging.d.ts +0 -11
- package/dist/bin/commands/staging.d.ts.map +0 -1
- package/dist/bin/commands/staging.js +0 -12
- package/dist/bin/commands/staging.js.map +0 -1
- package/templates/app-payload/.env.example +0 -28
- package/templates/app-payload/README.md.example +0 -233
- package/templates/app-payload/collections/Company.ts.example +0 -125
- package/templates/app-payload/collections/Hero.ts.example +0 -62
- package/templates/app-payload/collections/Media.ts.example +0 -41
- package/templates/app-payload/collections/Products.ts.example +0 -115
- package/templates/app-payload/collections/Services.ts.example +0 -104
- package/templates/app-payload/collections/Testimonials.ts.example +0 -92
- package/templates/app-payload/collections/Users.ts.example +0 -35
- package/templates/app-payload/src/server.ts.example +0 -79
- package/templates/app-payload/tsconfig.json.example +0 -24
|
@@ -20,6 +20,34 @@
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
+
## 0. Provider configuration (required)
|
|
24
|
+
|
|
25
|
+
CRUD operations use the **CRUD provider** registered at app startup. You must call `configureProviders()` before any component uses `useCrud`.
|
|
26
|
+
|
|
27
|
+
- **Where:** In a dedicated module, e.g. `src/config/providers.ts`.
|
|
28
|
+
- **When:** Import that module from your root component (e.g. `App.tsx`) so it runs before any CRUD usage.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// App.tsx
|
|
32
|
+
import './config/providers'; // Before any useCrud
|
|
33
|
+
// ...
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// config/providers.ts
|
|
38
|
+
import { configureProviders } from '@donotdev/core';
|
|
39
|
+
import { FirestoreAdapter } from '@donotdev/firebase'; // or SupabaseCrudAdapter from '@donotdev/supabase'
|
|
40
|
+
|
|
41
|
+
configureProviders({
|
|
42
|
+
crud: new FirestoreAdapter(),
|
|
43
|
+
// auth, storage optional
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If you skip this step, `useCrud` will throw at runtime: "Provider \"crud\" not available. Call configureProviders() at app startup."
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
23
51
|
## 1. Define Entity
|
|
24
52
|
|
|
25
53
|
```typescript
|
|
@@ -80,7 +108,7 @@ For multi-tenant apps where data belongs to a company/tenant/workspace:
|
|
|
80
108
|
### Step 1: Register Scope Provider (once at app startup)
|
|
81
109
|
|
|
82
110
|
```typescript
|
|
83
|
-
// src/
|
|
111
|
+
// src/App.tsx
|
|
84
112
|
import { registerScopeProvider } from '@donotdev/core';
|
|
85
113
|
import { useCurrentCompanyStore } from './stores/currentCompanyStore';
|
|
86
114
|
|
|
@@ -665,9 +693,15 @@ transmission: {
|
|
|
665
693
|
|
|
666
694
|
## 7. Custom Fields & Schemas
|
|
667
695
|
|
|
668
|
-
|
|
696
|
+
A custom field type (when the requirement is not a built-in type) needs up to four things: **(1)** entity field with your custom `type` and `validation.schema`, **(2)** form component(s) so the field is editable, **(3)** optional **display** formatter so list/card/detail views render the value correctly, **(4)** optional **filter** so the field appears in EntityFilters with the right filter UI. All four are wired through a single `registerFieldType({ ... })` call.
|
|
669
697
|
|
|
670
|
-
|
|
698
|
+
> **Custom type strings work out of the box.** Just use any string as the `type` — no declaration file or module augmentation needed. The framework accepts any string while still autocompleting built-in types.
|
|
699
|
+
>
|
|
700
|
+
> `CustomFieldOptionsMap` augmentation (see below) is only needed if you want type-checked `options.fieldSpecific` for your custom type.
|
|
701
|
+
|
|
702
|
+
### Form (required for editable fields)
|
|
703
|
+
|
|
704
|
+
You must register a **controlled component** so the field appears in forms. Optionally register an **uncontrolled component** (e.g. for submit/reset-style fields).
|
|
671
705
|
|
|
672
706
|
```typescript
|
|
673
707
|
import { registerFieldType, useController } from '@donotdev/crud';
|
|
@@ -687,7 +721,6 @@ function RepairOperationsField({
|
|
|
687
721
|
control: control,
|
|
688
722
|
});
|
|
689
723
|
|
|
690
|
-
// Use field.value and field.onChange for form state
|
|
691
724
|
const value = (field.value as any) || [];
|
|
692
725
|
|
|
693
726
|
return (
|
|
@@ -701,13 +734,13 @@ function RepairOperationsField({
|
|
|
701
734
|
);
|
|
702
735
|
}
|
|
703
736
|
|
|
704
|
-
//
|
|
737
|
+
// Minimal registration (form only)
|
|
705
738
|
registerFieldType({
|
|
706
739
|
type: 'repairOperations',
|
|
707
740
|
controlledComponent: RepairOperationsField,
|
|
708
741
|
});
|
|
709
742
|
|
|
710
|
-
//
|
|
743
|
+
// Entity with schema
|
|
711
744
|
export const carEntity = defineEntity({
|
|
712
745
|
name: 'Car',
|
|
713
746
|
collection: 'cars',
|
|
@@ -715,11 +748,10 @@ export const carEntity = defineEntity({
|
|
|
715
748
|
repairs: {
|
|
716
749
|
name: 'repairs',
|
|
717
750
|
label: 'repairs',
|
|
718
|
-
type: 'repairOperations'
|
|
751
|
+
type: 'repairOperations',
|
|
719
752
|
visibility: 'admin',
|
|
720
753
|
validation: {
|
|
721
754
|
required: false,
|
|
722
|
-
// Custom Valibot schema - single source of truth
|
|
723
755
|
schema: v.nullish(v.array(v.object({
|
|
724
756
|
operation: v.string(),
|
|
725
757
|
cost: v.number(),
|
|
@@ -730,10 +762,207 @@ export const carEntity = defineEntity({
|
|
|
730
762
|
});
|
|
731
763
|
```
|
|
732
764
|
|
|
733
|
-
**Important:**
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
765
|
+
**Important:** Custom controlled components receive `control` prop, NOT `field` prop. You MUST use `useController` to get `field` and `fieldState`. Define schema in `validation.schema` — it's the single source of truth.
|
|
766
|
+
|
|
767
|
+
### Display (list/card/detail)
|
|
768
|
+
|
|
769
|
+
Without a **displayFormatter**, list/card/detail views show the raw value (or a fallback). To control how the value is rendered in read-only views, pass `displayFormatter` in the same `registerFieldType` call. Signature: `(value, fieldConfig, t, options?) => string | ReactNode`. Use `options?.compact` for list/card vs detail.
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
// Example: format a custom object for display
|
|
773
|
+
displayFormatter: (value, fieldConfig, t, options) => {
|
|
774
|
+
if (value == null) return '';
|
|
775
|
+
const arr = Array.isArray(value) ? value : [];
|
|
776
|
+
if (options?.compact) return `${arr.length} item(s)`;
|
|
777
|
+
return arr.map((item: any) => `${item.operation}: ${item.cost}`).join(', ') || '—';
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### Filter (EntityFilters)
|
|
782
|
+
|
|
783
|
+
For the field to appear in the list/card **filters** section (EntityFilters), set **filterable: true** and **filterType** in `registerFieldType`. The filter type determines the filter UI:
|
|
784
|
+
|
|
785
|
+
| filterType | Use for |
|
|
786
|
+
|----------------|--------|
|
|
787
|
+
| `'text'` | Free text search |
|
|
788
|
+
| `'range'` | Numbers, dates (min/max) |
|
|
789
|
+
| `'select'` | Single choice (e.g. enum) |
|
|
790
|
+
| `'multiselect'`| Multiple choices |
|
|
791
|
+
| `'address'` | Address-based filter |
|
|
792
|
+
| `'none'` | Not filterable (omit or set filterable: false) |
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
registerFieldType({
|
|
796
|
+
type: 'repairOperations',
|
|
797
|
+
controlledComponent: RepairOperationsField,
|
|
798
|
+
displayFormatter: (value, fieldConfig, t, options) => { /* ... */ },
|
|
799
|
+
filterable: true,
|
|
800
|
+
filterType: 'text', // or 'range', 'select', 'multiselect', 'address', 'none'
|
|
801
|
+
});
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Full example: form + display + filter
|
|
805
|
+
|
|
806
|
+
One registration with form, displayFormatter, and filter:
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
import { registerFieldType, useController } from '@donotdev/crud';
|
|
810
|
+
import type { ControlledFieldProps } from '@donotdev/crud';
|
|
811
|
+
import { defineEntity } from '@donotdev/core';
|
|
812
|
+
import * as v from 'valibot';
|
|
813
|
+
|
|
814
|
+
function StatusTierField({ fieldConfig, control, errors, t }: ControlledFieldProps) {
|
|
815
|
+
const { field, fieldState } = useController({ name: fieldConfig.name, control });
|
|
816
|
+
return (
|
|
817
|
+
<div>
|
|
818
|
+
<label>{t(fieldConfig.label)}</label>
|
|
819
|
+
<select value={field.value ?? ''} onChange={(e) => field.onChange(e.target.value)}>
|
|
820
|
+
<option value="">—</option>
|
|
821
|
+
<option value="basic">Basic</option>
|
|
822
|
+
<option value="premium">Premium</option>
|
|
823
|
+
</select>
|
|
824
|
+
{fieldState?.error && <span className="error">{fieldState.error.message}</span>}
|
|
825
|
+
</div>
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
registerFieldType({
|
|
830
|
+
type: 'statusTier',
|
|
831
|
+
controlledComponent: StatusTierField,
|
|
832
|
+
displayFormatter: (value) => (value ? String(value) : '—'),
|
|
833
|
+
filterable: true,
|
|
834
|
+
filterType: 'select',
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
export const productEntity = defineEntity({
|
|
838
|
+
name: 'Product',
|
|
839
|
+
collection: 'products',
|
|
840
|
+
fields: {
|
|
841
|
+
statusTier: {
|
|
842
|
+
name: 'statusTier',
|
|
843
|
+
label: 'statusTier',
|
|
844
|
+
type: 'statusTier',
|
|
845
|
+
visibility: 'guest',
|
|
846
|
+
validation: { schema: v.optional(v.picklist(['basic', 'premium'])) },
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### Typing custom field options
|
|
853
|
+
|
|
854
|
+
To get type-checked `options.fieldSpecific` for custom types (e.g. `extractDistrictCode: boolean`), augment the framework's `CustomFieldOptionsMap` in your app:
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
// e.g. in src/types/crud.d.ts or next to your entity
|
|
858
|
+
import '@donotdev/core';
|
|
859
|
+
declare module '@donotdev/core' {
|
|
860
|
+
interface CustomFieldOptionsMap {
|
|
861
|
+
'repairOperations': { maxItems?: number };
|
|
862
|
+
'isousou-address': { extractDistrictCode?: boolean };
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
Then in entity fields you can use `type: 'repairOperations'` and `options: { fieldSpecific: { maxItems: 10 } }` without type errors or `as any`.
|
|
868
|
+
|
|
869
|
+
### Reference Select Fields (Entity Lookup)
|
|
870
|
+
|
|
871
|
+
A common pattern: a field stores a **reference ID** (e.g. `author_ref`) pointing to another entity (e.g. `Author`). The form needs a select/combobox populated with that entity's records, and list/detail views need to display a human-readable label instead of a raw ID.
|
|
872
|
+
|
|
873
|
+
The framework's built-in `reference` type uses `fieldSpecific.displayField` and `fieldSpecific.searchFields`. For **custom** reference types (e.g. `author-select`), follow the same pattern with `fieldSpecific.labelFields`.
|
|
874
|
+
|
|
875
|
+
**Step 1: Augment `CustomFieldOptionsMap`** so `labelFields` is typed:
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
// entities/crud.d.ts
|
|
879
|
+
import '@donotdev/core';
|
|
880
|
+
declare module '@donotdev/core' {
|
|
881
|
+
interface CustomFieldOptionsMap {
|
|
882
|
+
'author-select': { labelFields: string[] };
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
**Step 2: Declare `labelFields` in the entity definition** (SSOT — never hardcode field names in components):
|
|
888
|
+
|
|
889
|
+
```typescript
|
|
890
|
+
// entities/book.ts
|
|
891
|
+
import { defineEntity } from '@donotdev/core';
|
|
892
|
+
|
|
893
|
+
export const bookEntity = defineEntity({
|
|
894
|
+
name: 'Book',
|
|
895
|
+
collection: 'books',
|
|
896
|
+
fields: {
|
|
897
|
+
author_ref: {
|
|
898
|
+
name: 'author_ref',
|
|
899
|
+
label: 'fields.author',
|
|
900
|
+
type: 'author-select',
|
|
901
|
+
visibility: 'admin',
|
|
902
|
+
validation: { required: false, reference: 'authors' },
|
|
903
|
+
options: { fieldSpecific: { labelFields: ['first_name', 'last_name'] } },
|
|
904
|
+
},
|
|
905
|
+
// ...
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
**Step 3: Build labels from `labelFields`** in your shared formatters:
|
|
911
|
+
|
|
912
|
+
```typescript
|
|
913
|
+
// entities/formatters.ts
|
|
914
|
+
/**
|
|
915
|
+
* Build a display label from a record using labelFields from entity field config.
|
|
916
|
+
* Handles Supabase fieldMapper camelCase conversion: tries camelCase first, then snake_case.
|
|
917
|
+
*/
|
|
918
|
+
export function buildRecordLabel(
|
|
919
|
+
record: Record<string, unknown>,
|
|
920
|
+
labelFields: string[],
|
|
921
|
+
): string {
|
|
922
|
+
return labelFields
|
|
923
|
+
.map((field) => {
|
|
924
|
+
const camel = field.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
925
|
+
return record[camel] ?? record[field] ?? '';
|
|
926
|
+
})
|
|
927
|
+
.join(' ')
|
|
928
|
+
.trim();
|
|
929
|
+
}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
**Step 4: Read `labelFields` in your custom component** (never hardcode field names):
|
|
933
|
+
|
|
934
|
+
```typescript
|
|
935
|
+
// components/fields/AuthorSelectField.tsx
|
|
936
|
+
import { buildRecordLabel } from 'entities/formatters';
|
|
937
|
+
|
|
938
|
+
// Inside the component:
|
|
939
|
+
const labelFields: string[] =
|
|
940
|
+
fieldConfig.options?.fieldSpecific?.labelFields ?? [];
|
|
941
|
+
|
|
942
|
+
const options = useMemo(() => {
|
|
943
|
+
return items.map((item) => ({
|
|
944
|
+
value: String(item.id ?? ''),
|
|
945
|
+
label: buildRecordLabel(item, labelFields),
|
|
946
|
+
}));
|
|
947
|
+
}, [items, labelFields]);
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
**Step 5: Use the same utility for `displayFormatter`** so list/detail views resolve IDs to labels:
|
|
951
|
+
|
|
952
|
+
```typescript
|
|
953
|
+
// In registerFieldType or in your display formatter
|
|
954
|
+
displayFormatter: (value, config) => {
|
|
955
|
+
if (value == null) return '';
|
|
956
|
+
const options = config?.validation?.options;
|
|
957
|
+
if (Array.isArray(options)) {
|
|
958
|
+
const match = options.find((opt: any) => opt.value === value);
|
|
959
|
+
if (match?.label) return String(match.label);
|
|
960
|
+
}
|
|
961
|
+
return String(value);
|
|
962
|
+
}
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
> **Key principle:** Entity field definitions are SSOT. Components read `fieldConfig.options.fieldSpecific.labelFields` — they never hardcode which fields to display. This means changing the label format (e.g. adding `company_name`) only requires editing the entity definition, not every component.
|
|
737
966
|
|
|
738
967
|
### Custom Schemas (No Custom UI)
|
|
739
968
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
## Step 1: Run Firebase Setup
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
dndev firebase
|
|
10
|
+
dndev setup firebase
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
This command:
|
|
@@ -94,7 +94,7 @@ This handles everything:
|
|
|
94
94
|
| `functions/.env` | Server secrets: STRIPE_SECRET_KEY, OAuth secrets | Cloud Functions runtime |
|
|
95
95
|
| Root `.env` | **Not read by Vite.** Reference only. | Nothing |
|
|
96
96
|
|
|
97
|
-
**`dndev firebase
|
|
97
|
+
**`dndev setup firebase` writes Firebase vars to `apps/<app>/.env` automatically.**
|
|
98
98
|
|
|
99
99
|
**Custom domains:** Framework uses `APP_URL` hostname as `authDomain` in production (not Firebase's `projectId.firebaseapp.com`). Copy/paste `FIREBASE_AUTH_DOMAIN` from Firebase Console in both `.env.local` and `.env.production` — framework handles the rest.
|
|
100
100
|
|
|
@@ -103,7 +103,7 @@ This handles everything:
|
|
|
103
103
|
## Staging Environment (Optional)
|
|
104
104
|
|
|
105
105
|
1. Create a second Firebase project (e.g., `my-app-staging`)
|
|
106
|
-
2. Run `dndev firebase
|
|
106
|
+
2. Run `dndev setup firebase` again and select the staging project
|
|
107
107
|
3. Add to `.firebaserc`: `{ "projects": { "staging": "my-app-staging" } }`
|
|
108
108
|
4. Create `service-account-key.staging.json` (same steps as production)
|
|
109
109
|
5. Deploy: `dndev staging`
|
|
@@ -112,23 +112,29 @@ This handles everything:
|
|
|
112
112
|
|
|
113
113
|
## Secrets (Stripe, OAuth, etc.)
|
|
114
114
|
|
|
115
|
-
Server-side secrets go in `functions/.env`, not the app `.env
|
|
115
|
+
Server-side secrets go in `functions/.env`, not the app `.env`.
|
|
116
|
+
|
|
117
|
+
**We NEVER ask for secret keys.** You place them yourself:
|
|
116
118
|
|
|
117
119
|
```bash
|
|
118
120
|
# functions/.env
|
|
119
121
|
STRIPE_SECRET_KEY=sk_live_...
|
|
120
122
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
123
|
+
SUPABASE_SERVICE_ROLE_KEY=eyJ... # if using Supabase
|
|
121
124
|
GITHUB_CLIENT_SECRET=...
|
|
122
125
|
```
|
|
123
126
|
|
|
124
|
-
|
|
127
|
+
Then sync to your runtime and CI/CD:
|
|
125
128
|
|
|
126
129
|
```bash
|
|
127
|
-
dndev sync-secrets
|
|
130
|
+
dndev sync-secrets # Firebase Secret Manager / Vercel env
|
|
131
|
+
dndev sync-secrets --target github # GitHub Secrets (for CI/CD workflows)
|
|
128
132
|
```
|
|
129
133
|
|
|
130
134
|
Secrets are auto-loaded by Cloud Functions at runtime. Never put server secrets in `VITE_*` variables — those are exposed to the browser.
|
|
131
135
|
|
|
136
|
+
See [ENV_SETUP.md → Secrets Philosophy](./ENV_SETUP.md#secrets-philosophy) for the full policy.
|
|
137
|
+
|
|
132
138
|
---
|
|
133
139
|
|
|
134
140
|
## Cloud Run IAM (Technical Detail)
|
|
@@ -165,4 +171,4 @@ If you deploy manually with `firebase deploy`, you'll need to run this yourself.
|
|
|
165
171
|
|
|
166
172
|
---
|
|
167
173
|
|
|
168
|
-
**`dndev firebase
|
|
174
|
+
**`dndev setup firebase` → download service account key → enable Auth + Firestore → `dndev emu start` → `dndev deploy`. That's it.**
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# OAuth Provider Setup Guide
|
|
2
|
+
|
|
3
|
+
Each OAuth provider requires manual registration in their developer console.
|
|
4
|
+
`dndev setup oauth` computes the correct redirect URI for you.
|
|
5
|
+
|
|
6
|
+
## Redirect URIs
|
|
7
|
+
|
|
8
|
+
### Firebase
|
|
9
|
+
```
|
|
10
|
+
https://{{PROJECT_ID}}.firebaseapp.com/__/auth/handler
|
|
11
|
+
```
|
|
12
|
+
> If you use a custom auth domain, replace `{{PROJECT_ID}}.firebaseapp.com` with your custom domain.
|
|
13
|
+
|
|
14
|
+
### Supabase
|
|
15
|
+
```
|
|
16
|
+
https://{{PROJECT_ID}}.supabase.co/auth/v1/callback
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Google OAuth
|
|
22
|
+
|
|
23
|
+
1. Go to https://console.cloud.google.com/apis/credentials
|
|
24
|
+
2. Create **OAuth 2.0 Client ID** (type: Web application)
|
|
25
|
+
3. Add **Authorized redirect URI** (see above)
|
|
26
|
+
4. Copy **Client ID** and **Client Secret**
|
|
27
|
+
5. Enable in your backend:
|
|
28
|
+
- Firebase: Console > Authentication > Sign-in method > Google
|
|
29
|
+
- Supabase: Dashboard > Authentication > Providers > Google
|
|
30
|
+
|
|
31
|
+
## GitHub OAuth
|
|
32
|
+
|
|
33
|
+
1. Go to https://github.com/settings/developers
|
|
34
|
+
2. Click **New OAuth App**
|
|
35
|
+
3. Set **Authorization callback URL** (see redirect URI above)
|
|
36
|
+
4. Copy **Client ID** and **Client Secret**
|
|
37
|
+
5. Enable in your backend:
|
|
38
|
+
- Firebase: Console > Authentication > Sign-in method > GitHub
|
|
39
|
+
- Supabase: Dashboard > Authentication > Providers > GitHub
|
|
40
|
+
|
|
41
|
+
## Apple Sign In
|
|
42
|
+
|
|
43
|
+
1. Go to https://developer.apple.com/account/resources/identifiers/list/serviceId
|
|
44
|
+
2. Create a **Services ID**
|
|
45
|
+
3. Enable **Sign In with Apple**
|
|
46
|
+
4. Add **Redirect URL** (see above)
|
|
47
|
+
5. Create a **Key** for Sign In with Apple
|
|
48
|
+
6. Enable in your backend:
|
|
49
|
+
- Firebase: Console > Authentication > Sign-in method > Apple
|
|
50
|
+
- Supabase: Dashboard > Authentication > Providers > Apple
|
|
51
|
+
|
|
52
|
+
> **Note:** Apple requires an Apple Developer Program membership ($99/year).
|
|
53
|
+
|
|
54
|
+
## Verify
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
dndev doctor
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Check that auth providers are detected and backend configuration is referenced.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Setup: SOC2 Readiness
|
|
2
|
+
|
|
3
|
+
**DoNotDev auto-enforces SOC2 baseline controls.** Zero config needed for the MVP. This guide covers advanced controls and audit verification.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Zero-Config Baseline (Always On)
|
|
8
|
+
|
|
9
|
+
When you wire `DndevSecurity` into your app, these controls activate automatically:
|
|
10
|
+
|
|
11
|
+
| Control | Default | SOC2 Criteria |
|
|
12
|
+
|---------|---------|---------------|
|
|
13
|
+
| Structured audit logging (all CRUD + auth) | ✅ Automatic | CC7.1 |
|
|
14
|
+
| Rate limiting (100 writes/min, 500 reads/min) | ✅ Automatic | CC6.6 |
|
|
15
|
+
| Brute-force lockout (5 attempts → 15 min) | ✅ Automatic | CC6.1 |
|
|
16
|
+
| Session timeout (8h idle) | ✅ Automatic | CC6.1 |
|
|
17
|
+
| Field-level RBAC + visibility | ✅ Always on (entity config) | CC6.3 |
|
|
18
|
+
| Supabase RLS / Firestore default-deny | ✅ By convention | CC6.3 |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Wire `DndevSecurity` (Required Once)
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// src/security.ts
|
|
26
|
+
import { DndevSecurity } from '@donotdev/security/server';
|
|
27
|
+
|
|
28
|
+
export const security = new DndevSecurity({
|
|
29
|
+
// Optional: override defaults
|
|
30
|
+
// rateLimit: { writesPerMin: 50, readsPerMin: 200 },
|
|
31
|
+
// lockout: { maxAttempts: 3, lockoutMs: 30 * 60 * 1000 },
|
|
32
|
+
// sessionTimeoutMs: 4 * 60 * 60 * 1000,
|
|
33
|
+
|
|
34
|
+
// Required for PII encryption (see § PII Encryption below)
|
|
35
|
+
// piiSecret: process.env.PII_SECRET,
|
|
36
|
+
|
|
37
|
+
// Optional: send anomaly alerts to your SIEM/Slack
|
|
38
|
+
// anomalyHandler: async (alert) => { await fetch(process.env.SIEM_WEBHOOK!, { method: 'POST', body: JSON.stringify(alert) }); },
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// src/main.ts (or app entry point)
|
|
44
|
+
import { security } from './security';
|
|
45
|
+
import { crudService } from './crud'; // your CrudService instance
|
|
46
|
+
import { auth } from './auth'; // your SupabaseAuth or FirebaseSDK instance
|
|
47
|
+
|
|
48
|
+
crudService.setSecurity(security);
|
|
49
|
+
auth.setSecurity(security);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 2. PII Encryption (Opt-In — C1 Confidentiality)
|
|
55
|
+
|
|
56
|
+
Required only for entities storing personal data (email, phone, SSN, health data, etc.).
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// entities/user.ts
|
|
60
|
+
export const userEntity = defineEntity({
|
|
61
|
+
name: 'User',
|
|
62
|
+
collection: 'users',
|
|
63
|
+
security: {
|
|
64
|
+
piiFields: ['email', 'phone', 'dateOfBirth'], // Encrypted at rest with AES-256-GCM
|
|
65
|
+
},
|
|
66
|
+
fields: { /* ... */ },
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Add to .env (min 32 chars, generate with: openssl rand -hex 32)
|
|
72
|
+
PII_SECRET=your-secret-key-min-32-chars-here
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Pass to security:
|
|
76
|
+
```typescript
|
|
77
|
+
export const security = new DndevSecurity({
|
|
78
|
+
piiSecret: process.env.PII_SECRET,
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 3. MFA Enforcement (Opt-In — CC6.1)
|
|
85
|
+
|
|
86
|
+
Enforce MFA for specific roles on sensitive entities:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
export const paymentEntity = defineEntity({
|
|
90
|
+
name: 'Payment',
|
|
91
|
+
collection: 'payments',
|
|
92
|
+
security: {
|
|
93
|
+
requireMfa: 'admin', // 'admin' | 'manager' | 'user'
|
|
94
|
+
},
|
|
95
|
+
fields: { /* ... */ },
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Enable MFA in your provider:
|
|
100
|
+
- **Supabase:** Dashboard → Auth → Multi Factor Authentication → Enable TOTP
|
|
101
|
+
- **Firebase:** Firebase Console → Authentication → Multi-factor Auth → Enable
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 4. Data Retention (Opt-In — P6 Privacy)
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
export const auditLogEntity = defineEntity({
|
|
109
|
+
name: 'AuditLog',
|
|
110
|
+
collection: 'audit_logs',
|
|
111
|
+
security: {
|
|
112
|
+
retention: { days: 365 }, // Auto-flag records for purge after 1 year
|
|
113
|
+
},
|
|
114
|
+
fields: { /* ... */ },
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Run retention purge (e.g., in a scheduled Cloud Function):
|
|
119
|
+
```typescript
|
|
120
|
+
import { privacyManager } from '@donotdev/security/server';
|
|
121
|
+
// privacyManager.shouldPurge(record.createdAt, retentionDays) → boolean
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 5. Right to Erasure (GDPR Art. 17 / P8)
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { DndevSecurity } from '@donotdev/security/server';
|
|
130
|
+
|
|
131
|
+
// In your account deletion handler:
|
|
132
|
+
await security.privacyManager.eraseUser(userId, async (uid) => {
|
|
133
|
+
// Your custom erasure logic per collection
|
|
134
|
+
await db.from('orders').update({ userId: null }).eq('userId', uid);
|
|
135
|
+
await db.from('profiles').delete().eq('id', uid);
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 6. Health Monitoring (Opt-In — A1 Availability)
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// src/client/health.ts
|
|
145
|
+
import { HealthMonitor } from '@donotdev/security';
|
|
146
|
+
|
|
147
|
+
const dbMonitor = new HealthMonitor({ failureThreshold: 3, successThreshold: 2, timeoutMs: 5000 });
|
|
148
|
+
|
|
149
|
+
// Wrap all downstream DB calls through the circuit breaker
|
|
150
|
+
export const safeQuery = (fn: () => Promise<unknown>) => dbMonitor.protect(fn);
|
|
151
|
+
|
|
152
|
+
// Expose a health endpoint (Next.js example)
|
|
153
|
+
// app/api/health/route.ts
|
|
154
|
+
export async function GET() {
|
|
155
|
+
return Response.json({ status: dbMonitor.checkLiveness() });
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 7. Anomaly Detection (Opt-In — CC7.2)
|
|
162
|
+
|
|
163
|
+
Triggered automatically when brute-force or bulk-delete thresholds are exceeded. Wire a handler to send alerts:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
export const security = new DndevSecurity({
|
|
167
|
+
anomalyHandler: async (alert) => {
|
|
168
|
+
// Send to Slack, PagerDuty, Datadog, etc.
|
|
169
|
+
await fetch(process.env.SLACK_WEBHOOK!, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
body: JSON.stringify({ text: `🚨 SOC2 Anomaly: ${alert.type} — ${alert.details}` }),
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 8. Audit Log Output
|
|
180
|
+
|
|
181
|
+
Audit events write to `process.stdout` as NDJSON — pipe to your SIEM:
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{"timestamp":"2025-01-15T10:23:00.000Z","type":"crud.create","userId":"u_123","resource":"products","resourceId":"p_456","ip":"192.168.1.1"}
|
|
185
|
+
{"timestamp":"2025-01-15T10:23:01.000Z","type":"auth.login.failure","userId":"unknown","resource":"auth","ip":"10.0.0.5","details":"brute_force_lockout"}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Cloud Logging (Firebase / GCP):** Stdout is captured automatically.
|
|
189
|
+
**Datadog:** Set `DD_LOGS_INJECTION=true` and pipe stdout to the Datadog agent.
|
|
190
|
+
**Supabase:** Export `process.stderr` anomaly alerts to Logflare.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## SOC2 Readiness Check
|
|
195
|
+
|
|
196
|
+
Run before any audit:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
dn soc2 # Scan current app
|
|
200
|
+
dn soc2 --app my-app # Scan specific app
|
|
201
|
+
dn soc2 --json # Machine-readable JSON output (CI/CD)
|
|
202
|
+
dn soc2 --verbose # Show details for passing checks too
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Exit codes:** `0` = all checks pass, `1` = one or more checks failed (CI-friendly).
|
|
206
|
+
|
|
207
|
+
**Checks run:**
|
|
208
|
+
|
|
209
|
+
| Check | Criteria | What It Looks For |
|
|
210
|
+
|-------|----------|--------------------|
|
|
211
|
+
| SecurityContext wired | CC6.1 | `DndevSecurity` in source |
|
|
212
|
+
| PII encryption secret | C1 | `PII_SECRET` in env when `piiFields` defined |
|
|
213
|
+
| Firestore default-deny | CC6.3 | `allow read, write: if false` in `firestore.rules` |
|
|
214
|
+
| Supabase RLS | CC6.3 | `ENABLE ROW LEVEL SECURITY` in SQL migrations |
|
|
215
|
+
| Audit logging | CC7.1 | `.audit()` / `AuditLogger` usage |
|
|
216
|
+
| Rate limiting | CC6.6 | `rateLimit` / `DndevRateLimiter` usage |
|
|
217
|
+
| Retention policies | P6 | `retention.days` in entity config |
|
|
218
|
+
| Health monitoring | A1 | `HealthMonitor` or `/api/health` endpoint |
|
|
219
|
+
| MFA enforcement | CC6.1 | `requireMfa` in sensitive entities |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Coverage After Full Setup
|
|
224
|
+
|
|
225
|
+
| SOC2 Criteria | Control | Status |
|
|
226
|
+
|---------------|---------|--------|
|
|
227
|
+
| CC6.1 Auth hardening | Lockout + session timeout + MFA | Framework-enforced |
|
|
228
|
+
| CC6.3 Authorization | RBAC + field visibility + RLS/Firestore rules | Framework-enforced |
|
|
229
|
+
| CC6.6 Rate limiting | Per-user sliding window | Framework-enforced |
|
|
230
|
+
| CC7.1 Audit logging | All CRUD + auth events as NDJSON | Framework-enforced |
|
|
231
|
+
| CC7.2 Anomaly detection | Auth failures + bulk ops threshold alerts | Opt-in handler |
|
|
232
|
+
| A1 Availability | Circuit breaker + liveness probes | Opt-in `HealthMonitor` |
|
|
233
|
+
| C1 Confidentiality | AES-256-GCM field encryption | Opt-in `piiFields` |
|
|
234
|
+
| P1–P8 Privacy | Retention policies + right-to-erasure | Opt-in `retention` |
|