@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.
Files changed (184) hide show
  1. package/dependencies-matrix.json +372 -88
  2. package/dist/bin/commands/agent-setup.js +7 -1
  3. package/dist/bin/commands/build.js +141 -44
  4. package/dist/bin/commands/bump.js +81 -41
  5. package/dist/bin/commands/cacheout.js +37 -9
  6. package/dist/bin/commands/create-app.js +276 -121
  7. package/dist/bin/commands/create-project.js +506 -217
  8. package/dist/bin/commands/deploy.js +1785 -694
  9. package/dist/bin/commands/dev.js +177 -43
  10. package/dist/bin/commands/doctor.d.ts +6 -0
  11. package/dist/bin/commands/doctor.d.ts.map +1 -0
  12. package/dist/bin/commands/{lint.js → doctor.js} +1215 -156
  13. package/dist/bin/commands/doctor.js.map +1 -0
  14. package/dist/bin/commands/emu.js +451 -104
  15. package/dist/bin/commands/format.js +37 -9
  16. package/dist/bin/commands/make-admin.js +77499 -11
  17. package/dist/bin/commands/preview.js +181 -43
  18. package/dist/bin/commands/setup.d.ts +6 -0
  19. package/dist/bin/commands/setup.d.ts.map +1 -0
  20. package/dist/bin/commands/setup.js +11733 -0
  21. package/dist/bin/commands/setup.js.map +1 -0
  22. package/dist/bin/commands/supabase-setup.d.ts +6 -0
  23. package/dist/bin/commands/supabase-setup.d.ts.map +1 -0
  24. package/dist/bin/commands/supabase-setup.js +7 -0
  25. package/dist/bin/commands/supabase-setup.js.map +1 -0
  26. package/dist/bin/commands/sync-secrets.js +211 -34
  27. package/dist/bin/commands/type-check.d.ts +14 -0
  28. package/dist/bin/commands/type-check.d.ts.map +1 -0
  29. package/dist/bin/commands/type-check.js +2049 -0
  30. package/dist/bin/commands/type-check.js.map +1 -0
  31. package/dist/bin/commands/wai.js +3 -1
  32. package/dist/bin/dndev.js +73 -52
  33. package/dist/bin/donotdev.js +54 -45
  34. package/dist/index.js +4212 -3050
  35. package/package.json +3 -3
  36. package/templates/app-demo/src/App.tsx.example +1 -0
  37. package/templates/app-demo/src/pages/FullPage.tsx.example +2 -2
  38. package/templates/app-demo/src/pages/components/DemoLayout.tsx.example +2 -2
  39. package/templates/app-demo/src/themes.css.example +5 -12
  40. package/templates/app-expo/.env.example +44 -0
  41. package/templates/app-expo/.expo/README.md.example +5 -0
  42. package/templates/app-expo/.gitignore.example +36 -0
  43. package/templates/app-expo/README.md.example +58 -0
  44. package/templates/app-expo/app/.gitkeep +2 -0
  45. package/templates/app-expo/app/_layout.tsx.example +41 -0
  46. package/templates/app-expo/app/form.tsx.example +52 -0
  47. package/templates/app-expo/app/index.tsx.example +89 -0
  48. package/templates/app-expo/app/list.tsx.example +32 -0
  49. package/templates/app-expo/app/profile.tsx.example +76 -0
  50. package/templates/app-expo/app/signin.tsx.example +53 -0
  51. package/templates/app-expo/app.json.example +39 -0
  52. package/templates/app-expo/assets/adaptive-icon.png +0 -0
  53. package/templates/app-expo/assets/favicon.png +0 -0
  54. package/templates/app-expo/assets/icon.png +0 -0
  55. package/templates/app-expo/assets/splash.png +0 -0
  56. package/templates/app-expo/babel.config.js.example +10 -0
  57. package/templates/app-expo/eas.json.example +20 -0
  58. package/templates/app-expo/expo-env.d.ts.example +4 -0
  59. package/templates/app-expo/metro.config.js.example +20 -0
  60. package/templates/app-expo/service-account-key.json.example +12 -0
  61. package/templates/app-expo/src/config/app.ts.example +46 -0
  62. package/templates/app-expo/src/config/providers.ts.example +7 -0
  63. package/templates/app-expo/tsconfig.json.example +19 -0
  64. package/templates/app-next/.env.example +4 -33
  65. package/templates/app-next/src/app/ClientLayout.tsx.example +2 -0
  66. package/templates/app-next/src/app/layout.tsx.example +7 -6
  67. package/templates/app-next/src/config/providers.ts.example +7 -0
  68. package/templates/app-next/src/globals.css.example +2 -11
  69. package/templates/app-next/src/pages/HomePage.tsx.example +1 -1
  70. package/templates/app-next/src/themes.css.example +10 -13
  71. package/templates/app-vite/.env.example +3 -32
  72. package/templates/app-vite/index.html.example +2 -24
  73. package/templates/app-vite/src/App.tsx.example +2 -0
  74. package/templates/app-vite/src/config/providers.ts.example +7 -0
  75. package/templates/app-vite/src/globals.css.example +2 -12
  76. package/templates/app-vite/src/pages/FormPageExample.tsx.example +1 -2
  77. package/templates/app-vite/src/pages/HomePage.tsx.example +2 -2
  78. package/templates/app-vite/src/themes.css.example +109 -79
  79. package/templates/app-vite/vercel.json.example +11 -0
  80. package/templates/functions-firebase/README.md.example +1 -1
  81. package/templates/functions-firebase/build.mjs.example +2 -72
  82. package/templates/functions-firebase/functions-firebase/.env.example.example +24 -26
  83. package/templates/functions-firebase/functions-firebase/README.md.example +1 -1
  84. package/templates/functions-firebase/functions-firebase/build.mjs.example +2 -72
  85. package/templates/functions-firebase/functions-firebase/tsconfig.json.example +1 -1
  86. package/templates/functions-firebase/functions.config.js.example +1 -1
  87. package/templates/functions-supabase/supabase/config.toml.example +59 -0
  88. package/templates/functions-supabase/supabase/functions/.env.example +13 -0
  89. package/templates/functions-supabase/supabase/functions/cancel-subscription/index.ts.example +7 -0
  90. package/templates/functions-supabase/supabase/functions/change-plan/index.ts.example +11 -0
  91. package/templates/functions-supabase/supabase/functions/create-checkout-session/index.ts.example +11 -0
  92. package/templates/functions-supabase/supabase/functions/create-customer-portal/index.ts.example +7 -0
  93. package/templates/functions-supabase/supabase/functions/crud/index.ts.example +16 -0
  94. package/templates/functions-supabase/supabase/functions/delete-account/index.ts.example +7 -0
  95. package/templates/functions-supabase/supabase/functions/deno.json.example +8 -0
  96. package/templates/functions-supabase/supabase/functions/get-custom-claims/index.ts.example +7 -0
  97. package/templates/functions-supabase/supabase/functions/get-user-auth-status/index.ts.example +7 -0
  98. package/templates/functions-supabase/supabase/functions/refresh-subscription-status/index.ts.example +7 -0
  99. package/templates/functions-supabase/supabase/functions/remove-custom-claims/index.ts.example +7 -0
  100. package/templates/functions-supabase/supabase/functions/set-custom-claims/index.ts.example +7 -0
  101. package/templates/functions-supabase/supabase/migrations/20250101000000_idempotency.sql +24 -0
  102. package/templates/functions-supabase/supabase/migrations/20250101000001_rate_limits.sql +22 -0
  103. package/templates/functions-supabase/supabase/migrations/20250101000002_cleanup_jobs.sql +28 -0
  104. package/templates/functions-supabase/supabase/migrations/20250101000003_operation_metrics.sql +28 -0
  105. package/templates/functions-vercel/functions-vercel/tsconfig.json.example +1 -1
  106. package/templates/functions-vercel/functions-vercel/vercel.json.example +1 -1
  107. package/templates/functions-vercel/vercel.json.example +1 -1
  108. package/templates/github/github/workflows/firebase-deploy.yml.example +1 -1
  109. package/templates/github/workflows/firebase-deploy.yml.example +1 -1
  110. package/templates/overlay-firebase/env.fragment.example +34 -0
  111. package/templates/overlay-firebase/env.fragment.expo.example +34 -0
  112. package/templates/overlay-firebase/env.fragment.nextjs.example +34 -0
  113. package/templates/overlay-firebase/src/config/providers.expo.ts.example +49 -0
  114. package/templates/overlay-firebase/src/config/providers.ts.example +23 -0
  115. package/templates/overlay-supabase/env.fragment.example +12 -0
  116. package/templates/overlay-supabase/env.fragment.expo.example +12 -0
  117. package/templates/overlay-supabase/env.fragment.nextjs.example +12 -0
  118. package/templates/overlay-supabase/src/config/providers.expo.ts.example +35 -0
  119. package/templates/overlay-supabase/src/config/providers.ts.example +33 -0
  120. package/templates/overlay-supabase/vercel.headers.example +23 -0
  121. package/templates/overlay-supabase/vercel.json.example +22 -0
  122. package/templates/overlay-vercel/env.fragment.example +34 -0
  123. package/templates/overlay-vercel/env.fragment.nextjs.example +34 -0
  124. package/templates/overlay-vercel/src/config/providers.ts.example +24 -0
  125. package/templates/root-consumer/.claude/agents/architect.md.example +2 -310
  126. package/templates/root-consumer/.claude/agents/builder.md.example +2 -326
  127. package/templates/root-consumer/.claude/agents/coder.md.example +2 -83
  128. package/templates/root-consumer/.claude/agents/extractor.md.example +2 -231
  129. package/templates/root-consumer/.claude/agents/polisher.md.example +2 -132
  130. package/templates/root-consumer/.claude/agents/prompt-engineer.md.example +2 -81
  131. package/templates/root-consumer/.claude/commands/grill.md.example +30 -0
  132. package/templates/root-consumer/.claude/commands/techdebt.md.example +28 -0
  133. package/templates/root-consumer/.clinerules.example +1 -0
  134. package/templates/root-consumer/.cursor/rules/no-docs.mdc.example +15 -0
  135. package/templates/root-consumer/.cursorrules.example +1 -0
  136. package/templates/root-consumer/.github/copilot-instructions.md.example +1 -0
  137. package/templates/root-consumer/.windsurfrules.example +1 -0
  138. package/templates/root-consumer/AI.md.example +44 -123
  139. package/templates/root-consumer/CLAUDE.md.example +1 -134
  140. package/templates/root-consumer/CONVENTIONS.md.example +1 -0
  141. package/templates/root-consumer/GEMINI.md.example +1 -0
  142. package/templates/root-consumer/firebase.json.example +1 -1
  143. package/templates/root-consumer/guides/dndev/AGENT_START_HERE.md.example +22 -2
  144. package/templates/root-consumer/guides/dndev/COMPONENTS_ADV.md.example +0 -18
  145. package/templates/root-consumer/guides/dndev/COMPONENTS_UI.md.example +1 -1
  146. package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +101 -32
  147. package/templates/root-consumer/guides/dndev/INDEX.md.example +4 -2
  148. package/templates/root-consumer/guides/dndev/SETUP_APP_CONFIG.md.example +3 -3
  149. package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +241 -12
  150. package/templates/root-consumer/guides/dndev/SETUP_FIREBASE.md.example +13 -7
  151. package/templates/root-consumer/guides/dndev/SETUP_OAUTH_PROVIDERS.md.example +60 -0
  152. package/templates/root-consumer/guides/dndev/SETUP_SOC2.md.example +234 -0
  153. package/templates/root-consumer/guides/dndev/SETUP_STRIPE.md.example +62 -0
  154. package/templates/root-consumer/guides/dndev/SETUP_SUPABASE.md.example +124 -0
  155. package/templates/root-consumer/guides/dndev/SETUP_THEMES.md.example +6 -2
  156. package/templates/root-consumer/guides/dndev/SETUP_VERCEL.md.example +176 -0
  157. package/templates/root-consumer/guides/dndev/USE_ROUTING.md.example +5 -9
  158. package/templates/root-consumer/guides/dndev/essences_reference.css.example +174 -0
  159. package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +7 -8
  160. package/templates/root-consumer/guides/wai-way/agents/builder.md.example +10 -0
  161. package/templates/root-consumer/guides/wai-way/agents/extractor.md.example +25 -5
  162. package/templates/root-consumer/guides/wai-way/agents/polisher.md.example +13 -2
  163. package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +2 -2
  164. package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +55 -15
  165. package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +15 -4
  166. package/templates/root-consumer/guides/wai-way/spec_template.md.example +7 -6
  167. package/dist/bin/commands/lint.d.ts +0 -11
  168. package/dist/bin/commands/lint.d.ts.map +0 -1
  169. package/dist/bin/commands/lint.js.map +0 -1
  170. package/dist/bin/commands/staging.d.ts +0 -11
  171. package/dist/bin/commands/staging.d.ts.map +0 -1
  172. package/dist/bin/commands/staging.js +0 -12
  173. package/dist/bin/commands/staging.js.map +0 -1
  174. package/templates/app-payload/.env.example +0 -28
  175. package/templates/app-payload/README.md.example +0 -233
  176. package/templates/app-payload/collections/Company.ts.example +0 -125
  177. package/templates/app-payload/collections/Hero.ts.example +0 -62
  178. package/templates/app-payload/collections/Media.ts.example +0 -41
  179. package/templates/app-payload/collections/Products.ts.example +0 -115
  180. package/templates/app-payload/collections/Services.ts.example +0 -104
  181. package/templates/app-payload/collections/Testimonials.ts.example +0 -92
  182. package/templates/app-payload/collections/Users.ts.example +0 -35
  183. package/templates/app-payload/src/server.ts.example +0 -79
  184. 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/main.tsx or src/App.tsx
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
- ### Custom Field Types with UI Components
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
- For custom field types that need custom UI components:
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
- // Register UI component
737
+ // Minimal registration (form only)
705
738
  registerFieldType({
706
739
  type: 'repairOperations',
707
740
  controlledComponent: RepairOperationsField,
708
741
  });
709
742
 
710
- // Then define schema in entity
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' as any,
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
- - Custom controlled components receive `control` prop, NOT `field` prop
735
- - You MUST use `useController` hook to get `field` and `fieldState`
736
- - Define schema in `validation.schema` - it's the single source of truth
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:setup
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:setup` writes Firebase vars to `apps/<app>/.env` automatically.**
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:setup` again and select the staging project
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
- Push to Firebase Secret Manager:
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:setup` → download service account key → enable Auth + Firestore → `dndev emu start` → `dndev deploy`. That's it.**
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` |