@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.
- package/README.md +2 -2
- package/dist/access/index.js.map +1 -1
- package/dist/chunk-BEGXYHS4.js +566 -0
- package/dist/chunk-BEGXYHS4.js.map +1 -0
- package/dist/chunk-ED6NBH6U.js +348 -0
- package/dist/chunk-ED6NBH6U.js.map +1 -0
- package/dist/chunk-EWKX4VUM.js +429 -0
- package/dist/chunk-EWKX4VUM.js.map +1 -0
- package/dist/chunk-LARZWDHA.js +319 -0
- package/dist/chunk-LARZWDHA.js.map +1 -0
- package/dist/chunk-OXQGRJZT.js +125 -0
- package/dist/chunk-OXQGRJZT.js.map +1 -0
- package/dist/chunk-RRYFXUQX.js +1247 -0
- package/dist/chunk-RRYFXUQX.js.map +1 -0
- package/dist/{chunk-5RJVUC6J.js → chunk-TDI6F6PT.js} +373 -25
- package/dist/chunk-TDI6F6PT.js.map +1 -0
- package/dist/{chunk-V2MSAUHO.js → chunk-UVKH6RDX.js} +211 -335
- package/dist/chunk-UVKH6RDX.js.map +1 -0
- package/dist/chunk-V53GR4UT.js +289 -0
- package/dist/chunk-V53GR4UT.js.map +1 -0
- package/dist/connector-runtime/index.d.ts +15 -0
- package/dist/connector-runtime/index.d.ts.map +1 -0
- package/dist/connector-runtime/index.js +3 -0
- package/dist/connector-runtime/index.js.map +1 -0
- package/dist/data/archetype.d.ts.map +1 -1
- package/dist/data/backend-env.d.ts +38 -0
- package/dist/data/backend-env.d.ts.map +1 -0
- package/dist/data/bulk.d.ts +24 -0
- package/dist/data/bulk.d.ts.map +1 -1
- package/dist/data/cached.d.ts.map +1 -1
- package/dist/data/custom-fields.d.ts +5 -0
- package/dist/data/custom-fields.d.ts.map +1 -0
- package/dist/data/index.d.ts +2 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +3 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/platform-api.d.ts.map +1 -1
- package/dist/data/resolve.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +8 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/entity/events.d.ts +14 -0
- package/dist/entity/events.d.ts.map +1 -0
- package/dist/events/envelope.d.ts +31 -0
- package/dist/events/envelope.d.ts.map +1 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/log.d.ts +44 -0
- package/dist/events/log.d.ts.map +1 -0
- package/dist/events/types.d.ts +40 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/fields/store.d.ts +45 -0
- package/dist/fields/store.d.ts.map +1 -0
- package/dist/i18n/index.d.ts +10 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/shell-translations.en.d.ts.map +1 -1
- package/dist/i18n/shell-translations.pt-br.d.ts.map +1 -1
- package/dist/index.d.ts +45 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +543 -56
- package/dist/index.js.map +1 -1
- package/dist/integrations/events.d.ts +7 -0
- package/dist/integrations/events.d.ts.map +1 -0
- package/dist/integrations/index.d.ts +533 -29
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +3 -1
- package/dist/integrations/permissions.d.ts +132 -0
- package/dist/integrations/permissions.d.ts.map +1 -0
- package/dist/integrations/request.d.ts +170 -0
- package/dist/integrations/request.d.ts.map +1 -0
- package/dist/integrations/runtime-token.d.ts +33 -0
- package/dist/integrations/runtime-token.d.ts.map +1 -0
- package/dist/integrations/runtime.d.ts +398 -0
- package/dist/integrations/runtime.d.ts.map +1 -0
- package/dist/integrations/settings.d.ts +102 -0
- package/dist/integrations/settings.d.ts.map +1 -0
- package/dist/integrations/store.d.ts +145 -0
- package/dist/integrations/store.d.ts.map +1 -0
- package/dist/{address/index.d.ts → lib/address.d.ts} +1 -1
- package/dist/{address/index.d.ts.map → lib/address.d.ts.map} +1 -1
- package/dist/{phone/index.d.ts → lib/phone.d.ts} +1 -1
- package/dist/lib/phone.d.ts.map +1 -0
- package/dist/{seo/index.d.ts → lib/seo.d.ts} +18 -1
- package/dist/lib/seo.d.ts.map +1 -0
- package/dist/{tenant/index.d.ts → lib/tenant.d.ts} +1 -1
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/manifest/index.d.ts +1 -1
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/messaging/index.d.ts +3 -0
- package/dist/messaging/index.d.ts.map +1 -0
- package/dist/messaging/registry.d.ts +35 -0
- package/dist/messaging/registry.d.ts.map +1 -0
- package/dist/messaging/types.d.ts +67 -0
- package/dist/messaging/types.d.ts.map +1 -0
- package/dist/plugin/index.js +3 -1
- package/dist/plugin/runtime.d.ts +17 -0
- package/dist/plugin/runtime.d.ts.map +1 -1
- package/dist/registry/block-catalog.d.ts +78 -0
- package/dist/registry/block-catalog.d.ts.map +1 -0
- package/dist/registry/block-constraints.d.ts +64 -0
- package/dist/registry/block-constraints.d.ts.map +1 -0
- package/dist/registry/block-schema.d.ts +69 -0
- package/dist/registry/block-schema.d.ts.map +1 -0
- package/dist/registry/blocks.d.ts +84 -0
- package/dist/registry/blocks.d.ts.map +1 -0
- package/dist/registry/entities.d.ts +179 -0
- package/dist/registry/entities.d.ts.map +1 -0
- package/dist/registry/extension-points.d.ts +117 -0
- package/dist/registry/extension-points.d.ts.map +1 -0
- package/dist/registry/handles.d.ts +41 -0
- package/dist/registry/handles.d.ts.map +1 -0
- package/dist/registry/index.d.ts +41 -41
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/loaders.d.ts +55 -0
- package/dist/registry/loaders.d.ts.map +1 -0
- package/dist/registry/registry-class.d.ts +157 -0
- package/dist/registry/registry-class.d.ts.map +1 -0
- package/dist/registry/schema-check.d.ts +14 -0
- package/dist/registry/schema-check.d.ts.map +1 -0
- package/dist/router/index.d.ts +78 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +1 -1
- package/dist/{shell-translations.pt-br-2GWRQRRP.js → shell-translations.pt-br-BM5ZFSKJ.js} +72 -3
- package/dist/shell-translations.pt-br-BM5ZFSKJ.js.map +1 -0
- package/dist/telemetry/errors.d.ts +87 -0
- package/dist/telemetry/errors.d.ts.map +1 -0
- package/dist/testing/index.js +4 -1
- package/dist/{testing/index.d.ts → testing.d.ts} +5 -3
- package/dist/testing.d.ts.map +1 -0
- package/dist/types/analytics.d.ts +213 -0
- package/dist/types/analytics.d.ts.map +1 -0
- package/dist/types/crud.d.ts +37 -3
- package/dist/types/crud.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +138 -1
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +9 -17
- package/dist/blocks/index.d.ts +0 -25
- package/dist/blocks/index.d.ts.map +0 -1
- package/dist/chunk-5RJVUC6J.js.map +0 -1
- package/dist/chunk-L4TLMYGC.js +0 -42
- package/dist/chunk-L4TLMYGC.js.map +0 -1
- package/dist/chunk-V2MSAUHO.js.map +0 -1
- package/dist/chunk-X5THHG6T.js +0 -77
- package/dist/chunk-X5THHG6T.js.map +0 -1
- package/dist/chunk-Y2C2FBQP.js +0 -35
- package/dist/chunk-Y2C2FBQP.js.map +0 -1
- package/dist/chunk-YH25Y4FW.js +0 -311
- package/dist/chunk-YH25Y4FW.js.map +0 -1
- package/dist/phone/index.d.ts.map +0 -1
- package/dist/seo/index.d.ts.map +0 -1
- package/dist/shell-translations.pt-br-2GWRQRRP.js.map +0 -1
- package/dist/tenant/index.d.ts.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- package/src/access/denial.ts +0 -33
- package/src/access/engine.test.ts +0 -48
- package/src/access/engine.ts +0 -136
- package/src/access/index.ts +0 -14
- package/src/access/limits.ts +0 -18
- package/src/address/index.ts +0 -188
- package/src/app/render.tsx +0 -69
- package/src/blocks/index.tsx +0 -68
- package/src/data/archetype.ts +0 -255
- package/src/data/bulk.ts +0 -35
- package/src/data/cached.ts +0 -55
- package/src/data/count.test.ts +0 -152
- package/src/data/count.ts +0 -131
- package/src/data/filters.test.ts +0 -70
- package/src/data/filters.ts +0 -90
- package/src/data/index.ts +0 -11
- package/src/data/mock.ts +0 -90
- package/src/data/platform-api.ts +0 -209
- package/src/data/refresh.ts +0 -115
- package/src/data/resolve.ts +0 -126
- package/src/data/supabase.ts +0 -230
- package/src/data/types.ts +0 -50
- package/src/entity/index.ts +0 -2
- package/src/entity/registry.ts +0 -49
- package/src/events/index.tsx +0 -82
- package/src/i18n/index.ts +0 -476
- package/src/i18n/shell-translations.en.ts +0 -875
- package/src/i18n/shell-translations.pt-br.ts +0 -873
- package/src/index.ts +0 -217
- package/src/integrations/index.ts +0 -237
- package/src/lib/cache.ts +0 -118
- package/src/lib/csv.ts +0 -54
- package/src/lib/format.ts +0 -58
- package/src/manifest/app-manifest.schema.json +0 -676
- package/src/manifest/index.ts +0 -518
- package/src/manifest/serialize.ts +0 -52
- package/src/phone/index.ts +0 -85
- package/src/plugin/createSafeDataProvider.ts +0 -30
- package/src/plugin/index.ts +0 -1
- package/src/plugin/runtime.ts +0 -331
- package/src/registry/index.ts +0 -163
- package/src/router/history.ts +0 -86
- package/src/router/index.ts +0 -29
- package/src/runtime/index.ts +0 -15
- package/src/runtime/oauth.ts +0 -219
- package/src/search/engine.ts +0 -559
- package/src/search/index.ts +0 -25
- package/src/search/text.ts +0 -206
- package/src/search/types.ts +0 -75
- package/src/seo/index.ts +0 -128
- package/src/tenant/index.ts +0 -38
- package/src/testing/index.ts +0 -116
- package/src/types/auth.ts +0 -41
- package/src/types/billing.ts +0 -39
- package/src/types/crud.ts +0 -184
- package/src/types/entities.ts +0 -142
- package/src/types/entitlements.ts +0 -45
- package/src/types/i18n.ts +0 -7
- package/src/types/index.ts +0 -11
- package/src/types/org.ts +0 -137
- package/src/types/payment.ts +0 -44
- package/src/types/permissions.ts +0 -32
- package/src/types/plugins.ts +0 -480
- package/src/types/theme.ts +0 -8
package/src/search/text.ts
DELETED
|
@@ -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
|
-
}
|
package/src/search/types.ts
DELETED
|
@@ -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
|
-
}
|
package/src/tenant/index.ts
DELETED
|
@@ -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
|
-
}
|
package/src/testing/index.ts
DELETED
|
@@ -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
|
-
}
|
package/src/types/billing.ts
DELETED
|
@@ -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
|
-
}
|