@vennyx/solicrm-mcp 0.1.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/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/bin.js +4097 -0
- package/dist/index.d.ts +399 -0
- package/dist/index.js +4140 -0
- package/package.json +57 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,4097 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
//#region packages/contracts/src/fields.ts
|
|
7
|
+
/**
|
|
8
|
+
* Alan kataloğu — `field_key` → kolon/tip/yetenek eşlemesi.
|
|
9
|
+
*
|
|
10
|
+
* NEDEN VAR: kaydedilmiş görünümler, filtreler, sıralama, CSV içe/dışa aktarma
|
|
11
|
+
* ve MCP/SDK tip üretimi HEPSİ bu katalogdan okur. Hiçbir yerde kullanıcıdan
|
|
12
|
+
* gelen bir değer doğrudan kolon adına dönüşmez — `resolveField()` bilinmeyen
|
|
13
|
+
* anahtar için `undefined` döner ve çağıran 400 verir. Bu, SQL enjeksiyonu ve
|
|
14
|
+
* bilgi sızdırmanın sınırıdır.
|
|
15
|
+
*
|
|
16
|
+
* İLERİYE DÖNÜK: özel alanlar (custom fields) eklendiğinde katalog runtime'da
|
|
17
|
+
* tenant'ın tanımlarıyla genişletilir; tüketici kod değişmez (bkz. spec §6.7).
|
|
18
|
+
*/
|
|
19
|
+
const CRM_RESOURCES = [
|
|
20
|
+
"contacts",
|
|
21
|
+
"companies",
|
|
22
|
+
"deals",
|
|
23
|
+
"tasks"
|
|
24
|
+
];
|
|
25
|
+
const FIELD_CATALOG = {
|
|
26
|
+
contacts: {
|
|
27
|
+
first_name: {
|
|
28
|
+
column: "first_name",
|
|
29
|
+
type: "text",
|
|
30
|
+
filterable: true,
|
|
31
|
+
sortable: true
|
|
32
|
+
},
|
|
33
|
+
last_name: {
|
|
34
|
+
column: "last_name",
|
|
35
|
+
type: "text",
|
|
36
|
+
filterable: true,
|
|
37
|
+
sortable: true
|
|
38
|
+
},
|
|
39
|
+
email: {
|
|
40
|
+
column: "email",
|
|
41
|
+
type: "text",
|
|
42
|
+
filterable: true,
|
|
43
|
+
sortable: true
|
|
44
|
+
},
|
|
45
|
+
phone: {
|
|
46
|
+
column: "phone",
|
|
47
|
+
type: "text",
|
|
48
|
+
filterable: true,
|
|
49
|
+
sortable: false
|
|
50
|
+
},
|
|
51
|
+
job_title: {
|
|
52
|
+
column: "job_title",
|
|
53
|
+
type: "text",
|
|
54
|
+
filterable: true,
|
|
55
|
+
sortable: true
|
|
56
|
+
},
|
|
57
|
+
city: {
|
|
58
|
+
column: "city",
|
|
59
|
+
type: "text",
|
|
60
|
+
filterable: true,
|
|
61
|
+
sortable: true
|
|
62
|
+
},
|
|
63
|
+
country: {
|
|
64
|
+
column: "country",
|
|
65
|
+
type: "text",
|
|
66
|
+
filterable: true,
|
|
67
|
+
sortable: true
|
|
68
|
+
},
|
|
69
|
+
status: {
|
|
70
|
+
column: "status",
|
|
71
|
+
type: "enum",
|
|
72
|
+
filterable: true,
|
|
73
|
+
sortable: true,
|
|
74
|
+
values: [
|
|
75
|
+
"lead",
|
|
76
|
+
"active",
|
|
77
|
+
"inactive"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
company_id: {
|
|
81
|
+
column: "company_id",
|
|
82
|
+
type: "uuid",
|
|
83
|
+
filterable: true,
|
|
84
|
+
sortable: false
|
|
85
|
+
},
|
|
86
|
+
owner_user_id: {
|
|
87
|
+
column: "owner_user_id",
|
|
88
|
+
type: "uuid",
|
|
89
|
+
filterable: true,
|
|
90
|
+
sortable: false
|
|
91
|
+
},
|
|
92
|
+
created_at: {
|
|
93
|
+
column: "created_at",
|
|
94
|
+
type: "date",
|
|
95
|
+
filterable: true,
|
|
96
|
+
sortable: true
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
companies: {
|
|
100
|
+
name: {
|
|
101
|
+
column: "name",
|
|
102
|
+
type: "text",
|
|
103
|
+
filterable: true,
|
|
104
|
+
sortable: true
|
|
105
|
+
},
|
|
106
|
+
domain: {
|
|
107
|
+
column: "domain",
|
|
108
|
+
type: "text",
|
|
109
|
+
filterable: true,
|
|
110
|
+
sortable: true
|
|
111
|
+
},
|
|
112
|
+
industry: {
|
|
113
|
+
column: "industry",
|
|
114
|
+
type: "text",
|
|
115
|
+
filterable: true,
|
|
116
|
+
sortable: true
|
|
117
|
+
},
|
|
118
|
+
city: {
|
|
119
|
+
column: "city",
|
|
120
|
+
type: "text",
|
|
121
|
+
filterable: true,
|
|
122
|
+
sortable: true
|
|
123
|
+
},
|
|
124
|
+
country: {
|
|
125
|
+
column: "country",
|
|
126
|
+
type: "text",
|
|
127
|
+
filterable: true,
|
|
128
|
+
sortable: true
|
|
129
|
+
},
|
|
130
|
+
annual_revenue: {
|
|
131
|
+
column: "annual_revenue",
|
|
132
|
+
type: "money",
|
|
133
|
+
filterable: true,
|
|
134
|
+
sortable: true
|
|
135
|
+
},
|
|
136
|
+
owner_user_id: {
|
|
137
|
+
column: "owner_user_id",
|
|
138
|
+
type: "uuid",
|
|
139
|
+
filterable: true,
|
|
140
|
+
sortable: false
|
|
141
|
+
},
|
|
142
|
+
created_at: {
|
|
143
|
+
column: "created_at",
|
|
144
|
+
type: "date",
|
|
145
|
+
filterable: true,
|
|
146
|
+
sortable: true
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
deals: {
|
|
150
|
+
title: {
|
|
151
|
+
column: "title",
|
|
152
|
+
type: "text",
|
|
153
|
+
filterable: true,
|
|
154
|
+
sortable: true
|
|
155
|
+
},
|
|
156
|
+
amount: {
|
|
157
|
+
column: "amount",
|
|
158
|
+
type: "money",
|
|
159
|
+
filterable: true,
|
|
160
|
+
sortable: true
|
|
161
|
+
},
|
|
162
|
+
currency_code: {
|
|
163
|
+
column: "currency_code",
|
|
164
|
+
type: "text",
|
|
165
|
+
filterable: true,
|
|
166
|
+
sortable: false
|
|
167
|
+
},
|
|
168
|
+
stage_id: {
|
|
169
|
+
column: "stage_id",
|
|
170
|
+
type: "uuid",
|
|
171
|
+
filterable: true,
|
|
172
|
+
sortable: false
|
|
173
|
+
},
|
|
174
|
+
pipeline_id: {
|
|
175
|
+
column: "pipeline_id",
|
|
176
|
+
type: "uuid",
|
|
177
|
+
filterable: true,
|
|
178
|
+
sortable: false
|
|
179
|
+
},
|
|
180
|
+
company_id: {
|
|
181
|
+
column: "company_id",
|
|
182
|
+
type: "uuid",
|
|
183
|
+
filterable: true,
|
|
184
|
+
sortable: false
|
|
185
|
+
},
|
|
186
|
+
outcome: {
|
|
187
|
+
column: "outcome",
|
|
188
|
+
type: "enum",
|
|
189
|
+
filterable: true,
|
|
190
|
+
sortable: true,
|
|
191
|
+
values: [
|
|
192
|
+
"open",
|
|
193
|
+
"won",
|
|
194
|
+
"lost"
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
expected_close_date: {
|
|
198
|
+
column: "expected_close_date",
|
|
199
|
+
type: "date",
|
|
200
|
+
filterable: true,
|
|
201
|
+
sortable: true
|
|
202
|
+
},
|
|
203
|
+
owner_user_id: {
|
|
204
|
+
column: "owner_user_id",
|
|
205
|
+
type: "uuid",
|
|
206
|
+
filterable: true,
|
|
207
|
+
sortable: false
|
|
208
|
+
},
|
|
209
|
+
created_at: {
|
|
210
|
+
column: "created_at",
|
|
211
|
+
type: "date",
|
|
212
|
+
filterable: true,
|
|
213
|
+
sortable: true
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
tasks: {
|
|
217
|
+
title: {
|
|
218
|
+
column: "title",
|
|
219
|
+
type: "text",
|
|
220
|
+
filterable: true,
|
|
221
|
+
sortable: true
|
|
222
|
+
},
|
|
223
|
+
due_at: {
|
|
224
|
+
column: "due_at",
|
|
225
|
+
type: "date",
|
|
226
|
+
filterable: true,
|
|
227
|
+
sortable: true
|
|
228
|
+
},
|
|
229
|
+
completed_at: {
|
|
230
|
+
column: "completed_at",
|
|
231
|
+
type: "date",
|
|
232
|
+
filterable: true,
|
|
233
|
+
sortable: true
|
|
234
|
+
},
|
|
235
|
+
priority: {
|
|
236
|
+
column: "priority",
|
|
237
|
+
type: "enum",
|
|
238
|
+
filterable: true,
|
|
239
|
+
sortable: true,
|
|
240
|
+
values: [
|
|
241
|
+
"low",
|
|
242
|
+
"normal",
|
|
243
|
+
"high",
|
|
244
|
+
"urgent"
|
|
245
|
+
]
|
|
246
|
+
},
|
|
247
|
+
assignee_user_id: {
|
|
248
|
+
column: "assignee_user_id",
|
|
249
|
+
type: "uuid",
|
|
250
|
+
filterable: true,
|
|
251
|
+
sortable: false
|
|
252
|
+
},
|
|
253
|
+
created_at: {
|
|
254
|
+
column: "created_at",
|
|
255
|
+
type: "date",
|
|
256
|
+
filterable: true,
|
|
257
|
+
sortable: true
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Bir `field_key`'i katalogdan çözer. Bilinmeyen anahtar → `undefined`
|
|
263
|
+
* (çağıran 400 döner). ASLA anahtarı kolon adı olarak varsaymaz.
|
|
264
|
+
*/
|
|
265
|
+
function resolveField(resource, key) {
|
|
266
|
+
const catalog = FIELD_CATALOG[resource];
|
|
267
|
+
return Object.hasOwn(catalog, key) ? catalog[key] : void 0;
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region packages/contracts/src/crm.ts
|
|
271
|
+
/**
|
|
272
|
+
* CRM domain genelinde paylaşılan zod katalogları (spec §6.2/§6.3).
|
|
273
|
+
*
|
|
274
|
+
* NOT — Task 3 (deals) genişletmesi: `targetTypeSchema` artık `'deal'`
|
|
275
|
+
* içeriyor (tek satırlık, geriye uyumlu genişleme; hiçbir tüketici tipi
|
|
276
|
+
* DARALTILMADI, yalnız genişledi — bkz. `apps/api/src/lib/polymorphic-target.ts`,
|
|
277
|
+
* `packages/db/drizzle/0028_deals_guard.sql`'daki `CREATE OR REPLACE FUNCTION`).
|
|
278
|
+
*
|
|
279
|
+
* NOT — spec'in tam liste vermediği katalog değerleri (bu dosyanın kararı):
|
|
280
|
+
* `contactSourceSchema` ve `companySizeSchema` spec §6.2'de yalnız "text+katalog"
|
|
281
|
+
* / kendine özgü alan olarak anılıyor, değer kümesi verilmiyor — makul bir
|
|
282
|
+
* başlangıç kümesi seçildi (bkz. plan "Yeni ortak altyapı kararları / D").
|
|
283
|
+
*/
|
|
284
|
+
const targetTypeSchema = z.enum([
|
|
285
|
+
"contact",
|
|
286
|
+
"company",
|
|
287
|
+
"deal"
|
|
288
|
+
], "geçerli bir hedef türü olmalı (contact, company, deal)");
|
|
289
|
+
/** Fırsat sonucu (spec §6.2) — DB'de `deals_outcome_check` CHECK kısıtıyla eşleşir */
|
|
290
|
+
const dealOutcomeSchema = z.enum([
|
|
291
|
+
"open",
|
|
292
|
+
"won",
|
|
293
|
+
"lost"
|
|
294
|
+
], "geçerli bir sonuç olmalı (open, won, lost)");
|
|
295
|
+
const contactStatusSchema = z.enum([
|
|
296
|
+
"lead",
|
|
297
|
+
"active",
|
|
298
|
+
"inactive"
|
|
299
|
+
], "geçerli bir durum olmalı (lead, active, inactive)");
|
|
300
|
+
/** Spec'te tam liste verilmemiş — makul bir başlangıç kümesi (bkz. dosya başı NOT) */
|
|
301
|
+
const contactSourceSchema = z.enum([
|
|
302
|
+
"website",
|
|
303
|
+
"referral",
|
|
304
|
+
"cold_outreach",
|
|
305
|
+
"event",
|
|
306
|
+
"social_media",
|
|
307
|
+
"other"
|
|
308
|
+
], "geçerli bir kaynak olmalı");
|
|
309
|
+
/** Spec'te tam liste verilmemiş — makul bir başlangıç kümesi (bkz. dosya başı NOT) */
|
|
310
|
+
const companySizeSchema = z.enum([
|
|
311
|
+
"1-10",
|
|
312
|
+
"11-50",
|
|
313
|
+
"51-200",
|
|
314
|
+
"201-500",
|
|
315
|
+
"500+"
|
|
316
|
+
], "geçerli bir şirket büyüklüğü olmalı");
|
|
317
|
+
const sortDirectionSchema = z.enum(["asc", "desc"], "yön asc veya desc olmalı");
|
|
318
|
+
const filterOperandSchema = z.enum([
|
|
319
|
+
"eq",
|
|
320
|
+
"neq",
|
|
321
|
+
"contains",
|
|
322
|
+
"starts_with",
|
|
323
|
+
"gt",
|
|
324
|
+
"gte",
|
|
325
|
+
"lt",
|
|
326
|
+
"lte",
|
|
327
|
+
"in",
|
|
328
|
+
"is_null",
|
|
329
|
+
"is_not_null"
|
|
330
|
+
], "geçerli bir karşılaştırma operatörü olmalı");
|
|
331
|
+
/**
|
|
332
|
+
* Aktivite türü (spec §6.2) — `stage_change` Faz 5 Task 3'ün `/move` ucunun
|
|
333
|
+
* ürettiği otomatik aktiviteyle eşleşir (bkz. `apps/api/src/routes/deals.ts`).
|
|
334
|
+
*/
|
|
335
|
+
const activityKindSchema = z.enum([
|
|
336
|
+
"call",
|
|
337
|
+
"meeting",
|
|
338
|
+
"email",
|
|
339
|
+
"note",
|
|
340
|
+
"task",
|
|
341
|
+
"stage_change"
|
|
342
|
+
], "geçerli bir aktivite türü olmalı (call, meeting, email, note, task, stage_change)");
|
|
343
|
+
/** Spec'te tam liste verilmemiş — makul bir başlangıç kümesi (bkz. plan "Yeni ortak altyapı kararları / D") */
|
|
344
|
+
const taskStatusSchema = z.enum([
|
|
345
|
+
"todo",
|
|
346
|
+
"in_progress",
|
|
347
|
+
"done",
|
|
348
|
+
"cancelled"
|
|
349
|
+
], "geçerli bir durum olmalı (todo, in_progress, done, cancelled)");
|
|
350
|
+
/** Spec'in verdiği tam liste (§6.2) */
|
|
351
|
+
const taskPrioritySchema = z.enum([
|
|
352
|
+
"low",
|
|
353
|
+
"normal",
|
|
354
|
+
"high",
|
|
355
|
+
"urgent"
|
|
356
|
+
], "geçerli bir öncelik olmalı (low, normal, high, urgent)");
|
|
357
|
+
z.object({
|
|
358
|
+
targetType: targetTypeSchema,
|
|
359
|
+
targetId: z.uuid("geçerli bir hedef kimliği olmalı")
|
|
360
|
+
});
|
|
361
|
+
/**
|
|
362
|
+
* Kaydedilmiş görünüm katalogları (spec §6.5, Faz 5 Task 5).
|
|
363
|
+
* `savedViewResourceSchema` = `CRM_RESOURCES`'ın zod karşılığı — `fields.ts`
|
|
364
|
+
* DEĞİŞTİRİLMEDEN (`CRM_RESOURCES` sabiti oradan İÇE AKTARILIR, yeniden
|
|
365
|
+
* TANIMLANMAZ, tek gerçek kaynak korunur).
|
|
366
|
+
*/
|
|
367
|
+
const savedViewResourceSchema = z.enum(CRM_RESOURCES, "geçerli bir CRM kaynağı olmalı (contacts, companies, deals, tasks)");
|
|
368
|
+
const savedViewLayoutSchema = z.enum(["table", "kanban"], "geçerli bir düzen olmalı (table, kanban)");
|
|
369
|
+
const savedViewVisibilitySchema = z.enum(["private", "tenant"], "geçerli bir görünürlük olmalı (private, tenant)");
|
|
370
|
+
const filterLogicalOperatorSchema = z.enum([
|
|
371
|
+
"and",
|
|
372
|
+
"or",
|
|
373
|
+
"not"
|
|
374
|
+
], "geçerli bir mantıksal operatör olmalı (and, or, not)");
|
|
375
|
+
/**
|
|
376
|
+
* `numeric(18,2)` DB kolonuna eşlenen PARA alanları için ORTAK biçim
|
|
377
|
+
* doğrulaması (spec §6.1 — para asla native `number`/aritmetikle hesaplanmaz,
|
|
378
|
+
* yalnız string olarak taşınır, `bignumber.js` dışında ASLA işlenmez).
|
|
379
|
+
*
|
|
380
|
+
* [Fix round 2 — doğruluk reviewer'ının bulduğu, Task 3'ün `deals.amount`
|
|
381
|
+
* fix'iyle BİREBİR aynı sınıf hata]: `companies.annual_revenue` DB'de `text`
|
|
382
|
+
* kolondu ama `FIELD_CATALOG.companies.annual_revenue` `type:'money'`,
|
|
383
|
+
* `sortable:true`, `filterable:true` ilan ediyordu — `text` kolonda `ORDER
|
|
384
|
+
* BY`/`>`/`<` LEXICOGRAPHIC karşılaştırma yapar (`"100" < "20" < "9"`), bu
|
|
385
|
+
* SESSİZ bir doğruluk hatasıdır. Bu şema, Task 3'ün önceden yalnız
|
|
386
|
+
* `deals.ts` içinde TANIMLADIĞI `dealAmountSchema`'nın (aynı kısıtlar: en
|
|
387
|
+
* fazla 2 ondalık basamak, en fazla 18 anlamlı basamak — `numeric(18,2)`
|
|
388
|
+
* sınırı) buraya TAŞINMIŞ/PAYLAŞILAN hâlidir — İKİ ayrı DB kolonu (deals.amount,
|
|
389
|
+
* companies.annual_revenue) TEK doğrulama mantığından geçer (drift riski
|
|
390
|
+
* olmasın diye, DRY).
|
|
391
|
+
*/
|
|
392
|
+
const numericMoneySchema = z.string().trim().max(40, "en fazla 40 karakter olabilir").regex(/^-?\d+(\.\d{1,2})?$/, "geçerli bir ondalık tutar olmalı, en fazla 2 ondalık basamak (örn. 1234.56)").refine((v) => v.replace(/[-.]/g, "").length <= 18, "en fazla 18 anlamlı basamak olabilir (numeric(18,2) sınırı)");
|
|
393
|
+
const pageQuerySchema = z.object({
|
|
394
|
+
cursor: z.string().max(2e3, "geçersiz sayfalama imleci").optional(),
|
|
395
|
+
limit: z.coerce.number("sayı olmalı").int("tam sayı olmalı").min(1, "en az 1 olmalı").max(100, `en fazla 100 olabilir`).optional()
|
|
396
|
+
});
|
|
397
|
+
function pagedResponseSchema(itemSchema) {
|
|
398
|
+
return z.object({
|
|
399
|
+
items: z.array(itemSchema),
|
|
400
|
+
nextCursor: z.string().nullable(),
|
|
401
|
+
hasMore: z.boolean()
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region packages/contracts/src/notes.ts
|
|
406
|
+
/**
|
|
407
|
+
* Not (note) sözleşmeleri (spec §6.2/§6.3) — `tasks.ts`'in BİREBİR aynı kalıbı,
|
|
408
|
+
* `assigneeUserId` yerine `authorUserId`, `status`/`priority` YOK.
|
|
409
|
+
*/
|
|
410
|
+
const noteBodySchema = z.record(z.string(), z.unknown());
|
|
411
|
+
const createNoteRequestSchema = z.object({
|
|
412
|
+
title: z.string().trim().max(200, "en fazla 200 karakter olabilir").optional(),
|
|
413
|
+
body: noteBodySchema.optional(),
|
|
414
|
+
bodyFormat: z.string().trim().max(40, "en fazla 40 karakter olabilir").optional(),
|
|
415
|
+
authorUserId: z.uuid("geçerli bir kullanıcı kimliği olmalı").optional()
|
|
416
|
+
});
|
|
417
|
+
z.object({
|
|
418
|
+
title: z.string().trim().max(200, "en fazla 200 karakter olabilir").nullable().optional(),
|
|
419
|
+
body: noteBodySchema.optional(),
|
|
420
|
+
bodyFormat: z.string().trim().max(40, "en fazla 40 karakter olabilir").optional(),
|
|
421
|
+
authorUserId: z.uuid("geçerli bir kullanıcı kimliği olmalı").nullable().optional()
|
|
422
|
+
});
|
|
423
|
+
const noteLinkResponseSchema = z.object({
|
|
424
|
+
id: z.uuid(),
|
|
425
|
+
targetType: targetTypeSchema,
|
|
426
|
+
targetId: z.uuid(),
|
|
427
|
+
targetCachedName: z.string(),
|
|
428
|
+
createdAt: z.iso.datetime()
|
|
429
|
+
});
|
|
430
|
+
const noteResponseSchema = z.object({
|
|
431
|
+
id: z.uuid(),
|
|
432
|
+
title: z.string().nullable(),
|
|
433
|
+
body: noteBodySchema.nullable(),
|
|
434
|
+
bodyFormat: z.string().nullable(),
|
|
435
|
+
authorUserId: z.uuid().nullable(),
|
|
436
|
+
createdAt: z.iso.datetime(),
|
|
437
|
+
updatedAt: z.iso.datetime()
|
|
438
|
+
});
|
|
439
|
+
noteResponseSchema.extend({ links: z.array(noteLinkResponseSchema) });
|
|
440
|
+
pagedResponseSchema(noteResponseSchema);
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region packages/contracts/src/tasks.ts
|
|
443
|
+
/**
|
|
444
|
+
* Görev (task) sözleşmeleri (spec §6.2/§6.3).
|
|
445
|
+
*
|
|
446
|
+
* KRİTİK KURAL: `dueAt`/`completedAt` kullanıcının Date/Time Handling
|
|
447
|
+
* Guideline'ına uygun olarak `apps/api/src/routes/tasks.ts`'te luxon
|
|
448
|
+
* (`DateTime`) ile dönüştürülür — burada yalnız ISO 8601 biçim doğrulaması
|
|
449
|
+
* yapılır (zod), gerçek dönüşüm route katmanının işidir.
|
|
450
|
+
*
|
|
451
|
+
* `assigneeUserId` PATCH'te `null` gönderilerek atamanın KALDIRILMASI
|
|
452
|
+
* desteklenir (`nullable().optional()`) — `undefined` "değiştirme", `null`
|
|
453
|
+
* "kaldır" anlamına gelir (companies/contacts/deals'ın PARTIAL şemalarından
|
|
454
|
+
* FARKLI bir kalıp, çünkü onlarda "atamayı kaldır" ihtiyacı yoktu).
|
|
455
|
+
*/
|
|
456
|
+
const taskBodySchema = z.record(z.string(), z.unknown());
|
|
457
|
+
const createTaskRequestSchema = z.object({
|
|
458
|
+
title: z.string().trim().min(1, "başlık gerekli").max(200, "en fazla 200 karakter olabilir"),
|
|
459
|
+
body: taskBodySchema.optional(),
|
|
460
|
+
bodyFormat: z.string().trim().max(40, "en fazla 40 karakter olabilir").optional(),
|
|
461
|
+
dueAt: z.iso.datetime("geçerli bir tarih olmalı").optional(),
|
|
462
|
+
status: taskStatusSchema.default("todo"),
|
|
463
|
+
priority: taskPrioritySchema.default("normal"),
|
|
464
|
+
assigneeUserId: z.uuid("geçerli bir kullanıcı kimliği olmalı").optional()
|
|
465
|
+
});
|
|
466
|
+
z.object({
|
|
467
|
+
title: z.string().trim().min(1, "başlık gerekli").max(200, "en fazla 200 karakter olabilir").optional(),
|
|
468
|
+
body: taskBodySchema.optional(),
|
|
469
|
+
bodyFormat: z.string().trim().max(40, "en fazla 40 karakter olabilir").optional(),
|
|
470
|
+
dueAt: z.iso.datetime("geçerli bir tarih olmalı").nullable().optional(),
|
|
471
|
+
status: taskStatusSchema.optional(),
|
|
472
|
+
priority: taskPrioritySchema.optional(),
|
|
473
|
+
assigneeUserId: z.uuid("geçerli bir kullanıcı kimliği olmalı").nullable().optional()
|
|
474
|
+
});
|
|
475
|
+
const taskLinkResponseSchema = z.object({
|
|
476
|
+
id: z.uuid(),
|
|
477
|
+
targetType: targetTypeSchema,
|
|
478
|
+
targetId: z.uuid(),
|
|
479
|
+
targetCachedName: z.string(),
|
|
480
|
+
createdAt: z.iso.datetime()
|
|
481
|
+
});
|
|
482
|
+
const taskResponseSchema = z.object({
|
|
483
|
+
id: z.uuid(),
|
|
484
|
+
title: z.string(),
|
|
485
|
+
body: taskBodySchema.nullable(),
|
|
486
|
+
bodyFormat: z.string().nullable(),
|
|
487
|
+
dueAt: z.iso.datetime().nullable(),
|
|
488
|
+
completedAt: z.iso.datetime().nullable(),
|
|
489
|
+
status: taskStatusSchema,
|
|
490
|
+
priority: taskPrioritySchema,
|
|
491
|
+
assigneeUserId: z.uuid().nullable(),
|
|
492
|
+
createdAt: z.iso.datetime(),
|
|
493
|
+
updatedAt: z.iso.datetime()
|
|
494
|
+
});
|
|
495
|
+
taskResponseSchema.extend({ links: z.array(taskLinkResponseSchema) });
|
|
496
|
+
pagedResponseSchema(taskResponseSchema);
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region packages/contracts/src/activities.ts
|
|
499
|
+
/**
|
|
500
|
+
* Aktivite (activity) sözleşmeleri (spec §6.2/§6.3).
|
|
501
|
+
*
|
|
502
|
+
* KRİTİK KURAL: `updateActivityRequestSchema` `.strict()`'tir ve `kind`/
|
|
503
|
+
* `targetType`/`targetId`'yi hiç TANIMLAMAZ — bir aktivitenin hedefi/türü
|
|
504
|
+
* OLUŞTURULDUKTAN SONRA değiştirilemez (bkz. `packages/db/src/schema/activities.ts`
|
|
505
|
+
* dosya başı yorumu, `apps/api/src/routes/activities.ts`). `deals.ts`'teki
|
|
506
|
+
* `updateDealRequestSchema`'nın AYNI kalıbı (bkz. o dosyanın dosya başı yorumu).
|
|
507
|
+
*
|
|
508
|
+
* TEKNİK BORÇ KAPANIŞI (Faz 6 Task 7): `GET /v1/tenants/:id/targets/
|
|
509
|
+
* :targetType/:targetId/timeline` ucu Faz 5 Task 4'ün plan dışı eklediği bir
|
|
510
|
+
* uçtu ve YANIT ZARFI için bir sözleşme şeması export ETMEMİŞTİ — SPA
|
|
511
|
+
* (`apps/dashboard/src/lib/timeline-one-shot.ts`) bu yüzden kendi elle yazılmış
|
|
512
|
+
* bir tipe (`.array()` bileşimi) doğrulama yapıyordu. `targetTimelineResponseSchema`
|
|
513
|
+
* artık TEK gerçek kaynak; iki taraf da (`apps/api/src/routes/activities.ts`
|
|
514
|
+
* VE SPA) buradan okur. `TIMELINE_LIMIT_PER_RESOURCE` de AYNI nedenle burada:
|
|
515
|
+
* backend'in `activities.ts`'teki elle kopyalanmış `= 50` sabiti ile SPA'nın
|
|
516
|
+
* `timeline-panel.tsx`'teki `TIMELINE_ONE_SHOT_CAP = 50` sabiti birbirinden
|
|
517
|
+
* BAĞIMSIZ, elle senkron tutuluyordu (drift riski, bkz. Faz 6 Task 6 raporu
|
|
518
|
+
* "Endişeler").
|
|
519
|
+
*/
|
|
520
|
+
const activityPropertiesSchema = z.record(z.string(), z.unknown());
|
|
521
|
+
const createActivityRequestSchema = z.object({
|
|
522
|
+
kind: activityKindSchema,
|
|
523
|
+
subject: z.string().trim().max(200, "en fazla 200 karakter olabilir").optional(),
|
|
524
|
+
body: z.string().trim().max(1e4, "en fazla 10000 karakter olabilir").optional(),
|
|
525
|
+
happensAt: z.iso.datetime("geçerli bir tarih olmalı"),
|
|
526
|
+
durationMinutes: z.number("sayı olmalı").int("tam sayı olmalı").min(0, "negatif olamaz").max(43200, "en fazla 43200 dakika (30 gün) olabilir").optional(),
|
|
527
|
+
targetType: targetTypeSchema,
|
|
528
|
+
targetId: z.uuid("geçerli bir hedef kimliği olmalı"),
|
|
529
|
+
actorUserId: z.uuid("geçerli bir kullanıcı kimliği olmalı").optional(),
|
|
530
|
+
properties: activityPropertiesSchema.optional()
|
|
531
|
+
});
|
|
532
|
+
z.object({
|
|
533
|
+
subject: z.string().trim().max(200, "en fazla 200 karakter olabilir").optional(),
|
|
534
|
+
body: z.string().trim().max(1e4, "en fazla 10000 karakter olabilir").optional(),
|
|
535
|
+
happensAt: z.iso.datetime("geçerli bir tarih olmalı").optional(),
|
|
536
|
+
durationMinutes: z.number("sayı olmalı").int("tam sayı olmalı").min(0, "negatif olamaz").max(43200, "en fazla 43200 dakika (30 gün) olabilir").optional(),
|
|
537
|
+
actorUserId: z.uuid("geçerli bir kullanıcı kimliği olmalı").optional(),
|
|
538
|
+
properties: activityPropertiesSchema.optional()
|
|
539
|
+
}).strict();
|
|
540
|
+
const activityResponseSchema = z.object({
|
|
541
|
+
id: z.uuid(),
|
|
542
|
+
kind: activityKindSchema,
|
|
543
|
+
subject: z.string().nullable(),
|
|
544
|
+
body: z.string().nullable(),
|
|
545
|
+
happensAt: z.iso.datetime(),
|
|
546
|
+
durationMinutes: z.number().nullable(),
|
|
547
|
+
targetType: targetTypeSchema,
|
|
548
|
+
targetId: z.uuid(),
|
|
549
|
+
targetCachedName: z.string(),
|
|
550
|
+
properties: activityPropertiesSchema,
|
|
551
|
+
actorUserId: z.uuid().nullable(),
|
|
552
|
+
createdAt: z.iso.datetime(),
|
|
553
|
+
updatedAt: z.iso.datetime()
|
|
554
|
+
});
|
|
555
|
+
pagedResponseSchema(activityResponseSchema);
|
|
556
|
+
z.object({
|
|
557
|
+
activities: activityResponseSchema.array(),
|
|
558
|
+
tasks: taskResponseSchema.array(),
|
|
559
|
+
notes: noteResponseSchema.array()
|
|
560
|
+
});
|
|
561
|
+
clone();
|
|
562
|
+
var isNumeric = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
563
|
+
var mathceil = Math.ceil;
|
|
564
|
+
var mathfloor = Math.floor;
|
|
565
|
+
var bignumberError = "[BigNumber Error] ";
|
|
566
|
+
var BASE = 0x5af3107a4000;
|
|
567
|
+
var LOG_BASE = 14;
|
|
568
|
+
var MAX_SAFE_INTEGER = 9007199254740991;
|
|
569
|
+
var POWS_TEN = [
|
|
570
|
+
1,
|
|
571
|
+
10,
|
|
572
|
+
100,
|
|
573
|
+
1e3,
|
|
574
|
+
1e4,
|
|
575
|
+
1e5,
|
|
576
|
+
1e6,
|
|
577
|
+
1e7,
|
|
578
|
+
1e8,
|
|
579
|
+
1e9,
|
|
580
|
+
1e10,
|
|
581
|
+
1e11,
|
|
582
|
+
0xe8d4a51000,
|
|
583
|
+
0x9184e72a000
|
|
584
|
+
];
|
|
585
|
+
var SQRT_BASE = 1e7;
|
|
586
|
+
var MAX = 1e9;
|
|
587
|
+
function clone(configObject) {
|
|
588
|
+
var div, convertBase, basePrefix = /^(-?)0([xbo])(?=[^.])/i, isInfinityOrNaN = /^-?(Infinity|NaN)$/, whitespaceOrPlus = /^\s*\+(?!-)|^\s+|\s+$/g, P = BigNumber.prototype = {
|
|
589
|
+
constructor: BigNumber,
|
|
590
|
+
toString: null,
|
|
591
|
+
valueOf: null
|
|
592
|
+
}, ONE = new BigNumber(1), DECIMAL_PLACES = 20, ROUNDING_MODE = 4, TO_EXP_NEG = -7, TO_EXP_POS = 21, MIN_EXP = -1e7, MAX_EXP = 1e7, CRYPTO = false, STRICT = true, MODULO_MODE = 1, POW_PRECISION = 0, FORMAT = {
|
|
593
|
+
prefix: "",
|
|
594
|
+
negativeSign: "-",
|
|
595
|
+
positiveSign: "",
|
|
596
|
+
groupSeparator: ",",
|
|
597
|
+
groupSize: 3,
|
|
598
|
+
secondaryGroupSize: 0,
|
|
599
|
+
decimalSeparator: ".",
|
|
600
|
+
fractionGroupSeparator: "",
|
|
601
|
+
fractionGroupSize: 0,
|
|
602
|
+
suffix: ""
|
|
603
|
+
}, ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
604
|
+
function BigNumber(v, b) {
|
|
605
|
+
var e, i, str, t, x = this;
|
|
606
|
+
if (!(x instanceof BigNumber)) return new BigNumber(v, b);
|
|
607
|
+
t = typeof v;
|
|
608
|
+
if (b == null) {
|
|
609
|
+
if (isBigNumber(v)) {
|
|
610
|
+
x.s = v.s;
|
|
611
|
+
if (!v.c || v.e > MAX_EXP) x.c = x.e = null;
|
|
612
|
+
else if (v.e < MIN_EXP) x.c = [x.e = 0];
|
|
613
|
+
else {
|
|
614
|
+
x.e = v.e;
|
|
615
|
+
x.c = v.c.slice();
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (t == "number") {
|
|
620
|
+
if (v * 0 != 0) {
|
|
621
|
+
x.s = isNaN(v) ? null : v < 0 ? -1 : 1;
|
|
622
|
+
x.c = x.e = null;
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
x.s = 1 / v < 0 ? (v = -v, -1) : 1;
|
|
626
|
+
if (v === ~~v) {
|
|
627
|
+
for (e = 0, i = v; i >= 10; i /= 10, e++);
|
|
628
|
+
if (e > MAX_EXP) x.c = x.e = null;
|
|
629
|
+
else {
|
|
630
|
+
x.e = e;
|
|
631
|
+
x.c = [v];
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
return parseValidString(x, String(v));
|
|
636
|
+
}
|
|
637
|
+
if (t == "bigint") {
|
|
638
|
+
x.s = v < 0 ? (v = -v, -1) : 1;
|
|
639
|
+
return parseValidString(x, String(v));
|
|
640
|
+
}
|
|
641
|
+
if (t == "string") str = v;
|
|
642
|
+
else {
|
|
643
|
+
if (STRICT) throw Error(bignumberError + "BigNumber, string, number, or BigInt expected: " + v);
|
|
644
|
+
str = String(v);
|
|
645
|
+
}
|
|
646
|
+
if (isNumeric.test(str)) {
|
|
647
|
+
x.s = str.charCodeAt(0) == 45 ? (str = str.slice(1), -1) : 1;
|
|
648
|
+
return parseValidString(x, str);
|
|
649
|
+
}
|
|
650
|
+
str = str.replace(whitespaceOrPlus, "");
|
|
651
|
+
if (isInfinityOrNaN.test(str)) {
|
|
652
|
+
x.s = isNaN(str) ? null : str < 0 ? -1 : 1;
|
|
653
|
+
x.c = x.e = null;
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
str = str.replace(basePrefix, function(m, p1, p2) {
|
|
657
|
+
b = (p2 = p2.toLowerCase()) == "x" ? 16 : p2 == "b" ? 2 : 8;
|
|
658
|
+
return p1;
|
|
659
|
+
});
|
|
660
|
+
if (b) return parseBaseString(x, str, b, v);
|
|
661
|
+
str = str.replace(/(\d)_(?=\d)/g, "$1");
|
|
662
|
+
if (isNumeric.test(str)) {
|
|
663
|
+
x.s = str.charCodeAt(0) == 45 ? (str = str.slice(1), -1) : 1;
|
|
664
|
+
return parseValidString(x, str);
|
|
665
|
+
}
|
|
666
|
+
if (STRICT) throw Error(bignumberError + "Not a number: " + v);
|
|
667
|
+
x.s = x.c = x.e = null;
|
|
668
|
+
} else {
|
|
669
|
+
if (t != "string") {
|
|
670
|
+
if (STRICT) throw Error(bignumberError + "String expected: " + v);
|
|
671
|
+
v = String(v);
|
|
672
|
+
}
|
|
673
|
+
intCheck(b, 2, ALPHABET.length, "Base");
|
|
674
|
+
parseBaseString(x, v.replace(whitespaceOrPlus, ""), b, v);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
BigNumber.clone = clone;
|
|
678
|
+
BigNumber.ROUND_UP = 0;
|
|
679
|
+
BigNumber.ROUND_DOWN = 1;
|
|
680
|
+
BigNumber.ROUND_CEIL = 2;
|
|
681
|
+
BigNumber.ROUND_FLOOR = 3;
|
|
682
|
+
BigNumber.ROUND_HALF_UP = 4;
|
|
683
|
+
BigNumber.ROUND_HALF_DOWN = 5;
|
|
684
|
+
BigNumber.ROUND_HALF_EVEN = 6;
|
|
685
|
+
BigNumber.ROUND_HALF_CEIL = 7;
|
|
686
|
+
BigNumber.ROUND_HALF_FLOOR = 8;
|
|
687
|
+
BigNumber.EUCLID = 9;
|
|
688
|
+
BigNumber.config = BigNumber.set = function(obj) {
|
|
689
|
+
var p, v;
|
|
690
|
+
if (obj != null) if (typeof obj == "object") {
|
|
691
|
+
if (obj.hasOwnProperty(p = "DECIMAL_PLACES")) DECIMAL_PLACES = intCheck(obj[p], 0, MAX, p);
|
|
692
|
+
if (obj.hasOwnProperty(p = "ROUNDING_MODE")) ROUNDING_MODE = intCheck(obj[p], 0, 8, p);
|
|
693
|
+
if (obj.hasOwnProperty(p = "EXPONENTIAL_AT")) {
|
|
694
|
+
v = obj[p];
|
|
695
|
+
if (isArray(v)) {
|
|
696
|
+
intCheck(v[0], -MAX, 0, p);
|
|
697
|
+
intCheck(v[1], 0, MAX, p);
|
|
698
|
+
TO_EXP_NEG = v[0];
|
|
699
|
+
TO_EXP_POS = v[1];
|
|
700
|
+
} else TO_EXP_NEG = -(TO_EXP_POS = intCheck(v, -MAX, MAX, p) < 0 ? -v : v);
|
|
701
|
+
}
|
|
702
|
+
if (obj.hasOwnProperty(p = "RANGE")) {
|
|
703
|
+
v = obj[p];
|
|
704
|
+
if (v) if (isArray(v)) {
|
|
705
|
+
intCheck(v[0], -MAX, -1, p);
|
|
706
|
+
intCheck(v[1], 1, MAX, p);
|
|
707
|
+
MIN_EXP = v[0];
|
|
708
|
+
MAX_EXP = v[1];
|
|
709
|
+
} else MIN_EXP = -(MAX_EXP = intCheck(v, -MAX, MAX, p) < 0 ? -v : v);
|
|
710
|
+
else throw Error(bignumberError + p + " cannot be zero: " + v);
|
|
711
|
+
}
|
|
712
|
+
if (obj.hasOwnProperty(p = "CRYPTO")) {
|
|
713
|
+
v = obj[p];
|
|
714
|
+
if (v === !!v) if (v) if (typeof crypto != "undefined" && crypto && (crypto.getRandomValues || crypto.randomBytes)) CRYPTO = v;
|
|
715
|
+
else {
|
|
716
|
+
CRYPTO = !v;
|
|
717
|
+
throw Error(bignumberError + "crypto unavailable");
|
|
718
|
+
}
|
|
719
|
+
else CRYPTO = v;
|
|
720
|
+
else throw Error(bignumberError + p + " not true or false: " + v);
|
|
721
|
+
}
|
|
722
|
+
if (obj.hasOwnProperty(p = "STRICT")) {
|
|
723
|
+
v = obj[p];
|
|
724
|
+
if (v === !!v) STRICT = v;
|
|
725
|
+
else throw Error(bignumberError + p + " not true or false: " + v);
|
|
726
|
+
}
|
|
727
|
+
if (obj.hasOwnProperty(p = "MODULO_MODE")) MODULO_MODE = intCheck(obj[p], 0, 9, p);
|
|
728
|
+
if (obj.hasOwnProperty(p = "POW_PRECISION")) POW_PRECISION = intCheck(obj[p], 0, MAX, p);
|
|
729
|
+
if (obj.hasOwnProperty(p = "FORMAT")) {
|
|
730
|
+
v = obj[p];
|
|
731
|
+
if (typeof v == "object") {
|
|
732
|
+
for (p in v) if (v.hasOwnProperty(p) && FORMAT.hasOwnProperty(p)) FORMAT[p] = v[p];
|
|
733
|
+
} else throw Error(bignumberError + p + " not an object: " + v);
|
|
734
|
+
}
|
|
735
|
+
if (obj.hasOwnProperty(p = "ALPHABET")) {
|
|
736
|
+
v = obj[p];
|
|
737
|
+
if (typeof v == "string" && !/^.?$|[+\-.\s]|(.).*\1/.test(v)) ALPHABET = v;
|
|
738
|
+
else throw Error(bignumberError + p + " invalid: " + v);
|
|
739
|
+
}
|
|
740
|
+
} else throw Error(bignumberError + "Object expected: " + obj);
|
|
741
|
+
return {
|
|
742
|
+
DECIMAL_PLACES,
|
|
743
|
+
ROUNDING_MODE,
|
|
744
|
+
EXPONENTIAL_AT: [TO_EXP_NEG, TO_EXP_POS],
|
|
745
|
+
RANGE: [MIN_EXP, MAX_EXP],
|
|
746
|
+
CRYPTO,
|
|
747
|
+
STRICT,
|
|
748
|
+
MODULO_MODE,
|
|
749
|
+
POW_PRECISION,
|
|
750
|
+
FORMAT,
|
|
751
|
+
ALPHABET
|
|
752
|
+
};
|
|
753
|
+
};
|
|
754
|
+
BigNumber.fromFormat = function(str, options) {
|
|
755
|
+
if (typeof str !== "string") throw Error(bignumberError + "Not a string: " + str);
|
|
756
|
+
if (options == null) options = FORMAT;
|
|
757
|
+
else if (typeof options != "object") throw Error(bignumberError + "Argument not an object: " + options);
|
|
758
|
+
else options = resolveFormatOptions(options);
|
|
759
|
+
var i, isNeg, integerPart, fractionPart, negativeSign = options.negativeSign || "-", positiveSign = options.positiveSign || "", prefix = options.prefix || "", suffix = options.suffix || "", groupSeparator = options.groupSeparator || "", decimalSeparator = options.decimalSeparator || ".", fractionGroupSeparator = options.fractionGroupSeparator || "";
|
|
760
|
+
if (prefix && str.indexOf(prefix) === 0) str = str.slice(prefix.length);
|
|
761
|
+
if (suffix && str.lastIndexOf(suffix) === str.length - suffix.length) str = str.slice(0, -suffix.length);
|
|
762
|
+
if (negativeSign && str.indexOf(negativeSign) === 0) {
|
|
763
|
+
str = str.slice(negativeSign.length);
|
|
764
|
+
isNeg = true;
|
|
765
|
+
} else if (positiveSign && str.indexOf(positiveSign) === 0) str = str.slice(positiveSign.length);
|
|
766
|
+
i = str.indexOf(decimalSeparator);
|
|
767
|
+
if (i < 0) {
|
|
768
|
+
if (groupSeparator) while (str.indexOf(groupSeparator) > -1) str = str.replace(groupSeparator, "");
|
|
769
|
+
} else {
|
|
770
|
+
integerPart = str.slice(0, i);
|
|
771
|
+
fractionPart = str.slice(i + decimalSeparator.length);
|
|
772
|
+
if (groupSeparator) while (integerPart.indexOf(groupSeparator) > -1) integerPart = integerPart.replace(groupSeparator, "");
|
|
773
|
+
if (fractionGroupSeparator) while (fractionPart.indexOf(fractionGroupSeparator) > -1) fractionPart = fractionPart.replace(fractionGroupSeparator, "");
|
|
774
|
+
str = integerPart + "." + fractionPart;
|
|
775
|
+
}
|
|
776
|
+
return new BigNumber(isNeg ? "-" + str : str);
|
|
777
|
+
};
|
|
778
|
+
BigNumber.isBigNumber = function(v) {
|
|
779
|
+
if (!isBigNumber(v)) return false;
|
|
780
|
+
var i, n, c = v.c, e = v.e, s = v.s;
|
|
781
|
+
if (!isArray(c)) return c === null && e === null && (s === null || s === 1 || s === -1);
|
|
782
|
+
if (s !== 1 && s !== -1 || e < -MAX || e > MAX || e !== mathfloor(e)) return false;
|
|
783
|
+
if (c[0] === 0) return e === 0 && c.length === 1;
|
|
784
|
+
i = (e + 1) % LOG_BASE;
|
|
785
|
+
if (i < 1) i += LOG_BASE;
|
|
786
|
+
if (String(c[0]).length !== i) return false;
|
|
787
|
+
for (i = 0; i < c.length; i++) {
|
|
788
|
+
n = c[i];
|
|
789
|
+
if (n < 0 || n >= BASE || n !== mathfloor(n)) return false;
|
|
790
|
+
}
|
|
791
|
+
return n !== 0;
|
|
792
|
+
};
|
|
793
|
+
BigNumber.maximum = BigNumber.max = function() {
|
|
794
|
+
return maxOrMin(arguments, -1);
|
|
795
|
+
};
|
|
796
|
+
BigNumber.minimum = BigNumber.min = function() {
|
|
797
|
+
return maxOrMin(arguments, 1);
|
|
798
|
+
};
|
|
799
|
+
BigNumber.random = (function() {
|
|
800
|
+
var pow2_53 = 9007199254740992;
|
|
801
|
+
var random53bitInt = Math.random() * pow2_53 & 2097151 ? function() {
|
|
802
|
+
return mathfloor(Math.random() * pow2_53);
|
|
803
|
+
} : function() {
|
|
804
|
+
return (Math.random() * 1073741824 | 0) * 8388608 + (Math.random() * 8388608 | 0);
|
|
805
|
+
};
|
|
806
|
+
return function(dp) {
|
|
807
|
+
var a, b, e, k, v, i = 0, c = [], rand = new BigNumber(ONE);
|
|
808
|
+
dp = dp == null ? DECIMAL_PLACES : intCheck(dp, 0, MAX);
|
|
809
|
+
k = mathceil(dp / LOG_BASE);
|
|
810
|
+
if (CRYPTO) if (crypto.getRandomValues) {
|
|
811
|
+
a = crypto.getRandomValues(new Uint32Array(k *= 2));
|
|
812
|
+
for (; i < k;) {
|
|
813
|
+
v = a[i] * 131072 + (a[i + 1] >>> 11);
|
|
814
|
+
if (v >= 9e15) {
|
|
815
|
+
b = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2));
|
|
816
|
+
a[i] = b[0];
|
|
817
|
+
a[i + 1] = b[1];
|
|
818
|
+
} else {
|
|
819
|
+
c.push(v % 0x5af3107a4000);
|
|
820
|
+
i += 2;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
i = k / 2;
|
|
824
|
+
} else if (crypto.randomBytes) {
|
|
825
|
+
a = crypto.randomBytes(k *= 7);
|
|
826
|
+
for (; i < k;) {
|
|
827
|
+
v = (a[i] & 31) * 281474976710656 + a[i + 1] * 1099511627776 + a[i + 2] * 4294967296 + a[i + 3] * 16777216 + (a[i + 4] << 16) + (a[i + 5] << 8) + a[i + 6];
|
|
828
|
+
if (v >= 9e15) crypto.randomBytes(7).copy(a, i);
|
|
829
|
+
else {
|
|
830
|
+
c.push(v % 0x5af3107a4000);
|
|
831
|
+
i += 7;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
i = k / 7;
|
|
835
|
+
} else {
|
|
836
|
+
CRYPTO = false;
|
|
837
|
+
throw Error(bignumberError + "crypto unavailable");
|
|
838
|
+
}
|
|
839
|
+
if (!CRYPTO) for (; i < k;) {
|
|
840
|
+
v = random53bitInt();
|
|
841
|
+
if (v < 9e15) c[i++] = v % 0x5af3107a4000;
|
|
842
|
+
}
|
|
843
|
+
k = c[--i];
|
|
844
|
+
dp %= LOG_BASE;
|
|
845
|
+
if (k && dp) {
|
|
846
|
+
v = POWS_TEN[LOG_BASE - dp];
|
|
847
|
+
c[i] = mathfloor(k / v) * v;
|
|
848
|
+
}
|
|
849
|
+
for (; c[i] === 0; c.pop(), i--);
|
|
850
|
+
if (i < 0) c = [e = 0];
|
|
851
|
+
else {
|
|
852
|
+
for (e = -1; c[0] === 0; c.splice(0, 1), e -= LOG_BASE);
|
|
853
|
+
for (i = 1, v = c[0]; v >= 10; v /= 10, i++);
|
|
854
|
+
if (i < LOG_BASE) e -= LOG_BASE - i;
|
|
855
|
+
}
|
|
856
|
+
rand.e = e;
|
|
857
|
+
rand.c = c;
|
|
858
|
+
return rand;
|
|
859
|
+
};
|
|
860
|
+
})();
|
|
861
|
+
BigNumber.sum = function() {
|
|
862
|
+
var i = 0, sum = new BigNumber(0);
|
|
863
|
+
for (; i < arguments.length;) sum = sum.plus(arguments[i++]);
|
|
864
|
+
return sum;
|
|
865
|
+
};
|
|
866
|
+
function parseValidString(x, str) {
|
|
867
|
+
var e, i, len;
|
|
868
|
+
if ((e = str.indexOf(".")) > -1) str = str.replace(".", "");
|
|
869
|
+
if ((i = str.search(/e/i)) > 0) {
|
|
870
|
+
if (e < 0) e = i;
|
|
871
|
+
e += +str.slice(i + 1);
|
|
872
|
+
str = str.substring(0, i);
|
|
873
|
+
} else if (e < 0) e = str.length;
|
|
874
|
+
for (i = 0; str.charCodeAt(i) === 48; i++);
|
|
875
|
+
for (len = str.length; str.charCodeAt(--len) === 48;);
|
|
876
|
+
if (str = str.slice(i, ++len)) {
|
|
877
|
+
len -= i;
|
|
878
|
+
e = e - i - 1;
|
|
879
|
+
if (e > MAX_EXP) x.c = x.e = null;
|
|
880
|
+
else if (e < MIN_EXP) x.c = [x.e = 0];
|
|
881
|
+
else {
|
|
882
|
+
x.e = e;
|
|
883
|
+
x.c = [];
|
|
884
|
+
i = (e + 1) % LOG_BASE;
|
|
885
|
+
if (e < 0) i += LOG_BASE;
|
|
886
|
+
if (i < len) {
|
|
887
|
+
if (i) x.c.push(+str.slice(0, i));
|
|
888
|
+
for (len -= LOG_BASE; i < len;) x.c.push(+str.slice(i, i += LOG_BASE));
|
|
889
|
+
i = LOG_BASE - (str = str.slice(i)).length;
|
|
890
|
+
} else i -= len;
|
|
891
|
+
for (; i--; str += "0");
|
|
892
|
+
x.c.push(+str);
|
|
893
|
+
}
|
|
894
|
+
} else x.c = [x.e = 0];
|
|
895
|
+
}
|
|
896
|
+
function parseBaseString(x, str, b, v) {
|
|
897
|
+
var c, len, alphabet = ALPHABET.slice(0, b), i = 0, clean = "", hasDot = false, prevIsNumeral = false, caseChanged = false;
|
|
898
|
+
x.s = str.charCodeAt(0) === 45 ? (str = str.slice(1), -1) : 1;
|
|
899
|
+
for (len = str.length; i < len; i++) {
|
|
900
|
+
c = str.charAt(i);
|
|
901
|
+
if (alphabet.indexOf(c) >= 0) {
|
|
902
|
+
clean += c;
|
|
903
|
+
prevIsNumeral = true;
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (c == "_") {
|
|
907
|
+
if (prevIsNumeral && i + 1 < len) {
|
|
908
|
+
prevIsNumeral = false;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
} else if (c == ".") {
|
|
912
|
+
if (i == 0 || !hasDot && prevIsNumeral) {
|
|
913
|
+
if (i + 1 == len) break;
|
|
914
|
+
if (i == 0) clean = "0";
|
|
915
|
+
clean += c;
|
|
916
|
+
hasDot = true;
|
|
917
|
+
prevIsNumeral = false;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
} else if (!caseChanged) {
|
|
921
|
+
if (str == str.toUpperCase() && alphabet == alphabet.toLowerCase() && (str = str.toLowerCase()) || str == str.toLowerCase() && alphabet == alphabet.toUpperCase() && (str = str.toUpperCase())) {
|
|
922
|
+
i = -1;
|
|
923
|
+
clean = "";
|
|
924
|
+
caseChanged = true;
|
|
925
|
+
hasDot = prevIsNumeral = false;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (STRICT) throw Error(bignumberError + "Not a base " + b + " number: " + v);
|
|
930
|
+
x.s = x.c = x.e = null;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
parseValidString(x, convertBase(clean, b, 10, x.s));
|
|
934
|
+
}
|
|
935
|
+
convertBase = (function() {
|
|
936
|
+
var decimal = "0123456789";
|
|
937
|
+
function toBaseOut(str, baseIn, baseOut, alphabet) {
|
|
938
|
+
var j, arr = [0], arrL, i = 0, len = str.length;
|
|
939
|
+
for (; i < len;) {
|
|
940
|
+
for (arrL = arr.length; arrL--; arr[arrL] *= baseIn);
|
|
941
|
+
arr[0] += alphabet.indexOf(str.charAt(i++));
|
|
942
|
+
for (j = 0; j < arr.length; j++) if (arr[j] > baseOut - 1) {
|
|
943
|
+
if (arr[j + 1] == null) arr[j + 1] = 0;
|
|
944
|
+
arr[j + 1] += arr[j] / baseOut | 0;
|
|
945
|
+
arr[j] %= baseOut;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return arr.reverse();
|
|
949
|
+
}
|
|
950
|
+
return function(str, baseIn, baseOut, sign, callerIsToString) {
|
|
951
|
+
var alphabet, d, e, k, r, x, xc, y, i = str.indexOf("."), dp = DECIMAL_PLACES, rm = ROUNDING_MODE;
|
|
952
|
+
if (i >= 0) {
|
|
953
|
+
k = POW_PRECISION;
|
|
954
|
+
POW_PRECISION = 0;
|
|
955
|
+
str = str.replace(".", "");
|
|
956
|
+
y = new BigNumber(baseIn);
|
|
957
|
+
x = y.pow(str.length - i);
|
|
958
|
+
POW_PRECISION = k;
|
|
959
|
+
y.c = toBaseOut(toFixedPoint(coeffToString(x.c), x.e, "0"), 10, baseOut, decimal);
|
|
960
|
+
y.e = y.c.length;
|
|
961
|
+
}
|
|
962
|
+
xc = toBaseOut(str, baseIn, baseOut, callerIsToString ? (alphabet = ALPHABET, decimal) : (alphabet = decimal, ALPHABET));
|
|
963
|
+
e = k = xc.length;
|
|
964
|
+
for (; xc[--k] == 0; xc.pop());
|
|
965
|
+
if (!xc[0]) return alphabet.charAt(0);
|
|
966
|
+
if (i < 0) --e;
|
|
967
|
+
else {
|
|
968
|
+
x.c = xc;
|
|
969
|
+
x.e = e;
|
|
970
|
+
x.s = sign;
|
|
971
|
+
x = div(x, y, dp, rm, baseOut);
|
|
972
|
+
xc = x.c;
|
|
973
|
+
r = x.r;
|
|
974
|
+
e = x.e;
|
|
975
|
+
}
|
|
976
|
+
d = e + dp + 1;
|
|
977
|
+
i = xc[d];
|
|
978
|
+
k = baseOut / 2;
|
|
979
|
+
r = r || d < 0 || xc[d + 1] != null;
|
|
980
|
+
r = rm < 4 ? (i != null || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) : i > k || i == k && (rm == 4 || r || rm == 6 && xc[d - 1] & 1 || rm == (x.s < 0 ? 8 : 7));
|
|
981
|
+
if (d < 1 || !xc[0]) str = r ? toFixedPoint(alphabet.charAt(1), -dp, alphabet.charAt(0)) : alphabet.charAt(0);
|
|
982
|
+
else {
|
|
983
|
+
if (d < xc.length) xc.length = d;
|
|
984
|
+
if (r) for (--baseOut; ++xc[--d] > baseOut;) {
|
|
985
|
+
xc[d] = 0;
|
|
986
|
+
if (!d) {
|
|
987
|
+
++e;
|
|
988
|
+
xc = [1].concat(xc);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
for (k = xc.length; !xc[--k];);
|
|
992
|
+
for (i = 0, str = ""; i <= k; str += alphabet.charAt(xc[i++]));
|
|
993
|
+
str = toFixedPoint(str, e, alphabet.charAt(0));
|
|
994
|
+
}
|
|
995
|
+
return str;
|
|
996
|
+
};
|
|
997
|
+
})();
|
|
998
|
+
div = (function() {
|
|
999
|
+
function multiply(x, k, base) {
|
|
1000
|
+
var m, temp, xlo, xhi, carry = 0, i = x.length, klo = k % SQRT_BASE, khi = k / SQRT_BASE | 0;
|
|
1001
|
+
for (x = x.slice(); i--;) {
|
|
1002
|
+
xlo = x[i] % SQRT_BASE;
|
|
1003
|
+
xhi = x[i] / SQRT_BASE | 0;
|
|
1004
|
+
m = khi * xlo + xhi * klo;
|
|
1005
|
+
temp = klo * xlo + m % SQRT_BASE * SQRT_BASE + carry;
|
|
1006
|
+
carry = (temp / base | 0) + (m / SQRT_BASE | 0) + khi * xhi;
|
|
1007
|
+
x[i] = temp % base;
|
|
1008
|
+
}
|
|
1009
|
+
if (carry) x = [carry].concat(x);
|
|
1010
|
+
return x;
|
|
1011
|
+
}
|
|
1012
|
+
function compare(a, b, aL, bL) {
|
|
1013
|
+
var i, cmp;
|
|
1014
|
+
if (aL != bL) cmp = aL > bL ? 1 : -1;
|
|
1015
|
+
else for (i = cmp = 0; i < aL; i++) if (a[i] != b[i]) {
|
|
1016
|
+
cmp = a[i] > b[i] ? 1 : -1;
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
return cmp;
|
|
1020
|
+
}
|
|
1021
|
+
function subtract(a, b, aL, base) {
|
|
1022
|
+
var i = 0;
|
|
1023
|
+
for (; aL--;) {
|
|
1024
|
+
a[aL] -= i;
|
|
1025
|
+
i = a[aL] < b[aL] ? 1 : 0;
|
|
1026
|
+
a[aL] = i * base + a[aL] - b[aL];
|
|
1027
|
+
}
|
|
1028
|
+
for (; !a[0] && a.length > 1; a.splice(0, 1));
|
|
1029
|
+
}
|
|
1030
|
+
return function(x, y, dp, rm, base) {
|
|
1031
|
+
var cmp, e, i, more, n, prod, prodL, q, qc, rem, remL, rem0, xi, xL, yc0, yL, yz, s = x.s == y.s ? 1 : -1, xc = x.c, yc = y.c;
|
|
1032
|
+
if (!xc || !xc[0] || !yc || !yc[0]) return new BigNumber(!x.s || !y.s || (xc ? yc && xc[0] == yc[0] : !yc) ? NaN : xc && xc[0] == 0 || !yc ? s * 0 : s / 0);
|
|
1033
|
+
q = new BigNumber(s);
|
|
1034
|
+
qc = q.c = [];
|
|
1035
|
+
e = x.e - y.e;
|
|
1036
|
+
s = dp + e + 1;
|
|
1037
|
+
if (!base) {
|
|
1038
|
+
base = BASE;
|
|
1039
|
+
e = bitFloor(x.e / LOG_BASE) - bitFloor(y.e / LOG_BASE);
|
|
1040
|
+
s = s / LOG_BASE | 0;
|
|
1041
|
+
}
|
|
1042
|
+
for (i = 0; yc[i] == (xc[i] || 0); i++);
|
|
1043
|
+
if (yc[i] > (xc[i] || 0)) e--;
|
|
1044
|
+
if (s < 0) {
|
|
1045
|
+
qc.push(1);
|
|
1046
|
+
more = true;
|
|
1047
|
+
} else {
|
|
1048
|
+
xL = xc.length;
|
|
1049
|
+
yL = yc.length;
|
|
1050
|
+
i = 0;
|
|
1051
|
+
s += 2;
|
|
1052
|
+
n = mathfloor(base / (yc[0] + 1));
|
|
1053
|
+
if (n > 1) {
|
|
1054
|
+
yc = multiply(yc, n, base);
|
|
1055
|
+
xc = multiply(xc, n, base);
|
|
1056
|
+
yL = yc.length;
|
|
1057
|
+
xL = xc.length;
|
|
1058
|
+
}
|
|
1059
|
+
xi = yL;
|
|
1060
|
+
rem = xc.slice(0, yL);
|
|
1061
|
+
remL = rem.length;
|
|
1062
|
+
for (; remL < yL; rem[remL++] = 0);
|
|
1063
|
+
yz = yc.slice();
|
|
1064
|
+
yz = [0].concat(yz);
|
|
1065
|
+
yc0 = yc[0];
|
|
1066
|
+
if (yc[1] >= base / 2) yc0++;
|
|
1067
|
+
do {
|
|
1068
|
+
n = 0;
|
|
1069
|
+
cmp = compare(yc, rem, yL, remL);
|
|
1070
|
+
if (cmp < 0) {
|
|
1071
|
+
rem0 = rem[0];
|
|
1072
|
+
if (yL != remL) rem0 = rem0 * base + (rem[1] || 0);
|
|
1073
|
+
n = mathfloor(rem0 / yc0);
|
|
1074
|
+
if (n > 1) {
|
|
1075
|
+
if (n >= base) n = base - 1;
|
|
1076
|
+
prod = multiply(yc, n, base);
|
|
1077
|
+
prodL = prod.length;
|
|
1078
|
+
remL = rem.length;
|
|
1079
|
+
while (compare(prod, rem, prodL, remL) == 1) {
|
|
1080
|
+
n--;
|
|
1081
|
+
subtract(prod, yL < prodL ? yz : yc, prodL, base);
|
|
1082
|
+
prodL = prod.length;
|
|
1083
|
+
cmp = 1;
|
|
1084
|
+
}
|
|
1085
|
+
} else {
|
|
1086
|
+
if (n == 0) cmp = n = 1;
|
|
1087
|
+
prod = yc.slice();
|
|
1088
|
+
prodL = prod.length;
|
|
1089
|
+
}
|
|
1090
|
+
if (prodL < remL) prod = [0].concat(prod);
|
|
1091
|
+
subtract(rem, prod, remL, base);
|
|
1092
|
+
remL = rem.length;
|
|
1093
|
+
if (cmp == -1) while (compare(yc, rem, yL, remL) < 1) {
|
|
1094
|
+
n++;
|
|
1095
|
+
subtract(rem, yL < remL ? yz : yc, remL, base);
|
|
1096
|
+
remL = rem.length;
|
|
1097
|
+
}
|
|
1098
|
+
} else if (cmp === 0) {
|
|
1099
|
+
n++;
|
|
1100
|
+
rem = [0];
|
|
1101
|
+
}
|
|
1102
|
+
qc[i++] = n;
|
|
1103
|
+
if (rem[0]) rem[remL++] = xc[xi] || 0;
|
|
1104
|
+
else {
|
|
1105
|
+
rem = [xc[xi]];
|
|
1106
|
+
remL = 1;
|
|
1107
|
+
}
|
|
1108
|
+
} while ((xi++ < xL || rem[0] != null) && s--);
|
|
1109
|
+
more = rem[0] != null;
|
|
1110
|
+
if (!qc[0]) qc.splice(0, 1);
|
|
1111
|
+
}
|
|
1112
|
+
if (base == BASE) {
|
|
1113
|
+
for (i = 1, s = qc[0]; s >= 10; s /= 10, i++);
|
|
1114
|
+
round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm, more);
|
|
1115
|
+
} else {
|
|
1116
|
+
q.e = e;
|
|
1117
|
+
q.r = +more;
|
|
1118
|
+
}
|
|
1119
|
+
return q;
|
|
1120
|
+
};
|
|
1121
|
+
})();
|
|
1122
|
+
function format(n, i, rm, id) {
|
|
1123
|
+
var c0, e, ne, len, str;
|
|
1124
|
+
rm = rm == null ? ROUNDING_MODE : intCheck(rm, 0, 8);
|
|
1125
|
+
if (!n.c) return n.toString();
|
|
1126
|
+
c0 = n.c[0];
|
|
1127
|
+
ne = n.e;
|
|
1128
|
+
if (i == null) {
|
|
1129
|
+
str = coeffToString(n.c);
|
|
1130
|
+
str = id == 1 || id == 2 && (ne <= TO_EXP_NEG || ne >= TO_EXP_POS) ? toExponential(str, ne) : toFixedPoint(str, ne, "0");
|
|
1131
|
+
} else {
|
|
1132
|
+
n = round(new BigNumber(n), i, rm);
|
|
1133
|
+
e = n.e;
|
|
1134
|
+
str = coeffToString(n.c);
|
|
1135
|
+
len = str.length;
|
|
1136
|
+
if (id == 1 || id == 2 && (i <= e || e <= TO_EXP_NEG)) {
|
|
1137
|
+
for (; len < i; str += "0", len++);
|
|
1138
|
+
str = toExponential(str, e);
|
|
1139
|
+
} else {
|
|
1140
|
+
i -= ne + (id === 2 && e > ne);
|
|
1141
|
+
str = toFixedPoint(str, e, "0");
|
|
1142
|
+
if (e + 1 > len) {
|
|
1143
|
+
if (--i > 0) for (str += "."; i--; str += "0");
|
|
1144
|
+
} else {
|
|
1145
|
+
i += e - len;
|
|
1146
|
+
if (i > 0) {
|
|
1147
|
+
if (e + 1 == len) str += ".";
|
|
1148
|
+
for (; i--; str += "0");
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return n.s < 0 && c0 ? "-" + str : str;
|
|
1154
|
+
}
|
|
1155
|
+
function isBigNumber(v) {
|
|
1156
|
+
return v instanceof BigNumber || !!v && v._isBigNumber === true;
|
|
1157
|
+
}
|
|
1158
|
+
function maxOrMin(args, n) {
|
|
1159
|
+
var k, y, i = 1, x = new BigNumber(args[0]);
|
|
1160
|
+
for (; i < args.length; i++) {
|
|
1161
|
+
y = new BigNumber(args[i]);
|
|
1162
|
+
if (!y.s || (k = compare(x, y)) === n || k === 0 && x.s === n) x = y;
|
|
1163
|
+
}
|
|
1164
|
+
return x;
|
|
1165
|
+
}
|
|
1166
|
+
function normalise(n, c, e) {
|
|
1167
|
+
var i = 1, j = c.length;
|
|
1168
|
+
for (; !c[--j]; c.pop());
|
|
1169
|
+
for (j = c[0]; j >= 10; j /= 10, i++);
|
|
1170
|
+
if ((e = i + e * LOG_BASE - 1) > MAX_EXP) n.c = n.e = null;
|
|
1171
|
+
else if (e < MIN_EXP) n.c = [n.e = 0];
|
|
1172
|
+
else {
|
|
1173
|
+
n.e = e;
|
|
1174
|
+
n.c = c;
|
|
1175
|
+
}
|
|
1176
|
+
return n;
|
|
1177
|
+
}
|
|
1178
|
+
function resolveFormatOptions(options) {
|
|
1179
|
+
var key, resolved = {};
|
|
1180
|
+
for (key in FORMAT) if (FORMAT.hasOwnProperty(key)) resolved[key] = options.hasOwnProperty(key) ? options[key] : FORMAT[key];
|
|
1181
|
+
return resolved;
|
|
1182
|
+
}
|
|
1183
|
+
function round(x, sd, rm, r) {
|
|
1184
|
+
var d, i, j, k, n, ni, rd, xc = x.c, pows10 = POWS_TEN;
|
|
1185
|
+
if (xc) {
|
|
1186
|
+
out: {
|
|
1187
|
+
for (d = 1, k = xc[0]; k >= 10; k /= 10, d++);
|
|
1188
|
+
i = sd - d;
|
|
1189
|
+
if (i < 0) {
|
|
1190
|
+
i += LOG_BASE;
|
|
1191
|
+
j = sd;
|
|
1192
|
+
n = xc[ni = 0];
|
|
1193
|
+
rd = mathfloor(n / pows10[d - j - 1] % 10);
|
|
1194
|
+
} else {
|
|
1195
|
+
ni = mathceil((i + 1) / LOG_BASE);
|
|
1196
|
+
if (ni >= xc.length) if (r) {
|
|
1197
|
+
for (; xc.length <= ni; xc.push(0));
|
|
1198
|
+
n = rd = 0;
|
|
1199
|
+
d = 1;
|
|
1200
|
+
i %= LOG_BASE;
|
|
1201
|
+
j = i - LOG_BASE + 1;
|
|
1202
|
+
} else break out;
|
|
1203
|
+
else {
|
|
1204
|
+
n = k = xc[ni];
|
|
1205
|
+
for (d = 1; k >= 10; k /= 10, d++);
|
|
1206
|
+
i %= LOG_BASE;
|
|
1207
|
+
j = i - LOG_BASE + d;
|
|
1208
|
+
rd = j < 0 ? 0 : mathfloor(n / pows10[d - j - 1] % 10);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
r = r || sd < 0 || xc[ni + 1] != null || (j < 0 ? n : n % pows10[d - j - 1]);
|
|
1212
|
+
r = rm < 4 ? (rd || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) : rd > 5 || rd == 5 && (rm == 4 || r || rm == 6 && (i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10 & 1 || rm == (x.s < 0 ? 8 : 7));
|
|
1213
|
+
if (sd < 1 || !xc[0]) {
|
|
1214
|
+
xc.length = 0;
|
|
1215
|
+
if (r) {
|
|
1216
|
+
sd -= x.e + 1;
|
|
1217
|
+
xc[0] = pows10[(LOG_BASE - sd % LOG_BASE) % LOG_BASE];
|
|
1218
|
+
x.e = -sd || 0;
|
|
1219
|
+
} else xc[0] = x.e = 0;
|
|
1220
|
+
return x;
|
|
1221
|
+
}
|
|
1222
|
+
if (i == 0) {
|
|
1223
|
+
xc.length = ni;
|
|
1224
|
+
k = 1;
|
|
1225
|
+
ni--;
|
|
1226
|
+
} else {
|
|
1227
|
+
xc.length = ni + 1;
|
|
1228
|
+
k = pows10[LOG_BASE - i];
|
|
1229
|
+
xc[ni] = j > 0 ? mathfloor(n / pows10[d - j] % pows10[j]) * k : 0;
|
|
1230
|
+
}
|
|
1231
|
+
if (r) for (;;) if (ni == 0) {
|
|
1232
|
+
for (i = 1, j = xc[0]; j >= 10; j /= 10, i++);
|
|
1233
|
+
j = xc[0] += k;
|
|
1234
|
+
for (k = 1; j >= 10; j /= 10, k++);
|
|
1235
|
+
if (i != k) {
|
|
1236
|
+
x.e++;
|
|
1237
|
+
if (xc[0] == BASE) xc[0] = 1;
|
|
1238
|
+
}
|
|
1239
|
+
break;
|
|
1240
|
+
} else {
|
|
1241
|
+
xc[ni] += k;
|
|
1242
|
+
if (xc[ni] != BASE) break;
|
|
1243
|
+
xc[ni--] = 0;
|
|
1244
|
+
k = 1;
|
|
1245
|
+
}
|
|
1246
|
+
for (i = xc.length; xc[--i] === 0; xc.pop());
|
|
1247
|
+
}
|
|
1248
|
+
if (x.e > MAX_EXP) x.c = x.e = null;
|
|
1249
|
+
else if (x.e < MIN_EXP) x.c = [x.e = 0];
|
|
1250
|
+
}
|
|
1251
|
+
return x;
|
|
1252
|
+
}
|
|
1253
|
+
function valueOf(n) {
|
|
1254
|
+
var str, e = n.e;
|
|
1255
|
+
if (e === null) return n.toString();
|
|
1256
|
+
str = coeffToString(n.c);
|
|
1257
|
+
str = e <= TO_EXP_NEG || e >= TO_EXP_POS ? toExponential(str, e) : toFixedPoint(str, e, "0");
|
|
1258
|
+
return n.s < 0 ? "-" + str : str;
|
|
1259
|
+
}
|
|
1260
|
+
P.absoluteValue = P.abs = function() {
|
|
1261
|
+
var x = new BigNumber(this);
|
|
1262
|
+
if (x.s < 0) x.s = 1;
|
|
1263
|
+
return x;
|
|
1264
|
+
};
|
|
1265
|
+
P.comparedTo = function(y, b) {
|
|
1266
|
+
return compare(this, new BigNumber(y, b));
|
|
1267
|
+
};
|
|
1268
|
+
P.decimalPlaces = P.dp = function(dp, rm) {
|
|
1269
|
+
var c, n, v, x = this;
|
|
1270
|
+
if (dp != null) return round(new BigNumber(x), intCheck(dp, -MAX, MAX) + x.e + 1, rm == null ? ROUNDING_MODE : intCheck(rm, 0, 8));
|
|
1271
|
+
if (!(c = x.c)) return null;
|
|
1272
|
+
n = ((v = c.length - 1) - bitFloor(this.e / LOG_BASE)) * LOG_BASE;
|
|
1273
|
+
if (v = c[v]) for (; v % 10 == 0; v /= 10, n--);
|
|
1274
|
+
if (n < 0) n = 0;
|
|
1275
|
+
return n;
|
|
1276
|
+
};
|
|
1277
|
+
P.dividedBy = P.div = function(y, b) {
|
|
1278
|
+
return div(this, new BigNumber(y, b), DECIMAL_PLACES, ROUNDING_MODE);
|
|
1279
|
+
};
|
|
1280
|
+
P.dividedToIntegerBy = P.idiv = function(y, b) {
|
|
1281
|
+
return div(this, new BigNumber(y, b), 0, 1);
|
|
1282
|
+
};
|
|
1283
|
+
P.exponentiatedBy = P.pow = function(n, m) {
|
|
1284
|
+
var half, isModExp, i, k, more, nIsBig, nIsNeg, nIsOdd, y, x = this;
|
|
1285
|
+
n = new BigNumber(n);
|
|
1286
|
+
if (n.c && !n.isInteger()) throw Error(bignumberError + "Exponent not an integer: " + valueOf(n));
|
|
1287
|
+
if (m != null) m = new BigNumber(m);
|
|
1288
|
+
nIsBig = n.e > 14;
|
|
1289
|
+
if (!x.c || !x.c[0] || x.c[0] == 1 && !x.e && x.c.length == 1 || !n.c || !n.c[0]) {
|
|
1290
|
+
y = new BigNumber(Math.pow(+valueOf(x), nIsBig ? n.s * (2 - isOdd(n)) : +valueOf(n)));
|
|
1291
|
+
return m ? y.mod(m) : y;
|
|
1292
|
+
}
|
|
1293
|
+
nIsNeg = n.s < 0;
|
|
1294
|
+
if (m) {
|
|
1295
|
+
if (m.c ? !m.c[0] : !m.s) return new BigNumber(NaN);
|
|
1296
|
+
isModExp = !nIsNeg && x.isInteger() && m.isInteger();
|
|
1297
|
+
if (isModExp) x = x.mod(m);
|
|
1298
|
+
} else if (n.e > 9 && (x.e > 0 || x.e < -1 || (x.e == 0 ? x.c[0] > 1 || nIsBig && x.c[1] >= 24e7 : x.c[0] < 8e13 || nIsBig && x.c[0] <= 9999975e7))) {
|
|
1299
|
+
k = x.s < 0 && isOdd(n) ? -0 : 0;
|
|
1300
|
+
if (x.e > -1) k = 1 / k;
|
|
1301
|
+
return new BigNumber(nIsNeg ? 1 / k : k);
|
|
1302
|
+
} else if (POW_PRECISION) k = mathceil(POW_PRECISION / LOG_BASE + 2);
|
|
1303
|
+
if (nIsBig) {
|
|
1304
|
+
half = new BigNumber(.5);
|
|
1305
|
+
if (nIsNeg) n.s = 1;
|
|
1306
|
+
nIsOdd = isOdd(n);
|
|
1307
|
+
} else {
|
|
1308
|
+
i = Math.abs(+valueOf(n));
|
|
1309
|
+
nIsOdd = i % 2;
|
|
1310
|
+
}
|
|
1311
|
+
y = new BigNumber(ONE);
|
|
1312
|
+
for (;;) {
|
|
1313
|
+
if (nIsOdd) {
|
|
1314
|
+
y = y.times(x);
|
|
1315
|
+
if (!y.c) break;
|
|
1316
|
+
if (k) {
|
|
1317
|
+
if (y.c.length > k) y.c.length = k;
|
|
1318
|
+
} else if (isModExp) y = y.mod(m);
|
|
1319
|
+
}
|
|
1320
|
+
if (i) {
|
|
1321
|
+
i = mathfloor(i / 2);
|
|
1322
|
+
if (i === 0) break;
|
|
1323
|
+
nIsOdd = i % 2;
|
|
1324
|
+
} else {
|
|
1325
|
+
n = n.times(half);
|
|
1326
|
+
round(n, n.e + 1, 1);
|
|
1327
|
+
if (n.e > 14) nIsOdd = isOdd(n);
|
|
1328
|
+
else {
|
|
1329
|
+
i = +valueOf(n);
|
|
1330
|
+
if (i === 0) break;
|
|
1331
|
+
nIsOdd = i % 2;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
x = x.times(x);
|
|
1335
|
+
if (k) {
|
|
1336
|
+
if (x.c && x.c.length > k) x.c.length = k;
|
|
1337
|
+
} else if (isModExp) x = x.mod(m);
|
|
1338
|
+
}
|
|
1339
|
+
if (isModExp) return y;
|
|
1340
|
+
if (nIsNeg) y = ONE.div(y);
|
|
1341
|
+
return m ? y.mod(m) : k ? round(y, POW_PRECISION, ROUNDING_MODE, more) : y;
|
|
1342
|
+
};
|
|
1343
|
+
P.integerValue = function(rm) {
|
|
1344
|
+
var n = new BigNumber(this);
|
|
1345
|
+
return round(n, n.e + 1, rm == null ? ROUNDING_MODE : intCheck(rm, 0, 8));
|
|
1346
|
+
};
|
|
1347
|
+
P.isEqualTo = P.eq = function(y, b) {
|
|
1348
|
+
return compare(this, new BigNumber(y, b)) === 0;
|
|
1349
|
+
};
|
|
1350
|
+
P.isFinite = function() {
|
|
1351
|
+
return !!this.c;
|
|
1352
|
+
};
|
|
1353
|
+
P.isGreaterThan = P.gt = function(y, b) {
|
|
1354
|
+
return compare(this, new BigNumber(y, b)) > 0;
|
|
1355
|
+
};
|
|
1356
|
+
P.isGreaterThanOrEqualTo = P.gte = function(y, b) {
|
|
1357
|
+
return (b = compare(this, new BigNumber(y, b))) === 1 || b === 0;
|
|
1358
|
+
};
|
|
1359
|
+
P.isInteger = function() {
|
|
1360
|
+
return !!this.c && bitFloor(this.e / LOG_BASE) > this.c.length - 2;
|
|
1361
|
+
};
|
|
1362
|
+
P.isLessThan = P.lt = function(y, b) {
|
|
1363
|
+
return compare(this, new BigNumber(y, b)) < 0;
|
|
1364
|
+
};
|
|
1365
|
+
P.isLessThanOrEqualTo = P.lte = function(y, b) {
|
|
1366
|
+
return (b = compare(this, new BigNumber(y, b))) === -1 || b === 0;
|
|
1367
|
+
};
|
|
1368
|
+
P.isNaN = function() {
|
|
1369
|
+
return !this.s;
|
|
1370
|
+
};
|
|
1371
|
+
P.isNegative = function() {
|
|
1372
|
+
return this.s < 0;
|
|
1373
|
+
};
|
|
1374
|
+
P.isPositive = function() {
|
|
1375
|
+
return this.s > 0;
|
|
1376
|
+
};
|
|
1377
|
+
P.isZero = function() {
|
|
1378
|
+
return !!this.c && this.c[0] == 0;
|
|
1379
|
+
};
|
|
1380
|
+
P.minus = function(y, b) {
|
|
1381
|
+
var i, j, t, xLTy, x = this, a = x.s;
|
|
1382
|
+
y = new BigNumber(y, b);
|
|
1383
|
+
b = y.s;
|
|
1384
|
+
if (!a || !b) return new BigNumber(NaN);
|
|
1385
|
+
if (a != b) {
|
|
1386
|
+
y.s = -b;
|
|
1387
|
+
return x.plus(y);
|
|
1388
|
+
}
|
|
1389
|
+
var xe = x.e / LOG_BASE, ye = y.e / LOG_BASE, xc = x.c, yc = y.c;
|
|
1390
|
+
if (!xe || !ye) {
|
|
1391
|
+
if (!xc || !yc) return xc ? (y.s = -b, y) : new BigNumber(yc ? x : NaN);
|
|
1392
|
+
if (!xc[0] || !yc[0]) return yc[0] ? (y.s = -b, y) : new BigNumber(xc[0] ? x : ROUNDING_MODE == 3 ? -0 : 0);
|
|
1393
|
+
}
|
|
1394
|
+
xe = bitFloor(xe);
|
|
1395
|
+
ye = bitFloor(ye);
|
|
1396
|
+
xc = xc.slice();
|
|
1397
|
+
if (a = xe - ye) {
|
|
1398
|
+
if (xLTy = a < 0) {
|
|
1399
|
+
a = -a;
|
|
1400
|
+
t = xc;
|
|
1401
|
+
} else {
|
|
1402
|
+
ye = xe;
|
|
1403
|
+
t = yc;
|
|
1404
|
+
}
|
|
1405
|
+
t.reverse();
|
|
1406
|
+
for (b = a; b--; t.push(0));
|
|
1407
|
+
t.reverse();
|
|
1408
|
+
} else {
|
|
1409
|
+
j = (xLTy = (a = xc.length) < (b = yc.length)) ? a : b;
|
|
1410
|
+
for (a = b = 0; b < j; b++) if (xc[b] != yc[b]) {
|
|
1411
|
+
xLTy = xc[b] < yc[b];
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (xLTy) {
|
|
1416
|
+
t = xc;
|
|
1417
|
+
xc = yc;
|
|
1418
|
+
yc = t;
|
|
1419
|
+
y.s = -y.s;
|
|
1420
|
+
}
|
|
1421
|
+
b = (j = yc.length) - (i = xc.length);
|
|
1422
|
+
if (b > 0) for (; b--; xc[i++] = 0);
|
|
1423
|
+
b = BASE - 1;
|
|
1424
|
+
for (; j > a;) {
|
|
1425
|
+
if (xc[--j] < yc[j]) {
|
|
1426
|
+
for (i = j; i && !xc[--i]; xc[i] = b);
|
|
1427
|
+
--xc[i];
|
|
1428
|
+
xc[j] += BASE;
|
|
1429
|
+
}
|
|
1430
|
+
xc[j] -= yc[j];
|
|
1431
|
+
}
|
|
1432
|
+
for (; xc[0] == 0; xc.splice(0, 1), --ye);
|
|
1433
|
+
if (!xc[0]) {
|
|
1434
|
+
y.s = ROUNDING_MODE == 3 ? -1 : 1;
|
|
1435
|
+
y.c = [y.e = 0];
|
|
1436
|
+
return y;
|
|
1437
|
+
}
|
|
1438
|
+
return normalise(y, xc, ye);
|
|
1439
|
+
};
|
|
1440
|
+
P.modulo = P.mod = function(y, b) {
|
|
1441
|
+
var q, s, x = this;
|
|
1442
|
+
y = new BigNumber(y, b);
|
|
1443
|
+
if (!x.c || !y.s || y.c && !y.c[0]) return new BigNumber(NaN);
|
|
1444
|
+
else if (!y.c || x.c && !x.c[0]) return new BigNumber(x);
|
|
1445
|
+
if (MODULO_MODE == 9) {
|
|
1446
|
+
s = y.s;
|
|
1447
|
+
y.s = 1;
|
|
1448
|
+
q = div(x, y, 0, 3);
|
|
1449
|
+
y.s = s;
|
|
1450
|
+
q.s *= s;
|
|
1451
|
+
} else q = div(x, y, 0, MODULO_MODE);
|
|
1452
|
+
y = x.minus(q.times(y));
|
|
1453
|
+
if (!y.c[0] && MODULO_MODE == 1) y.s = x.s;
|
|
1454
|
+
return y;
|
|
1455
|
+
};
|
|
1456
|
+
P.multipliedBy = P.times = function(y, b) {
|
|
1457
|
+
var c, e, i, j, k, m, xcL, xlo, xhi, ycL, ylo, yhi, zc, base, sqrtBase, x = this, xc = x.c, yc = (y = new BigNumber(y, b)).c;
|
|
1458
|
+
if (!xc || !yc || !xc[0] || !yc[0]) {
|
|
1459
|
+
if (!x.s || !y.s || xc && !xc[0] && !yc || yc && !yc[0] && !xc) y.c = y.e = y.s = null;
|
|
1460
|
+
else {
|
|
1461
|
+
y.s *= x.s;
|
|
1462
|
+
if (!xc || !yc) y.c = y.e = null;
|
|
1463
|
+
else {
|
|
1464
|
+
y.c = [0];
|
|
1465
|
+
y.e = 0;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return y;
|
|
1469
|
+
}
|
|
1470
|
+
e = bitFloor(x.e / LOG_BASE) + bitFloor(y.e / LOG_BASE);
|
|
1471
|
+
y.s *= x.s;
|
|
1472
|
+
xcL = xc.length;
|
|
1473
|
+
ycL = yc.length;
|
|
1474
|
+
if (xcL < ycL) {
|
|
1475
|
+
zc = xc;
|
|
1476
|
+
xc = yc;
|
|
1477
|
+
yc = zc;
|
|
1478
|
+
i = xcL;
|
|
1479
|
+
xcL = ycL;
|
|
1480
|
+
ycL = i;
|
|
1481
|
+
}
|
|
1482
|
+
for (i = xcL + ycL, zc = []; i--; zc.push(0));
|
|
1483
|
+
base = BASE;
|
|
1484
|
+
sqrtBase = SQRT_BASE;
|
|
1485
|
+
for (i = ycL; --i >= 0;) {
|
|
1486
|
+
c = 0;
|
|
1487
|
+
ylo = yc[i] % sqrtBase;
|
|
1488
|
+
yhi = yc[i] / sqrtBase | 0;
|
|
1489
|
+
for (k = xcL, j = i + k; j > i;) {
|
|
1490
|
+
xlo = xc[--k] % sqrtBase;
|
|
1491
|
+
xhi = xc[k] / sqrtBase | 0;
|
|
1492
|
+
m = yhi * xlo + xhi * ylo;
|
|
1493
|
+
xlo = ylo * xlo + m % sqrtBase * sqrtBase + zc[j] + c;
|
|
1494
|
+
c = (xlo / base | 0) + (m / sqrtBase | 0) + yhi * xhi;
|
|
1495
|
+
zc[j--] = xlo % base;
|
|
1496
|
+
}
|
|
1497
|
+
zc[j] = c;
|
|
1498
|
+
}
|
|
1499
|
+
if (c) ++e;
|
|
1500
|
+
else zc.splice(0, 1);
|
|
1501
|
+
return normalise(y, zc, e);
|
|
1502
|
+
};
|
|
1503
|
+
P.negated = function() {
|
|
1504
|
+
var x = new BigNumber(this);
|
|
1505
|
+
x.s = -x.s || null;
|
|
1506
|
+
return x;
|
|
1507
|
+
};
|
|
1508
|
+
P.plus = function(y, b) {
|
|
1509
|
+
var t, x = this, a = x.s;
|
|
1510
|
+
y = new BigNumber(y, b);
|
|
1511
|
+
b = y.s;
|
|
1512
|
+
if (!a || !b) return new BigNumber(NaN);
|
|
1513
|
+
if (a != b) {
|
|
1514
|
+
y.s = -b;
|
|
1515
|
+
return x.minus(y);
|
|
1516
|
+
}
|
|
1517
|
+
var xe = x.e / LOG_BASE, ye = y.e / LOG_BASE, xc = x.c, yc = y.c;
|
|
1518
|
+
if (!xe || !ye) {
|
|
1519
|
+
if (!xc || !yc) return new BigNumber(a / 0);
|
|
1520
|
+
if (!xc[0] || !yc[0]) return yc[0] ? y : new BigNumber(xc[0] ? x : a * 0);
|
|
1521
|
+
}
|
|
1522
|
+
xe = bitFloor(xe);
|
|
1523
|
+
ye = bitFloor(ye);
|
|
1524
|
+
xc = xc.slice();
|
|
1525
|
+
if (a = xe - ye) {
|
|
1526
|
+
if (a > 0) {
|
|
1527
|
+
ye = xe;
|
|
1528
|
+
t = yc;
|
|
1529
|
+
} else {
|
|
1530
|
+
a = -a;
|
|
1531
|
+
t = xc;
|
|
1532
|
+
}
|
|
1533
|
+
t.reverse();
|
|
1534
|
+
for (; a--; t.push(0));
|
|
1535
|
+
t.reverse();
|
|
1536
|
+
}
|
|
1537
|
+
a = xc.length;
|
|
1538
|
+
b = yc.length;
|
|
1539
|
+
if (a - b < 0) {
|
|
1540
|
+
t = yc;
|
|
1541
|
+
yc = xc;
|
|
1542
|
+
xc = t;
|
|
1543
|
+
b = a;
|
|
1544
|
+
}
|
|
1545
|
+
for (a = 0; b;) {
|
|
1546
|
+
a = (xc[--b] = xc[b] + yc[b] + a) / BASE | 0;
|
|
1547
|
+
xc[b] = BASE === xc[b] ? 0 : xc[b] % BASE;
|
|
1548
|
+
}
|
|
1549
|
+
if (a) {
|
|
1550
|
+
xc = [a].concat(xc);
|
|
1551
|
+
++ye;
|
|
1552
|
+
}
|
|
1553
|
+
return normalise(y, xc, ye);
|
|
1554
|
+
};
|
|
1555
|
+
P.precision = P.sd = function(sd, rm) {
|
|
1556
|
+
var c, n, v, x = this;
|
|
1557
|
+
if (sd != null && sd !== !!sd) return round(new BigNumber(x), intCheck(sd, 1, MAX), rm == null ? ROUNDING_MODE : intCheck(rm, 0, 8));
|
|
1558
|
+
if (!(c = x.c)) return null;
|
|
1559
|
+
v = c.length - 1;
|
|
1560
|
+
n = v * LOG_BASE + 1;
|
|
1561
|
+
if (v = c[v]) {
|
|
1562
|
+
for (; v % 10 == 0; v /= 10, n--);
|
|
1563
|
+
for (v = c[0]; v >= 10; v /= 10, n++);
|
|
1564
|
+
}
|
|
1565
|
+
if (sd && x.e + 1 > n) n = x.e + 1;
|
|
1566
|
+
return n;
|
|
1567
|
+
};
|
|
1568
|
+
P.shiftedBy = function(k) {
|
|
1569
|
+
return this.times("1e" + intCheck(k, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER));
|
|
1570
|
+
};
|
|
1571
|
+
P.squareRoot = P.sqrt = function() {
|
|
1572
|
+
var m, n, r, rep, t, x = this, c = x.c, s = x.s, e = x.e, dp = DECIMAL_PLACES + 4, half = new BigNumber("0.5");
|
|
1573
|
+
if (s !== 1 || !c || !c[0]) return new BigNumber(!s || s < 0 && (!c || c[0]) ? NaN : c ? x : Infinity);
|
|
1574
|
+
s = Math.sqrt(+valueOf(x));
|
|
1575
|
+
if (s == 0 || s == Infinity) {
|
|
1576
|
+
n = coeffToString(c);
|
|
1577
|
+
if ((n.length + e) % 2 == 0) n += "0";
|
|
1578
|
+
s = Math.sqrt(+n);
|
|
1579
|
+
e = bitFloor((e + 1) / 2) - (e < 0 || e % 2);
|
|
1580
|
+
if (s == Infinity) n = "5e" + e;
|
|
1581
|
+
else {
|
|
1582
|
+
n = s.toExponential();
|
|
1583
|
+
n = n.slice(0, n.indexOf("e") + 1) + e;
|
|
1584
|
+
}
|
|
1585
|
+
r = new BigNumber(n);
|
|
1586
|
+
} else r = new BigNumber(s + "");
|
|
1587
|
+
if (r.c[0]) {
|
|
1588
|
+
e = r.e;
|
|
1589
|
+
s = e + dp;
|
|
1590
|
+
if (s < 3) s = 0;
|
|
1591
|
+
for (;;) {
|
|
1592
|
+
t = r;
|
|
1593
|
+
r = half.times(t.plus(div(x, t, dp, 1)));
|
|
1594
|
+
if (coeffToString(t.c).slice(0, s) === (n = coeffToString(r.c)).slice(0, s)) {
|
|
1595
|
+
if (r.e < e) --s;
|
|
1596
|
+
n = n.slice(s - 3, s + 1);
|
|
1597
|
+
if (n == "9999" || !rep && n == "4999") {
|
|
1598
|
+
if (!rep) {
|
|
1599
|
+
round(t, t.e + DECIMAL_PLACES + 2, 0);
|
|
1600
|
+
if (t.times(t).eq(x)) {
|
|
1601
|
+
r = t;
|
|
1602
|
+
break;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
dp += 4;
|
|
1606
|
+
s += 4;
|
|
1607
|
+
rep = 1;
|
|
1608
|
+
} else {
|
|
1609
|
+
if (!+n || !+n.slice(1) && n.charAt(0) == "5") {
|
|
1610
|
+
round(r, r.e + DECIMAL_PLACES + 2, 1);
|
|
1611
|
+
m = !r.times(r).eq(x);
|
|
1612
|
+
}
|
|
1613
|
+
break;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return round(r, r.e + DECIMAL_PLACES + 1, ROUNDING_MODE, m);
|
|
1619
|
+
};
|
|
1620
|
+
if (typeof BigInt == "function") P.toBigInt = function(rm) {
|
|
1621
|
+
var x = this;
|
|
1622
|
+
if (!x.c) return null;
|
|
1623
|
+
return BigInt(format(x, x.e + 1, rm));
|
|
1624
|
+
};
|
|
1625
|
+
P.toExponential = function(dp, rm) {
|
|
1626
|
+
return format(this, dp == null ? dp : intCheck(dp, 0, MAX) + 1, rm, 1);
|
|
1627
|
+
};
|
|
1628
|
+
P.toFixed = function(dp, rm) {
|
|
1629
|
+
return format(this, dp == null ? dp : intCheck(dp, -MAX, MAX) + this.e + 1, rm);
|
|
1630
|
+
};
|
|
1631
|
+
P.toFormat = function(dp, rm, options) {
|
|
1632
|
+
var isNeg, min, max, str, x = this;
|
|
1633
|
+
if (options == null) {
|
|
1634
|
+
options = FORMAT;
|
|
1635
|
+
if (dp != null) {
|
|
1636
|
+
if (rm != null) {
|
|
1637
|
+
if (typeof rm == "object") {
|
|
1638
|
+
options = resolveFormatOptions(rm);
|
|
1639
|
+
rm = null;
|
|
1640
|
+
}
|
|
1641
|
+
} else if (typeof dp == "object" && !isArray(dp)) {
|
|
1642
|
+
options = resolveFormatOptions(dp);
|
|
1643
|
+
dp = rm = null;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
} else if (typeof options != "object") throw Error(bignumberError + "Argument not an object: " + options);
|
|
1647
|
+
else options = resolveFormatOptions(options);
|
|
1648
|
+
if (dp != null) if (isArray(dp) && dp.length <= 2) {
|
|
1649
|
+
min = dp[0];
|
|
1650
|
+
max = dp[1];
|
|
1651
|
+
dp = x.dp();
|
|
1652
|
+
if (max != null && dp > intCheck(max, 0, MAX)) dp = max;
|
|
1653
|
+
if (min != null && intCheck(min, 0, MAX) !== 0) {
|
|
1654
|
+
if (max != null && min > max) throw Error(bignumberError + "Minimum must not exceed maximum");
|
|
1655
|
+
if (dp < min) dp = min;
|
|
1656
|
+
}
|
|
1657
|
+
} else intCheck(dp, -MAX, MAX);
|
|
1658
|
+
str = x.toFixed(dp, rm);
|
|
1659
|
+
isNeg = str.charCodeAt(0) === 45;
|
|
1660
|
+
if (isNeg) str = str.slice(1);
|
|
1661
|
+
if (x.c) {
|
|
1662
|
+
var i, arr = str.split("."), g1 = +options.groupSize, g2 = +options.secondaryGroupSize, groupSeparator = options.groupSeparator || "", intPart = arr[0], fractionPart = arr[1], len = intPart.length;
|
|
1663
|
+
if (g2) {
|
|
1664
|
+
i = g1;
|
|
1665
|
+
g1 = g2;
|
|
1666
|
+
g2 = i;
|
|
1667
|
+
len -= i;
|
|
1668
|
+
}
|
|
1669
|
+
if (g1 > 0 && len > 0) {
|
|
1670
|
+
i = len % g1 || g1;
|
|
1671
|
+
str = intPart.substr(0, i);
|
|
1672
|
+
for (; i < len; i += g1) str += groupSeparator + intPart.substr(i, g1);
|
|
1673
|
+
if (g2 > 0) str += groupSeparator + intPart.slice(i);
|
|
1674
|
+
} else str = intPart;
|
|
1675
|
+
if (fractionPart) {
|
|
1676
|
+
i = +options.fractionGroupSize;
|
|
1677
|
+
if (i) fractionPart = fractionPart.replace(new RegExp("\\d{" + i + "}\\B", "g"), "$&" + (options.fractionGroupSeparator || ""));
|
|
1678
|
+
str += (options.decimalSeparator || "") + fractionPart;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
return (options.prefix || "") + (isNeg ? options.negativeSign || "" : x.s > 0 ? options.positiveSign || "" : "") + str + (options.suffix || "");
|
|
1682
|
+
};
|
|
1683
|
+
P.toFraction = function(md) {
|
|
1684
|
+
var d, d0, d1, d2, e, exp, n, n0, n1, q, r, s, xd, xn, x = this, xc = x.c;
|
|
1685
|
+
if (md != null) {
|
|
1686
|
+
n = new BigNumber(md);
|
|
1687
|
+
if (!n.isInteger() && (n.c || n.s !== 1) || n.lt(ONE)) throw Error(bignumberError + "Argument " + (n.isInteger() ? "out of range: " : "not an integer: ") + valueOf(n));
|
|
1688
|
+
}
|
|
1689
|
+
if (!xc) return [new BigNumber(x.s || 0), new BigNumber(0)];
|
|
1690
|
+
d = new BigNumber(ONE);
|
|
1691
|
+
n1 = d0 = new BigNumber(ONE);
|
|
1692
|
+
d1 = n0 = new BigNumber(ONE);
|
|
1693
|
+
s = coeffToString(xc);
|
|
1694
|
+
e = d.e = s.length - x.e - 1;
|
|
1695
|
+
d.c[0] = POWS_TEN[(exp = e % LOG_BASE) < 0 ? LOG_BASE + exp : exp];
|
|
1696
|
+
md = !md || n.comparedTo(d) > 0 ? e > 0 ? d : n1 : n;
|
|
1697
|
+
exp = MAX_EXP;
|
|
1698
|
+
MAX_EXP = Infinity;
|
|
1699
|
+
n = new BigNumber(s);
|
|
1700
|
+
xn = n;
|
|
1701
|
+
xd = d;
|
|
1702
|
+
n0.c[0] = 0;
|
|
1703
|
+
for (;;) {
|
|
1704
|
+
q = div(n, d, 0, 1);
|
|
1705
|
+
d2 = d0.plus(q.times(d1));
|
|
1706
|
+
if (d2.comparedTo(md) == 1) break;
|
|
1707
|
+
d0 = d1;
|
|
1708
|
+
d1 = d2;
|
|
1709
|
+
n1 = n0.plus(q.times(d2 = n1));
|
|
1710
|
+
n0 = d2;
|
|
1711
|
+
d = n.minus(q.times(d2 = d));
|
|
1712
|
+
n = d2;
|
|
1713
|
+
}
|
|
1714
|
+
d2 = div(md.minus(d0), d1, 0, 1);
|
|
1715
|
+
n0 = n0.plus(d2.times(n1));
|
|
1716
|
+
d0 = d0.plus(d2.times(d1));
|
|
1717
|
+
r = n1.times(xd).minus(xn.times(d1)).abs().times(d0).comparedTo(n0.times(xd).minus(xn.times(d0)).abs().times(d1)) < 1 ? [n1, d1] : [n0, d0];
|
|
1718
|
+
r[0].s = x.s;
|
|
1719
|
+
MAX_EXP = exp;
|
|
1720
|
+
return r;
|
|
1721
|
+
};
|
|
1722
|
+
P.toNumber = function() {
|
|
1723
|
+
return +valueOf(this);
|
|
1724
|
+
};
|
|
1725
|
+
P.toObject = function() {
|
|
1726
|
+
var x = this;
|
|
1727
|
+
return {
|
|
1728
|
+
c: x.c ? x.c.slice() : null,
|
|
1729
|
+
e: x.e,
|
|
1730
|
+
s: x.s
|
|
1731
|
+
};
|
|
1732
|
+
};
|
|
1733
|
+
P.toPrecision = function(sd, rm) {
|
|
1734
|
+
return format(this, sd == null ? sd : intCheck(sd, 1, MAX), rm, 2);
|
|
1735
|
+
};
|
|
1736
|
+
P.toString = function(b) {
|
|
1737
|
+
var str, n = this, s = n.s, e = n.e;
|
|
1738
|
+
if (e === null) if (s) {
|
|
1739
|
+
str = "Infinity";
|
|
1740
|
+
if (s < 0) str = "-" + str;
|
|
1741
|
+
} else str = "NaN";
|
|
1742
|
+
else {
|
|
1743
|
+
if (b == null) str = e <= TO_EXP_NEG || e >= TO_EXP_POS ? toExponential(coeffToString(n.c), e) : toFixedPoint(coeffToString(n.c), e, "0");
|
|
1744
|
+
else {
|
|
1745
|
+
intCheck(b, 2, ALPHABET.length, "Base");
|
|
1746
|
+
str = convertBase(toFixedPoint(coeffToString(n.c), e, "0"), 10, b, s, true);
|
|
1747
|
+
}
|
|
1748
|
+
if (s < 0 && n.c[0]) str = "-" + str;
|
|
1749
|
+
}
|
|
1750
|
+
return str;
|
|
1751
|
+
};
|
|
1752
|
+
P.valueOf = P.toJSON = function() {
|
|
1753
|
+
return valueOf(this);
|
|
1754
|
+
};
|
|
1755
|
+
P._isBigNumber = true;
|
|
1756
|
+
if (configObject != null) BigNumber.set(configObject);
|
|
1757
|
+
return BigNumber;
|
|
1758
|
+
}
|
|
1759
|
+
function bitFloor(n) {
|
|
1760
|
+
var i = n | 0;
|
|
1761
|
+
return n > 0 || n === i ? i : i - 1;
|
|
1762
|
+
}
|
|
1763
|
+
function coeffToString(a) {
|
|
1764
|
+
var s, z, i = 1, j = a.length, r = a[0] + "";
|
|
1765
|
+
for (; i < j;) {
|
|
1766
|
+
s = a[i++] + "";
|
|
1767
|
+
z = LOG_BASE - s.length;
|
|
1768
|
+
for (; z--; s = "0" + s);
|
|
1769
|
+
r += s;
|
|
1770
|
+
}
|
|
1771
|
+
for (j = r.length; r.charCodeAt(--j) === 48;);
|
|
1772
|
+
return r.slice(0, j + 1 || 1);
|
|
1773
|
+
}
|
|
1774
|
+
function compare(x, y) {
|
|
1775
|
+
var a, b, xc = x.c, yc = y.c, i = x.s, j = y.s, k = x.e, l = y.e;
|
|
1776
|
+
if (!i || !j) return null;
|
|
1777
|
+
a = xc && !xc[0];
|
|
1778
|
+
b = yc && !yc[0];
|
|
1779
|
+
if (a || b) return a ? b ? 0 : -j : i;
|
|
1780
|
+
if (i != j) return i;
|
|
1781
|
+
a = i < 0;
|
|
1782
|
+
b = k == l;
|
|
1783
|
+
if (!xc || !yc) return b ? 0 : !xc ^ a ? 1 : -1;
|
|
1784
|
+
if (!b) return k > l ^ a ? 1 : -1;
|
|
1785
|
+
j = (k = xc.length) < (l = yc.length) ? k : l;
|
|
1786
|
+
for (i = 0; i < j; i++) if (xc[i] != yc[i]) return xc[i] > yc[i] ^ a ? 1 : -1;
|
|
1787
|
+
return k == l ? 0 : k > l ^ a ? 1 : -1;
|
|
1788
|
+
}
|
|
1789
|
+
function intCheck(n, min, max, name) {
|
|
1790
|
+
if (n < min || n > max || n !== mathfloor(n)) throw Error(bignumberError + (name || "Argument") + (typeof n == "number" ? n < min || n > max ? " out of range: " : " not an integer: " : " not a primitive number: ") + String(n));
|
|
1791
|
+
return n;
|
|
1792
|
+
}
|
|
1793
|
+
function isArray(obj) {
|
|
1794
|
+
return {}.toString.call(obj) == "[object Array]";
|
|
1795
|
+
}
|
|
1796
|
+
function isOdd(n) {
|
|
1797
|
+
var k = n.c.length - 1;
|
|
1798
|
+
return bitFloor(n.e / LOG_BASE) == k && n.c[k] % 2 != 0;
|
|
1799
|
+
}
|
|
1800
|
+
function toExponential(str, e) {
|
|
1801
|
+
return (str.length > 1 ? str.charAt(0) + "." + str.slice(1) : str) + (e < 0 ? "e" : "e+") + e;
|
|
1802
|
+
}
|
|
1803
|
+
function toFixedPoint(str, e, z) {
|
|
1804
|
+
var len, zs;
|
|
1805
|
+
if (e < 0) {
|
|
1806
|
+
for (zs = z + "."; ++e; zs += z);
|
|
1807
|
+
str = zs + str;
|
|
1808
|
+
} else {
|
|
1809
|
+
len = str.length;
|
|
1810
|
+
if (++e > len) {
|
|
1811
|
+
for (zs = z, e -= len; --e; zs += z);
|
|
1812
|
+
str += zs;
|
|
1813
|
+
} else if (e < len) str = str.slice(0, e) + "." + str.slice(e);
|
|
1814
|
+
}
|
|
1815
|
+
return str;
|
|
1816
|
+
}
|
|
1817
|
+
//#endregion
|
|
1818
|
+
//#region packages/contracts/src/api-keys.ts
|
|
1819
|
+
/**
|
|
1820
|
+
* API anahtarı sözleşmeleri — permission-mask modeli (spec §5.5).
|
|
1821
|
+
*
|
|
1822
|
+
* KRİTİK KURAL: `scope`, oluşturma anındaki tavandır — gerçek etkili kapsam
|
|
1823
|
+
* istek anında sahibinin GÜNCEL tenant rolüyle kesişiminden hesaplanır (bkz.
|
|
1824
|
+
* apps/api/src/lib/api-key-auth.ts). Bu şemalar yalnız gövde şeklini tanımlar,
|
|
1825
|
+
* yetki hesabını YAPMAZ.
|
|
1826
|
+
*/
|
|
1827
|
+
const apiKeyScopeSchema = z.enum([
|
|
1828
|
+
"read",
|
|
1829
|
+
"write",
|
|
1830
|
+
"admin"
|
|
1831
|
+
], "geçerli bir kapsam olmalı (read, write, admin)");
|
|
1832
|
+
z.object({
|
|
1833
|
+
name: z.string().trim().min(2, "ad en az 2 karakter olmalı").max(80, "ad en fazla 80 karakter olabilir"),
|
|
1834
|
+
scope: apiKeyScopeSchema
|
|
1835
|
+
});
|
|
1836
|
+
z.object({
|
|
1837
|
+
id: z.uuid(),
|
|
1838
|
+
name: z.string(),
|
|
1839
|
+
scope: apiKeyScopeSchema,
|
|
1840
|
+
prefix: z.string(),
|
|
1841
|
+
token: z.string()
|
|
1842
|
+
});
|
|
1843
|
+
const apiKeyResponseSchema = z.object({
|
|
1844
|
+
id: z.uuid(),
|
|
1845
|
+
name: z.string(),
|
|
1846
|
+
scope: apiKeyScopeSchema,
|
|
1847
|
+
prefix: z.string(),
|
|
1848
|
+
lastUsedAt: z.iso.datetime().nullable(),
|
|
1849
|
+
revokedAt: z.iso.datetime().nullable(),
|
|
1850
|
+
createdAt: z.iso.datetime()
|
|
1851
|
+
});
|
|
1852
|
+
z.object({ keys: z.array(apiKeyResponseSchema) });
|
|
1853
|
+
//#endregion
|
|
1854
|
+
//#region packages/contracts/src/common.ts
|
|
1855
|
+
/**
|
|
1856
|
+
* Ortak zod şemaları ve doğrulayıcılar — birden fazla domain tarafından
|
|
1857
|
+
* paylaşılan küçük yapı taşları. Domain'e özgü şemalar kendi dosyalarında
|
|
1858
|
+
* durur (ör. plans.ts, fields.ts).
|
|
1859
|
+
*
|
|
1860
|
+
* Hata mesajları Türkçe: bu şemalar doğrudan API yanıtına dönüşüyor.
|
|
1861
|
+
*/
|
|
1862
|
+
/** Tenant üyeliği rolü — sıralı: viewer < member < admin < owner */
|
|
1863
|
+
const memberRoleSchema = z.enum([
|
|
1864
|
+
"owner",
|
|
1865
|
+
"admin",
|
|
1866
|
+
"member",
|
|
1867
|
+
"viewer"
|
|
1868
|
+
], "geçerli bir rol olmalı (owner, admin, member, viewer)");
|
|
1869
|
+
/**
|
|
1870
|
+
* Tenant slug'ı. NOT: "ayrılmış adlar" (ör. `admin`, `api`, `www`) şu anda
|
|
1871
|
+
* route katmanında FİLTRELENMİYOR — slug bir subdomain değil path-prefix
|
|
1872
|
+
* olarak kullanıldığından (bkz. spec) bu bir güvenlik açığı oluşturmuyor;
|
|
1873
|
+
* yalnız kullanıcı deneyimi/marka çakışması riski Faz 4A kapsamı dışında
|
|
1874
|
+
* bırakıldı. İleride ayrılmış ad listesi eklenirse burası genişletilir.
|
|
1875
|
+
*/
|
|
1876
|
+
const tenantSlugSchema = z.string().min(2, "en az 2 karakter olmalı").max(64, "en fazla 64 karakter olabilir").regex(/^[a-z][a-z0-9-]*$/, "küçük harf, rakam ve tire kullanılabilir");
|
|
1877
|
+
/**
|
|
1878
|
+
* Katı hex renk — tenant branding'inde doğrudan CSS'e yazıldığı için
|
|
1879
|
+
* serbest string ASLA kabul edilmez (CSS enjeksiyonu sınırı).
|
|
1880
|
+
*/
|
|
1881
|
+
const hexColorSchema = z.string().regex(/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/, "geçerli bir hex renk olmalı (örn. #14b8a6)");
|
|
1882
|
+
z.string().regex(/^\+[1-9]\d{7,14}$/, "uluslararası biçimde olmalı (örn. +905321234567)");
|
|
1883
|
+
/** Muhasebe/bildirim e-postası */
|
|
1884
|
+
const emailSchema = z.email("geçerli bir e-posta adresi olmalı").max(160, "en fazla 160 karakter olabilir");
|
|
1885
|
+
/**
|
|
1886
|
+
* TC Kimlik Numarası checksum doğrulaması (11 hane, sıfırla başlamaz).
|
|
1887
|
+
* 10. hane: (tek konumların toplamı×7 - çift konumların toplamı) mod 10
|
|
1888
|
+
* 11. hane: ilk 10 hanenin toplamı mod 10
|
|
1889
|
+
*/
|
|
1890
|
+
function validateTckn(value) {
|
|
1891
|
+
if (!/^[1-9]\d{10}$/.test(value)) return false;
|
|
1892
|
+
const d = [...value].map(Number);
|
|
1893
|
+
let oddSum = 0;
|
|
1894
|
+
let evenSum = 0;
|
|
1895
|
+
for (let i = 0; i < 9; i += 1) {
|
|
1896
|
+
const digit = d[i];
|
|
1897
|
+
if (digit === void 0) return false;
|
|
1898
|
+
if (i % 2 === 0) oddSum += digit;
|
|
1899
|
+
else evenSum += digit;
|
|
1900
|
+
}
|
|
1901
|
+
const tenth = d[9];
|
|
1902
|
+
const eleventh = d[10];
|
|
1903
|
+
if (tenth === void 0 || eleventh === void 0) return false;
|
|
1904
|
+
if (((oddSum * 7 - evenSum) % 10 + 10) % 10 !== tenth) return false;
|
|
1905
|
+
return d.slice(0, 10).reduce((a, b) => a + b, 0) % 10 === eleventh;
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Vergi Kimlik Numarası checksum doğrulaması (10 hane).
|
|
1909
|
+
* Maliye Bakanlığı algoritması.
|
|
1910
|
+
*/
|
|
1911
|
+
function validateVkn(value) {
|
|
1912
|
+
if (!/^\d{10}$/.test(value)) return false;
|
|
1913
|
+
const d = [...value].map(Number);
|
|
1914
|
+
let sum = 0;
|
|
1915
|
+
for (let i = 0; i < 9; i += 1) {
|
|
1916
|
+
const digit = d[i];
|
|
1917
|
+
if (digit === void 0) return false;
|
|
1918
|
+
const tmp = (digit + 10 - (i + 1)) % 10;
|
|
1919
|
+
if (tmp === 9) sum += tmp;
|
|
1920
|
+
else sum += tmp * 2 ** (10 - (i + 1)) % 9;
|
|
1921
|
+
}
|
|
1922
|
+
const last = d[9];
|
|
1923
|
+
if (last === void 0) return false;
|
|
1924
|
+
return (10 - sum % 10) % 10 === last;
|
|
1925
|
+
}
|
|
1926
|
+
z.object({
|
|
1927
|
+
/** Zitadel'den PKCE akışıyla alınan id_token */
|
|
1928
|
+
idToken: z.string().min(1, "idToken gerekli").max(8192, "idToken çok uzun"),
|
|
1929
|
+
/** id_token profil claim'i taşımıyorsa userinfo tamamlaması için (opsiyonel) */
|
|
1930
|
+
accessToken: z.string().min(1, "accessToken boş olamaz").max(8192, "accessToken çok uzun").optional()
|
|
1931
|
+
});
|
|
1932
|
+
z.object({
|
|
1933
|
+
token: z.string(),
|
|
1934
|
+
user: z.object({
|
|
1935
|
+
id: z.uuid(),
|
|
1936
|
+
email: z.string(),
|
|
1937
|
+
name: z.string()
|
|
1938
|
+
})
|
|
1939
|
+
});
|
|
1940
|
+
z.object({
|
|
1941
|
+
user: z.object({
|
|
1942
|
+
id: z.uuid(),
|
|
1943
|
+
email: z.string(),
|
|
1944
|
+
name: z.string()
|
|
1945
|
+
}),
|
|
1946
|
+
tenants: z.array(z.object({
|
|
1947
|
+
id: z.uuid(),
|
|
1948
|
+
name: z.string(),
|
|
1949
|
+
slug: z.string(),
|
|
1950
|
+
role: memberRoleSchema
|
|
1951
|
+
}))
|
|
1952
|
+
});
|
|
1953
|
+
z.object({
|
|
1954
|
+
name: z.string().trim().min(2, "ad en az 2 karakter olmalı").max(120, "ad en fazla 120 karakter olabilir"),
|
|
1955
|
+
slug: tenantSlugSchema
|
|
1956
|
+
});
|
|
1957
|
+
z.object({
|
|
1958
|
+
id: z.uuid(),
|
|
1959
|
+
name: z.string(),
|
|
1960
|
+
slug: z.string(),
|
|
1961
|
+
role: memberRoleSchema
|
|
1962
|
+
});
|
|
1963
|
+
/** `GET /v1/tenants/:id/members` yanıtındaki tek üye satırı */
|
|
1964
|
+
const tenantMemberSchema = z.object({
|
|
1965
|
+
userId: z.uuid(),
|
|
1966
|
+
email: z.string(),
|
|
1967
|
+
name: z.string(),
|
|
1968
|
+
role: memberRoleSchema
|
|
1969
|
+
});
|
|
1970
|
+
z.object({ members: z.array(tenantMemberSchema) });
|
|
1971
|
+
z.object({ role: memberRoleSchema });
|
|
1972
|
+
z.object({
|
|
1973
|
+
userId: z.uuid(),
|
|
1974
|
+
role: memberRoleSchema
|
|
1975
|
+
});
|
|
1976
|
+
const COUNTRY_CODE_SET = /* @__PURE__ */ new Set([
|
|
1977
|
+
"AD",
|
|
1978
|
+
"AE",
|
|
1979
|
+
"AF",
|
|
1980
|
+
"AG",
|
|
1981
|
+
"AI",
|
|
1982
|
+
"AL",
|
|
1983
|
+
"AM",
|
|
1984
|
+
"AO",
|
|
1985
|
+
"AQ",
|
|
1986
|
+
"AR",
|
|
1987
|
+
"AS",
|
|
1988
|
+
"AT",
|
|
1989
|
+
"AU",
|
|
1990
|
+
"AW",
|
|
1991
|
+
"AX",
|
|
1992
|
+
"AZ",
|
|
1993
|
+
"BA",
|
|
1994
|
+
"BB",
|
|
1995
|
+
"BD",
|
|
1996
|
+
"BE",
|
|
1997
|
+
"BF",
|
|
1998
|
+
"BG",
|
|
1999
|
+
"BH",
|
|
2000
|
+
"BI",
|
|
2001
|
+
"BJ",
|
|
2002
|
+
"BL",
|
|
2003
|
+
"BM",
|
|
2004
|
+
"BN",
|
|
2005
|
+
"BO",
|
|
2006
|
+
"BQ",
|
|
2007
|
+
"BR",
|
|
2008
|
+
"BS",
|
|
2009
|
+
"BT",
|
|
2010
|
+
"BV",
|
|
2011
|
+
"BW",
|
|
2012
|
+
"BY",
|
|
2013
|
+
"BZ",
|
|
2014
|
+
"CA",
|
|
2015
|
+
"CC",
|
|
2016
|
+
"CD",
|
|
2017
|
+
"CF",
|
|
2018
|
+
"CG",
|
|
2019
|
+
"CH",
|
|
2020
|
+
"CI",
|
|
2021
|
+
"CK",
|
|
2022
|
+
"CL",
|
|
2023
|
+
"CM",
|
|
2024
|
+
"CN",
|
|
2025
|
+
"CO",
|
|
2026
|
+
"CR",
|
|
2027
|
+
"CU",
|
|
2028
|
+
"CV",
|
|
2029
|
+
"CW",
|
|
2030
|
+
"CX",
|
|
2031
|
+
"CY",
|
|
2032
|
+
"CZ",
|
|
2033
|
+
"DE",
|
|
2034
|
+
"DJ",
|
|
2035
|
+
"DK",
|
|
2036
|
+
"DM",
|
|
2037
|
+
"DO",
|
|
2038
|
+
"DZ",
|
|
2039
|
+
"EC",
|
|
2040
|
+
"EE",
|
|
2041
|
+
"EG",
|
|
2042
|
+
"EH",
|
|
2043
|
+
"ER",
|
|
2044
|
+
"ES",
|
|
2045
|
+
"ET",
|
|
2046
|
+
"FI",
|
|
2047
|
+
"FJ",
|
|
2048
|
+
"FK",
|
|
2049
|
+
"FM",
|
|
2050
|
+
"FO",
|
|
2051
|
+
"FR",
|
|
2052
|
+
"GA",
|
|
2053
|
+
"GB",
|
|
2054
|
+
"GD",
|
|
2055
|
+
"GE",
|
|
2056
|
+
"GF",
|
|
2057
|
+
"GG",
|
|
2058
|
+
"GH",
|
|
2059
|
+
"GI",
|
|
2060
|
+
"GL",
|
|
2061
|
+
"GM",
|
|
2062
|
+
"GN",
|
|
2063
|
+
"GP",
|
|
2064
|
+
"GQ",
|
|
2065
|
+
"GR",
|
|
2066
|
+
"GS",
|
|
2067
|
+
"GT",
|
|
2068
|
+
"GU",
|
|
2069
|
+
"GW",
|
|
2070
|
+
"GY",
|
|
2071
|
+
"HK",
|
|
2072
|
+
"HM",
|
|
2073
|
+
"HN",
|
|
2074
|
+
"HR",
|
|
2075
|
+
"HT",
|
|
2076
|
+
"HU",
|
|
2077
|
+
"ID",
|
|
2078
|
+
"IE",
|
|
2079
|
+
"IL",
|
|
2080
|
+
"IM",
|
|
2081
|
+
"IN",
|
|
2082
|
+
"IO",
|
|
2083
|
+
"IQ",
|
|
2084
|
+
"IR",
|
|
2085
|
+
"IS",
|
|
2086
|
+
"IT",
|
|
2087
|
+
"JE",
|
|
2088
|
+
"JM",
|
|
2089
|
+
"JO",
|
|
2090
|
+
"JP",
|
|
2091
|
+
"KE",
|
|
2092
|
+
"KG",
|
|
2093
|
+
"KH",
|
|
2094
|
+
"KI",
|
|
2095
|
+
"KM",
|
|
2096
|
+
"KN",
|
|
2097
|
+
"KP",
|
|
2098
|
+
"KR",
|
|
2099
|
+
"KW",
|
|
2100
|
+
"KY",
|
|
2101
|
+
"KZ",
|
|
2102
|
+
"LA",
|
|
2103
|
+
"LB",
|
|
2104
|
+
"LC",
|
|
2105
|
+
"LI",
|
|
2106
|
+
"LK",
|
|
2107
|
+
"LR",
|
|
2108
|
+
"LS",
|
|
2109
|
+
"LT",
|
|
2110
|
+
"LU",
|
|
2111
|
+
"LV",
|
|
2112
|
+
"LY",
|
|
2113
|
+
"MA",
|
|
2114
|
+
"MC",
|
|
2115
|
+
"MD",
|
|
2116
|
+
"ME",
|
|
2117
|
+
"MF",
|
|
2118
|
+
"MG",
|
|
2119
|
+
"MH",
|
|
2120
|
+
"MK",
|
|
2121
|
+
"ML",
|
|
2122
|
+
"MM",
|
|
2123
|
+
"MN",
|
|
2124
|
+
"MO",
|
|
2125
|
+
"MP",
|
|
2126
|
+
"MQ",
|
|
2127
|
+
"MR",
|
|
2128
|
+
"MS",
|
|
2129
|
+
"MT",
|
|
2130
|
+
"MU",
|
|
2131
|
+
"MV",
|
|
2132
|
+
"MW",
|
|
2133
|
+
"MX",
|
|
2134
|
+
"MY",
|
|
2135
|
+
"MZ",
|
|
2136
|
+
"NA",
|
|
2137
|
+
"NC",
|
|
2138
|
+
"NE",
|
|
2139
|
+
"NF",
|
|
2140
|
+
"NG",
|
|
2141
|
+
"NI",
|
|
2142
|
+
"NL",
|
|
2143
|
+
"NO",
|
|
2144
|
+
"NP",
|
|
2145
|
+
"NR",
|
|
2146
|
+
"NU",
|
|
2147
|
+
"NZ",
|
|
2148
|
+
"OM",
|
|
2149
|
+
"PA",
|
|
2150
|
+
"PE",
|
|
2151
|
+
"PF",
|
|
2152
|
+
"PG",
|
|
2153
|
+
"PH",
|
|
2154
|
+
"PK",
|
|
2155
|
+
"PL",
|
|
2156
|
+
"PM",
|
|
2157
|
+
"PN",
|
|
2158
|
+
"PR",
|
|
2159
|
+
"PS",
|
|
2160
|
+
"PT",
|
|
2161
|
+
"PW",
|
|
2162
|
+
"PY",
|
|
2163
|
+
"QA",
|
|
2164
|
+
"RE",
|
|
2165
|
+
"RO",
|
|
2166
|
+
"RS",
|
|
2167
|
+
"RU",
|
|
2168
|
+
"RW",
|
|
2169
|
+
"SA",
|
|
2170
|
+
"SB",
|
|
2171
|
+
"SC",
|
|
2172
|
+
"SD",
|
|
2173
|
+
"SE",
|
|
2174
|
+
"SG",
|
|
2175
|
+
"SH",
|
|
2176
|
+
"SI",
|
|
2177
|
+
"SJ",
|
|
2178
|
+
"SK",
|
|
2179
|
+
"SL",
|
|
2180
|
+
"SM",
|
|
2181
|
+
"SN",
|
|
2182
|
+
"SO",
|
|
2183
|
+
"SR",
|
|
2184
|
+
"SS",
|
|
2185
|
+
"ST",
|
|
2186
|
+
"SV",
|
|
2187
|
+
"SX",
|
|
2188
|
+
"SY",
|
|
2189
|
+
"SZ",
|
|
2190
|
+
"TC",
|
|
2191
|
+
"TD",
|
|
2192
|
+
"TF",
|
|
2193
|
+
"TG",
|
|
2194
|
+
"TH",
|
|
2195
|
+
"TJ",
|
|
2196
|
+
"TK",
|
|
2197
|
+
"TL",
|
|
2198
|
+
"TM",
|
|
2199
|
+
"TN",
|
|
2200
|
+
"TO",
|
|
2201
|
+
"TR",
|
|
2202
|
+
"TT",
|
|
2203
|
+
"TV",
|
|
2204
|
+
"TW",
|
|
2205
|
+
"TZ",
|
|
2206
|
+
"UA",
|
|
2207
|
+
"UG",
|
|
2208
|
+
"UM",
|
|
2209
|
+
"US",
|
|
2210
|
+
"UY",
|
|
2211
|
+
"UZ",
|
|
2212
|
+
"VA",
|
|
2213
|
+
"VC",
|
|
2214
|
+
"VE",
|
|
2215
|
+
"VG",
|
|
2216
|
+
"VI",
|
|
2217
|
+
"VN",
|
|
2218
|
+
"VU",
|
|
2219
|
+
"WF",
|
|
2220
|
+
"WS",
|
|
2221
|
+
"YE",
|
|
2222
|
+
"YT",
|
|
2223
|
+
"ZA",
|
|
2224
|
+
"ZM",
|
|
2225
|
+
"ZW"
|
|
2226
|
+
]);
|
|
2227
|
+
//#endregion
|
|
2228
|
+
//#region packages/contracts/src/plans.ts
|
|
2229
|
+
const PLAN_CODES = [
|
|
2230
|
+
"free",
|
|
2231
|
+
"pro",
|
|
2232
|
+
"team",
|
|
2233
|
+
"enterprise"
|
|
2234
|
+
];
|
|
2235
|
+
/** Platform admin yanıtlarında (bkz. contracts/platform.ts) `tenants.planCode`'un doğrulaması */
|
|
2236
|
+
const planCodeSchema = z.enum(PLAN_CODES, "geçerli bir plan olmalı");
|
|
2237
|
+
const BILLING_PERIODS = ["monthly", "yearly"];
|
|
2238
|
+
//#endregion
|
|
2239
|
+
//#region packages/contracts/src/billing.ts
|
|
2240
|
+
/**
|
|
2241
|
+
* Faturalama sözleşmeleri — fatura profili (ülke/bireysel/kurumsal/yurtdışı),
|
|
2242
|
+
* checkout isteği, abonelik/fatura yanıt şekilleri (spec §8.5–§8.8).
|
|
2243
|
+
*
|
|
2244
|
+
* KRİTİK KURAL: `accountingEmail` HER ÜÇ varyantta da ZORUNLU ve ülkeden
|
|
2245
|
+
* BAĞIMSIZDIR (brief madde 8) — tüm muhasebe e-postaları BURAYA gider.
|
|
2246
|
+
*/
|
|
2247
|
+
const countryCodeSchema = z.string().length(2, "ISO 3166-1 iki harfli ülke kodu olmalı").refine((value) => COUNTRY_CODE_SET.has(value), "geçerli bir ülke kodu olmalı");
|
|
2248
|
+
z.enum([
|
|
2249
|
+
"tr_individual",
|
|
2250
|
+
"tr_corporate",
|
|
2251
|
+
"international"
|
|
2252
|
+
], "geçerli bir fatura profili türü olmalı");
|
|
2253
|
+
const trIndividualInputSchema = z.object({
|
|
2254
|
+
kind: z.literal("tr_individual"),
|
|
2255
|
+
country: z.literal("TR"),
|
|
2256
|
+
fullName: z.string().trim().min(1, "ad soyad gerekli").max(200, "en fazla 200 karakter olabilir"),
|
|
2257
|
+
tckn: z.string().refine(validateTckn, "geçerli bir TC Kimlik Numarası olmalı"),
|
|
2258
|
+
address: z.string().trim().min(1, "adres gerekli").max(500, "en fazla 500 karakter olabilir"),
|
|
2259
|
+
city: z.string().trim().min(1, "şehir gerekli").max(120, "en fazla 120 karakter olabilir"),
|
|
2260
|
+
district: z.string().trim().min(1, "ilçe gerekli").max(120, "en fazla 120 karakter olabilir"),
|
|
2261
|
+
accountingEmail: emailSchema
|
|
2262
|
+
});
|
|
2263
|
+
const trCorporateInputSchema = z.object({
|
|
2264
|
+
kind: z.literal("tr_corporate"),
|
|
2265
|
+
country: z.literal("TR"),
|
|
2266
|
+
legalName: z.string().trim().min(1, "unvan gerekli").max(250, "en fazla 250 karakter olabilir"),
|
|
2267
|
+
vkn: z.string().refine(validateVkn, "geçerli bir Vergi Kimlik Numarası olmalı"),
|
|
2268
|
+
taxOffice: z.string().trim().min(1, "vergi dairesi gerekli").max(120, "en fazla 120 karakter olabilir"),
|
|
2269
|
+
address: z.string().trim().min(1, "adres gerekli").max(500, "en fazla 500 karakter olabilir"),
|
|
2270
|
+
city: z.string().trim().min(1, "şehir gerekli").max(120, "en fazla 120 karakter olabilir"),
|
|
2271
|
+
district: z.string().trim().min(1, "ilçe gerekli").max(120, "en fazla 120 karakter olabilir"),
|
|
2272
|
+
accountingEmail: emailSchema
|
|
2273
|
+
});
|
|
2274
|
+
const internationalInputSchema = z.object({
|
|
2275
|
+
kind: z.literal("international"),
|
|
2276
|
+
country: countryCodeSchema.refine((value) => value !== "TR", "yurt dışı profilde ülke TR olamaz"),
|
|
2277
|
+
fullName: z.string().trim().min(1, "ad/unvan gerekli").max(250, "en fazla 250 karakter olabilir"),
|
|
2278
|
+
address: z.string().trim().min(1, "adres gerekli").max(500, "en fazla 500 karakter olabilir"),
|
|
2279
|
+
taxId: z.string().trim().max(60, "en fazla 60 karakter olabilir").optional(),
|
|
2280
|
+
accountingEmail: emailSchema
|
|
2281
|
+
});
|
|
2282
|
+
const billingProfileInputSchema = z.discriminatedUnion("kind", [
|
|
2283
|
+
trIndividualInputSchema,
|
|
2284
|
+
trCorporateInputSchema,
|
|
2285
|
+
internationalInputSchema
|
|
2286
|
+
]);
|
|
2287
|
+
z.object({
|
|
2288
|
+
data: billingProfileInputSchema,
|
|
2289
|
+
maskedTaxId: z.string().nullable(),
|
|
2290
|
+
createdAt: z.iso.datetime(),
|
|
2291
|
+
updatedAt: z.iso.datetime()
|
|
2292
|
+
});
|
|
2293
|
+
const purchasablePlanCodeSchema = z.enum(PLAN_CODES, "geçerli bir plan olmalı").refine((value) => value === "pro" || value === "team", "yalnız pro veya team paketleri için satın alma başlatılabilir");
|
|
2294
|
+
z.object({
|
|
2295
|
+
planCode: purchasablePlanCodeSchema,
|
|
2296
|
+
billingPeriod: z.enum(BILLING_PERIODS, "geçerli bir dönem olmalı")
|
|
2297
|
+
});
|
|
2298
|
+
z.object({ purchaseUrl: z.url() });
|
|
2299
|
+
z.object({ code: z.literal("billing_profile_required") });
|
|
2300
|
+
const subscriptionStatusValueSchema = z.enum([
|
|
2301
|
+
"trialing",
|
|
2302
|
+
"active",
|
|
2303
|
+
"past_due",
|
|
2304
|
+
"cancelled",
|
|
2305
|
+
"refunded"
|
|
2306
|
+
]);
|
|
2307
|
+
z.object({
|
|
2308
|
+
planCode: z.enum(PLAN_CODES),
|
|
2309
|
+
billingPeriod: z.enum(BILLING_PERIODS).nullable(),
|
|
2310
|
+
status: subscriptionStatusValueSchema.nullable(),
|
|
2311
|
+
currentPeriodEnd: z.iso.datetime().nullable(),
|
|
2312
|
+
cancelAtPeriodEnd: z.boolean()
|
|
2313
|
+
});
|
|
2314
|
+
const invoiceResponseSchema = z.object({
|
|
2315
|
+
id: z.uuid(),
|
|
2316
|
+
invoiceNumber: z.string(),
|
|
2317
|
+
kind: z.enum(["invoice", "refund"]),
|
|
2318
|
+
status: z.enum([
|
|
2319
|
+
"draft",
|
|
2320
|
+
"formalized",
|
|
2321
|
+
"cancelled"
|
|
2322
|
+
]),
|
|
2323
|
+
amountGross: z.string(),
|
|
2324
|
+
amountNet: z.string(),
|
|
2325
|
+
amountVat: z.string(),
|
|
2326
|
+
currencyCode: z.string(),
|
|
2327
|
+
vatRate: z.string(),
|
|
2328
|
+
vatExemptionCode: z.string().nullable(),
|
|
2329
|
+
hasPdf: z.boolean(),
|
|
2330
|
+
issuedAt: z.iso.datetime()
|
|
2331
|
+
});
|
|
2332
|
+
z.object({
|
|
2333
|
+
items: z.array(invoiceResponseSchema),
|
|
2334
|
+
nextCursor: z.string().nullable(),
|
|
2335
|
+
hasMore: z.boolean()
|
|
2336
|
+
});
|
|
2337
|
+
z.enum(["invoice", "refund"]);
|
|
2338
|
+
//#endregion
|
|
2339
|
+
//#region packages/contracts/src/companies.ts
|
|
2340
|
+
/**
|
|
2341
|
+
* Şirket (company) sözleşmeleri (spec §6.2).
|
|
2342
|
+
*
|
|
2343
|
+
* KRİTİK KURAL: `annualRevenue` PARA alanıdır — string olarak taşınır, hiçbir
|
|
2344
|
+
* yerde native `number`'a çevrilip aritmetik yapılmaz (bkz. spec §6.1,
|
|
2345
|
+
* `bignumber.js` dışında ASLA hesaplanmaz).
|
|
2346
|
+
*
|
|
2347
|
+
* [Fix round 2 — doğruluk reviewer'ının bulduğu, Task 3'ün `deals.amount`
|
|
2348
|
+
* fix'iyle BİREBİR aynı sınıf hata]: DB kolonu `text`TEN `numeric(18,2)`'ye
|
|
2349
|
+
* taşındı (`FIELD_CATALOG.companies.annual_revenue` `sortable`/`filterable`
|
|
2350
|
+
* ilan ediyordu ama `text`te `ORDER BY`/`>`/`<` LEXICOGRAPHIC karşılaştırma
|
|
2351
|
+
* yapıyordu — bkz. `packages/db/src/schema/companies.ts` dosya başı yorumu,
|
|
2352
|
+
* `packages/db/drizzle/0035_companies_annual_revenue_numeric.sql`). Biçim
|
|
2353
|
+
* kısıtı `deals.amount` ile AYNI (`./crm.ts` `numericMoneySchema`, DRY).
|
|
2354
|
+
*/
|
|
2355
|
+
const createCompanyRequestSchema = z.object({
|
|
2356
|
+
name: z.string().trim().min(1, "ad gerekli").max(200, "en fazla 200 karakter olabilir"),
|
|
2357
|
+
domain: z.string().trim().max(255, "en fazla 255 karakter olabilir").optional(),
|
|
2358
|
+
industry: z.string().trim().max(120, "en fazla 120 karakter olabilir").optional(),
|
|
2359
|
+
size: companySizeSchema.optional(),
|
|
2360
|
+
phone: z.string().trim().max(40, "en fazla 40 karakter olabilir").optional(),
|
|
2361
|
+
website: z.url("geçerli bir URL olmalı").max(500, "en fazla 500 karakter olabilir").optional(),
|
|
2362
|
+
address: z.string().trim().max(500, "en fazla 500 karakter olabilir").optional(),
|
|
2363
|
+
city: z.string().trim().max(120, "en fazla 120 karakter olabilir").optional(),
|
|
2364
|
+
country: z.string().trim().max(120, "en fazla 120 karakter olabilir").optional(),
|
|
2365
|
+
/** PARA — bkz. dosya başı KRİTİK KURAL, `numeric(18,2)` ile eşleşen biçim kısıtı */
|
|
2366
|
+
annualRevenue: numericMoneySchema.optional(),
|
|
2367
|
+
linkedinUrl: z.url("geçerli bir URL olmalı").max(500, "en fazla 500 karakter olabilir").optional(),
|
|
2368
|
+
ownerUserId: z.uuid("geçerli bir kullanıcı kimliği olmalı").optional()
|
|
2369
|
+
});
|
|
2370
|
+
const updateCompanyRequestSchema = createCompanyRequestSchema.partial();
|
|
2371
|
+
pagedResponseSchema(z.object({
|
|
2372
|
+
id: z.uuid(),
|
|
2373
|
+
name: z.string(),
|
|
2374
|
+
domain: z.string().nullable(),
|
|
2375
|
+
industry: z.string().nullable(),
|
|
2376
|
+
size: companySizeSchema.nullable(),
|
|
2377
|
+
phone: z.string().nullable(),
|
|
2378
|
+
website: z.string().nullable(),
|
|
2379
|
+
address: z.string().nullable(),
|
|
2380
|
+
city: z.string().nullable(),
|
|
2381
|
+
country: z.string().nullable(),
|
|
2382
|
+
annualRevenue: z.string().nullable(),
|
|
2383
|
+
linkedinUrl: z.string().nullable(),
|
|
2384
|
+
ownerUserId: z.uuid().nullable(),
|
|
2385
|
+
createdAt: z.iso.datetime(),
|
|
2386
|
+
updatedAt: z.iso.datetime()
|
|
2387
|
+
}));
|
|
2388
|
+
//#endregion
|
|
2389
|
+
//#region packages/contracts/src/contacts.ts
|
|
2390
|
+
/**
|
|
2391
|
+
* Kişi (contact) sözleşmeleri (spec §6.2). `companyId` nullable — bir kişi
|
|
2392
|
+
* şirketsiz de var olabilir (bireysel müşteri).
|
|
2393
|
+
*/
|
|
2394
|
+
const createContactRequestSchema = z.object({
|
|
2395
|
+
firstName: z.string().trim().min(1, "ad gerekli").max(120, "en fazla 120 karakter olabilir"),
|
|
2396
|
+
lastName: z.string().trim().min(1, "soyad gerekli").max(120, "en fazla 120 karakter olabilir"),
|
|
2397
|
+
email: z.email("geçerli bir e-posta adresi olmalı").max(160).optional(),
|
|
2398
|
+
phone: z.string().trim().max(40, "en fazla 40 karakter olabilir").optional(),
|
|
2399
|
+
mobile: z.string().trim().max(40, "en fazla 40 karakter olabilir").optional(),
|
|
2400
|
+
jobTitle: z.string().trim().max(160, "en fazla 160 karakter olabilir").optional(),
|
|
2401
|
+
linkedinUrl: z.url("geçerli bir URL olmalı").max(500, "en fazla 500 karakter olabilir").optional(),
|
|
2402
|
+
avatarUrl: z.url("geçerli bir URL olmalı").max(500, "en fazla 500 karakter olabilir").optional(),
|
|
2403
|
+
city: z.string().trim().max(120, "en fazla 120 karakter olabilir").optional(),
|
|
2404
|
+
country: z.string().trim().max(120, "en fazla 120 karakter olabilir").optional(),
|
|
2405
|
+
ownerUserId: z.uuid("geçerli bir kullanıcı kimliği olmalı").optional(),
|
|
2406
|
+
source: contactSourceSchema.optional(),
|
|
2407
|
+
status: contactStatusSchema.default("lead"),
|
|
2408
|
+
companyId: z.uuid("geçerli bir şirket kimliği olmalı").optional()
|
|
2409
|
+
});
|
|
2410
|
+
/**
|
|
2411
|
+
* [Fix round 1 — Faz 10 Task 3] `status` AÇIKÇA yeniden tanımlanır; düz
|
|
2412
|
+
* `.partial()` YETMEZ.
|
|
2413
|
+
*
|
|
2414
|
+
* ÖLÇÜLEN KUSUR: zod 4'te `.partial()` bir alanı `.optional()` yapar ama
|
|
2415
|
+
* `.default()`'unu KALDIRMAZ ⇒ `createContactRequestSchema.partial()`'in
|
|
2416
|
+
* `status`'ü hâlâ `.default('lead')` taşıyordu ve
|
|
2417
|
+
* `updateContactRequestSchema.parse({ city: 'x' })` → `{ city:'x',
|
|
2418
|
+
* status:'lead' }` üretiyordu (canlı ölçüldü). `PATCH /v1/tenants/:id/
|
|
2419
|
+
* contacts/:contactId` gövdeyi `validateJson(updateContactRequestSchema)` ile
|
|
2420
|
+
* doğrulayıp `.set({ ...body, updatedById })` yazdığı için
|
|
2421
|
+
* (`apps/api/src/routes/contacts.ts:314`) `status` GÖNDERMEYEN her kısmi
|
|
2422
|
+
* güncelleme, `active`/`inactive` bir kişiyi SESSİZCE `lead`'e geri
|
|
2423
|
+
* düşürüyordu — dashboard'ın düzenleme formu `status`'ü her zaman gönderdiği
|
|
2424
|
+
* için belirti orada görünmüyordu; kusuru MCP'nin `update_contact` aracı
|
|
2425
|
+
* (yalnız verilen alanları gönderir) ortaya çıkardı.
|
|
2426
|
+
*
|
|
2427
|
+
* KARDEŞ ŞEMALARDA AYNI KUSUR YOK ama gerekçesi TESADÜF: `companies`/
|
|
2428
|
+
* `pipelines`'ın create şemalarında `.default()`'lu alan HİÇ YOK,
|
|
2429
|
+
* `updateDealRequestSchema` ise `.partial()` DEĞİL elle yazılmış bir şemadır.
|
|
2430
|
+
* ⇒ `.default()`'lu bir alan içeren bir create şemasından `.partial()` ile
|
|
2431
|
+
* update şeması TÜRETİRKEN o alanı DAİMA açıkça `.optional()` olarak ez.
|
|
2432
|
+
*/
|
|
2433
|
+
const updateContactRequestSchema = createContactRequestSchema.partial().extend({ status: contactStatusSchema.optional() });
|
|
2434
|
+
pagedResponseSchema(z.object({
|
|
2435
|
+
id: z.uuid(),
|
|
2436
|
+
firstName: z.string(),
|
|
2437
|
+
lastName: z.string(),
|
|
2438
|
+
email: z.string().nullable(),
|
|
2439
|
+
phone: z.string().nullable(),
|
|
2440
|
+
mobile: z.string().nullable(),
|
|
2441
|
+
jobTitle: z.string().nullable(),
|
|
2442
|
+
linkedinUrl: z.string().nullable(),
|
|
2443
|
+
avatarUrl: z.string().nullable(),
|
|
2444
|
+
city: z.string().nullable(),
|
|
2445
|
+
country: z.string().nullable(),
|
|
2446
|
+
ownerUserId: z.uuid().nullable(),
|
|
2447
|
+
source: contactSourceSchema.nullable(),
|
|
2448
|
+
status: contactStatusSchema,
|
|
2449
|
+
companyId: z.uuid().nullable(),
|
|
2450
|
+
createdAt: z.iso.datetime(),
|
|
2451
|
+
updatedAt: z.iso.datetime()
|
|
2452
|
+
}));
|
|
2453
|
+
z.object({
|
|
2454
|
+
code: z.literal("contact_limit_reached"),
|
|
2455
|
+
limit: z.number(),
|
|
2456
|
+
planCode: planCodeSchema
|
|
2457
|
+
});
|
|
2458
|
+
z.string().regex(/^[A-Z]{3}$/, "geçerli bir 3 harfli para birimi kodu olmalı (örn. TRY)");
|
|
2459
|
+
//#endregion
|
|
2460
|
+
//#region packages/contracts/src/deals.ts
|
|
2461
|
+
/**
|
|
2462
|
+
* Fırsat (deal) sözleşmeleri (spec §6.2/§6.4).
|
|
2463
|
+
*
|
|
2464
|
+
* KRİTİK KURAL: `updateDealRequestSchema` `.strict()`'tir ve `stageId`/
|
|
2465
|
+
* `pipelineId`/`position`/`outcome`/`closedAt` alanlarını hiç TANIMLAMAZ —
|
|
2466
|
+
* bu alanlar yalnız `moveDealRequestSchema` (`/move` ucu) üzerinden değişir
|
|
2467
|
+
* (bkz. `packages/db/src/schema/deals.ts` dosya başı KRİTİK KURAL 2,
|
|
2468
|
+
* `apps/api/src/routes/deals.ts`). `.strict()` altında bilinmeyen bir anahtar
|
|
2469
|
+
* zod'un `unrecognized_keys` hatasını üretir; bu, `apps/api/src/lib/validate.ts`
|
|
2470
|
+
* içindeki `ZOD_DEFAULT_MESSAGE` deseniyle ZATEN eşleşiyor (`Unrecognized key`
|
|
2471
|
+
* ile başlar) — bu yüzden ayrı bir Türkçe mesaj GEREKMEZ, jenerik "Geçersiz
|
|
2472
|
+
* istek gövdesi" mesajına düşer (bu, Global Constraints #3'ün belgelediği
|
|
2473
|
+
* bilinçli istisna).
|
|
2474
|
+
*
|
|
2475
|
+
* FIX ROUND 1 (bkz. task-3-report.md): `amount` DB'de `numeric(18,2)`'ye
|
|
2476
|
+
* taşındı (önceden `text` idi — `sortable`/`filterable` ilan edilmiş bir
|
|
2477
|
+
* alanın metin karşılaştırmasıyla sıralanması sessiz bir doğruluk hatasıydı).
|
|
2478
|
+
* Biçim kısıtı (yalnız `-?\d+(\.\d{1,2})?`, en fazla 18 anlamlı basamak) ARTIK
|
|
2479
|
+
* `./crm.ts`'teki `numericMoneySchema`'da PAYLAŞILIYOR (Fix round 2 —
|
|
2480
|
+
* `companies.annual_revenue` AYNI sınıf hatayı taşıdığı için oraya taşındı,
|
|
2481
|
+
* bkz. o dosyanın JSDoc'u) — bilimsel gösterim (`1e10`), `NaN`/`Infinity`,
|
|
2482
|
+
* çift işaret (`--5`), virgüllü ondalık (`1,5`) ve 2'den fazla ondalık basamak
|
|
2483
|
+
* hep REDDEDİLİR. PARA YİNE string olarak taşınır, `bignumber.js` dışında
|
|
2484
|
+
* hesaplanmaz.
|
|
2485
|
+
*/
|
|
2486
|
+
/** ISO 4217 kodu — 3 büyük harf (DB `char(3)` kolonuyla eşleşir) */
|
|
2487
|
+
const currencyCodeSchema = z.string().trim().regex(/^[A-Z]{3}$/, "ISO 4217 biçiminde 3 büyük harf olmalı (örn. TRY)");
|
|
2488
|
+
/** Fırsat tutarı — DB `numeric(18,2)` ile BİREBİR eşleşen ortak biçim kısıtı (bkz. `crm.ts` `numericMoneySchema`) */
|
|
2489
|
+
const dealAmountSchema = numericMoneySchema;
|
|
2490
|
+
const createDealRequestSchema = z.object({
|
|
2491
|
+
title: z.string().trim().min(1, "başlık gerekli").max(200, "en fazla 200 karakter olabilir"),
|
|
2492
|
+
pipelineId: z.uuid("geçerli bir pipeline kimliği olmalı"),
|
|
2493
|
+
stageId: z.uuid("geçerli bir aşama kimliği olmalı"),
|
|
2494
|
+
amount: dealAmountSchema.optional(),
|
|
2495
|
+
currencyCode: currencyCodeSchema.default("TRY"),
|
|
2496
|
+
companyId: z.uuid("geçerli bir şirket kimliği olmalı").optional(),
|
|
2497
|
+
primaryContactId: z.uuid("geçerli bir kişi kimliği olmalı").optional(),
|
|
2498
|
+
expectedCloseDate: z.iso.datetime("geçerli bir tarih olmalı").optional(),
|
|
2499
|
+
ownerUserId: z.uuid("geçerli bir kullanıcı kimliği olmalı").optional()
|
|
2500
|
+
});
|
|
2501
|
+
/**
|
|
2502
|
+
* Genel güncelleme — `stageId`/`pipelineId`/`position`/`outcome`/`closedAt`
|
|
2503
|
+
* BİLEREK YOK (bkz. dosya başı KRİTİK KURAL). `.strict()`: bu alanlardan biri
|
|
2504
|
+
* gönderilirse 400 (bkz. `apps/api/test/deals.test.ts`).
|
|
2505
|
+
*/
|
|
2506
|
+
const updateDealRequestSchema = z.object({
|
|
2507
|
+
title: z.string().trim().min(1, "başlık gerekli").max(200, "en fazla 200 karakter olabilir").optional(),
|
|
2508
|
+
amount: dealAmountSchema.optional(),
|
|
2509
|
+
currencyCode: currencyCodeSchema.optional(),
|
|
2510
|
+
companyId: z.uuid("geçerli bir şirket kimliği olmalı").optional(),
|
|
2511
|
+
primaryContactId: z.uuid("geçerli bir kişi kimliği olmalı").optional(),
|
|
2512
|
+
expectedCloseDate: z.iso.datetime("geçerli bir tarih olmalı").optional(),
|
|
2513
|
+
ownerUserId: z.uuid("geçerli bir kullanıcı kimliği olmalı").optional(),
|
|
2514
|
+
lostReason: z.string().trim().max(500, "en fazla 500 karakter olabilir").optional()
|
|
2515
|
+
}).strict();
|
|
2516
|
+
/**
|
|
2517
|
+
* Kanban taşıma kontratı (spec §6.4) — pipeline'ın `reorderRequestSchema`'sıyla
|
|
2518
|
+
* AYNI komşu şekli (`beforeId`/`afterId`), yalnız `stageId` hedefi eklenir.
|
|
2519
|
+
* `pipelineId` YOK: hedef aşamanın pipeline'ı, fırsatın yeni pipeline'ı olarak
|
|
2520
|
+
* KABUL EDİLİR (bkz. routes/deals.ts) — istemcinin ayrıca tutarlı bir
|
|
2521
|
+
* `pipelineId` göndermesini gerektirmeden çapraz-pipeline taşımaya izin verir.
|
|
2522
|
+
*/
|
|
2523
|
+
const moveDealRequestSchema = z.object({
|
|
2524
|
+
stageId: z.uuid("geçerli bir aşama kimliği olmalı"),
|
|
2525
|
+
beforeId: z.uuid("geçerli bir kimlik olmalı").optional(),
|
|
2526
|
+
afterId: z.uuid("geçerli bir kimlik olmalı").optional()
|
|
2527
|
+
});
|
|
2528
|
+
z.object({
|
|
2529
|
+
contactId: z.uuid("geçerli bir kişi kimliği olmalı"),
|
|
2530
|
+
role: z.string().trim().max(80, "en fazla 80 karakter olabilir").optional()
|
|
2531
|
+
});
|
|
2532
|
+
const dealContactResponseSchema = z.object({
|
|
2533
|
+
contactId: z.uuid(),
|
|
2534
|
+
role: z.string().nullable(),
|
|
2535
|
+
createdAt: z.iso.datetime()
|
|
2536
|
+
});
|
|
2537
|
+
const dealResponseSchema = z.object({
|
|
2538
|
+
id: z.uuid(),
|
|
2539
|
+
pipelineId: z.uuid(),
|
|
2540
|
+
stageId: z.uuid(),
|
|
2541
|
+
companyId: z.uuid().nullable(),
|
|
2542
|
+
primaryContactId: z.uuid().nullable(),
|
|
2543
|
+
title: z.string(),
|
|
2544
|
+
amount: z.string().nullable(),
|
|
2545
|
+
currencyCode: z.string(),
|
|
2546
|
+
position: z.number(),
|
|
2547
|
+
expectedCloseDate: z.iso.datetime().nullable(),
|
|
2548
|
+
closedAt: z.iso.datetime().nullable(),
|
|
2549
|
+
outcome: dealOutcomeSchema,
|
|
2550
|
+
lostReason: z.string().nullable(),
|
|
2551
|
+
ownerUserId: z.uuid().nullable(),
|
|
2552
|
+
createdAt: z.iso.datetime(),
|
|
2553
|
+
updatedAt: z.iso.datetime()
|
|
2554
|
+
});
|
|
2555
|
+
dealResponseSchema.extend({ contacts: z.array(dealContactResponseSchema) });
|
|
2556
|
+
pagedResponseSchema(dealResponseSchema);
|
|
2557
|
+
z.object({
|
|
2558
|
+
email: emailSchema,
|
|
2559
|
+
role: memberRoleSchema
|
|
2560
|
+
});
|
|
2561
|
+
const inviteStatusSchema = z.enum([
|
|
2562
|
+
"pending",
|
|
2563
|
+
"accepted",
|
|
2564
|
+
"revoked"
|
|
2565
|
+
]);
|
|
2566
|
+
/** Tenant içi davet listesi satırı — token/hash ASLA yer almaz */
|
|
2567
|
+
const inviteResponseSchema = z.object({
|
|
2568
|
+
id: z.uuid(),
|
|
2569
|
+
email: z.string(),
|
|
2570
|
+
role: memberRoleSchema,
|
|
2571
|
+
status: inviteStatusSchema,
|
|
2572
|
+
expiresAt: z.iso.datetime(),
|
|
2573
|
+
createdAt: z.iso.datetime()
|
|
2574
|
+
});
|
|
2575
|
+
z.object({ invites: z.array(inviteResponseSchema) });
|
|
2576
|
+
z.object({
|
|
2577
|
+
tenantName: z.string(),
|
|
2578
|
+
email: z.string(),
|
|
2579
|
+
role: memberRoleSchema,
|
|
2580
|
+
status: inviteStatusSchema,
|
|
2581
|
+
expiresAt: z.iso.datetime()
|
|
2582
|
+
});
|
|
2583
|
+
z.object({
|
|
2584
|
+
tenantId: z.uuid(),
|
|
2585
|
+
tenantName: z.string(),
|
|
2586
|
+
role: memberRoleSchema
|
|
2587
|
+
});
|
|
2588
|
+
//#endregion
|
|
2589
|
+
//#region packages/contracts/src/legal/company.ts
|
|
2590
|
+
const VENNYX_COMPANY = {
|
|
2591
|
+
legalName: "Vennyx Yazılım Danışmanlık A.Ş.",
|
|
2592
|
+
taxOffice: "Uluçınar Vergi Dairesi",
|
|
2593
|
+
taxNumber: "9241072735",
|
|
2594
|
+
tradeRegistryNo: "40690",
|
|
2595
|
+
tradeRegistryCity: "Gebze",
|
|
2596
|
+
mersisNo: "0924107273500001",
|
|
2597
|
+
address: "Muallimköy Mah. Deniz Cad. No: 143/8 - 1.1.C1 Blok - Zemin Kat - Kapı No: Z01 - 41400 Gebze/Kocaeli",
|
|
2598
|
+
kepAddress: "vennyxyazilim@hs01.kep.tr",
|
|
2599
|
+
phone: "+90 (850) 242 8992",
|
|
2600
|
+
website: "https://solicrm.com",
|
|
2601
|
+
legalEmail: "legal@solicrm.com",
|
|
2602
|
+
privacyEmail: "privacy@solicrm.com"
|
|
2603
|
+
};
|
|
2604
|
+
/** Hukuki belgelerin "Veri Sorumlusu/Sağlayıcı Kimliği" bölümünde tekrar eden paragraf */
|
|
2605
|
+
function companyIdentityParagraph(locale) {
|
|
2606
|
+
const c = VENNYX_COMPANY;
|
|
2607
|
+
if (locale === "tr") return `${c.legalName} · ${c.taxOffice} · VN ${c.taxNumber} · ${c.tradeRegistryCity} Ticaret Sicil No ${c.tradeRegistryNo} · MERSİS ${c.mersisNo} · ${c.address}`;
|
|
2608
|
+
return `${c.legalName} · Tax office: ${c.taxOffice} · Tax No: ${c.taxNumber} · Trade Registry No (${c.tradeRegistryCity}): ${c.tradeRegistryNo} · MERSIS: ${c.mersisNo} · ${c.address}, Türkiye`;
|
|
2609
|
+
}
|
|
2610
|
+
/** İletişim/başvuru bölümünde tekrar eden paragraf */
|
|
2611
|
+
function contactParagraph(locale) {
|
|
2612
|
+
const c = VENNYX_COMPANY;
|
|
2613
|
+
if (locale === "tr") return `KEP: ${c.kepAddress} · Tel: ${c.phone} · Hukuki başvurular: ${c.legalEmail} · Gizlilik başvuruları: ${c.privacyEmail}`;
|
|
2614
|
+
return `Registered e-mail (KEP): ${c.kepAddress} · Phone: ${c.phone} · Legal inquiries: ${c.legalEmail} · Privacy inquiries: ${c.privacyEmail}`;
|
|
2615
|
+
}
|
|
2616
|
+
contactParagraph("tr"), contactParagraph("en");
|
|
2617
|
+
companyIdentityParagraph("tr"), contactParagraph("tr"), companyIdentityParagraph("en"), contactParagraph("en");
|
|
2618
|
+
companyIdentityParagraph("tr"), contactParagraph("tr"), companyIdentityParagraph("en"), contactParagraph("en");
|
|
2619
|
+
companyIdentityParagraph("tr"), contactParagraph("tr"), companyIdentityParagraph("en"), contactParagraph("en");
|
|
2620
|
+
companyIdentityParagraph("tr"), contactParagraph("tr"), companyIdentityParagraph("en"), contactParagraph("en");
|
|
2621
|
+
contactParagraph("tr"), contactParagraph("en");
|
|
2622
|
+
contactParagraph("tr"), contactParagraph("en");
|
|
2623
|
+
contactParagraph("tr"), contactParagraph("en");
|
|
2624
|
+
companyIdentityParagraph("tr"), contactParagraph("tr"), companyIdentityParagraph("en"), contactParagraph("en");
|
|
2625
|
+
//#endregion
|
|
2626
|
+
//#region packages/contracts/src/legal/slugs.ts
|
|
2627
|
+
/** Spec §10'un tam listesi — 9 belge, sıra `/legal` hub'ının kart sırasıdır */
|
|
2628
|
+
const LEGAL_SLUGS = [
|
|
2629
|
+
"terms",
|
|
2630
|
+
"privacy",
|
|
2631
|
+
"cookies",
|
|
2632
|
+
"kvkk",
|
|
2633
|
+
"distance-sales",
|
|
2634
|
+
"refund-policy",
|
|
2635
|
+
"dpa",
|
|
2636
|
+
"subprocessors",
|
|
2637
|
+
"sla"
|
|
2638
|
+
];
|
|
2639
|
+
//#endregion
|
|
2640
|
+
//#region packages/contracts/src/legal/types.ts
|
|
2641
|
+
/**
|
|
2642
|
+
* `@solicrm/i18n`'in `SupportedLanguage`'ı İLE AYNI kümedir ama BAĞIMSIZ
|
|
2643
|
+
* tanımlanır (contracts, i18n'e bağımlı OLMAMALI — katman ihlali). Senkron
|
|
2644
|
+
* kalması `test/legal-locale-parity.test.ts` ile mekanik olarak zorlanır.
|
|
2645
|
+
*/
|
|
2646
|
+
const LEGAL_LOCALES = ["tr", "en"];
|
|
2647
|
+
z.object({ documentSlug: z.enum(LEGAL_SLUGS, "geçerli bir belge olmalı") });
|
|
2648
|
+
const legalAcceptanceResponseSchema = z.object({
|
|
2649
|
+
id: z.uuid(),
|
|
2650
|
+
documentSlug: z.enum(LEGAL_SLUGS),
|
|
2651
|
+
locale: z.enum(LEGAL_LOCALES),
|
|
2652
|
+
effectiveDate: z.string(),
|
|
2653
|
+
acceptedAt: z.iso.datetime(),
|
|
2654
|
+
isCurrentVersion: z.boolean()
|
|
2655
|
+
});
|
|
2656
|
+
z.object({ acceptances: z.array(legalAcceptanceResponseSchema) });
|
|
2657
|
+
//#endregion
|
|
2658
|
+
//#region packages/contracts/src/pipelines.ts
|
|
2659
|
+
/**
|
|
2660
|
+
* Pipeline + aşama (stage) sözleşmeleri (spec §6.2/§6.4).
|
|
2661
|
+
*
|
|
2662
|
+
* KRİTİK KURAL: `probability` (yüzde 0-100) PARA DEĞİLDİR — native `number`
|
|
2663
|
+
* kabul edilir (bkz. spec §6.1/plan Global Constraints #19, DB kolonu
|
|
2664
|
+
* `numeric(5,2)`). `pipelineStageColorSchema`, `common.ts`'teki `hexColorSchema`'yı
|
|
2665
|
+
* YENİDEN KULLANIR — spec renk formatı vermiyor, yeni bir renk kataloğu
|
|
2666
|
+
* İCAT EDİLMEDİ.
|
|
2667
|
+
*/
|
|
2668
|
+
const pipelineStageColorSchema = hexColorSchema;
|
|
2669
|
+
z.object({
|
|
2670
|
+
name: z.string().trim().min(1, "ad gerekli").max(120, "en fazla 120 karakter olabilir"),
|
|
2671
|
+
isDefault: z.boolean().optional()
|
|
2672
|
+
}).partial();
|
|
2673
|
+
const pipelineResponseSchema = z.object({
|
|
2674
|
+
id: z.uuid(),
|
|
2675
|
+
name: z.string(),
|
|
2676
|
+
isDefault: z.boolean(),
|
|
2677
|
+
position: z.number(),
|
|
2678
|
+
createdAt: z.iso.datetime(),
|
|
2679
|
+
updatedAt: z.iso.datetime()
|
|
2680
|
+
});
|
|
2681
|
+
z.object({ pipelines: z.array(pipelineResponseSchema) });
|
|
2682
|
+
/**
|
|
2683
|
+
* `key`, programatik eşleştirme için kullanılır (ör. `is_won`/`is_lost`
|
|
2684
|
+
* kanban kolonunu bulmak) — bu yüzden slug benzeri kısıtlı bir biçim taşır
|
|
2685
|
+
* (küçük harf, rakam, alt çizgi).
|
|
2686
|
+
*/
|
|
2687
|
+
const stageKeySchema = z.string().trim().min(1, "anahtar gerekli").max(60, "en fazla 60 karakter olabilir").regex(/^[a-z][a-z0-9_]*$/, "küçük harf, rakam ve alt çizgi kullanılabilir");
|
|
2688
|
+
z.object({
|
|
2689
|
+
key: stageKeySchema,
|
|
2690
|
+
label: z.string().trim().min(1, "etiket gerekli").max(120, "en fazla 120 karakter olabilir"),
|
|
2691
|
+
color: pipelineStageColorSchema.optional(),
|
|
2692
|
+
probability: z.number().min(0, "en az 0 olmalı").max(100, "en fazla 100 olmalı").optional(),
|
|
2693
|
+
isWon: z.boolean().optional(),
|
|
2694
|
+
isLost: z.boolean().optional()
|
|
2695
|
+
}).partial();
|
|
2696
|
+
const stageResponseSchema = z.object({
|
|
2697
|
+
id: z.uuid(),
|
|
2698
|
+
pipelineId: z.uuid(),
|
|
2699
|
+
key: z.string(),
|
|
2700
|
+
label: z.string(),
|
|
2701
|
+
color: z.string().nullable(),
|
|
2702
|
+
position: z.number(),
|
|
2703
|
+
probability: z.string().nullable(),
|
|
2704
|
+
isWon: z.boolean(),
|
|
2705
|
+
isLost: z.boolean(),
|
|
2706
|
+
createdAt: z.iso.datetime(),
|
|
2707
|
+
updatedAt: z.iso.datetime()
|
|
2708
|
+
});
|
|
2709
|
+
z.object({ stages: z.array(stageResponseSchema) });
|
|
2710
|
+
z.object({
|
|
2711
|
+
beforeId: z.uuid("geçerli bir kimlik olmalı").optional(),
|
|
2712
|
+
afterId: z.uuid("geçerli bir kimlik olmalı").optional()
|
|
2713
|
+
});
|
|
2714
|
+
//#endregion
|
|
2715
|
+
//#region packages/contracts/src/platform.ts
|
|
2716
|
+
/**
|
|
2717
|
+
* Platform yönetimi sözleşmeleri (spec §5.8) — yalnız Vennyx personeli
|
|
2718
|
+
* (`SOLICRM_ADMIN_EMAILS` allowlist'i, bkz. `apps/api/src/lib/platform-admin.ts`)
|
|
2719
|
+
* tarafından tüketilir.
|
|
2720
|
+
*/
|
|
2721
|
+
const platformTenantSummarySchema = z.object({
|
|
2722
|
+
id: z.uuid(),
|
|
2723
|
+
name: z.string(),
|
|
2724
|
+
slug: z.string(),
|
|
2725
|
+
planCode: planCodeSchema,
|
|
2726
|
+
suspendedAt: z.iso.datetime().nullable(),
|
|
2727
|
+
createdAt: z.iso.datetime(),
|
|
2728
|
+
memberCount: z.number().int().nonnegative()
|
|
2729
|
+
});
|
|
2730
|
+
z.object({ tenants: z.array(platformTenantSummarySchema) });
|
|
2731
|
+
z.object({
|
|
2732
|
+
memberCount: z.number().int().nonnegative(),
|
|
2733
|
+
activeApiKeyCount: z.number().int().nonnegative(),
|
|
2734
|
+
pendingInviteCount: z.number().int().nonnegative(),
|
|
2735
|
+
ssoEnabled: z.boolean()
|
|
2736
|
+
});
|
|
2737
|
+
z.object({ email: z.string() });
|
|
2738
|
+
/**
|
|
2739
|
+
* `GET /v1/admin/tenants/:id/audit-logs` satırı (Faz 9 Task 2, plan kararı "E").
|
|
2740
|
+
*
|
|
2741
|
+
* `data` NEDEN `z.unknown()`: `audit_logs.data` şemasız bir jsonb kolonudur —
|
|
2742
|
+
* her `writeAudit()` çağrısı kendi eylemine özgü bir şekil yazar (18 dosyada 55
|
|
2743
|
+
* çağrı), ortak bir yapı YOKTUR. Burada bir şekil DAYATMAK, bugün geçerli bir
|
|
2744
|
+
* denetim satırını yarın şema hatasıyla GÖRÜNMEZ kılardı — denetim kaydı
|
|
2745
|
+
* append-only olduğu için o satır bir daha DÜZELTİLEMEZ. Zod v4'te `z.unknown()`
|
|
2746
|
+
* anahtarın VARLIĞINI yine zorunlu kılar (ölçüldü: `{}` girdisi `success:false`),
|
|
2747
|
+
* yani "alan hiç gelmedi" ile "alan null" ayrımı KORUNUR.
|
|
2748
|
+
*
|
|
2749
|
+
* KRİTİK — `data` NORMALİZE EDİLMİŞ şekli tanımlar: ham `tx.execute()` yolu bu
|
|
2750
|
+
* kolonu jsonb çift-encode kusuru nedeniyle STRING olarak döndürebilir; route
|
|
2751
|
+
* katmanı tek bir `JSON.parse` ile bunu geri alır (bkz.
|
|
2752
|
+
* `apps/api/src/routes/platform-audit.ts`).
|
|
2753
|
+
*/
|
|
2754
|
+
const adminAuditLogEntrySchema = z.object({
|
|
2755
|
+
id: z.uuid(),
|
|
2756
|
+
actorEmail: z.string().nullable(),
|
|
2757
|
+
action: z.string(),
|
|
2758
|
+
target: z.string().nullable(),
|
|
2759
|
+
data: z.unknown().nullable(),
|
|
2760
|
+
createdAt: z.iso.datetime()
|
|
2761
|
+
});
|
|
2762
|
+
z.object({
|
|
2763
|
+
items: z.array(adminAuditLogEntrySchema),
|
|
2764
|
+
nextCursor: z.string().nullable(),
|
|
2765
|
+
hasMore: z.boolean()
|
|
2766
|
+
});
|
|
2767
|
+
/**
|
|
2768
|
+
* Zehirli/çözülmemiş webhook olayı satırı — `GET /v1/admin/webhook-events`
|
|
2769
|
+
* (Faz 7 Task 6'nın ürettiği uç, Faz 9 Task 3 tüketicisi).
|
|
2770
|
+
*
|
|
2771
|
+
* KRİTİK — `payload` BİLİNÇLİ OLARAK YOK: `webhook_events.payload` Whop'un ham
|
|
2772
|
+
* gövdesidir (müşteri/ödeme bilgisi taşır, `safeJsonb` kolonu). Bu sözleşme onu
|
|
2773
|
+
* TAŞIMAZ ve route katmanı kolonu SELECT bile ETMEZ — operatör ekranı olayı
|
|
2774
|
+
* TANIMLAMAYA yeten alanlarla sınırlıdır (kaynak/tür/zaman/hata). Ham gövdeye
|
|
2775
|
+
* ihtiyaç duyulursa doğru yer DB erişimidir, bir HTTP yanıtı değil.
|
|
2776
|
+
*
|
|
2777
|
+
* `error` SERBEST METİNDİR ve route katmanında hassas sayı dizileri (VKN/TCKN/
|
|
2778
|
+
* kart benzeri) MASKELENMİŞ olarak gelir — bkz. `apps/api/src/lib/mask-sensitive.ts`.
|
|
2779
|
+
*
|
|
2780
|
+
* İKİ "çözülmemiş" sınıfı bu üç alandan TÜRETİLİR (bkz. `lib/webhook-health.ts`):
|
|
2781
|
+
* `error !== null` ⇒ iş kuralı hatası; `error === null && processedAt === null`
|
|
2782
|
+
* ⇒ hiç işlenemedi/sıkıştı.
|
|
2783
|
+
*/
|
|
2784
|
+
const adminWebhookEventSchema = z.object({
|
|
2785
|
+
id: z.uuid(),
|
|
2786
|
+
source: z.string(),
|
|
2787
|
+
eventType: z.string(),
|
|
2788
|
+
receivedAt: z.iso.datetime(),
|
|
2789
|
+
processedAt: z.iso.datetime().nullable(),
|
|
2790
|
+
error: z.string().nullable()
|
|
2791
|
+
});
|
|
2792
|
+
z.object({ items: z.array(adminWebhookEventSchema) });
|
|
2793
|
+
z.object({ reason: z.string().trim().min(1, "kapatma gerekçesi gerekli").max(1e3, "en fazla 1000 karakter olabilir") });
|
|
2794
|
+
z.object({
|
|
2795
|
+
id: z.uuid(),
|
|
2796
|
+
closedAt: z.iso.datetime(),
|
|
2797
|
+
closedReason: z.string(),
|
|
2798
|
+
closedBy: z.string()
|
|
2799
|
+
});
|
|
2800
|
+
z.object({
|
|
2801
|
+
effective: z.boolean(),
|
|
2802
|
+
source: z.enum(["platform-setting", "env-fallback"]),
|
|
2803
|
+
envFallback: z.boolean(),
|
|
2804
|
+
updatedAt: z.iso.datetime().nullable(),
|
|
2805
|
+
updatedByEmail: z.string().nullable()
|
|
2806
|
+
});
|
|
2807
|
+
z.object({ enabled: z.boolean("etkin değeri true veya false olmalı") });
|
|
2808
|
+
z.object({
|
|
2809
|
+
paymentId: z.uuid(),
|
|
2810
|
+
tenantId: z.uuid(),
|
|
2811
|
+
tenantName: z.string(),
|
|
2812
|
+
whopPaymentId: z.string(),
|
|
2813
|
+
paymentStatus: z.string(),
|
|
2814
|
+
/** `numeric(18,2)` — API'de DAİMA dizge (kesinlik korunur, bkz. CLAUDE.md) */
|
|
2815
|
+
amountGross: z.string(),
|
|
2816
|
+
refundedAmount: z.string(),
|
|
2817
|
+
currencyCode: z.string(),
|
|
2818
|
+
/** `'passed' | 'out_of_band' | 'not_verifiable'` — bant dışı bir ödemeyi iade etmek ÖZEL dikkat ister */
|
|
2819
|
+
amountCheckStatus: z.string(),
|
|
2820
|
+
paidAt: z.iso.datetime().nullable(),
|
|
2821
|
+
invoice: z.object({
|
|
2822
|
+
id: z.uuid(),
|
|
2823
|
+
invoiceNumber: z.string(),
|
|
2824
|
+
status: z.string(),
|
|
2825
|
+
amountGross: z.string(),
|
|
2826
|
+
currencyCode: z.string(),
|
|
2827
|
+
formalizedAt: z.iso.datetime().nullable()
|
|
2828
|
+
}).nullable(),
|
|
2829
|
+
billing: z.object({
|
|
2830
|
+
kind: z.string(),
|
|
2831
|
+
country: z.string(),
|
|
2832
|
+
maskedTaxId: z.string().nullable()
|
|
2833
|
+
}).nullable(),
|
|
2834
|
+
openRefund: z.object({
|
|
2835
|
+
id: z.uuid(),
|
|
2836
|
+
status: z.string(),
|
|
2837
|
+
amountGross: z.string(),
|
|
2838
|
+
attemptCount: z.number().int().nonnegative(),
|
|
2839
|
+
lastAttemptAt: z.iso.datetime().nullable()
|
|
2840
|
+
}).nullable()
|
|
2841
|
+
});
|
|
2842
|
+
z.object({
|
|
2843
|
+
refundId: z.uuid(),
|
|
2844
|
+
status: z.enum([
|
|
2845
|
+
"pending",
|
|
2846
|
+
"completed",
|
|
2847
|
+
"awaiting_balance",
|
|
2848
|
+
"failed"
|
|
2849
|
+
]),
|
|
2850
|
+
message: z.string()
|
|
2851
|
+
});
|
|
2852
|
+
const filterLeafSchema = z.object({
|
|
2853
|
+
fieldKey: z.string().trim().min(1, "alan adı gerekli").max(80, "en fazla 80 karakter olabilir"),
|
|
2854
|
+
operand: filterOperandSchema,
|
|
2855
|
+
value: z.unknown()
|
|
2856
|
+
});
|
|
2857
|
+
const filterGroupSchema = z.lazy(() => z.object({
|
|
2858
|
+
logicalOperator: filterLogicalOperatorSchema,
|
|
2859
|
+
filters: z.array(filterLeafSchema).max(20, "bir grupta en fazla 20 filtre olabilir"),
|
|
2860
|
+
groups: z.array(filterGroupSchema).max(5, "bir grupta en fazla 5 alt grup olabilir")
|
|
2861
|
+
}));
|
|
2862
|
+
/** Bir filtre grubu ağacının GERÇEK azami derinliğini ölçer (bkz. dosya başı KRİTİK KURAL) */
|
|
2863
|
+
function filterGroupDepth(group) {
|
|
2864
|
+
if (group.groups.length === 0) return 1;
|
|
2865
|
+
return 1 + Math.max(...group.groups.map(filterGroupDepth));
|
|
2866
|
+
}
|
|
2867
|
+
const fieldSelectionSchema = z.object({
|
|
2868
|
+
fieldKey: z.string().trim().min(1, "alan adı gerekli").max(80, "en fazla 80 karakter olabilir"),
|
|
2869
|
+
isVisible: z.boolean().default(true)
|
|
2870
|
+
});
|
|
2871
|
+
const sortSelectionSchema = z.object({
|
|
2872
|
+
fieldKey: z.string().trim().min(1, "alan adı gerekli").max(80, "en fazla 80 karakter olabilir"),
|
|
2873
|
+
direction: sortDirectionSchema
|
|
2874
|
+
});
|
|
2875
|
+
/** Derinlik aşımını yakalayan paylaşılan `superRefine` — create/update ŞEMASI ikisi de kullanır */
|
|
2876
|
+
function checkFilterGroupDepth(rootFilterGroup, ctx) {
|
|
2877
|
+
if (rootFilterGroup === void 0) return;
|
|
2878
|
+
if (filterGroupDepth(rootFilterGroup) > 5) ctx.addIssue({
|
|
2879
|
+
code: "custom",
|
|
2880
|
+
path: ["rootFilterGroup"],
|
|
2881
|
+
message: `filtre grubu en fazla 5 seviye iç içe olabilir`
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
z.object({
|
|
2885
|
+
name: z.string().trim().min(1, "ad gerekli").max(120, "en fazla 120 karakter olabilir"),
|
|
2886
|
+
icon: z.string().trim().max(60, "en fazla 60 karakter olabilir").optional(),
|
|
2887
|
+
resource: savedViewResourceSchema,
|
|
2888
|
+
layout: savedViewLayoutSchema.default("table"),
|
|
2889
|
+
visibility: savedViewVisibilitySchema.default("private"),
|
|
2890
|
+
groupByFieldKey: z.string().trim().min(1, "alan adı gerekli").max(80, "en fazla 80 karakter olabilir").optional(),
|
|
2891
|
+
isDefault: z.boolean().optional(),
|
|
2892
|
+
fields: z.array(fieldSelectionSchema).max(60, "en fazla 60 kolon olabilir").default([]),
|
|
2893
|
+
sorts: z.array(sortSelectionSchema).max(5, "en fazla 5 sıralama alanı olabilir").default([]),
|
|
2894
|
+
rootFilterGroup: filterGroupSchema.optional()
|
|
2895
|
+
}).superRefine((data, ctx) => checkFilterGroupDepth(data.rootFilterGroup, ctx));
|
|
2896
|
+
z.object({
|
|
2897
|
+
name: z.string().trim().min(1, "ad gerekli").max(120, "en fazla 120 karakter olabilir").optional(),
|
|
2898
|
+
icon: z.string().trim().max(60, "en fazla 60 karakter olabilir").nullable().optional(),
|
|
2899
|
+
layout: savedViewLayoutSchema.optional(),
|
|
2900
|
+
visibility: savedViewVisibilitySchema.optional(),
|
|
2901
|
+
groupByFieldKey: z.string().trim().min(1, "alan adı gerekli").max(80, "en fazla 80 karakter olabilir").nullable().optional(),
|
|
2902
|
+
isDefault: z.boolean().optional(),
|
|
2903
|
+
fields: z.array(fieldSelectionSchema).max(60, "en fazla 60 kolon olabilir").optional(),
|
|
2904
|
+
sorts: z.array(sortSelectionSchema).max(5, "en fazla 5 sıralama alanı olabilir").optional(),
|
|
2905
|
+
rootFilterGroup: filterGroupSchema.nullable().optional()
|
|
2906
|
+
}).strict().superRefine((data, ctx) => {
|
|
2907
|
+
if (data.rootFilterGroup !== null) checkFilterGroupDepth(data.rootFilterGroup, ctx);
|
|
2908
|
+
});
|
|
2909
|
+
const savedViewFieldResponseSchema = z.object({
|
|
2910
|
+
fieldKey: z.string(),
|
|
2911
|
+
isVisible: z.boolean()
|
|
2912
|
+
});
|
|
2913
|
+
const savedViewSortResponseSchema = z.object({
|
|
2914
|
+
fieldKey: z.string(),
|
|
2915
|
+
direction: sortDirectionSchema
|
|
2916
|
+
});
|
|
2917
|
+
z.object({
|
|
2918
|
+
id: z.uuid(),
|
|
2919
|
+
name: z.string(),
|
|
2920
|
+
icon: z.string().nullable(),
|
|
2921
|
+
resource: savedViewResourceSchema,
|
|
2922
|
+
layout: savedViewLayoutSchema,
|
|
2923
|
+
/** Sidebar sırası — `pipelineResponseSchema`/`stageResponseSchema` ile TUTARLILIK için döndürülür (bu fazda bir /reorder ucu YOK, yalnız append-only, bkz. routes/saved-views.ts `nextViewPosition`) */
|
|
2924
|
+
position: z.number(),
|
|
2925
|
+
visibility: savedViewVisibilitySchema,
|
|
2926
|
+
createdById: z.uuid(),
|
|
2927
|
+
groupByFieldKey: z.string().nullable(),
|
|
2928
|
+
isDefault: z.boolean(),
|
|
2929
|
+
fields: z.array(savedViewFieldResponseSchema),
|
|
2930
|
+
sorts: z.array(savedViewSortResponseSchema),
|
|
2931
|
+
rootFilterGroup: filterGroupSchema.nullable(),
|
|
2932
|
+
createdAt: z.iso.datetime(),
|
|
2933
|
+
updatedAt: z.iso.datetime()
|
|
2934
|
+
});
|
|
2935
|
+
/**
|
|
2936
|
+
* Liste ucu (`GET /saved-views`) YANITI — özet şekil, `fields`/`sorts`/
|
|
2937
|
+
* `rootFilterGroup` İÇERMEZ (bkz. `apps/api/src/routes/saved-views.ts`
|
|
2938
|
+
* KRİTİK KURAL — N+1 önleme: bir tenant'ın TÜM görünümlerini listelerken
|
|
2939
|
+
* her biri için 4 ek sorgu (fields/sorts/groups/filters) atmak N+1 olurdu;
|
|
2940
|
+
* tam detay yalnız TEKİL görünüm uçlarında (`GET .../saved-views/:viewId`,
|
|
2941
|
+
* `POST .../apply`) döner — o uçlarda "N" zaten 1'dir).
|
|
2942
|
+
*/
|
|
2943
|
+
const savedViewSummaryResponseSchema = z.object({
|
|
2944
|
+
id: z.uuid(),
|
|
2945
|
+
name: z.string(),
|
|
2946
|
+
icon: z.string().nullable(),
|
|
2947
|
+
resource: savedViewResourceSchema,
|
|
2948
|
+
layout: savedViewLayoutSchema,
|
|
2949
|
+
position: z.number(),
|
|
2950
|
+
visibility: savedViewVisibilitySchema,
|
|
2951
|
+
createdById: z.uuid(),
|
|
2952
|
+
isDefault: z.boolean(),
|
|
2953
|
+
createdAt: z.iso.datetime(),
|
|
2954
|
+
updatedAt: z.iso.datetime()
|
|
2955
|
+
});
|
|
2956
|
+
z.object({ views: z.array(savedViewSummaryResponseSchema) });
|
|
2957
|
+
const searchableResourceSchema = z.enum([
|
|
2958
|
+
"contacts",
|
|
2959
|
+
"companies",
|
|
2960
|
+
"deals"
|
|
2961
|
+
]);
|
|
2962
|
+
const searchResultItemSchema = z.object({
|
|
2963
|
+
id: z.uuid("geçersiz kimlik"),
|
|
2964
|
+
resource: searchableResourceSchema,
|
|
2965
|
+
/** Kaynağın görünen adı — contacts: ad soyad, companies: unvan, deals: başlık */
|
|
2966
|
+
title: z.string(),
|
|
2967
|
+
/** İkincil satır (e-posta / alan adı / tutar) — kolon boşsa `null` */
|
|
2968
|
+
snippet: z.string().nullable()
|
|
2969
|
+
});
|
|
2970
|
+
z.object({ items: z.array(searchResultItemSchema) });
|
|
2971
|
+
const searchQuerySchema = z.object({
|
|
2972
|
+
q: z.string().trim().min(1, "arama metni boş olamaz").max(200, `en fazla 200 karakter olabilir`),
|
|
2973
|
+
limit: z.coerce.number("sayı olmalı").int("tam sayı olmalı").min(1, "en az 1 olmalı").max(50, `en fazla 50 olabilir`).optional(),
|
|
2974
|
+
/**
|
|
2975
|
+
* Aranacak kaynakları daraltır (virgülle ayrılmış). Verilmezse ÜÇÜ de
|
|
2976
|
+
* aranır. Bilinmeyen bir kaynak adı 400 üretir — sessizce yok SAYILMAZ
|
|
2977
|
+
* (fail-closed: "tanımadığım filtreyi görmezden gel" bir fail-open kalıbıdır).
|
|
2978
|
+
*/
|
|
2979
|
+
resources: z.string().transform((raw) => raw.split(",").map((part) => part.trim())).pipe(z.array(searchableResourceSchema).min(1, "en az bir kaynak belirtilmeli")).optional()
|
|
2980
|
+
});
|
|
2981
|
+
//#endregion
|
|
2982
|
+
//#region packages/contracts/src/sso.ts
|
|
2983
|
+
/**
|
|
2984
|
+
* BYO-SSO tenant bağlantısı sözleşmeleri (spec §5.7).
|
|
2985
|
+
*
|
|
2986
|
+
* KRİTİK KURAL: `ssoConnectionResponseSchema` `clientSecret`/`clientSecretEncrypted`
|
|
2987
|
+
* ALANI TAŞIMAZ — sır listeleme/okuma yanıtlarında ASLA dönmez.
|
|
2988
|
+
*/
|
|
2989
|
+
const DOMAIN_RE = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.[a-z0-9-]{1,63})+$/;
|
|
2990
|
+
/**
|
|
2991
|
+
* Genel (kişisel) e-posta sağlayıcı alan adları — bunlar SSO `emailDomain`
|
|
2992
|
+
* olarak KABUL EDİLMEZ (spec'in "gmail.com gibi genel bir alan adını
|
|
2993
|
+
* doğrulamaya çalışırsa?" tehdidi — bir tenant bu alan adını doğrulayabilseydi
|
|
2994
|
+
* TÜM Gmail kullanıcılarını kendi tenant'ına çekebilirdi). TAM/kapsayıcı bir
|
|
2995
|
+
* liste DEĞİLDİR — ileride genişletilebilir bir başlangıç kümesidir (bkz.
|
|
2996
|
+
* plan "Belirsiz bırakılan noktalar" #4).
|
|
2997
|
+
*
|
|
2998
|
+
* Plandaki temel liste (gmail/outlook/yahoo/icloud ailesi ve büyük bölgesel
|
|
2999
|
+
* sağlayıcılar) OLDUĞU GİBİ alındı. Task brifinginin "eksik görürsen genişlet"
|
|
3000
|
+
* talimatıyla implementasyon anında GENUINE olarak eklenenler (regex her biri
|
|
3001
|
+
* AYRI bir alan adı olduğu için kök alan adı listede olsa da eşleşmez):
|
|
3002
|
+
* Microsoft'un bölgesel tüketici alan adları (`outlook.co.uk`, `hotmail.co.uk`,
|
|
3003
|
+
* `live.co.uk`, `hotmail.fr`, `live.fr`, `hotmail.de`, `live.de` — `outlook.com`/
|
|
3004
|
+
* `hotmail.com`/`live.com` zaten listede ama bunlar ayrı TLD'ler) ve büyük ISP
|
|
3005
|
+
* webmail'leri (kurumsal alan adı OLMASI beklenmeyen, geniş bireysel kullanıcı
|
|
3006
|
+
* tabanına sahip): `comcast.net`/`att.net`/`verizon.net`/`sbcglobal.net` (ABD),
|
|
3007
|
+
* `btinternet.com` (İngiltere), `free.fr` (Fransa).
|
|
3008
|
+
*/
|
|
3009
|
+
const PUBLIC_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
3010
|
+
"gmail.com",
|
|
3011
|
+
"googlemail.com",
|
|
3012
|
+
"outlook.com",
|
|
3013
|
+
"hotmail.com",
|
|
3014
|
+
"live.com",
|
|
3015
|
+
"msn.com",
|
|
3016
|
+
"yahoo.com",
|
|
3017
|
+
"yahoo.co.uk",
|
|
3018
|
+
"yahoo.fr",
|
|
3019
|
+
"ymail.com",
|
|
3020
|
+
"icloud.com",
|
|
3021
|
+
"me.com",
|
|
3022
|
+
"mac.com",
|
|
3023
|
+
"aol.com",
|
|
3024
|
+
"protonmail.com",
|
|
3025
|
+
"proton.me",
|
|
3026
|
+
"pm.me",
|
|
3027
|
+
"gmx.com",
|
|
3028
|
+
"gmx.de",
|
|
3029
|
+
"gmx.net",
|
|
3030
|
+
"mail.com",
|
|
3031
|
+
"yandex.com",
|
|
3032
|
+
"yandex.ru",
|
|
3033
|
+
"zoho.com",
|
|
3034
|
+
"fastmail.com",
|
|
3035
|
+
"hey.com",
|
|
3036
|
+
"hushmail.com",
|
|
3037
|
+
"tutanota.com",
|
|
3038
|
+
"tuta.io",
|
|
3039
|
+
"inbox.com",
|
|
3040
|
+
"mail.ru",
|
|
3041
|
+
"qq.com",
|
|
3042
|
+
"163.com",
|
|
3043
|
+
"126.com",
|
|
3044
|
+
"naver.com",
|
|
3045
|
+
"daum.net",
|
|
3046
|
+
"web.de",
|
|
3047
|
+
"t-online.de",
|
|
3048
|
+
"orange.fr",
|
|
3049
|
+
"laposte.net",
|
|
3050
|
+
"libero.it",
|
|
3051
|
+
"virgilio.it",
|
|
3052
|
+
"seznam.cz",
|
|
3053
|
+
"wp.pl",
|
|
3054
|
+
"onet.pl",
|
|
3055
|
+
"rocketmail.com",
|
|
3056
|
+
"outlook.co.uk",
|
|
3057
|
+
"hotmail.co.uk",
|
|
3058
|
+
"live.co.uk",
|
|
3059
|
+
"hotmail.fr",
|
|
3060
|
+
"live.fr",
|
|
3061
|
+
"hotmail.de",
|
|
3062
|
+
"live.de",
|
|
3063
|
+
"comcast.net",
|
|
3064
|
+
"att.net",
|
|
3065
|
+
"verizon.net",
|
|
3066
|
+
"sbcglobal.net",
|
|
3067
|
+
"btinternet.com",
|
|
3068
|
+
"free.fr"
|
|
3069
|
+
]);
|
|
3070
|
+
/**
|
|
3071
|
+
* [Fix round 1 — Minor 1, code review bulgusu]: `PUBLIC_EMAIL_DOMAINS.has(value)`
|
|
3072
|
+
* yalnız TAM eşleşme kontrol ediyordu — `mail.gmail.com` gibi bir ALT alan adı
|
|
3073
|
+
* engeli atlıyordu (canlı doğrulandı: `ACCEPTED` dönüyordu). Pratikte risk
|
|
3074
|
+
* düşük (saldırgan gerçek sağlayıcının alt alan DNS'ini kontrol edemez, TXT
|
|
3075
|
+
* sahiplik kanıtı hâlâ gerekir) ama kafa karıştırıcı/gereksiz bir açık —
|
|
3076
|
+
* engellenen bir kök alan adının HER alt alan adı da (`x.gmail.com`,
|
|
3077
|
+
* `a.b.gmail.com` vb.) reddedilir. `value` zaten `.toLowerCase()`'dan SONRA
|
|
3078
|
+
* buraya gelir (zincirleme sırası), bu yüzden büyük/küçük harf normalize
|
|
3079
|
+
* edilmiş durumda.
|
|
3080
|
+
*/
|
|
3081
|
+
function isPublicEmailProviderDomain(domain) {
|
|
3082
|
+
if (PUBLIC_EMAIL_DOMAINS.has(domain)) return true;
|
|
3083
|
+
for (const blocked of PUBLIC_EMAIL_DOMAINS) if (domain.endsWith(`.${blocked}`)) return true;
|
|
3084
|
+
return false;
|
|
3085
|
+
}
|
|
3086
|
+
const ssoEmailDomainSchema = z.string().trim().toLowerCase().max(255, "en fazla 255 karakter olabilir").regex(DOMAIN_RE, "geçerli bir alan adı olmalı (örn. ornek.com)").refine((value) => !isPublicEmailProviderDomain(value), "genel bir e-posta sağlayıcı alan adı SSO için kullanılamaz");
|
|
3087
|
+
/**
|
|
3088
|
+
* Format kontrolü — prod'da yalnız https zorunluluğu ROUTE katmanında ayrıca
|
|
3089
|
+
* uygulanır (bkz. sso-admin.ts, `NODE_ENV==='production'` kontrolü).
|
|
3090
|
+
*
|
|
3091
|
+
* [Fix round 1 — Minor 2, code review bulgusu]: bu şema ÖNCEDEN
|
|
3092
|
+
* `http://127.0.0.1:PORT`'u da (localhost ile AYNI) dev istisnasına
|
|
3093
|
+
* sokuyordu, ama `lib/ssrf-guard.ts`'in `assertPublicHttpsUrl`'i yalnız
|
|
3094
|
+
* `localhost` HOST ADINI dev-bypass eder — çıplak `127.0.0.1` IP'sini HER
|
|
3095
|
+
* ZAMAN reddeder (bkz. o dosyanın KRİTİK KURAL 1 yorumu: kritik güvenlik
|
|
3096
|
+
* kuralı "özel IP aralıklarına (…, 127/8, …) gitmemeli" der, istisna YALNIZ
|
|
3097
|
+
* `localhost` için tanımlıdır). Sonuç: `http://127.0.0.1:9000` issuer'ıyla
|
|
3098
|
+
* bağlantı OLUŞTURULABİLİYORDU (bu şema kabul ediyordu) ama `/test` ucu HER
|
|
3099
|
+
* ZAMAN başarısız oluyordu (guard reddediyordu) — güvenlik açığı değil ama
|
|
3100
|
+
* kafa karıştırıcı bir tutarsızlık.
|
|
3101
|
+
*
|
|
3102
|
+
* ÇÖZÜM YÖNÜ — şema DARALTILDI (guard GENİŞLETİLMEDİ): guard'ın davranışı
|
|
3103
|
+
* kritik güvenlik kuralının kelimesi kelimesine yazdığı, implementasyon
|
|
3104
|
+
* anında canlı test edilip reviewer tarafından bağımsız doğrulanmış bir
|
|
3105
|
+
* karardı (bkz. Task 1 raporu) — bunu GEVŞETMEK yerine şemayı guard'a
|
|
3106
|
+
* UYDURMAK, sistemin her yerinde AYNI (daha sıkı) kuralı korur. Artık yalnız
|
|
3107
|
+
* `http://localhost` (port'lu/portsuz) dev istisnası kabul edilir; çıplak
|
|
3108
|
+
* `127.0.0.1` (ve `::1`) issuer olarak asla kabul edilmez — bu ikisi zaten
|
|
3109
|
+
* `ssrf-guard.ts`'te de reddediliyordu.
|
|
3110
|
+
*/
|
|
3111
|
+
const ssoIssuerSchema = z.url("geçerli bir URL olmalı").max(500, "en fazla 500 karakter olabilir").refine((value) => value.startsWith("https://") || /^http:\/\/localhost(:\d+)?(\/|$)/.test(value), "issuer https olmalı (yalnız geliştirmede localhost istisnası var)");
|
|
3112
|
+
z.object({
|
|
3113
|
+
issuer: ssoIssuerSchema,
|
|
3114
|
+
clientId: z.string().trim().min(1, "clientId gerekli").max(255, "en fazla 255 karakter olabilir"),
|
|
3115
|
+
clientSecret: z.string().trim().min(1, "clientSecret gerekli").max(2e3, "en fazla 2000 karakter olabilir"),
|
|
3116
|
+
emailDomain: ssoEmailDomainSchema
|
|
3117
|
+
});
|
|
3118
|
+
z.object({
|
|
3119
|
+
id: z.uuid(),
|
|
3120
|
+
issuer: z.string(),
|
|
3121
|
+
clientId: z.string(),
|
|
3122
|
+
emailDomain: z.string(),
|
|
3123
|
+
domainVerificationToken: z.string(),
|
|
3124
|
+
domainVerified: z.boolean(),
|
|
3125
|
+
enabled: z.boolean(),
|
|
3126
|
+
createdAt: z.iso.datetime(),
|
|
3127
|
+
updatedAt: z.iso.datetime()
|
|
3128
|
+
});
|
|
3129
|
+
z.object({
|
|
3130
|
+
ok: z.boolean(),
|
|
3131
|
+
reason: z.string().optional()
|
|
3132
|
+
});
|
|
3133
|
+
z.object({
|
|
3134
|
+
code: z.literal("integration_requires_plan"),
|
|
3135
|
+
requiredPlans: z.array(z.enum(["team", "enterprise"]))
|
|
3136
|
+
});
|
|
3137
|
+
z.object({
|
|
3138
|
+
ssoAvailable: z.boolean(),
|
|
3139
|
+
tenantId: z.uuid().optional(),
|
|
3140
|
+
tenantName: z.string().optional()
|
|
3141
|
+
});
|
|
3142
|
+
z.object({ code: z.string().min(1, "code gerekli").max(200, "en fazla 200 karakter olabilir") });
|
|
3143
|
+
//#endregion
|
|
3144
|
+
//#region packages/mcp/src/config.ts
|
|
3145
|
+
/**
|
|
3146
|
+
* `npx @vennyx/solicrm-mcp`'nin ortam değişkeninden yapılandırma okuması.
|
|
3147
|
+
*
|
|
3148
|
+
* KRİTİK KURAL 1 — ZORUNLU DEĞER YOKSA AÇIK HATA, VARSAYILANA DÜŞÜLMEZ.
|
|
3149
|
+
* `SOLICRM_API_KEY` ve `SOLICRM_TENANT_ID` ikisi de ZORUNLUDUR ve eksikse
|
|
3150
|
+
* süreç net bir Türkçe mesajla ölür. Sessiz bir varsayılan (`''`, `'default'`)
|
|
3151
|
+
* ilk araç çağrısında anlaşılmaz bir 401/404'e dönüşürdü — bu projenin beş
|
|
3152
|
+
* ayrı yerden çıkmış kusur sınıfı (bkz. kök `CLAUDE.md` "Sessiz kusurlar").
|
|
3153
|
+
*
|
|
3154
|
+
* KRİTİK KURAL 2 — `SOLICRM_TENANT_ID` NEDEN GEREKLİ. SoliCRM'in URL şeması
|
|
3155
|
+
* `/v1/tenants/:tenantId/...`'dir (bkz. `@vennyx/solicrm`'in
|
|
3156
|
+
* `TenantScopedResource`) ve API anahtarının hangi tenant'a bağlı olduğunu
|
|
3157
|
+
* SÖYLEYEN bir uç YOK — anahtarı çözen tek yer sunucunun kendi
|
|
3158
|
+
* `app_resolve_api_key`'idir ve o HTTP'ye açık değildir. Yani istemci URL'i
|
|
3159
|
+
* kuramaz. İleride "anahtarımın tenant'ı ne" ucu eklenirse bu env
|
|
3160
|
+
* GEREKSİZLEŞİR (barındırılan `POST /mcp` ucu zaten tenant'ı anahtardan
|
|
3161
|
+
* çözdüğü için ondan hiçbir env istemez).
|
|
3162
|
+
*
|
|
3163
|
+
* KRİTİK KURAL 3 — `SOLICRM_SCOPE` BİR GÜVENLİK KAPISI DEĞİLDİR, UX
|
|
3164
|
+
* FİLTRESİDİR. Bu değer `tools/list`'in hangi araçları göstereceğini belirler
|
|
3165
|
+
* (bkz. `scope.ts` KRİTİK KURAL 3a: "LLM göremediği aracı denemez"). GERÇEK
|
|
3166
|
+
* yetki sınırı sunucudadır ve HER istekte yeniden hesaplanır
|
|
3167
|
+
* (`ROLE_SCOPE_CEILING[güncel rol] ∩ anahtar kapsamı`,
|
|
3168
|
+
* `apps/api/src/lib/crm-access.ts`). Bu yüzden VERİLMEZSE `admin`'e düşer:
|
|
3169
|
+
* tüm araçlar kaydedilir, yetkisiz olanı sunucu 403'le reddeder. `read`'e
|
|
3170
|
+
* düşmek daha "güvenli" GÖRÜNÜR ama değildir — hiçbir şeyi kapatmaz, yalnız
|
|
3171
|
+
* yazma yetkisi OLAN bir anahtarın yazma araçlarını görünmez yapıp
|
|
3172
|
+
* kullanıcının teşhis edemeyeceği bir arıza üretir. VERİLİRSE ve geçersizse
|
|
3173
|
+
* (`Admin`, `full`, ...) SESSİZCE varsayılana düşülMEZ, açık hata verilir.
|
|
3174
|
+
*/
|
|
3175
|
+
/** Bkz. KRİTİK KURAL 3 — kapsam verilmediğinde sunucuyu yetkili taraf sayarız */
|
|
3176
|
+
const DEFAULT_MCP_SCOPE = "admin";
|
|
3177
|
+
function readTrimmed(env, name) {
|
|
3178
|
+
const raw = env[name];
|
|
3179
|
+
if (raw === void 0) return void 0;
|
|
3180
|
+
const trimmed = raw.trim();
|
|
3181
|
+
return trimmed.length === 0 ? void 0 : trimmed;
|
|
3182
|
+
}
|
|
3183
|
+
function loadConfigFromEnv(env) {
|
|
3184
|
+
const apiKey = readTrimmed(env, "SOLICRM_API_KEY");
|
|
3185
|
+
if (apiKey === void 0) throw new Error("SOLICRM_API_KEY ortam değişkeni zorunludur (dashboard → API anahtarları bölümünden oluşturun).");
|
|
3186
|
+
const tenantId = readTrimmed(env, "SOLICRM_TENANT_ID");
|
|
3187
|
+
if (tenantId === void 0) throw new Error("SOLICRM_TENANT_ID ortam değişkeni zorunludur (dashboard → kiracı ayarlarındaki kiracı kimliği).");
|
|
3188
|
+
const rawScope = readTrimmed(env, "SOLICRM_SCOPE");
|
|
3189
|
+
let scope = DEFAULT_MCP_SCOPE;
|
|
3190
|
+
if (rawScope !== void 0) {
|
|
3191
|
+
const parsed = apiKeyScopeSchema.safeParse(rawScope);
|
|
3192
|
+
if (!parsed.success) throw new Error(`SOLICRM_SCOPE geçersiz: "${rawScope}". Geçerli değerler: read, write, admin (boş bırakılırsa ${DEFAULT_MCP_SCOPE}).`);
|
|
3193
|
+
scope = parsed.data;
|
|
3194
|
+
}
|
|
3195
|
+
return {
|
|
3196
|
+
apiKey,
|
|
3197
|
+
tenantId,
|
|
3198
|
+
baseUrl: readTrimmed(env, "SOLICRM_BASE_URL"),
|
|
3199
|
+
scope
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
//#endregion
|
|
3203
|
+
//#region packages/mcp/src/prompts.ts
|
|
3204
|
+
function registerSummarizePipelinePrompt(server) {
|
|
3205
|
+
return server.registerPrompt("summarize_pipeline", {
|
|
3206
|
+
title: "Pipeline özeti",
|
|
3207
|
+
description: "Guides an agent through summarising the health of one sales pipeline using the SoliCRM tools.",
|
|
3208
|
+
argsSchema: { pipelineName: z.string().describe("Name of the pipeline to summarise, as shown by list_pipelines") }
|
|
3209
|
+
}, ({ pipelineName }) => ({ messages: [{
|
|
3210
|
+
role: "user",
|
|
3211
|
+
content: {
|
|
3212
|
+
type: "text",
|
|
3213
|
+
text: [
|
|
3214
|
+
`Summarise the health of the SoliCRM pipeline named "${pipelineName}".`,
|
|
3215
|
+
"1. Call list_pipelines and find the pipeline whose name matches; note its id and the ordered stages.",
|
|
3216
|
+
"2. Call list_deals with filters { pipeline_id: \"<that id>\" } and page through with the returned nextCursor until hasMore is false.",
|
|
3217
|
+
"3. Group the deals by stage. For each stage report the count, the summed amount and the oldest deal.",
|
|
3218
|
+
"4. Amounts are decimal STRINGS (numeric(18,2)) — add them with exact decimal arithmetic, never with floating-point math, and keep the currency code with every total.",
|
|
3219
|
+
"5. Close with the two or three stages that look most at risk (largest value sitting in the earliest stages, or deals with the oldest expectedCloseDate)."
|
|
3220
|
+
].join("\n")
|
|
3221
|
+
}
|
|
3222
|
+
}] }));
|
|
3223
|
+
}
|
|
3224
|
+
//#endregion
|
|
3225
|
+
//#region packages/mcp/src/errors.ts
|
|
3226
|
+
/**
|
|
3227
|
+
* MCP katmanının KENDİ hata sınıfları — sunucudan GELMEYEN, bu paketin
|
|
3228
|
+
* ürettiği başarısızlıklar.
|
|
3229
|
+
*
|
|
3230
|
+
* DİL İSTİSNASI (Global Constraints #8): buradaki mesajlar İngilizce'dir.
|
|
3231
|
+
* "Kullanıcıya dönen mesajlar Türkçe" kuralı `apps/api`'nin `jsonError()`
|
|
3232
|
+
* zarfı içindir; bu paket global npm tüketicilerine (Claude Code/Desktop) gider
|
|
3233
|
+
* ve mesajları bir LLM okur. Sunucudan gelen TÜRKÇE metin ise ÇEVRİLMEZ,
|
|
3234
|
+
* `toMcpToolError` onu AYNEN taşır (bkz. `result.ts`).
|
|
3235
|
+
*/
|
|
3236
|
+
/** Bu paketin ürettiği her hatanın taban sınıfı — tüketici tek `catch` ile kapsar. */
|
|
3237
|
+
var SolicrmMcpError = class extends Error {
|
|
3238
|
+
constructor(message) {
|
|
3239
|
+
super(message);
|
|
3240
|
+
this.name = "SolicrmMcpError";
|
|
3241
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
3242
|
+
}
|
|
3243
|
+
};
|
|
3244
|
+
/**
|
|
3245
|
+
* Çağıranın etkin kapsamı aracın gerektirdiği minimumun ALTINDA.
|
|
3246
|
+
*
|
|
3247
|
+
* KRİTİK KURAL — bu hata TEK yetki sınırı DEĞİLDİR, İKİNCİ katmandır. Gerçek
|
|
3248
|
+
* yetki kontrolü sunucu tarafındadır: her araç çağrısı SDK üzerinden
|
|
3249
|
+
* `/v1/tenants/:id/...` uçlarına gider ve orada
|
|
3250
|
+
* `checkApiKeyTenantAndScope` (`apps/api/src/lib/crm-access.ts`) çalışır — o
|
|
3251
|
+
* kontrol `effectiveScope`'u (= `ROLE_SCOPE_CEILING[güncel rol] ∩ anahtar
|
|
3252
|
+
* kapsamı`) HER istekte DB'den YENİDEN hesaplar. Bu paketteki `scope` ise
|
|
3253
|
+
* sunucu kurulurken bir kez alınmış bir ANLIK GÖRÜNTÜDÜR (bkz. `scope.ts`).
|
|
3254
|
+
*/
|
|
3255
|
+
var McpScopeError = class extends SolicrmMcpError {
|
|
3256
|
+
toolName;
|
|
3257
|
+
requiredScope;
|
|
3258
|
+
actualScope;
|
|
3259
|
+
constructor(params) {
|
|
3260
|
+
super(`Tool "${params.toolName}" requires the "${params.requiredScope}" API key scope, but this key's effective scope is "${params.actualScope}".`);
|
|
3261
|
+
this.name = "McpScopeError";
|
|
3262
|
+
this.toolName = params.toolName;
|
|
3263
|
+
this.requiredScope = params.requiredScope;
|
|
3264
|
+
this.actualScope = params.actualScope;
|
|
3265
|
+
}
|
|
3266
|
+
};
|
|
3267
|
+
/**
|
|
3268
|
+
* Araç girdisi zod şemasını GEÇTİ ama anlamsal olarak tutarsız (ör. sıralama
|
|
3269
|
+
* yönü verilmiş, sıralanacak alan verilmemiş).
|
|
3270
|
+
*
|
|
3271
|
+
* NEDEN AYRI BİR HATA: bu projenin "fail-open savunma savunma değildir"
|
|
3272
|
+
* dersinin doğrudan uygulaması — tutarsız girdiyi SESSİZCE yok saymak
|
|
3273
|
+
* (`sortDirection`'ı görmezden gelip sırasız sonuç döndürmek) çağırana
|
|
3274
|
+
* "istediğin sıralamayı yaptım" yalanını söylerdi.
|
|
3275
|
+
*/
|
|
3276
|
+
var McpInputError = class extends SolicrmMcpError {
|
|
3277
|
+
constructor(message) {
|
|
3278
|
+
super(message);
|
|
3279
|
+
this.name = "McpInputError";
|
|
3280
|
+
}
|
|
3281
|
+
};
|
|
3282
|
+
//#endregion
|
|
3283
|
+
//#region packages/mcp/src/scope.ts
|
|
3284
|
+
/**
|
|
3285
|
+
* Araç adı → gereken MİNİMUM coarse kapsam kataloğu, ve iki kapsam katmanının
|
|
3286
|
+
* paylaştığı karşılaştırma.
|
|
3287
|
+
*
|
|
3288
|
+
* KRİTİK KURAL 1 — KATALOG TOTAL, `Record<string, ApiKeyScope>` DEĞİL.
|
|
3289
|
+
* `TOOL_MIN_SCOPE`'un anahtarı `McpToolName` literal union'ıdır, bu yüzden
|
|
3290
|
+
* `TOOL_MIN_SCOPE[name]` HİÇBİR ZAMAN `undefined` olamaz (`noUncheckedIndexedAccess`
|
|
3291
|
+
* altında bile). Plan taslağı `Record<string, ApiKeyScope>` + `if (need !== undefined)`
|
|
3292
|
+
* yazıyordu; o biçimde katalogda UNUTULAN bir araç adı `undefined` verir ve kapı
|
|
3293
|
+
* SESSİZCE HİÇ UYGULANMAZ — projenin "fail-open savunma savunma değildir"
|
|
3294
|
+
* dersinin ta kendisi. Bu biçimde ise yeni bir araç adı eklerken kapsamını
|
|
3295
|
+
* yazmamak DERLEME HATASIDIR.
|
|
3296
|
+
*
|
|
3297
|
+
* KRİTİK KURAL 2 — BU PAKETTEKİ `scope` BİR ANLIK GÖRÜNTÜDÜR, YETKİ SINIRI
|
|
3298
|
+
* DEĞİLDİR. `createSolicrmMcpServer(client, scope)` çağrıldığı anda geçerli olan
|
|
3299
|
+
* etkin kapsam kapanışta (closure) taşınır; süreç yaşarken YENİDEN
|
|
3300
|
+
* HESAPLANMAZ. Gerçek yetki sınırı sunucudadır: her araç çağrısı SDK üzerinden
|
|
3301
|
+
* API'ye gider ve `checkApiKeyTenantAndScope` (`apps/api/src/lib/crm-access.ts`)
|
|
3302
|
+
* `effectiveScope`'u (= `ROLE_SCOPE_CEILING[güncel rol] ∩ anahtar kapsamı`) HER
|
|
3303
|
+
* istekte DB'den yeniden hesaplar — bu yüzden anahtarın sahibi demote edilince
|
|
3304
|
+
* ayrı bir iptal job'ı gerekmiyor. Plan Task 3 Step 4'ün "çağrı-anı kontrolü
|
|
3305
|
+
* gerekli ÇÜNKÜ stdio süreci uzun ömürlü ve sahibin rolü düşürülebilir"
|
|
3306
|
+
* gerekçesi YANLIŞTIR: çağrı-anı kontrolü de AYNI dondurulmuş `scope`'u okur,
|
|
3307
|
+
* demote'u göremez. Çağrı-anı kontrolünün GERÇEK değeri aşağıdaki KRİTİK
|
|
3308
|
+
* KURAL 3'tür.
|
|
3309
|
+
*
|
|
3310
|
+
* KRİTİK KURAL 3 — İKİ KATMAN, İKİ AYRI KUSURA KARŞI. (a) Kayıt-anı
|
|
3311
|
+
* `.disable()` `tools/list`'i filtreler: LLM göremediği bir aracı denemez
|
|
3312
|
+
* (bağlam kirliliği + gereksiz 403 turu yok). (b) Çağrı-anı `assertScope`
|
|
3313
|
+
* BİZİM KENDİ REGRESYONUMUZA karşıdır: `server.ts`'in disable döngüsü
|
|
3314
|
+
* kaldırılır/yanlış sıraya konursa ya da bir araç kayıt listesine eklenip
|
|
3315
|
+
* döngüye girmezse, handler yine reddeder. İkisi bağımsız testlerle sabitlenir.
|
|
3316
|
+
*/
|
|
3317
|
+
/**
|
|
3318
|
+
* Kapsam rütbeleri — `apps/api/src/lib/api-key-auth.ts`'in `SCOPE_RANK`'ıyla
|
|
3319
|
+
* BİREBİR aynı. İçe aktarılMAZ çünkü bu paket npm'e yayınlanır ve `apps/api`'ye
|
|
3320
|
+
* (Hono, Drizzle, Postgres) bağımlı OLAMAZ. Drift riski TİP SEVİYESİNDE
|
|
3321
|
+
* kapatılmıştır: `Record<ApiKeyScope, number>` `@solicrm/contracts`'ın
|
|
3322
|
+
* `apiKeyScopeSchema` enum'una bağlıdır ⇒ contracts'a dördüncü bir kapsam
|
|
3323
|
+
* eklenirse bu satır DERLENMEZ.
|
|
3324
|
+
*/
|
|
3325
|
+
const SCOPE_RANK = {
|
|
3326
|
+
read: 0,
|
|
3327
|
+
write: 1,
|
|
3328
|
+
admin: 2
|
|
3329
|
+
};
|
|
3330
|
+
apiKeyScopeSchema.options;
|
|
3331
|
+
const TOOL_MIN_SCOPE = {
|
|
3332
|
+
list_contacts: "read",
|
|
3333
|
+
get_contact: "read",
|
|
3334
|
+
create_contact: "write",
|
|
3335
|
+
update_contact: "write",
|
|
3336
|
+
list_companies: "read",
|
|
3337
|
+
get_company: "read",
|
|
3338
|
+
create_company: "write",
|
|
3339
|
+
update_company: "write",
|
|
3340
|
+
list_deals: "read",
|
|
3341
|
+
get_deal: "read",
|
|
3342
|
+
create_deal: "write",
|
|
3343
|
+
update_deal: "write",
|
|
3344
|
+
move_deal_stage: "write",
|
|
3345
|
+
list_pipelines: "read",
|
|
3346
|
+
list_activities: "read",
|
|
3347
|
+
create_activity: "write",
|
|
3348
|
+
list_tasks: "read",
|
|
3349
|
+
create_task: "write",
|
|
3350
|
+
complete_task: "write",
|
|
3351
|
+
create_note: "write",
|
|
3352
|
+
search: "read",
|
|
3353
|
+
list_saved_views: "read"
|
|
3354
|
+
};
|
|
3355
|
+
/** `actual`, `required` kapsamını karşılıyor mu (read < write < admin) */
|
|
3356
|
+
function scopeSatisfies(actual, required) {
|
|
3357
|
+
return SCOPE_RANK[actual] >= SCOPE_RANK[required];
|
|
3358
|
+
}
|
|
3359
|
+
/**
|
|
3360
|
+
* Çağrı-anı kapsam kapısı — HER araç handler'ının İLK satırı (bkz. KRİTİK
|
|
3361
|
+
* KURAL 3/b). Yetersizse fırlatır; `toMcpToolError` bunu `isError: true`
|
|
3362
|
+
* sonucuna çevirir.
|
|
3363
|
+
*/
|
|
3364
|
+
function assertScope(scope, toolName) {
|
|
3365
|
+
const required = TOOL_MIN_SCOPE[toolName];
|
|
3366
|
+
if (!scopeSatisfies(scope, required)) throw new McpScopeError({
|
|
3367
|
+
toolName,
|
|
3368
|
+
requiredScope: required,
|
|
3369
|
+
actualScope: scope
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
//#endregion
|
|
3373
|
+
//#region packages/mcp/src/resources.ts
|
|
3374
|
+
/** Kaynak → gereken minimum kapsam. `TOOL_MIN_SCOPE` gibi TOTAL (fail-open yok). */
|
|
3375
|
+
const RESOURCE_MIN_SCOPE = {
|
|
3376
|
+
contact: "read",
|
|
3377
|
+
deal: "read"
|
|
3378
|
+
};
|
|
3379
|
+
/** URI şablonu değişkeni tek değer de dizi de olabilir — ilkini alır, yoksa hata. */
|
|
3380
|
+
function firstVariable(value, name) {
|
|
3381
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
3382
|
+
if (first === void 0 || first.length === 0) throw new McpInputError(`Resource URI is missing the "${name}" segment.`);
|
|
3383
|
+
return first;
|
|
3384
|
+
}
|
|
3385
|
+
function assertResourceScope(scope, name) {
|
|
3386
|
+
const required = RESOURCE_MIN_SCOPE[name];
|
|
3387
|
+
if (!scopeSatisfies(scope, required)) throw new McpInputError(`Resource "${name}" requires the "${required}" API key scope, but this key's effective scope is "${scope}".`);
|
|
3388
|
+
}
|
|
3389
|
+
function registerCrmResources(server, client, scope) {
|
|
3390
|
+
return [["contact", server.registerResource("contact", new ResourceTemplate("solicrm://contacts/{id}", { list: void 0 }), {
|
|
3391
|
+
title: "SoliCRM kişisi",
|
|
3392
|
+
mimeType: "application/json"
|
|
3393
|
+
}, async (uri, variables) => {
|
|
3394
|
+
assertResourceScope(scope, "contact");
|
|
3395
|
+
const contact = await client.contacts.get(firstVariable(variables.id, "id"));
|
|
3396
|
+
return { contents: [{
|
|
3397
|
+
uri: uri.href,
|
|
3398
|
+
mimeType: "application/json",
|
|
3399
|
+
text: JSON.stringify(contact, null, 2)
|
|
3400
|
+
}] };
|
|
3401
|
+
})], ["deal", server.registerResource("deal", new ResourceTemplate("solicrm://deals/{id}", { list: void 0 }), {
|
|
3402
|
+
title: "SoliCRM fırsatı",
|
|
3403
|
+
mimeType: "application/json"
|
|
3404
|
+
}, async (uri, variables) => {
|
|
3405
|
+
assertResourceScope(scope, "deal");
|
|
3406
|
+
const deal = await client.deals.get(firstVariable(variables.id, "id"));
|
|
3407
|
+
return { contents: [{
|
|
3408
|
+
uri: uri.href,
|
|
3409
|
+
mimeType: "application/json",
|
|
3410
|
+
text: JSON.stringify(deal, null, 2)
|
|
3411
|
+
}] };
|
|
3412
|
+
})]];
|
|
3413
|
+
}
|
|
3414
|
+
//#endregion
|
|
3415
|
+
//#region packages/mcp/src/result.ts
|
|
3416
|
+
/** Beklenmeyen (bizim kusurumuz olan) hatalar için TEK jenerik metin. */
|
|
3417
|
+
const UNEXPECTED_ERROR_MESSAGE = "An unexpected error occurred while calling the SoliCRM API.";
|
|
3418
|
+
/**
|
|
3419
|
+
* SDK'nın hata sınıflarının `this.name` değerlerinin KAPALI listesi. Listede
|
|
3420
|
+
* OLMAYAN her şey jenerik metne düşer (fail-closed).
|
|
3421
|
+
*/
|
|
3422
|
+
const SDK_ERROR_NAMES = [
|
|
3423
|
+
"SolicrmError",
|
|
3424
|
+
"SolicrmApiError",
|
|
3425
|
+
"SolicrmResponseError"
|
|
3426
|
+
];
|
|
3427
|
+
/** Her aracın ORTAK başarı biçimi — veriyi okunabilir (girintili) JSON metne çevirir. */
|
|
3428
|
+
function jsonResult(data) {
|
|
3429
|
+
return { content: [{
|
|
3430
|
+
type: "text",
|
|
3431
|
+
text: JSON.stringify(data, null, 2)
|
|
3432
|
+
}] };
|
|
3433
|
+
}
|
|
3434
|
+
/** Bir hatayı MCP'nin `isError: true` sonucuna çevirir (JSON-RPC hatası DEĞİL). */
|
|
3435
|
+
function toMcpToolError(err) {
|
|
3436
|
+
return {
|
|
3437
|
+
isError: true,
|
|
3438
|
+
content: [{
|
|
3439
|
+
type: "text",
|
|
3440
|
+
text: describeError(err)
|
|
3441
|
+
}]
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
/** Ham gövdeyi + status'ü koruyan API hatası okuması; hata bu şekilde değilse `undefined`. */
|
|
3445
|
+
function readApiError(err) {
|
|
3446
|
+
if (!(err instanceof Error) || err.name !== "SolicrmApiError") return void 0;
|
|
3447
|
+
if (!("status" in err) || typeof err.status !== "number") return void 0;
|
|
3448
|
+
if (!("body" in err) || !("serverMessage" in err)) return void 0;
|
|
3449
|
+
const serverMessage = err.serverMessage;
|
|
3450
|
+
return {
|
|
3451
|
+
status: err.status,
|
|
3452
|
+
body: err.body,
|
|
3453
|
+
serverMessage: typeof serverMessage === "string" ? serverMessage : void 0,
|
|
3454
|
+
message: err.message
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
function describeError(err) {
|
|
3458
|
+
if (err instanceof SolicrmMcpError) return err.message;
|
|
3459
|
+
const apiError = readApiError(err);
|
|
3460
|
+
if (apiError !== void 0) return JSON.stringify({
|
|
3461
|
+
status: apiError.status,
|
|
3462
|
+
message: apiError.serverMessage ?? apiError.message,
|
|
3463
|
+
body: apiError.body
|
|
3464
|
+
}, null, 2);
|
|
3465
|
+
if (err instanceof Error && SDK_ERROR_NAMES.includes(err.name)) return err.message;
|
|
3466
|
+
return UNEXPECTED_ERROR_MESSAGE;
|
|
3467
|
+
}
|
|
3468
|
+
//#endregion
|
|
3469
|
+
//#region packages/mcp/src/tools/define.ts
|
|
3470
|
+
/**
|
|
3471
|
+
* Bir araç handler'ını MCP callback'ine çevirir: kapsam kapısı → handler →
|
|
3472
|
+
* `jsonResult`; her fırlatma `isError: true` sonucuna dönüşür (JSON-RPC
|
|
3473
|
+
* hatasına DEĞİL — LLM hatayı okuyup düzeltebilsin).
|
|
3474
|
+
*/
|
|
3475
|
+
function guarded(scope, name, handler) {
|
|
3476
|
+
return async (args) => {
|
|
3477
|
+
try {
|
|
3478
|
+
assertScope(scope, name);
|
|
3479
|
+
return jsonResult(await handler(args));
|
|
3480
|
+
} catch (err) {
|
|
3481
|
+
return toMcpToolError(err);
|
|
3482
|
+
}
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
/** Girdi almayan araçlar için (`list_pipelines`) — aynı üç garanti, argümansız. */
|
|
3486
|
+
function guardedNoArgs(scope, name, handler) {
|
|
3487
|
+
return async () => {
|
|
3488
|
+
try {
|
|
3489
|
+
assertScope(scope, name);
|
|
3490
|
+
return jsonResult(await handler());
|
|
3491
|
+
} catch (err) {
|
|
3492
|
+
return toMcpToolError(err);
|
|
3493
|
+
}
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
//#endregion
|
|
3497
|
+
//#region packages/mcp/src/tools/ids.ts
|
|
3498
|
+
/**
|
|
3499
|
+
* Araç girdilerindeki kimlik (id) alanlarının TEK tanımı. `@solicrm/contracts`
|
|
3500
|
+
* kaynak gövdelerinde `z.uuid('geçerli bir <şey> kimliği olmalı')` kalıbını
|
|
3501
|
+
* kullanıyor; yol parametreleri için bir sözleşme şeması YOK (uçlar onları yol
|
|
3502
|
+
* segmentinden okur), bu yüzden aynı kalıp burada tek yerde kurulur — 8 araç
|
|
3503
|
+
* dosyasında tekrarlanmaz.
|
|
3504
|
+
*/
|
|
3505
|
+
/** `label` Türkçe okunur bir isimdir ("kişi", "şirket", "fırsat"...) */
|
|
3506
|
+
function idField(label) {
|
|
3507
|
+
return z.uuid(`geçerli bir ${label} kimliği olmalı`);
|
|
3508
|
+
}
|
|
3509
|
+
//#endregion
|
|
3510
|
+
//#region packages/mcp/src/tools/activities.ts
|
|
3511
|
+
const listActivitiesInputSchema = pageQuerySchema.extend({
|
|
3512
|
+
targetType: targetTypeSchema.optional(),
|
|
3513
|
+
targetId: idField("hedef").optional()
|
|
3514
|
+
});
|
|
3515
|
+
function toActivityListQuery(args) {
|
|
3516
|
+
if (args.targetType !== void 0 !== (args.targetId !== void 0)) throw new McpInputError("targetType and targetId must be provided together. Send both to filter by a target, or neither to list every activity.");
|
|
3517
|
+
const query = {
|
|
3518
|
+
cursor: args.cursor,
|
|
3519
|
+
limit: args.limit
|
|
3520
|
+
};
|
|
3521
|
+
if (args.targetType !== void 0 && args.targetId !== void 0) query.target = {
|
|
3522
|
+
type: args.targetType,
|
|
3523
|
+
id: args.targetId
|
|
3524
|
+
};
|
|
3525
|
+
return query;
|
|
3526
|
+
}
|
|
3527
|
+
function registerActivityTools(server, client, scope) {
|
|
3528
|
+
return [["list_activities", server.registerTool("list_activities", {
|
|
3529
|
+
title: "Aktiviteleri listele",
|
|
3530
|
+
description: "Lists activities, cursor-paginated. Pass targetType AND targetId together to see one record’s timeline.",
|
|
3531
|
+
inputSchema: listActivitiesInputSchema
|
|
3532
|
+
}, guarded(scope, "list_activities", (args) => client.activities.list(toActivityListQuery(args))))], ["create_activity", server.registerTool("create_activity", {
|
|
3533
|
+
title: "Aktivite oluştur",
|
|
3534
|
+
description: "Logs an activity (call, meeting, email, note...) against a contact, company or deal. `happensAt` is an ISO 8601 UTC timestamp.",
|
|
3535
|
+
inputSchema: createActivityRequestSchema
|
|
3536
|
+
}, guarded(scope, "create_activity", (args) => client.activities.create(args)))]];
|
|
3537
|
+
}
|
|
3538
|
+
//#endregion
|
|
3539
|
+
//#region packages/mcp/src/list-input.ts
|
|
3540
|
+
/**
|
|
3541
|
+
* CRM liste araçlarının (`list_contacts`/`list_companies`/`list_deals`/
|
|
3542
|
+
* `list_tasks`) PAYLAŞTIĞI girdi şeması ve SDK sorgusuna çevirisi.
|
|
3543
|
+
*
|
|
3544
|
+
* KRİTİK KURAL 1 — ALAN KATALOĞU KOPYALANMAZ. Geçerli `sortField`/`filters`
|
|
3545
|
+
* anahtarları `@solicrm/contracts`'ın `FIELD_CATALOG`'undan ÇALIŞMA ZAMANINDA
|
|
3546
|
+
* okunur (araç açıklaması bile oradan üretilir, bkz. `describeCrmListFields`);
|
|
3547
|
+
* bu dosyada tek bir alan adı literali YOKTUR. Doğrulamanın kendisi de burada
|
|
3548
|
+
* TEKRARLANMAZ — SDK'nın `buildCrmListQuery`'si `resolveField` + `sortable`/
|
|
3549
|
+
* `filterable` bayrak kontrolünü zaten yapıyor ve bilinmeyen/uygun olmayan
|
|
3550
|
+
* alanda `SolicrmError` fırlatıyor (fail-closed).
|
|
3551
|
+
*
|
|
3552
|
+
* KRİTİK KURAL 2 — SAYFA SINIRLARI DA KOPYALANMAZ. `cursor`/`limit`
|
|
3553
|
+
* `pageQuerySchema`'dan `.extend()` ile MİRAS ALINIR ⇒ `PAGE_MAX_LIMIT`
|
|
3554
|
+
* değişirse araç şeması da değişir. `z.coerce.number()`'ın JSON Schema
|
|
3555
|
+
* karşılığının `{"type":"integer","minimum":1,"maximum":100}` olduğu canlı
|
|
3556
|
+
* ölçüldü — MCP tarafında sorun ÜRETMİYOR.
|
|
3557
|
+
*
|
|
3558
|
+
* KRİTİK KURAL 3 — `sortDirection` VERİLİP `sortField` VERİLMEZSE SESSİZCE
|
|
3559
|
+
* YOK SAYILMAZ, HATA VERİLİR (`McpInputError`). "Tanımadığım/eksik girdiyi
|
|
3560
|
+
* görmezden gel" bir fail-open kalıbıdır: çağıran sıralama istediğini sanır,
|
|
3561
|
+
* sırasız sonuç alır.
|
|
3562
|
+
*
|
|
3563
|
+
* KRİTİK KURAL 4 — ALAN ADLARI KANONİK KATALOĞA KARŞI DOĞRULANIR, DESENE
|
|
3564
|
+
* KARŞI DEĞİL. İlk uygulama `propertyNames` deseni (`^[a-z][a-z0-9_]*$`)
|
|
3565
|
+
* kullanıyor ve JSDoc'u "`__proto__`/`constructor` şema seviyesinde
|
|
3566
|
+
* reddedilir" diye İDDİA ediyordu. **Ölçüm bu iddiayı ÇÜRÜTTÜ:** `constructor`
|
|
3567
|
+
* deseni SAĞLIYOR (tamamı küçük harf) ⇒ belgelenen koruma YOKTU. Bu, bu
|
|
3568
|
+
* deponun "belgelenen koruma hiç çağrılmamış olabilir" kusur sınıfının ta
|
|
3569
|
+
* kendisi. Düzeltme: anahtarlar `resolveField(resource, key)` ile doğrulanır;
|
|
3570
|
+
* o fonksiyon `Object.hasOwn` allowlist'i olduğu için prototip zincirinden
|
|
3571
|
+
* gelen hiçbir ad kolon adına dönüşemez. Bu yüzden şema artık bir FABRİKADIR
|
|
3572
|
+
* — kaynağı bilmeden doğrulama yapılamaz.
|
|
3573
|
+
*
|
|
3574
|
+
* `__proto__` İSTİSNASI, DÜRÜST HÂLİ (ölçüldü): `zod@4.4.3`'ün `z.record`'u
|
|
3575
|
+
* `JSON.parse` ile gelen own `__proto__` anahtarını **doğrulamadan SESSİZCE
|
|
3576
|
+
* ATAR** — `safeParse` `success: true` döner ama çıktı `{}`'dir, yani anahtar
|
|
3577
|
+
* `refine`'a HİÇ ULAŞMAZ. Sonuç güvenlik açısından güvenlidir (anahtar
|
|
3578
|
+
* `buildCrmListQuery`'ye ve dolayısıyla SQL'e hiç geçmez) ama bir REDDETME
|
|
3579
|
+
* DEĞİL, bir DÜŞÜRMEDİR; `test/list-input.test.ts` bu ayrımı olduğu gibi
|
|
3580
|
+
* sabitler. Diğer prototip adları (`constructor`, `hasOwnProperty`,
|
|
3581
|
+
* `toString`, `valueOf`, `prototype`) GERÇEKTEN reddedilir.
|
|
3582
|
+
*
|
|
3583
|
+
* BAYRAK KONTROLÜ BURADA TEKRARLANMAZ: `sortable`/`filterable` denetimi
|
|
3584
|
+
* SDK'nın `buildCrmListQuery`'sinde yaşar (TEK sahip). Buradaki kontrol yalnız
|
|
3585
|
+
* "bu kaynakta böyle bir alan VAR MI" sorusunu yanıtlar.
|
|
3586
|
+
*/
|
|
3587
|
+
/**
|
|
3588
|
+
* Bir CRM kaynağı için liste aracı girdi şeması. Alan adları o kaynağın
|
|
3589
|
+
* `FIELD_CATALOG` girdisine karşı doğrulanır (bkz. KRİTİK KURAL 4).
|
|
3590
|
+
*/
|
|
3591
|
+
function buildCrmListInputSchema(resource) {
|
|
3592
|
+
const knownField = (key) => resolveField(resource, key) !== void 0;
|
|
3593
|
+
const unknownFieldMessage = `${resource} kaynağında böyle bir alan yok`;
|
|
3594
|
+
return pageQuerySchema.extend({
|
|
3595
|
+
sortField: z.string().trim().refine(knownField, unknownFieldMessage).optional(),
|
|
3596
|
+
sortDirection: sortDirectionSchema.optional(),
|
|
3597
|
+
filters: z.record(z.string().trim().refine(knownField, unknownFieldMessage), z.string().max(500, "en fazla 500 karakter olabilir")).optional()
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
/**
|
|
3601
|
+
* Araç girdisini SDK'nın `CrmListQuery`'sine çevirir. `tenantId` DİYE BİR ALAN
|
|
3602
|
+
* YOKTUR ve olamaz — tenant her zaman `SolicrmClient`'ın kurulduğu andaki
|
|
3603
|
+
* değerdir (bkz. `server.ts` dosya başı).
|
|
3604
|
+
*/
|
|
3605
|
+
function toCrmListQuery(args) {
|
|
3606
|
+
if (args.sortField === void 0 && args.sortDirection !== void 0) throw new McpInputError("sortDirection was given without sortField. Provide the field to sort by, or omit both.");
|
|
3607
|
+
const query = {
|
|
3608
|
+
cursor: args.cursor,
|
|
3609
|
+
limit: args.limit,
|
|
3610
|
+
filters: args.filters
|
|
3611
|
+
};
|
|
3612
|
+
if (args.sortField !== void 0) query.sort = {
|
|
3613
|
+
field: args.sortField,
|
|
3614
|
+
direction: args.sortDirection ?? "asc"
|
|
3615
|
+
};
|
|
3616
|
+
return query;
|
|
3617
|
+
}
|
|
3618
|
+
/**
|
|
3619
|
+
* Bir kaynağın sıralanabilir/filtrelenebilir alanlarını araç açıklamasına
|
|
3620
|
+
* yazılacak metne çevirir — LLM'in geçersiz alan adı denemesini azaltır.
|
|
3621
|
+
* Liste `FIELD_CATALOG`'dan ÜRETİLİR, elle yazılmaz.
|
|
3622
|
+
*/
|
|
3623
|
+
function describeCrmListFields(resource) {
|
|
3624
|
+
const catalog = FIELD_CATALOG[resource];
|
|
3625
|
+
const sortable = [];
|
|
3626
|
+
const filterable = [];
|
|
3627
|
+
for (const [key, def] of Object.entries(catalog)) {
|
|
3628
|
+
if (def.sortable) sortable.push(key);
|
|
3629
|
+
if (def.filterable) filterable.push(key);
|
|
3630
|
+
}
|
|
3631
|
+
return `Sortable fields: ${sortable.join(", ")}. Filterable fields: ${filterable.join(", ")}.`;
|
|
3632
|
+
}
|
|
3633
|
+
//#endregion
|
|
3634
|
+
//#region packages/mcp/src/tools/companies.ts
|
|
3635
|
+
const listCompaniesInputSchema = buildCrmListInputSchema("companies");
|
|
3636
|
+
const getCompanyInputSchema = z.object({ companyId: idField("şirket") });
|
|
3637
|
+
const updateCompanyInputSchema = updateCompanyRequestSchema.extend({ companyId: idField("şirket") });
|
|
3638
|
+
function registerCompanyTools(server, client, scope) {
|
|
3639
|
+
return [
|
|
3640
|
+
["list_companies", server.registerTool("list_companies", {
|
|
3641
|
+
title: "Şirketleri listele",
|
|
3642
|
+
description: `Lists companies in the tenant, cursor-paginated. ${describeCrmListFields("companies")}`,
|
|
3643
|
+
inputSchema: listCompaniesInputSchema
|
|
3644
|
+
}, guarded(scope, "list_companies", (args) => client.companies.list(toCrmListQuery(args))))],
|
|
3645
|
+
["get_company", server.registerTool("get_company", {
|
|
3646
|
+
title: "Şirket getir",
|
|
3647
|
+
description: "Fetches a single company by id.",
|
|
3648
|
+
inputSchema: getCompanyInputSchema
|
|
3649
|
+
}, guarded(scope, "get_company", (args) => client.companies.get(args.companyId)))],
|
|
3650
|
+
["create_company", server.registerTool("create_company", {
|
|
3651
|
+
title: "Şirket oluştur",
|
|
3652
|
+
description: "Creates a company. `annualRevenue` is a decimal STRING (e.g. \"1250000.00\"), never a JSON number — the API stores it as numeric(18,2).",
|
|
3653
|
+
inputSchema: createCompanyRequestSchema
|
|
3654
|
+
}, guarded(scope, "create_company", (args) => client.companies.create(args)))],
|
|
3655
|
+
["update_company", server.registerTool("update_company", {
|
|
3656
|
+
title: "Şirketi güncelle",
|
|
3657
|
+
description: "Updates a company. Only the fields you send are changed.",
|
|
3658
|
+
inputSchema: updateCompanyInputSchema
|
|
3659
|
+
}, guarded(scope, "update_company", ({ companyId, ...body }) => client.companies.update(companyId, body)))]
|
|
3660
|
+
];
|
|
3661
|
+
}
|
|
3662
|
+
//#endregion
|
|
3663
|
+
//#region packages/mcp/src/tools/contacts.ts
|
|
3664
|
+
const listContactsInputSchema = buildCrmListInputSchema("contacts");
|
|
3665
|
+
const getContactInputSchema = z.object({ contactId: idField("kişi") });
|
|
3666
|
+
const updateContactInputSchema = updateContactRequestSchema.extend({ contactId: idField("kişi") });
|
|
3667
|
+
function registerContactTools(server, client, scope) {
|
|
3668
|
+
return [
|
|
3669
|
+
["list_contacts", server.registerTool("list_contacts", {
|
|
3670
|
+
title: "Kişileri listele",
|
|
3671
|
+
description: `Lists contacts in the tenant, cursor-paginated. ${describeCrmListFields("contacts")}`,
|
|
3672
|
+
inputSchema: listContactsInputSchema
|
|
3673
|
+
}, guarded(scope, "list_contacts", (args) => client.contacts.list(toCrmListQuery(args))))],
|
|
3674
|
+
["get_contact", server.registerTool("get_contact", {
|
|
3675
|
+
title: "Kişi getir",
|
|
3676
|
+
description: "Fetches a single contact by id.",
|
|
3677
|
+
inputSchema: getContactInputSchema
|
|
3678
|
+
}, guarded(scope, "get_contact", (args) => client.contacts.get(args.contactId)))],
|
|
3679
|
+
["create_contact", server.registerTool("create_contact", {
|
|
3680
|
+
title: "Kişi oluştur",
|
|
3681
|
+
description: "Creates a contact. Counts against the tenant plan limit — a 402 result carries the limit and plan code in its body.",
|
|
3682
|
+
inputSchema: createContactRequestSchema
|
|
3683
|
+
}, guarded(scope, "create_contact", (args) => client.contacts.create(args)))],
|
|
3684
|
+
["update_contact", server.registerTool("update_contact", {
|
|
3685
|
+
title: "Kişiyi güncelle",
|
|
3686
|
+
description: "Updates a contact. Only the fields you send are changed.",
|
|
3687
|
+
inputSchema: updateContactInputSchema
|
|
3688
|
+
}, guarded(scope, "update_contact", ({ contactId, ...body }) => client.contacts.update(contactId, body)))]
|
|
3689
|
+
];
|
|
3690
|
+
}
|
|
3691
|
+
//#endregion
|
|
3692
|
+
//#region packages/mcp/src/tools/deals.ts
|
|
3693
|
+
const listDealsInputSchema = buildCrmListInputSchema("deals");
|
|
3694
|
+
const getDealInputSchema = z.object({ dealId: idField("fırsat") });
|
|
3695
|
+
const updateDealInputSchema = updateDealRequestSchema.extend({ dealId: idField("fırsat") });
|
|
3696
|
+
const moveDealInputSchema = moveDealRequestSchema.extend({ dealId: idField("fırsat") });
|
|
3697
|
+
function registerDealTools(server, client, scope) {
|
|
3698
|
+
return [
|
|
3699
|
+
["list_deals", server.registerTool("list_deals", {
|
|
3700
|
+
title: "Fırsatları listele",
|
|
3701
|
+
description: `Lists deals in the tenant, cursor-paginated. ${describeCrmListFields("deals")}`,
|
|
3702
|
+
inputSchema: listDealsInputSchema
|
|
3703
|
+
}, guarded(scope, "list_deals", (args) => client.deals.list(toCrmListQuery(args))))],
|
|
3704
|
+
["get_deal", server.registerTool("get_deal", {
|
|
3705
|
+
title: "Fırsat getir",
|
|
3706
|
+
description: "Fetches a single deal by id, including its stage and linked contacts.",
|
|
3707
|
+
inputSchema: getDealInputSchema
|
|
3708
|
+
}, guarded(scope, "get_deal", (args) => client.deals.get(args.dealId)))],
|
|
3709
|
+
["create_deal", server.registerTool("create_deal", {
|
|
3710
|
+
title: "Fırsat oluştur",
|
|
3711
|
+
description: "Creates a deal in a pipeline stage. `amount` is a decimal STRING (e.g. \"45000.00\"), never a JSON number. Call list_pipelines first to get valid pipelineId/stageId values.",
|
|
3712
|
+
inputSchema: createDealRequestSchema
|
|
3713
|
+
}, guarded(scope, "create_deal", (args) => client.deals.create(args)))],
|
|
3714
|
+
["update_deal", server.registerTool("update_deal", {
|
|
3715
|
+
title: "Fırsatı güncelle",
|
|
3716
|
+
description: "Updates a deal. Stage/pipeline changes are rejected here by design — use move_deal_stage instead.",
|
|
3717
|
+
inputSchema: updateDealInputSchema
|
|
3718
|
+
}, guarded(scope, "update_deal", ({ dealId, ...body }) => client.deals.update(dealId, body)))],
|
|
3719
|
+
["move_deal_stage", server.registerTool("move_deal_stage", {
|
|
3720
|
+
title: "Fırsatı aşamaya taşı",
|
|
3721
|
+
description: "Moves a deal to another stage (kanban). Optional beforeId/afterId place it between two neighbouring deals; omit both to append.",
|
|
3722
|
+
inputSchema: moveDealInputSchema
|
|
3723
|
+
}, guarded(scope, "move_deal_stage", ({ dealId, ...body }) => client.deals.move(dealId, body)))]
|
|
3724
|
+
];
|
|
3725
|
+
}
|
|
3726
|
+
//#endregion
|
|
3727
|
+
//#region packages/mcp/src/tools/notes.ts
|
|
3728
|
+
function registerNoteTools(server, client, scope) {
|
|
3729
|
+
return [["create_note", server.registerTool("create_note", {
|
|
3730
|
+
title: "Not oluştur",
|
|
3731
|
+
description: "Creates a note. `body` is a free-form JSON object (rich text document). NOTE: the note is created unlinked — it will not appear on any contact/company/deal timeline until it is attached, which this MCP server does not expose.",
|
|
3732
|
+
inputSchema: createNoteRequestSchema
|
|
3733
|
+
}, guarded(scope, "create_note", (args) => client.notes.create(args)))]];
|
|
3734
|
+
}
|
|
3735
|
+
//#endregion
|
|
3736
|
+
//#region packages/mcp/src/tools/pipelines.ts
|
|
3737
|
+
function registerPipelineTools(server, client, scope) {
|
|
3738
|
+
return [["list_pipelines", server.registerTool("list_pipelines", {
|
|
3739
|
+
title: "Pipeline’ları listele",
|
|
3740
|
+
description: "Lists every pipeline with its ordered stages. Use this to resolve pipelineId/stageId before creating or moving a deal."
|
|
3741
|
+
}, guardedNoArgs(scope, "list_pipelines", () => client.pipelines.list()))]];
|
|
3742
|
+
}
|
|
3743
|
+
//#endregion
|
|
3744
|
+
//#region packages/mcp/src/tools/search.ts
|
|
3745
|
+
const searchInputSchema = z.object({
|
|
3746
|
+
q: searchQuerySchema.shape.q,
|
|
3747
|
+
limit: searchQuerySchema.shape.limit,
|
|
3748
|
+
resources: z.array(searchableResourceSchema).min(1, "en az bir kaynak belirtilmeli").optional()
|
|
3749
|
+
});
|
|
3750
|
+
function toSearchParams(args) {
|
|
3751
|
+
return {
|
|
3752
|
+
q: args.q,
|
|
3753
|
+
limit: args.limit,
|
|
3754
|
+
resources: args.resources
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
function registerSearchTools(server, client, scope) {
|
|
3758
|
+
return [["search", server.registerTool("search", {
|
|
3759
|
+
title: "Çapraz arama",
|
|
3760
|
+
description: "Full-text search across contacts, companies and deals in one call. Returns the best matches only (single page, no cursor). Omit `resources` to search all three.",
|
|
3761
|
+
inputSchema: searchInputSchema
|
|
3762
|
+
}, guarded(scope, "search", (args) => client.search.query(toSearchParams(args))))]];
|
|
3763
|
+
}
|
|
3764
|
+
//#endregion
|
|
3765
|
+
//#region packages/mcp/src/tools/tasks.ts
|
|
3766
|
+
const listTasksInputSchema = buildCrmListInputSchema("tasks");
|
|
3767
|
+
const completeTaskInputSchema = z.object({ taskId: idField("görev") });
|
|
3768
|
+
function registerTaskTools(server, client, scope) {
|
|
3769
|
+
return [
|
|
3770
|
+
["list_tasks", server.registerTool("list_tasks", {
|
|
3771
|
+
title: "Görevleri listele",
|
|
3772
|
+
description: `Lists tasks in the tenant, cursor-paginated. ${describeCrmListFields("tasks")}`,
|
|
3773
|
+
inputSchema: listTasksInputSchema
|
|
3774
|
+
}, guarded(scope, "list_tasks", (args) => client.tasks.list(toCrmListQuery(args))))],
|
|
3775
|
+
["create_task", server.registerTool("create_task", {
|
|
3776
|
+
title: "Görev oluştur",
|
|
3777
|
+
description: "Creates a task. NOTE: the task is created unlinked — it will not appear on any contact/company/deal timeline until it is attached, which this MCP server does not expose. `dueAt` is an ISO 8601 UTC timestamp.",
|
|
3778
|
+
inputSchema: createTaskRequestSchema
|
|
3779
|
+
}, guarded(scope, "create_task", (args) => client.tasks.create(args)))],
|
|
3780
|
+
["complete_task", server.registerTool("complete_task", {
|
|
3781
|
+
title: "Görevi tamamla",
|
|
3782
|
+
description: "Marks a task as completed and returns the updated task.",
|
|
3783
|
+
inputSchema: completeTaskInputSchema
|
|
3784
|
+
}, guarded(scope, "complete_task", (args) => client.tasks.complete(args.taskId)))]
|
|
3785
|
+
];
|
|
3786
|
+
}
|
|
3787
|
+
//#endregion
|
|
3788
|
+
//#region packages/mcp/src/tools/views.ts
|
|
3789
|
+
const listSavedViewsInputSchema = z.object({ resource: savedViewResourceSchema.optional() });
|
|
3790
|
+
function registerSavedViewTools(server, client, scope) {
|
|
3791
|
+
return [["list_saved_views", server.registerTool("list_saved_views", {
|
|
3792
|
+
title: "Kaydedilmiş görünümleri listele",
|
|
3793
|
+
description: "Lists saved views (the tenant’s stored filter/sort presets) with their fields, sorts and filters. Optionally narrow to one CRM resource.",
|
|
3794
|
+
inputSchema: listSavedViewsInputSchema
|
|
3795
|
+
}, guarded(scope, "list_saved_views", (args) => client.views.list(args)))]];
|
|
3796
|
+
}
|
|
3797
|
+
//#endregion
|
|
3798
|
+
//#region packages/mcp/src/tools/index.ts
|
|
3799
|
+
function registerAllTools(server, client, scope) {
|
|
3800
|
+
return [
|
|
3801
|
+
...registerContactTools(server, client, scope),
|
|
3802
|
+
...registerCompanyTools(server, client, scope),
|
|
3803
|
+
...registerDealTools(server, client, scope),
|
|
3804
|
+
...registerPipelineTools(server, client, scope),
|
|
3805
|
+
...registerActivityTools(server, client, scope),
|
|
3806
|
+
...registerTaskTools(server, client, scope),
|
|
3807
|
+
...registerNoteTools(server, client, scope),
|
|
3808
|
+
...registerSearchTools(server, client, scope),
|
|
3809
|
+
...registerSavedViewTools(server, client, scope)
|
|
3810
|
+
];
|
|
3811
|
+
}
|
|
3812
|
+
//#endregion
|
|
3813
|
+
//#region packages/mcp/src/server.ts
|
|
3814
|
+
/**
|
|
3815
|
+
* SoliCRM ürün MCP sunucusu — 22 araç, 2 kaynak, 1 prompt. TRANSPORT-AGNOSTİK:
|
|
3816
|
+
* bu dosya ne HTTP ne stdio bilir; çağıran (Task 4) `server.connect(transport)`
|
|
3817
|
+
* ile bir taşımaya bağlar.
|
|
3818
|
+
*
|
|
3819
|
+
* KRİTİK KURAL 1 — TENANT HER ZAMAN `SolicrmClient`'IN KURULDUĞU ANDAKİ
|
|
3820
|
+
* DEĞERDİR, HİÇBİR ARAÇ GİRDİSİNDEN GELMEZ. Bu iddia üç bağımsız mekanizmayla
|
|
3821
|
+
* ayakta duruyor ve üçü de ölçülmüş durumda:
|
|
3822
|
+
* (a) Hiçbir araç girdi şemasında `tenantId` alanı TANIMLI DEĞİL, ve
|
|
3823
|
+
* `test/server.test.ts` `tools/list`'in ÜRETTİĞİ JSON Schema'ları
|
|
3824
|
+
* tarayarak bunu her araç için doğruluyor (gelecekte eklenen bir araç da
|
|
3825
|
+
* yakalanır).
|
|
3826
|
+
* (b) MCP SDK girdiyi zod ile ayrıştırır ve zod obje şemaları BİLİNMEYEN
|
|
3827
|
+
* ANAHTARLARI ATAR (canlı ölçüldü: `{firstName, lastName, tenantId:
|
|
3828
|
+
* 'ATTACKER-TENANT'}` → handler `{firstName, lastName, status:'lead'}`
|
|
3829
|
+
* görüyor) ⇒ bir LLM `tenantId` uydursa bile handler'a hiç ULAŞMAZ.
|
|
3830
|
+
* (c) Araç handler'ları argümanları SDK'nın `CallOptions` (`{ tenantId }`)
|
|
3831
|
+
* parametresine GEÇİRMEZ — `guarded()`'ın imzası öyle bir kanal
|
|
3832
|
+
* sunmuyor (bkz. `tools/define.ts` KRİTİK KURAL 1).
|
|
3833
|
+
* `[H4]` kanıtı `test/tenant-isolation.test.ts`'te: şemaya GEÇİCİ bir
|
|
3834
|
+
* `tenantId` alanı eklenip (yani (b) devre dışı bırakılıp) isteğin YİNE
|
|
3835
|
+
* client'ın tenant'ına gittiği ölçülüyor.
|
|
3836
|
+
*
|
|
3837
|
+
* KRİTİK KURAL 2 — STATELESS: her `createSolicrmMcpServer` çağrısı KENDİ
|
|
3838
|
+
* kapanışını kurar. Paylaşılan mutable state, modül düzeyi cache veya
|
|
3839
|
+
* `reset*()` gerektiren bir singleton YOKTUR ⇒ eşzamanlı isteklerde yarış
|
|
3840
|
+
* yapısal olarak imkânsız. Bu pakete cache EKLENMEZ; gerekirse önce
|
|
3841
|
+
* "ilk dolum yarışı + hata sonrası zehirlenme" sorularının ikisi de
|
|
3842
|
+
* yanıtlanır (bkz. kök `CLAUDE.md` "Yeni cache = iki zorunlu soru").
|
|
3843
|
+
*
|
|
3844
|
+
* KRİTİK KURAL 3 — KAPSAM İKİ KATMANLIDIR, ama İKİSİ DE SUNUCUNUN YERİNE
|
|
3845
|
+
* GEÇMEZ. Ayrıntı ve dürüst sınırlar için bkz. `scope.ts` KRİTİK KURAL 2/3.
|
|
3846
|
+
*/
|
|
3847
|
+
/** `dist/cli.js`'nin (Task 4) `package.json` okumadan taşıdığı sürüm sabiti. */
|
|
3848
|
+
const SOLICRM_MCP_VERSION = "0.1.0";
|
|
3849
|
+
const SOLICRM_MCP_SERVER_NAME = "solicrm";
|
|
3850
|
+
/**
|
|
3851
|
+
* Verilen istemci ve etkin kapsam için TAZE bir `McpServer` kurar.
|
|
3852
|
+
*
|
|
3853
|
+
* @param client Kurulmuş `SolicrmClient` — `tenantId`'si (ve API anahtarı)
|
|
3854
|
+
* çağıranın kimliğinden gelir; bu fonksiyon onu SORGULAMAZ ve DEĞİŞTİRMEZ.
|
|
3855
|
+
* @param scope Çağıranın ŞU ANDAKİ etkin kapsamı (`ROLE_SCOPE_CEILING[rol] ∩
|
|
3856
|
+
* anahtar kapsamı`). Anlık görüntüdür — bkz. `scope.ts` KRİTİK KURAL 2.
|
|
3857
|
+
*/
|
|
3858
|
+
function createSolicrmMcpServer(client, scope) {
|
|
3859
|
+
const server = new McpServer({
|
|
3860
|
+
name: SOLICRM_MCP_SERVER_NAME,
|
|
3861
|
+
version: SOLICRM_MCP_VERSION
|
|
3862
|
+
});
|
|
3863
|
+
const toolHandles = registerAllTools(server, client, scope);
|
|
3864
|
+
const resourceHandles = registerCrmResources(server, client, scope);
|
|
3865
|
+
registerSummarizePipelinePrompt(server);
|
|
3866
|
+
for (const [name, handle] of toolHandles) if (!scopeSatisfies(scope, TOOL_MIN_SCOPE[name])) handle.disable();
|
|
3867
|
+
for (const [name, handle] of resourceHandles) if (!scopeSatisfies(scope, RESOURCE_MIN_SCOPE[name])) handle.disable();
|
|
3868
|
+
return server;
|
|
3869
|
+
}
|
|
3870
|
+
const DEFAULT_HTTP_PORT = 3737;
|
|
3871
|
+
/** `--http` sunucusunun MCP yolu */
|
|
3872
|
+
const MCP_HTTP_PATH = "/mcp";
|
|
3873
|
+
const BROWSER_ORIGIN_REJECTED_MESSAGE = "Tarayıcıdan gelen istekler reddedilir (Origin başlığı taşıyor).";
|
|
3874
|
+
const METHOD_NOT_ALLOWED_MESSAGE = "Bu uçta yalnız POST desteklenir (stateless MCP).";
|
|
3875
|
+
const NOT_FOUND_MESSAGE = `Yalnız POST ${MCP_HTTP_PATH} desteklenir.`;
|
|
3876
|
+
function jsonResponse(status, body, headers = {}) {
|
|
3877
|
+
return new Response(JSON.stringify(body), {
|
|
3878
|
+
status,
|
|
3879
|
+
headers: {
|
|
3880
|
+
"content-type": "application/json",
|
|
3881
|
+
...headers
|
|
3882
|
+
}
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
/** Bkz. KRİTİK KURAL 1(b) — saf, tek satırlık, ayrı test edilebilir karar */
|
|
3886
|
+
function hasBrowserOrigin(request) {
|
|
3887
|
+
const origin = request.headers.get("origin");
|
|
3888
|
+
return origin !== null && origin.trim().length > 0;
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* `Request → Response`. Port açmaz, soket kurmaz ⇒ testte doğrudan çağrılır.
|
|
3892
|
+
* Protokol hataları JSON-RPC zarfıyla transport'tan gelir; BU katmanın kendi
|
|
3893
|
+
* redleri (405/404/403) `{error}` zarfını kullanır — hosted uçtaki AYNI ayrım
|
|
3894
|
+
* (bkz. `apps/api/src/routes/mcp.ts` KRİTİK KURAL 5).
|
|
3895
|
+
*/
|
|
3896
|
+
function createMcpHttpHandler(deps) {
|
|
3897
|
+
return async (request) => {
|
|
3898
|
+
if (hasBrowserOrigin(request)) return jsonResponse(403, { error: BROWSER_ORIGIN_REJECTED_MESSAGE });
|
|
3899
|
+
const { pathname } = new URL(request.url);
|
|
3900
|
+
if (pathname !== "/mcp") return jsonResponse(404, { error: NOT_FOUND_MESSAGE });
|
|
3901
|
+
if (request.method !== "POST") return jsonResponse(405, { error: METHOD_NOT_ALLOWED_MESSAGE }, { Allow: "POST" });
|
|
3902
|
+
const server = deps.createServer();
|
|
3903
|
+
const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
|
|
3904
|
+
await server.connect(transport);
|
|
3905
|
+
return await transport.handleRequest(request);
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
/**
|
|
3909
|
+
* `node:http` yapıştırıcısı — Node ve Bun'da AYNI şekilde çalışır (`Bun.serve`
|
|
3910
|
+
* kullanılmadı: paket npm'e yayınlanıyor ve `npx` altında Node ile koşuyor).
|
|
3911
|
+
*/
|
|
3912
|
+
async function startMcpHttpServer(options) {
|
|
3913
|
+
const { createServer } = await import("node:http");
|
|
3914
|
+
const host = options.host ?? "127.0.0.1";
|
|
3915
|
+
const port = options.port ?? 3737;
|
|
3916
|
+
const server = createServer((req, res) => {
|
|
3917
|
+
(async () => {
|
|
3918
|
+
try {
|
|
3919
|
+
const chunks = [];
|
|
3920
|
+
for await (const chunk of req) chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
3921
|
+
const method = req.method ?? "GET";
|
|
3922
|
+
const headers = new Headers();
|
|
3923
|
+
for (const [key, value] of Object.entries(req.headers)) if (typeof value === "string") headers.set(key, value);
|
|
3924
|
+
else if (Array.isArray(value)) for (const item of value) headers.append(key, item);
|
|
3925
|
+
const hasBody = method !== "GET" && method !== "HEAD" && chunks.length > 0;
|
|
3926
|
+
const request = new Request(`http://${host}:${port}${req.url ?? "/"}`, {
|
|
3927
|
+
method,
|
|
3928
|
+
headers,
|
|
3929
|
+
...hasBody ? { body: Buffer.concat(chunks) } : {}
|
|
3930
|
+
});
|
|
3931
|
+
for (const [key, value] of headers) request.headers.set(key, value);
|
|
3932
|
+
const response = await options.handler(request);
|
|
3933
|
+
const outHeaders = {};
|
|
3934
|
+
response.headers.forEach((value, key) => {
|
|
3935
|
+
outHeaders[key] = value;
|
|
3936
|
+
});
|
|
3937
|
+
res.writeHead(response.status, outHeaders);
|
|
3938
|
+
res.end(response.body === null ? void 0 : Buffer.from(await response.arrayBuffer()));
|
|
3939
|
+
} catch (error) {
|
|
3940
|
+
console.error("solicrm-mcp http isteği başarısız:", error);
|
|
3941
|
+
if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" });
|
|
3942
|
+
res.end(JSON.stringify({ error: "Sunucu hatası" }));
|
|
3943
|
+
}
|
|
3944
|
+
})();
|
|
3945
|
+
});
|
|
3946
|
+
await new Promise((resolve, reject) => {
|
|
3947
|
+
server.once("error", reject);
|
|
3948
|
+
server.listen(port, host, () => {
|
|
3949
|
+
server.removeListener("error", reject);
|
|
3950
|
+
resolve();
|
|
3951
|
+
});
|
|
3952
|
+
});
|
|
3953
|
+
const address = server.address();
|
|
3954
|
+
return {
|
|
3955
|
+
host,
|
|
3956
|
+
port: typeof address === "object" && address !== null ? address.port : port,
|
|
3957
|
+
close: () => new Promise((resolve, reject) => {
|
|
3958
|
+
server.close((err) => err === void 0 || err === null ? resolve() : reject(err));
|
|
3959
|
+
})
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
//#endregion
|
|
3963
|
+
//#region packages/mcp/src/transports/stdio.ts
|
|
3964
|
+
async function connectStdioTransport(server) {
|
|
3965
|
+
const transport = new StdioServerTransport();
|
|
3966
|
+
await server.connect(transport);
|
|
3967
|
+
return transport;
|
|
3968
|
+
}
|
|
3969
|
+
//#endregion
|
|
3970
|
+
//#region packages/mcp/src/cli.ts
|
|
3971
|
+
const PORT_PATTERN = /^[0-9]+$/;
|
|
3972
|
+
const USAGE = `solicrm-mcp — SoliCRM MCP sunucusu (v${SOLICRM_MCP_VERSION})
|
|
3973
|
+
|
|
3974
|
+
Kullanım:
|
|
3975
|
+
solicrm-mcp stdio taşımasıyla başlar (varsayılan)
|
|
3976
|
+
solicrm-mcp --http [port] bağımsız Streamable HTTP sunucusu (varsayılan port ${DEFAULT_HTTP_PORT})
|
|
3977
|
+
solicrm-mcp --help bu metni yazar
|
|
3978
|
+
solicrm-mcp --version sürümü yazar
|
|
3979
|
+
|
|
3980
|
+
Ortam değişkenleri:
|
|
3981
|
+
SOLICRM_API_KEY (zorunlu) scrm_ ile başlayan tenant API anahtarı
|
|
3982
|
+
SOLICRM_TENANT_ID (zorunlu) kiracı kimliği (UUID)
|
|
3983
|
+
SOLICRM_BASE_URL (opsiyonel) API tabanı; verilmezse https://api.solicrm.com
|
|
3984
|
+
SOLICRM_SCOPE (opsiyonel) read|write|admin — yalnız tools/list filtresi,
|
|
3985
|
+
gerçek yetki sunucuda her istekte yeniden hesaplanır`;
|
|
3986
|
+
/** Bkz. KRİTİK KURAL 3 */
|
|
3987
|
+
function parsePort(raw) {
|
|
3988
|
+
if (!PORT_PATTERN.test(raw)) throw new Error(`--http için geçersiz port: "${raw}". 1–65535 arası bir tam sayı bekleniyor.`);
|
|
3989
|
+
const port = Number.parseInt(raw, 10);
|
|
3990
|
+
if (port < 1 || port > 65535) throw new Error(`--http için geçersiz port: "${raw}". 1–65535 arası bir tam sayı bekleniyor.`);
|
|
3991
|
+
return port;
|
|
3992
|
+
}
|
|
3993
|
+
function parseCliArgs(argv) {
|
|
3994
|
+
let transport = "stdio";
|
|
3995
|
+
let port;
|
|
3996
|
+
let help = false;
|
|
3997
|
+
let version = false;
|
|
3998
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
3999
|
+
const arg = argv[i];
|
|
4000
|
+
if (arg === void 0) continue;
|
|
4001
|
+
if (arg === "--help" || arg === "-h") {
|
|
4002
|
+
help = true;
|
|
4003
|
+
continue;
|
|
4004
|
+
}
|
|
4005
|
+
if (arg === "--version" || arg === "-v") {
|
|
4006
|
+
version = true;
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
4009
|
+
if (arg === "--http") {
|
|
4010
|
+
transport = "http";
|
|
4011
|
+
const next = argv[i + 1];
|
|
4012
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
4013
|
+
port = parsePort(next);
|
|
4014
|
+
i += 1;
|
|
4015
|
+
}
|
|
4016
|
+
continue;
|
|
4017
|
+
}
|
|
4018
|
+
if (arg.startsWith("--http=")) {
|
|
4019
|
+
transport = "http";
|
|
4020
|
+
port = parsePort(arg.slice(7));
|
|
4021
|
+
continue;
|
|
4022
|
+
}
|
|
4023
|
+
throw new Error(`Bilinmeyen argüman: "${arg}". Kullanım için --help.`);
|
|
4024
|
+
}
|
|
4025
|
+
return {
|
|
4026
|
+
transport,
|
|
4027
|
+
port,
|
|
4028
|
+
help,
|
|
4029
|
+
version
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* `@vennyx/solicrm`'i ÇALIŞMA ZAMANINDA import eder. `await import(...)`
|
|
4034
|
+
* BİLİNÇLİ: modül düzeyinde statik bir import, `--help`/`--version` gibi
|
|
4035
|
+
* SDK'ya hiç ihtiyaç duymayan yolları da SDK'nın yüklenmesine bağlardı.
|
|
4036
|
+
*/
|
|
4037
|
+
async function createRealClient(config) {
|
|
4038
|
+
const { SolicrmClient } = await import("@vennyx/solicrm");
|
|
4039
|
+
return new SolicrmClient({
|
|
4040
|
+
apiKey: config.apiKey,
|
|
4041
|
+
tenantId: config.tenantId,
|
|
4042
|
+
baseUrl: config.baseUrl
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
async function runCli(argv, deps = {}) {
|
|
4046
|
+
const log = deps.log ?? ((message) => console.error(message));
|
|
4047
|
+
const args = parseCliArgs(argv);
|
|
4048
|
+
if (args.help) {
|
|
4049
|
+
log(USAGE);
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
if (args.version) {
|
|
4053
|
+
log(SOLICRM_MCP_VERSION);
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
const config = (deps.loadConfig ?? loadConfigFromEnv)(deps.env ?? process.env);
|
|
4057
|
+
const createClient = deps.createClient;
|
|
4058
|
+
const client = createClient === void 0 ? await createRealClient(config) : createClient(config);
|
|
4059
|
+
const createServer = deps.createServer ?? createSolicrmMcpServer;
|
|
4060
|
+
if (args.transport === "http") {
|
|
4061
|
+
const running = await (deps.startHttp ?? (async ({ handler, port }) => await startMcpHttpServer({
|
|
4062
|
+
handler,
|
|
4063
|
+
port
|
|
4064
|
+
})))({
|
|
4065
|
+
handler: createMcpHttpHandler({ createServer: () => createServer(client, config.scope) }),
|
|
4066
|
+
port: args.port
|
|
4067
|
+
});
|
|
4068
|
+
log(`solicrm-mcp HTTP dinliyor: http://${running.host}:${running.port}${MCP_HTTP_PATH}`);
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
await (deps.connectStdio ?? connectStdioTransport)(createServer(client, config.scope));
|
|
4072
|
+
log("solicrm-mcp stdio taşımasında hazır.");
|
|
4073
|
+
}
|
|
4074
|
+
//#endregion
|
|
4075
|
+
//#region packages/mcp/src/bin.ts
|
|
4076
|
+
/**
|
|
4077
|
+
* `solicrm-mcp` çalıştırılabilir girişi — `package.json`'ın `bin` alanı buraya
|
|
4078
|
+
* (`dist/bin.js`) işaret eder.
|
|
4079
|
+
*
|
|
4080
|
+
* KRİTİK KURAL — BURADA `import.meta.main` GİBİ BİR GUARD YOKTUR ve olmamalı.
|
|
4081
|
+
* Bu dosya YALNIZ giriş noktası olarak çalışır (hiçbir modül onu import
|
|
4082
|
+
* etmez), dolayısıyla guard'ın koruyacağı bir durum yok. Ayrıca
|
|
4083
|
+
* `import.meta.main` Bun'da her dosya için tanımlıdır ama Node'da yalnız çok
|
|
4084
|
+
* yeni sürümlerde vardır ⇒ `npx` altında (Node) kalıcı `undefined` olur ve
|
|
4085
|
+
* guard'lı bir sürüm SESSİZCE HİÇ ÇALIŞMAZDI. Tüm mantık `cli.ts`'te ve orada
|
|
4086
|
+
* test ediliyor; bu dosya bilinçli olarak dallanmasız tutuluyor.
|
|
4087
|
+
*
|
|
4088
|
+
* Hata yolu: `runCli` fırlatırsa mesaj `stderr`'a yazılır (stdout PROTOKOL
|
|
4089
|
+
* kanalıdır) ve çıkış kodu 1 olur — sessizce 0 dönmek, istemciye "sunucu
|
|
4090
|
+
* başladı" yalanını söylerdi.
|
|
4091
|
+
*/
|
|
4092
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
4093
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
4094
|
+
process.exit(1);
|
|
4095
|
+
});
|
|
4096
|
+
//#endregion
|
|
4097
|
+
export {};
|