@fayz-ai/core 0.9.0 → 0.11.0

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 (218) hide show
  1. package/README.md +2 -2
  2. package/dist/access/index.js.map +1 -1
  3. package/dist/chunk-BEGXYHS4.js +566 -0
  4. package/dist/chunk-BEGXYHS4.js.map +1 -0
  5. package/dist/chunk-ED6NBH6U.js +348 -0
  6. package/dist/chunk-ED6NBH6U.js.map +1 -0
  7. package/dist/chunk-EWKX4VUM.js +429 -0
  8. package/dist/chunk-EWKX4VUM.js.map +1 -0
  9. package/dist/chunk-LARZWDHA.js +319 -0
  10. package/dist/chunk-LARZWDHA.js.map +1 -0
  11. package/dist/chunk-OXQGRJZT.js +125 -0
  12. package/dist/chunk-OXQGRJZT.js.map +1 -0
  13. package/dist/chunk-RRYFXUQX.js +1247 -0
  14. package/dist/chunk-RRYFXUQX.js.map +1 -0
  15. package/dist/{chunk-5RJVUC6J.js → chunk-TDI6F6PT.js} +373 -25
  16. package/dist/chunk-TDI6F6PT.js.map +1 -0
  17. package/dist/{chunk-V2MSAUHO.js → chunk-UVKH6RDX.js} +211 -335
  18. package/dist/chunk-UVKH6RDX.js.map +1 -0
  19. package/dist/chunk-V53GR4UT.js +289 -0
  20. package/dist/chunk-V53GR4UT.js.map +1 -0
  21. package/dist/connector-runtime/index.d.ts +15 -0
  22. package/dist/connector-runtime/index.d.ts.map +1 -0
  23. package/dist/connector-runtime/index.js +3 -0
  24. package/dist/connector-runtime/index.js.map +1 -0
  25. package/dist/data/archetype.d.ts.map +1 -1
  26. package/dist/data/backend-env.d.ts +38 -0
  27. package/dist/data/backend-env.d.ts.map +1 -0
  28. package/dist/data/bulk.d.ts +24 -0
  29. package/dist/data/bulk.d.ts.map +1 -1
  30. package/dist/data/cached.d.ts.map +1 -1
  31. package/dist/data/custom-fields.d.ts +5 -0
  32. package/dist/data/custom-fields.d.ts.map +1 -0
  33. package/dist/data/index.d.ts +2 -1
  34. package/dist/data/index.d.ts.map +1 -1
  35. package/dist/data/index.js +3 -1
  36. package/dist/data/mock.d.ts.map +1 -1
  37. package/dist/data/platform-api.d.ts.map +1 -1
  38. package/dist/data/resolve.d.ts.map +1 -1
  39. package/dist/data/supabase.d.ts.map +1 -1
  40. package/dist/data/types.d.ts +8 -0
  41. package/dist/data/types.d.ts.map +1 -1
  42. package/dist/entity/events.d.ts +14 -0
  43. package/dist/entity/events.d.ts.map +1 -0
  44. package/dist/events/envelope.d.ts +31 -0
  45. package/dist/events/envelope.d.ts.map +1 -0
  46. package/dist/events/index.d.ts +4 -0
  47. package/dist/events/index.d.ts.map +1 -1
  48. package/dist/events/log.d.ts +44 -0
  49. package/dist/events/log.d.ts.map +1 -0
  50. package/dist/events/types.d.ts +40 -0
  51. package/dist/events/types.d.ts.map +1 -0
  52. package/dist/fields/store.d.ts +45 -0
  53. package/dist/fields/store.d.ts.map +1 -0
  54. package/dist/i18n/index.d.ts +10 -0
  55. package/dist/i18n/index.d.ts.map +1 -1
  56. package/dist/i18n/index.js +1 -1
  57. package/dist/i18n/shell-translations.en.d.ts.map +1 -1
  58. package/dist/i18n/shell-translations.pt-br.d.ts.map +1 -1
  59. package/dist/index.d.ts +45 -21
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +543 -56
  62. package/dist/index.js.map +1 -1
  63. package/dist/integrations/events.d.ts +7 -0
  64. package/dist/integrations/events.d.ts.map +1 -0
  65. package/dist/integrations/index.d.ts +533 -29
  66. package/dist/integrations/index.d.ts.map +1 -1
  67. package/dist/integrations/index.js +3 -1
  68. package/dist/integrations/permissions.d.ts +132 -0
  69. package/dist/integrations/permissions.d.ts.map +1 -0
  70. package/dist/integrations/request.d.ts +170 -0
  71. package/dist/integrations/request.d.ts.map +1 -0
  72. package/dist/integrations/runtime-token.d.ts +33 -0
  73. package/dist/integrations/runtime-token.d.ts.map +1 -0
  74. package/dist/integrations/runtime.d.ts +398 -0
  75. package/dist/integrations/runtime.d.ts.map +1 -0
  76. package/dist/integrations/settings.d.ts +102 -0
  77. package/dist/integrations/settings.d.ts.map +1 -0
  78. package/dist/integrations/store.d.ts +145 -0
  79. package/dist/integrations/store.d.ts.map +1 -0
  80. package/dist/{address/index.d.ts → lib/address.d.ts} +1 -1
  81. package/dist/{address/index.d.ts.map → lib/address.d.ts.map} +1 -1
  82. package/dist/{phone/index.d.ts → lib/phone.d.ts} +1 -1
  83. package/dist/lib/phone.d.ts.map +1 -0
  84. package/dist/{seo/index.d.ts → lib/seo.d.ts} +18 -1
  85. package/dist/lib/seo.d.ts.map +1 -0
  86. package/dist/{tenant/index.d.ts → lib/tenant.d.ts} +1 -1
  87. package/dist/lib/tenant.d.ts.map +1 -0
  88. package/dist/manifest/index.d.ts +1 -1
  89. package/dist/manifest/index.d.ts.map +1 -1
  90. package/dist/messaging/index.d.ts +3 -0
  91. package/dist/messaging/index.d.ts.map +1 -0
  92. package/dist/messaging/registry.d.ts +35 -0
  93. package/dist/messaging/registry.d.ts.map +1 -0
  94. package/dist/messaging/types.d.ts +67 -0
  95. package/dist/messaging/types.d.ts.map +1 -0
  96. package/dist/plugin/index.js +3 -1
  97. package/dist/plugin/runtime.d.ts +17 -0
  98. package/dist/plugin/runtime.d.ts.map +1 -1
  99. package/dist/registry/block-catalog.d.ts +78 -0
  100. package/dist/registry/block-catalog.d.ts.map +1 -0
  101. package/dist/registry/block-constraints.d.ts +64 -0
  102. package/dist/registry/block-constraints.d.ts.map +1 -0
  103. package/dist/registry/block-schema.d.ts +69 -0
  104. package/dist/registry/block-schema.d.ts.map +1 -0
  105. package/dist/registry/blocks.d.ts +84 -0
  106. package/dist/registry/blocks.d.ts.map +1 -0
  107. package/dist/registry/entities.d.ts +179 -0
  108. package/dist/registry/entities.d.ts.map +1 -0
  109. package/dist/registry/extension-points.d.ts +117 -0
  110. package/dist/registry/extension-points.d.ts.map +1 -0
  111. package/dist/registry/handles.d.ts +41 -0
  112. package/dist/registry/handles.d.ts.map +1 -0
  113. package/dist/registry/index.d.ts +41 -41
  114. package/dist/registry/index.d.ts.map +1 -1
  115. package/dist/registry/loaders.d.ts +55 -0
  116. package/dist/registry/loaders.d.ts.map +1 -0
  117. package/dist/registry/registry-class.d.ts +157 -0
  118. package/dist/registry/registry-class.d.ts.map +1 -0
  119. package/dist/registry/schema-check.d.ts +14 -0
  120. package/dist/registry/schema-check.d.ts.map +1 -0
  121. package/dist/router/index.d.ts +78 -1
  122. package/dist/router/index.d.ts.map +1 -1
  123. package/dist/router/index.js +1 -1
  124. package/dist/{shell-translations.pt-br-2GWRQRRP.js → shell-translations.pt-br-BM5ZFSKJ.js} +72 -3
  125. package/dist/shell-translations.pt-br-BM5ZFSKJ.js.map +1 -0
  126. package/dist/telemetry/errors.d.ts +87 -0
  127. package/dist/telemetry/errors.d.ts.map +1 -0
  128. package/dist/testing/index.js +4 -1
  129. package/dist/{testing/index.d.ts → testing.d.ts} +5 -3
  130. package/dist/testing.d.ts.map +1 -0
  131. package/dist/types/analytics.d.ts +213 -0
  132. package/dist/types/analytics.d.ts.map +1 -0
  133. package/dist/types/crud.d.ts +37 -3
  134. package/dist/types/crud.d.ts.map +1 -1
  135. package/dist/types/plugins.d.ts +138 -1
  136. package/dist/types/plugins.d.ts.map +1 -1
  137. package/package.json +9 -17
  138. package/dist/blocks/index.d.ts +0 -25
  139. package/dist/blocks/index.d.ts.map +0 -1
  140. package/dist/chunk-5RJVUC6J.js.map +0 -1
  141. package/dist/chunk-L4TLMYGC.js +0 -42
  142. package/dist/chunk-L4TLMYGC.js.map +0 -1
  143. package/dist/chunk-V2MSAUHO.js.map +0 -1
  144. package/dist/chunk-X5THHG6T.js +0 -77
  145. package/dist/chunk-X5THHG6T.js.map +0 -1
  146. package/dist/chunk-Y2C2FBQP.js +0 -35
  147. package/dist/chunk-Y2C2FBQP.js.map +0 -1
  148. package/dist/chunk-YH25Y4FW.js +0 -311
  149. package/dist/chunk-YH25Y4FW.js.map +0 -1
  150. package/dist/phone/index.d.ts.map +0 -1
  151. package/dist/seo/index.d.ts.map +0 -1
  152. package/dist/shell-translations.pt-br-2GWRQRRP.js.map +0 -1
  153. package/dist/tenant/index.d.ts.map +0 -1
  154. package/dist/testing/index.d.ts.map +0 -1
  155. package/src/access/denial.ts +0 -33
  156. package/src/access/engine.test.ts +0 -48
  157. package/src/access/engine.ts +0 -136
  158. package/src/access/index.ts +0 -14
  159. package/src/access/limits.ts +0 -18
  160. package/src/address/index.ts +0 -188
  161. package/src/app/render.tsx +0 -69
  162. package/src/blocks/index.tsx +0 -68
  163. package/src/data/archetype.ts +0 -255
  164. package/src/data/bulk.ts +0 -35
  165. package/src/data/cached.ts +0 -55
  166. package/src/data/count.test.ts +0 -152
  167. package/src/data/count.ts +0 -131
  168. package/src/data/filters.test.ts +0 -70
  169. package/src/data/filters.ts +0 -90
  170. package/src/data/index.ts +0 -11
  171. package/src/data/mock.ts +0 -90
  172. package/src/data/platform-api.ts +0 -209
  173. package/src/data/refresh.ts +0 -115
  174. package/src/data/resolve.ts +0 -126
  175. package/src/data/supabase.ts +0 -230
  176. package/src/data/types.ts +0 -50
  177. package/src/entity/index.ts +0 -2
  178. package/src/entity/registry.ts +0 -49
  179. package/src/events/index.tsx +0 -82
  180. package/src/i18n/index.ts +0 -476
  181. package/src/i18n/shell-translations.en.ts +0 -875
  182. package/src/i18n/shell-translations.pt-br.ts +0 -873
  183. package/src/index.ts +0 -217
  184. package/src/integrations/index.ts +0 -237
  185. package/src/lib/cache.ts +0 -118
  186. package/src/lib/csv.ts +0 -54
  187. package/src/lib/format.ts +0 -58
  188. package/src/manifest/app-manifest.schema.json +0 -676
  189. package/src/manifest/index.ts +0 -518
  190. package/src/manifest/serialize.ts +0 -52
  191. package/src/phone/index.ts +0 -85
  192. package/src/plugin/createSafeDataProvider.ts +0 -30
  193. package/src/plugin/index.ts +0 -1
  194. package/src/plugin/runtime.ts +0 -331
  195. package/src/registry/index.ts +0 -163
  196. package/src/router/history.ts +0 -86
  197. package/src/router/index.ts +0 -29
  198. package/src/runtime/index.ts +0 -15
  199. package/src/runtime/oauth.ts +0 -219
  200. package/src/search/engine.ts +0 -559
  201. package/src/search/index.ts +0 -25
  202. package/src/search/text.ts +0 -206
  203. package/src/search/types.ts +0 -75
  204. package/src/seo/index.ts +0 -128
  205. package/src/tenant/index.ts +0 -38
  206. package/src/testing/index.ts +0 -116
  207. package/src/types/auth.ts +0 -41
  208. package/src/types/billing.ts +0 -39
  209. package/src/types/crud.ts +0 -184
  210. package/src/types/entities.ts +0 -142
  211. package/src/types/entitlements.ts +0 -45
  212. package/src/types/i18n.ts +0 -7
  213. package/src/types/index.ts +0 -11
  214. package/src/types/org.ts +0 -137
  215. package/src/types/payment.ts +0 -44
  216. package/src/types/permissions.ts +0 -32
  217. package/src/types/plugins.ts +0 -480
  218. package/src/types/theme.ts +0 -8
@@ -1,206 +0,0 @@
1
- // Text folding + ranking for global search. The same folding must exist in SQL
2
- // as public.fayz_norm() (@fayz-ai/db 018_global_search.sql), or server and
3
- // client disagree on what matched.
4
-
5
- const COMBINING_MARKS = /[\u0300-\u036f]/g
6
-
7
- /** Strip diacritics, lowercase, collapse non-alphanumerics to single spaces. */
8
- export function foldText(value: unknown): string {
9
- if (typeof value !== 'string' || value === '') return ''
10
- return value
11
- .normalize('NFD')
12
- .replace(COMBINING_MARKS, '')
13
- .toLowerCase()
14
- .replace(/[^a-z0-9]+/g, ' ')
15
- .trim()
16
- }
17
-
18
- /** Fold, keeping each folded char's index in the source — used for highlighting. */
19
- export function foldWithMap(value: string): { folded: string; map: number[] } {
20
- const out: string[] = []
21
- const map: number[] = []
22
- let pendingSpace = false
23
- for (let i = 0; i < value.length; i++) {
24
- const ch = value[i]!
25
- const folded = ch.normalize('NFD').replace(COMBINING_MARKS, '').toLowerCase()
26
- for (const f of folded) {
27
- const alnum = (f >= 'a' && f <= 'z') || (f >= '0' && f <= '9')
28
- if (!alnum) { pendingSpace = out.length > 0; continue }
29
- if (pendingSpace) { out.push(' '); map.push(i); pendingSpace = false }
30
- out.push(f)
31
- map.push(i)
32
- }
33
- }
34
- return { folded: out.join(''), map }
35
- }
36
-
37
- /** Digits only — phone numbers, CPF/CNPJ, SKUs typed with separators. */
38
- export function digitsOf(value: unknown): string {
39
- return typeof value === 'string' ? value.replace(/\D+/g, '') : ''
40
- }
41
-
42
- export interface NormalizedQuery {
43
- /** What the user typed, untouched. */
44
- raw: string
45
- /** Folded form of the whole query — the "phrase". */
46
- folded: string
47
- /** Folded query split on whitespace. */
48
- tokens: string[]
49
- /** Digits the user typed, when they typed enough of them to mean a number. */
50
- digits: string
51
- /** The longest token — the most selective thing to send to a `%LIKE%` backend. */
52
- anchor: string
53
- }
54
-
55
- /** Below this a phone/document lookup is indistinguishable from noise. */
56
- const MIN_DIGIT_QUERY = 4
57
-
58
- export function normalizeQuery(raw: string): NormalizedQuery {
59
- const folded = foldText(raw)
60
- const tokens = folded ? folded.split(' ').filter(Boolean) : []
61
- const digits = raw.replace(/\D+/g, '')
62
- let anchor = ''
63
- for (const token of tokens) if (token.length > anchor.length) anchor = token
64
- return {
65
- raw,
66
- folded,
67
- tokens,
68
- digits: digits.length >= MIN_DIGIT_QUERY ? digits : '',
69
- anchor,
70
- }
71
- }
72
-
73
- // Fuzzy similarity — Sørensen–Dice over bigrams. Same family as pg_trgm's
74
- // similarity(), so both sides rank near-misses the same way.
75
-
76
- function bigrams(value: string): string[] {
77
- if (value.length < 2) return value ? [value] : []
78
- const out: string[] = []
79
- for (let i = 0; i < value.length - 1; i++) out.push(value.slice(i, i + 2))
80
- return out
81
- }
82
-
83
- /** 0 (nothing in common) … 1 (identical). Both inputs must already be folded. */
84
- export function similarity(a: string, b: string): number {
85
- if (!a || !b) return 0
86
- if (a === b) return 1
87
- const left = bigrams(a)
88
- const right = bigrams(b)
89
- if (left.length === 0 || right.length === 0) return 0
90
- const pool = new Map<string, number>()
91
- for (const g of left) pool.set(g, (pool.get(g) ?? 0) + 1)
92
- let shared = 0
93
- for (const g of right) {
94
- const count = pool.get(g)
95
- if (count) { shared++; pool.set(g, count - 1) }
96
- }
97
- return (2 * shared) / (left.length + right.length)
98
- }
99
-
100
- /** Best similarity between the query token and any single word of the text. */
101
- function wordSimilarity(text: string, token: string): number {
102
- let best = similarity(text, token)
103
- for (const word of text.split(' ')) {
104
- if (!word) continue
105
- const s = similarity(word, token)
106
- if (s > best) best = s
107
- }
108
- return best
109
- }
110
-
111
- // ---------------------------------------------------------------------------
112
- // Ranking
113
- // ---------------------------------------------------------------------------
114
-
115
- export interface RankCandidate {
116
- /** Folded display name — what the user is most likely aiming at. */
117
- title: string
118
- /** Folded concatenation of every searchable value on the record. */
119
- haystack: string
120
- /** Digits of every phone/document-ish value on the record. */
121
- digits?: string
122
- }
123
-
124
- /** A fuzzy word has to be at least this close before it counts as a match. */
125
- const FUZZY_FLOOR = 0.62
126
-
127
- /** Score a candidate 0…1. Zero means "not a match" — the score is also the filter. */
128
- export function scoreCandidate(query: NormalizedQuery, candidate: RankCandidate): number {
129
- const { folded, tokens } = query
130
- if (!folded || tokens.length === 0) return 0
131
- const title = candidate.title
132
- const haystack = candidate.haystack || title
133
-
134
- // Phrase: the query as one string.
135
- let phrase = 0
136
- if (title === folded) phrase = 1
137
- else if (title.startsWith(folded)) phrase = 0.94
138
- else if (title.includes(' ' + folded)) phrase = 0.88
139
- else if (title.includes(folded)) phrase = 0.76
140
- else if (haystack.startsWith(folded) || haystack.includes(' ' + folded)) phrase = 0.7
141
- else if (haystack.includes(folded)) phrase = 0.62
142
-
143
- // Tokens: every word must land somewhere ("maria silva" → "Maria da Silva").
144
- let tokenTotal = 0
145
- let fuzzyUsed = false
146
- for (const token of tokens) {
147
- let best = 0
148
- if (title.startsWith(token)) best = 0.95
149
- else if (title.includes(' ' + token)) best = 0.85
150
- else if (title.includes(token)) best = 0.7
151
- else if (haystack.startsWith(token) || haystack.includes(' ' + token)) best = 0.6
152
- else if (haystack.includes(token)) best = 0.5
153
- else {
154
- const sim = wordSimilarity(title, token)
155
- if (sim >= FUZZY_FLOOR) { best = 0.45 * sim; fuzzyUsed = true }
156
- }
157
- if (best === 0) return phrase > 0 ? phrase : 0
158
- tokenTotal += best
159
- }
160
- let tokenScore = tokenTotal / tokens.length
161
- // A typo-only match must never outrank a literal one.
162
- if (fuzzyUsed) tokenScore *= 0.8
163
-
164
- let score = Math.max(phrase, tokenScore)
165
-
166
- // Digits: a phone number has no words to match.
167
- if (query.digits && candidate.digits && candidate.digits.includes(query.digits)) {
168
- score = Math.max(score, 0.72 + Math.min(0.2, query.digits.length / 50))
169
- }
170
-
171
- if (score === 0) return 0
172
-
173
- // Tie-break: shorter title wins ("Ana" over "Ana Beatriz" for "ana").
174
- const brevity = 1 / (1 + Math.max(0, title.length - folded.length) / 40)
175
- return score * 0.97 + brevity * 0.03
176
- }
177
-
178
- /** Ranges in the ORIGINAL text covered by the query, merged and sorted. */
179
- export function highlightRanges(text: string, query: NormalizedQuery): Array<[number, number]> {
180
- if (!text || query.tokens.length === 0) return []
181
- const { folded, map } = foldWithMap(text)
182
- if (!folded) return []
183
- const spans: Array<[number, number]> = []
184
- const needles = query.tokens.length > 1 ? [query.folded, ...query.tokens] : [query.folded]
185
- for (const needle of needles) {
186
- if (!needle) continue
187
- let from = 0
188
- for (;;) {
189
- const at = folded.indexOf(needle, from)
190
- if (at === -1) break
191
- const start = map[at]
192
- const endChar = map[at + needle.length - 1]
193
- if (start !== undefined && endChar !== undefined) spans.push([start, endChar + 1])
194
- from = at + needle.length
195
- }
196
- }
197
- if (spans.length === 0) return []
198
- spans.sort((a, b) => a[0] - b[0])
199
- const merged: Array<[number, number]> = [spans[0]!]
200
- for (const span of spans.slice(1)) {
201
- const last = merged[merged.length - 1]!
202
- if (span[0] <= last[1]) last[1] = Math.max(last[1], span[1])
203
- else merged.push(span)
204
- }
205
- return merged
206
- }
@@ -1,75 +0,0 @@
1
- import type { EntityDef } from '../types/crud'
2
-
3
- /** One searchable thing — declaring an entity is all it takes to be findable. */
4
- export interface SearchTarget {
5
- /** Stable entity key (`deriveEntityKey`): 'person:client', 'product', … */
6
- key: string
7
- /** Plural human label; becomes the result group heading. */
8
- label: string
9
- icon?: string
10
- entity: EntityDef
11
- mockData?: Array<{ id: string }>
12
- /** Multiplies the final score. Default 1. */
13
- boost?: number
14
- }
15
-
16
- export interface SearchHit {
17
- /** `${target.key}:${record id}` — stable across both search paths. */
18
- uid: string
19
- id: string
20
- /** Target key this record came from. */
21
- key: string
22
- /** Group heading (the target's plural label). */
23
- group: string
24
- icon?: string
25
- title: string
26
- subtitle?: string
27
- /** 0 … 1 (times the target boost). Higher is better. */
28
- score: number
29
- archetype?: string
30
- archetypeKind?: string
31
- table?: string
32
- /** The row, when the path that found it had one. The index path returns
33
- * titles only. */
34
- record?: Record<string, unknown>
35
- }
36
-
37
- export interface SearchGroup {
38
- key: string
39
- label: string
40
- icon?: string
41
- hits: SearchHit[]
42
- /** True when this group hit its per-target cap — more matches exist. */
43
- hasMore: boolean
44
- }
45
-
46
- export type SearchPath = 'index' | 'scan' | 'cache'
47
-
48
- export interface GlobalSearchResult {
49
- query: string
50
- hits: SearchHit[]
51
- groups: SearchGroup[]
52
- /** Which path produced this result — surfaced for diagnostics, not for UI. */
53
- via: SearchPath
54
- /** Targets whose query failed. Never fatal: a broken table cannot blind the box. */
55
- failed: string[]
56
- /** Wall-clock milliseconds. */
57
- elapsedMs: number
58
- /** True while a better (network) answer is still on its way. */
59
- partial: boolean
60
- }
61
-
62
- export interface SearchOptions {
63
- /** Permission filtering is the caller's job — the engine searches what it's handed. */
64
- targets: SearchTarget[]
65
- /** Max hits returned overall. Default 30. */
66
- limit?: number
67
- /** Max hits per target. Default 5. */
68
- perTarget?: number
69
- /** Fired with early/partial answers before the authoritative one resolves. */
70
- onPartial?: (result: GlobalSearchResult) => void
71
- /** Discard the result if this aborts. */
72
- signal?: AbortSignal
73
- /** Skip the server index and force the per-entity scan (tests, diagnostics). */
74
- forceScan?: boolean
75
- }
package/src/seo/index.ts DELETED
@@ -1,128 +0,0 @@
1
- import { useEffect } from 'react'
2
-
3
- // ---------------------------------------------------------------------------
4
- // Generic, domain-agnostic SEO / structured-data head manager (SDK-level).
5
- // ANY surface (blog, booking, reviews, product, landing) uses `useSeo` to set
6
- // <title>, meta description, canonical, OpenGraph/Twitter cards and JSON-LD per
7
- // route. Domain plugins build their own schema.org objects (BlogPosting,
8
- // Product, Event…) and pass them via `jsonLd` — the mechanics live here once.
9
- //
10
- // NOTE: a SPA still benefits from SSR/prerendering for the strongest SEO — that
11
- // is the real upgrade; this covers everything achievable client-side.
12
- // ---------------------------------------------------------------------------
13
-
14
- const MANAGED = 'data-fayz-seo'
15
-
16
- export interface SeoConfig {
17
- title: string
18
- description?: string
19
- canonical?: string
20
- image?: string
21
- type?: 'website' | 'article' | (string & {})
22
- siteName?: string
23
- /** One or more JSON-LD objects injected as <script type="application/ld+json">. */
24
- jsonLd?: Array<Record<string, unknown>>
25
- }
26
-
27
- /** Current page origin (empty during SSR). */
28
- export function seoOrigin(): string {
29
- return typeof window !== 'undefined' ? window.location.origin : ''
30
- }
31
-
32
- /** Build an absolute URL from a path, using the current origin. */
33
- export function absoluteUrl(path: string): string {
34
- return `${seoOrigin()}${path.startsWith('/') ? path : `/${path}`}`
35
- }
36
-
37
- function upsertMeta(attr: 'name' | 'property', key: string, content: string) {
38
- if (!content) return
39
- let el = document.head.querySelector<HTMLMetaElement>(`meta[${attr}="${key}"]`)
40
- if (!el) {
41
- el = document.createElement('meta')
42
- el.setAttribute(attr, key)
43
- el.setAttribute(MANAGED, '')
44
- document.head.appendChild(el)
45
- }
46
- el.setAttribute('content', content)
47
- }
48
-
49
- function upsertLink(rel: string, href: string) {
50
- if (!href) return
51
- let el = document.head.querySelector<HTMLLinkElement>(`link[rel="${rel}"]`)
52
- if (!el) {
53
- el = document.createElement('link')
54
- el.setAttribute('rel', rel)
55
- el.setAttribute(MANAGED, '')
56
- document.head.appendChild(el)
57
- }
58
- el.setAttribute('href', href)
59
- }
60
-
61
- function clearManaged() {
62
- document.head.querySelectorAll(`[${MANAGED}]`).forEach((el) => el.remove())
63
- }
64
-
65
- /** Imperatively apply SEO tags (used by useSeo; exported for non-React callers). */
66
- export function applySeo(config: SeoConfig): void {
67
- if (typeof document === 'undefined') return
68
- clearManaged()
69
- document.title = config.title
70
- upsertMeta('name', 'description', config.description ?? '')
71
- if (config.canonical) upsertLink('canonical', config.canonical)
72
-
73
- upsertMeta('property', 'og:title', config.title)
74
- upsertMeta('property', 'og:description', config.description ?? '')
75
- upsertMeta('property', 'og:type', config.type === 'article' ? 'article' : 'website')
76
- if (config.canonical) upsertMeta('property', 'og:url', config.canonical)
77
- if (config.image) upsertMeta('property', 'og:image', config.image)
78
- if (config.siteName) upsertMeta('property', 'og:site_name', config.siteName)
79
-
80
- upsertMeta('name', 'twitter:card', config.image ? 'summary_large_image' : 'summary')
81
- upsertMeta('name', 'twitter:title', config.title)
82
- upsertMeta('name', 'twitter:description', config.description ?? '')
83
- if (config.image) upsertMeta('name', 'twitter:image', config.image)
84
-
85
- for (const obj of config.jsonLd ?? []) {
86
- const script = document.createElement('script')
87
- script.type = 'application/ld+json'
88
- script.setAttribute(MANAGED, '')
89
- script.text = JSON.stringify(obj)
90
- document.head.appendChild(script)
91
- }
92
- }
93
-
94
- /** Apply SEO head tags for the current route; cleans up on unmount. */
95
- export function useSeo(config: SeoConfig): void {
96
- const key = JSON.stringify(config)
97
- useEffect(() => {
98
- applySeo(config)
99
- return () => clearManaged()
100
- // eslint-disable-next-line react-hooks/exhaustive-deps
101
- }, [key])
102
- }
103
-
104
- // --- Generic schema.org builders (reusable across plugins) -----------------
105
-
106
- export interface BreadcrumbItem {
107
- name: string
108
- url: string
109
- }
110
-
111
- /** schema.org BreadcrumbList from an ordered list of crumbs. */
112
- export function breadcrumbJsonLd(items: BreadcrumbItem[]): Record<string, unknown> {
113
- return {
114
- '@context': 'https://schema.org',
115
- '@type': 'BreadcrumbList',
116
- itemListElement: items.map((c, i) => ({
117
- '@type': 'ListItem',
118
- position: i + 1,
119
- name: c.name,
120
- item: c.url,
121
- })),
122
- }
123
- }
124
-
125
- /** schema.org Organization publisher block. */
126
- export function organizationJsonLd(name: string, extra?: Record<string, unknown>): Record<string, unknown> {
127
- return { '@type': 'Organization', name, ...extra }
128
- }
@@ -1,38 +0,0 @@
1
- // Active tenant context — a global accessor the data layer reads for tenant
2
- // scoping and cache keying. The host (saas shell / app) sets it from its org
3
- // store via setActiveTenantId; the data providers stay decoupled from any
4
- // specific org-store implementation (runtime DI).
5
- import * as React from 'react'
6
- import { clearGlobalCache } from '../lib/cache'
7
-
8
- let _activeTenantId: string | undefined
9
- const listeners = new Set<() => void>()
10
-
11
- export function setActiveTenantId(id: string | undefined): void {
12
- if (id !== _activeTenantId) {
13
- _activeTenantId = id
14
- clearGlobalCache()
15
- for (const l of listeners) l()
16
- }
17
- }
18
-
19
- export function getActiveTenantId(): string | undefined {
20
- return _activeTenantId
21
- }
22
-
23
- /**
24
- * Subscribes to tenant changes. A getter alone makes every read a race with the
25
- * shell: a view that mounts before the org store resolves reads `undefined`,
26
- * and a tenant-scoped fetch that answers "no tenant" looks exactly like "no
27
- * rows" — it never retries and the screen stays empty until a remount.
28
- */
29
- export function subscribeActiveTenant(listener: () => void): () => void {
30
- listeners.add(listener)
31
- return () => { listeners.delete(listener) }
32
- }
33
-
34
- /** The active tenant, re-rendering when it resolves or changes. Use this to key
35
- * effects that fetch tenant-scoped data. */
36
- export function useActiveTenantId(): string | undefined {
37
- return React.useSyncExternalStore(subscribeActiveTenant, getActiveTenantId, getActiveTenantId)
38
- }
@@ -1,116 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Plugin contract assertions — the integrity half of the deploy gate.
3
- // ---------------------------------------------------------------------------
4
- // Framework-agnostic runtime checks (no test-runner dependency) that any
5
- // plugin's *.test.ts calls to prove its manifest/connector conform to the
6
- // platform contract BEFORE it can ship. A plugin that fails these would break
7
- // the host app at mount time (missing route component, duplicate ids, malformed
8
- // settings tab). Pair with a capability test (does the plugin do its job?) — see
9
- // docs/PLUGIN-PATTERNS.md → capability anatomy. These throw on the first violation.
10
- import type { PluginManifest } from '../types/plugins'
11
- import type { Connector } from '../integrations'
12
-
13
- export class PluginContractError extends Error {
14
- constructor(message: string) {
15
- super(message)
16
- this.name = 'PluginContractError'
17
- }
18
- }
19
-
20
- function fail(scope: string, msg: string): never {
21
- throw new PluginContractError(`[${scope}] ${msg}`)
22
- }
23
-
24
- function nonEmptyString(v: unknown): v is string {
25
- return typeof v === 'string' && v.trim().length > 0
26
- }
27
-
28
- /**
29
- * Assert a PluginManifest is structurally sound enough to mount safely.
30
- * Checks identity, navigation/route wiring, settings tabs, declared features,
31
- * and id uniqueness. Throws PluginContractError on the first problem.
32
- */
33
- export function assertPluginManifestContract(manifest: PluginManifest): void {
34
- const id = manifest?.id
35
- const scope = nonEmptyString(id) ? id : '<unknown plugin>'
36
-
37
- for (const field of ['id', 'name', 'icon', 'version'] as const) {
38
- if (!nonEmptyString(manifest?.[field])) fail(scope, `manifest.${field} must be a non-empty string`)
39
- }
40
-
41
- if (!Array.isArray(manifest.navigation)) fail(scope, 'manifest.navigation must be an array (use [] for settings-only plugins)')
42
- if (!Array.isArray(manifest.routes)) fail(scope, 'manifest.routes must be an array (use [] for settings-only plugins)')
43
-
44
- // Every nav entry must point at a route; every route needs a path + component.
45
- for (const [i, nav] of manifest.navigation.entries()) {
46
- if (!nonEmptyString((nav as any)?.route)) fail(scope, `navigation[${i}].route must be a non-empty string`)
47
- }
48
- const routePaths = new Set<string>()
49
- for (const [i, route] of manifest.routes.entries()) {
50
- if (!nonEmptyString((route as any)?.path)) fail(scope, `routes[${i}].path must be a non-empty string`)
51
- if (typeof (route as any)?.component !== 'function') fail(scope, `routes[${i}].component must be a component`)
52
- if (routePaths.has((route as any).path)) fail(scope, `duplicate route path "${(route as any).path}"`)
53
- routePaths.add((route as any).path)
54
- }
55
- // A nav entry that links to a route the plugin doesn't declare is dead UI.
56
- for (const [i, nav] of manifest.navigation.entries()) {
57
- const r = (nav as any).route
58
- if (routePaths.size > 0 && !routePaths.has(r)) {
59
- fail(scope, `navigation[${i}].route "${r}" has no matching route in manifest.routes`)
60
- }
61
- }
62
-
63
- // Settings tabs (the only UI surface integrations need) must be renderable.
64
- const settingIds = new Set<string>()
65
- for (const [i, tab] of (manifest.settings ?? []).entries()) {
66
- if (!nonEmptyString((tab as any)?.id)) fail(scope, `settings[${i}].id must be a non-empty string`)
67
- if (!nonEmptyString((tab as any)?.label)) fail(scope, `settings[${i}].label must be a non-empty string`)
68
- if (typeof (tab as any)?.component !== 'function') fail(scope, `settings[${i}].component must be a component`)
69
- if (settingIds.has((tab as any).id)) fail(scope, `duplicate settings tab id "${(tab as any).id}"`)
70
- settingIds.add((tab as any).id)
71
- }
72
-
73
- for (const [i, feat] of (manifest.declaredFeatures ?? []).entries()) {
74
- if (!nonEmptyString((feat as any)?.id)) fail(scope, `declaredFeatures[${i}].id must be a non-empty string`)
75
- }
76
-
77
- // A plugin that contributes nothing mountable is almost always a mistake.
78
- // (Connector-only addon plugins contribute via `connectors` instead.)
79
- if (
80
- manifest.navigation.length === 0 &&
81
- manifest.routes.length === 0 &&
82
- (manifest.settings ?? []).length === 0 &&
83
- (manifest.connectors ?? []).length === 0
84
- ) {
85
- fail(scope, 'plugin contributes no navigation, routes, settings, or connectors — it would mount but do nothing')
86
- }
87
- }
88
-
89
- const AUTH_KINDS = new Set(['oauth', 'api-key', 'mtls'])
90
- const DIRECTIONS = new Set(['inbound', 'outbound', 'bidirectional'])
91
- const TRIGGERS = new Set(['on-write', 'scheduled', 'manual', 'webhook'])
92
-
93
- /**
94
- * Assert an integration Connector descriptor is well-formed: identity present,
95
- * a known auth kind, and at least one capability with a valid direction +
96
- * trigger set. This is the contract a connector must satisfy BEFORE its
97
- * data-plane (edge function) exists — the TDD anchor for an integration.
98
- */
99
- export function assertConnectorContract(connector: Connector): void {
100
- const scope = nonEmptyString(connector?.id) ? `connector:${connector.id}` : '<unknown connector>'
101
- for (const field of ['id', 'provider', 'pluginId'] as const) {
102
- if (!nonEmptyString(connector?.[field])) fail(scope, `connector.${field} must be a non-empty string`)
103
- }
104
- if (!AUTH_KINDS.has(connector.authKind)) fail(scope, `connector.authKind "${connector.authKind}" is not one of ${[...AUTH_KINDS].join(', ')}`)
105
- if (!Array.isArray(connector.capabilities) || connector.capabilities.length === 0) {
106
- fail(scope, 'connector.capabilities must list at least one capability')
107
- }
108
- for (const [i, cap] of connector.capabilities.entries()) {
109
- if (!nonEmptyString(cap?.entity)) fail(scope, `capabilities[${i}].entity must be a non-empty string`)
110
- if (!DIRECTIONS.has(cap.direction)) fail(scope, `capabilities[${i}].direction "${cap.direction}" is invalid`)
111
- if (!Array.isArray(cap.triggers) || cap.triggers.length === 0) fail(scope, `capabilities[${i}].triggers must be non-empty`)
112
- for (const trig of cap.triggers) {
113
- if (!TRIGGERS.has(trig)) fail(scope, `capabilities[${i}] has unknown trigger "${trig}"`)
114
- }
115
- }
116
- }
package/src/types/auth.ts DELETED
@@ -1,41 +0,0 @@
1
- export type AuthProvider = 'google' | 'github' | 'apple' | 'facebook' | (string & {})
2
-
3
- export interface AuthUser {
4
- id: string
5
- email: string
6
- fullName?: string
7
- avatarUrl?: string
8
- role?: string
9
- /** Set on users created by an invite (magic link, no password). The shell shows
10
- * the set-password screen until they choose one — otherwise the only way back
11
- * in is another magic link. */
12
- mustSetPassword?: boolean
13
- }
14
-
15
- export interface AuthSession {
16
- user: AuthUser
17
- accessToken: string
18
- refreshToken?: string
19
- expiresAt?: number
20
- }
21
-
22
- export interface AuthAdapter {
23
- getSession(): Promise<{ user: AuthUser; session: AuthSession } | null>
24
- onAuthStateChange(cb: (user: AuthUser | null) => void): () => void
25
- signIn(email: string, password: string): Promise<AuthSession>
26
- signUp(email: string, password: string, fullName: string): Promise<AuthSession>
27
- signOut(): Promise<void>
28
- signInWithOAuth(provider: AuthProvider): Promise<void>
29
- resetPassword(email: string, options?: { redirectTo?: string }): Promise<void>
30
- updatePassword?(password: string): Promise<AuthSession | void>
31
- handleCallback?(url?: string): Promise<AuthSession | null>
32
- /**
33
- * Invite a user by e-mail via the provider's admin invite endpoint (e.g. Supabase
34
- * GoTrue `auth.admin.inviteUserByEmail`). Creates the auth user in an "invited"
35
- * state and sends the invite e-mail. `data` is written to the user's metadata so
36
- * an accept-time trigger can provision membership (tenant_id/role). Requires a
37
- * privileged path (service-role edge function) — not callable from the anon client.
38
- * Optional: adapters without an admin path (mock) may omit it.
39
- */
40
- inviteUser?(email: string, options?: { redirectTo?: string; data?: Record<string, unknown> }): Promise<void>
41
- }
@@ -1,39 +0,0 @@
1
- import type { PlanEntitlements } from './entitlements'
2
-
3
- export interface Plan {
4
- id: string
5
- name: string
6
- description?: string
7
- price: number
8
- currency: string
9
- interval: 'month' | 'year'
10
- /** Marketing bullets shown on the pricing page (display only). */
11
- features: string[]
12
- highlighted?: boolean
13
- stripePriceId?: string
14
- /**
15
- * Structured access grants for this plan (feature gates + quantity caps). This
16
- * is the enforcement source of truth — distinct from `features` (display
17
- * bullets). See {@link PlanEntitlements}.
18
- */
19
- entitlements?: PlanEntitlements
20
- }
21
-
22
- /**
23
- * Signature of the checkout seam. When `BillingConfig.onCheckout` is defined the
24
- * shell's Subscription page calls it (passing the target plan id) INSTEAD of
25
- * writing `plan` straight onto the org via `adapter.updateOrg`. This is where a
26
- * real payment gateway (Stripe Checkout, Pix, Mercado Pago, …) plugs in: return
27
- * once the user is redirected / the intent is created, and let the webhook be the
28
- * source of truth for the persisted plan. Left undefined, the shell mutates the
29
- * plan optimistically (dev / self-serve / free-tier flows).
30
- */
31
- export type BillingCheckoutFn = (planId: string) => Promise<void> | void
32
-
33
- export interface BillingConfig {
34
- plans: Plan[]
35
- stripePublishableKey?: string
36
- portalUrl?: string
37
- /** Payment-gateway seam — see {@link BillingCheckoutFn}. */
38
- onCheckout?: BillingCheckoutFn
39
- }