@adula/kit 0.2.0-alpha.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +12 -2
  2. package/build/agent/AGENTS.template.md +2 -2
  3. package/build/agent/capabilities.md +54 -7
  4. package/build/agent/skills/adula-frontend-design/SKILL.md +1 -1
  5. package/build/agent/skills/idea-review/SKILL.md +26 -1
  6. package/build/agent/skills/module-review/SKILL.md +22 -0
  7. package/build/agent/skills/perf-review/SKILL.md +24 -0
  8. package/build/agent/skills/schema-review/SKILL.md +22 -0
  9. package/build/agent/skills/security-review/SKILL.md +24 -0
  10. package/build/agent/skills/ui-review/SKILL.md +22 -0
  11. package/build/commands/capabilities.d.ts +4 -0
  12. package/build/commands/capabilities.js +35 -4
  13. package/build/commands/doctor.js +37 -1
  14. package/build/commands/gaps.js +16 -6
  15. package/build/commands/install.js +19 -5
  16. package/build/commands/main.d.ts +4 -2
  17. package/build/commands/main.js +2 -0
  18. package/build/commands/module_add.js +2 -2
  19. package/build/commands/resource.js +1 -1
  20. package/build/commands/resource_snapshot.d.ts +15 -0
  21. package/build/commands/resource_snapshot.js +61 -0
  22. package/build/database/migrations/1770000000004_kit_collaboration.d.ts +5 -0
  23. package/build/database/migrations/1770000000004_kit_collaboration.js +10 -0
  24. package/build/database/migrations/1770000000005_kit_assignments.d.ts +5 -0
  25. package/build/database/migrations/1770000000005_kit_assignments.js +10 -0
  26. package/build/database/migrations/1770000000006_kit_messaging.d.ts +5 -0
  27. package/build/database/migrations/1770000000006_kit_messaging.js +10 -0
  28. package/build/database/migrations/1770000000007_kit_webhooks.d.ts +5 -0
  29. package/build/database/migrations/1770000000007_kit_webhooks.js +10 -0
  30. package/build/database/migrations/1770000000008_kit_imports.d.ts +5 -0
  31. package/build/database/migrations/1770000000008_kit_imports.js +10 -0
  32. package/build/database/migrations/1770000000010_kit_workflows.d.ts +5 -0
  33. package/build/database/migrations/1770000000010_kit_workflows.js +10 -0
  34. package/build/database/migrations/1770000000011_kit_managed_assignments.d.ts +5 -0
  35. package/build/database/migrations/1770000000011_kit_managed_assignments.js +10 -0
  36. package/build/database/migrations/1770000000012_kit_role_keys.d.ts +5 -0
  37. package/build/database/migrations/1770000000012_kit_role_keys.js +10 -0
  38. package/build/database/migrations/1770000000013_kit_notification_targets.d.ts +5 -0
  39. package/build/database/migrations/1770000000013_kit_notification_targets.js +10 -0
  40. package/build/database/migrations/1770000000014_kit_upload_grants.d.ts +5 -0
  41. package/build/database/migrations/1770000000014_kit_upload_grants.js +10 -0
  42. package/build/database/migrations/1770000000015_kit_inbound_webhooks.d.ts +5 -0
  43. package/build/database/migrations/1770000000015_kit_inbound_webhooks.js +10 -0
  44. package/build/index.d.ts +31 -2
  45. package/build/index.js +17 -2
  46. package/build/src/admin/contracts.d.ts +7 -0
  47. package/build/src/admin/contracts.js +34 -12
  48. package/build/src/admin/controller.d.ts +2 -0
  49. package/build/src/admin/controller.js +52 -1
  50. package/build/src/admin/presentation.d.ts +6 -0
  51. package/build/src/admin/record_title.d.ts +14 -0
  52. package/build/src/admin/record_title.js +50 -0
  53. package/build/src/admin/resource_service.d.ts +184 -2
  54. package/build/src/admin/resource_service.js +713 -48
  55. package/build/src/attachments/attachment_service.d.ts +12 -0
  56. package/build/src/attachments/attachment_service.js +28 -2
  57. package/build/src/attachments/upload_grants.d.ts +44 -0
  58. package/build/src/attachments/upload_grants.js +105 -0
  59. package/build/src/auth/ability.d.ts +1 -1
  60. package/build/src/auth/ability.js +4 -1
  61. package/build/src/auth/actor_store.js +6 -1
  62. package/build/src/auth/conditions.d.ts +15 -0
  63. package/build/src/auth/conditions.js +36 -0
  64. package/build/src/auth/sql.js +6 -2
  65. package/build/src/collaboration/assignments.d.ts +129 -0
  66. package/build/src/collaboration/assignments.js +333 -0
  67. package/build/src/collaboration/record_collaboration.d.ts +86 -0
  68. package/build/src/collaboration/record_collaboration.js +348 -0
  69. package/build/src/commands/agent_assets.js +5 -0
  70. package/build/src/commands/capabilities.d.ts +19 -0
  71. package/build/src/commands/capabilities.js +179 -0
  72. package/build/src/commands/doctor.d.ts +31 -0
  73. package/build/src/commands/doctor.js +114 -0
  74. package/build/src/commands/gap_report.d.ts +50 -2
  75. package/build/src/commands/gap_report.js +102 -4
  76. package/build/src/commands/generator.js +3 -3
  77. package/build/src/commands/snapshot.d.ts +28 -0
  78. package/build/src/commands/snapshot.js +48 -0
  79. package/build/src/commands/source_markers.d.ts +10 -1
  80. package/build/src/commands/source_markers.js +36 -4
  81. package/build/src/core/administration_guard.js +3 -1
  82. package/build/src/core/message_templates.d.ts +83 -0
  83. package/build/src/core/message_templates.js +293 -0
  84. package/build/src/core/module_seed.d.ts +15 -0
  85. package/build/src/core/module_seed.js +31 -0
  86. package/build/src/core/notifications.d.ts +18 -1
  87. package/build/src/core/notifications.js +27 -1
  88. package/build/src/core/roles.d.ts +27 -1
  89. package/build/src/core/roles.js +133 -5
  90. package/build/src/database/schema.d.ts +45 -0
  91. package/build/src/database/schema.js +290 -0
  92. package/build/src/eslint/index.js +26 -0
  93. package/build/src/events/outbox.d.ts +1 -0
  94. package/build/src/events/outbox.js +1 -1
  95. package/build/src/events/record_mutation.d.ts +11 -0
  96. package/build/src/events/record_mutation.js +15 -2
  97. package/build/src/integrations/imports.d.ts +74 -0
  98. package/build/src/integrations/imports.js +333 -0
  99. package/build/src/integrations/inbound_webhooks.d.ts +94 -0
  100. package/build/src/integrations/inbound_webhooks.js +276 -0
  101. package/build/src/integrations/openapi.d.ts +39 -0
  102. package/build/src/integrations/openapi.js +323 -0
  103. package/build/src/integrations/print.d.ts +37 -0
  104. package/build/src/integrations/print.js +124 -0
  105. package/build/src/integrations/webhooks.d.ts +98 -0
  106. package/build/src/integrations/webhooks.js +298 -0
  107. package/build/src/resource/define_resource.d.ts +1 -0
  108. package/build/src/resource/define_resource.js +21 -1
  109. package/build/src/resource/registry.d.ts +3 -0
  110. package/build/src/resource/registry.js +42 -0
  111. package/build/src/resource/types.d.ts +67 -1
  112. package/build/src/resource/values.js +2 -1
  113. package/build/src/services/settings.d.ts +13 -1
  114. package/build/src/services/settings.js +9 -2
  115. package/build/src/workflows/define_workflow.d.ts +124 -0
  116. package/build/src/workflows/define_workflow.js +123 -0
  117. package/build/src/workflows/engine.d.ts +141 -0
  118. package/build/src/workflows/engine.js +752 -0
  119. package/build/stubs/resource_contract.txt +103 -41
  120. package/package.json +7 -3
@@ -0,0 +1,74 @@
1
+ import type { Knex } from 'knex';
2
+ import type { Actor } from '../auth/ability.js';
3
+ import type { ResourceService } from '../admin/resource_service.js';
4
+ import type { ResourceRegistry } from '../resource/registry.js';
5
+ import type { Field } from '../resource/types.js';
6
+ export type ImportStatus = 'mapping' | 'queued' | 'running' | 'done' | 'failed';
7
+ export type ImportTarget = {
8
+ key: string;
9
+ label: string;
10
+ type: string;
11
+ required: boolean;
12
+ };
13
+ export type ImportBatch = {
14
+ id: number;
15
+ resource: string;
16
+ resourceLabel: string;
17
+ fileName: string;
18
+ status: ImportStatus;
19
+ headers: string[];
20
+ mapping: Record<string, string>;
21
+ sample: string[][];
22
+ total: number;
23
+ processed: number;
24
+ created: number;
25
+ failed: number;
26
+ errors: {
27
+ row: number;
28
+ message: string;
29
+ }[];
30
+ createdAt: string;
31
+ finishedAt: string | null;
32
+ targets: ImportTarget[];
33
+ };
34
+ export type ActorSource = {
35
+ load(id: number): Promise<Actor>;
36
+ };
37
+ export declare const IMPORT_ROW_LIMIT = 5000;
38
+ /** Converts one CSV cell to the field's stored representation, or throws a readable error. */
39
+ export declare function importCell(field: Field, raw: string, lookups: Map<string, string>): unknown;
40
+ /**
41
+ * CSV imports in batches: the upload is parsed and stored, the user maps columns
42
+ * to writable fields, and a worker saves each row through ResourceService with
43
+ * the importing user's current permissions. Row failures never stop the batch.
44
+ */
45
+ export declare class ImportBatches {
46
+ private db;
47
+ private registry;
48
+ private resources;
49
+ private actors;
50
+ constructor(db: Knex, registry: ResourceRegistry, resources: ResourceService, actors: ActorSource);
51
+ /** Writable, importable fields for this actor. */
52
+ targets(name: string, actor: Actor): ImportTarget[];
53
+ create(name: string, actor: Actor, input: {
54
+ fileName: unknown;
55
+ content: unknown;
56
+ }): Promise<ImportBatch>;
57
+ show(id: unknown, actor: Actor): Promise<ImportBatch>;
58
+ list(actor: Actor): Promise<ImportBatch[]>;
59
+ /** Stores the column mapping and queues the batch for the worker. */
60
+ start(id: unknown, actor: Actor, mapping: unknown): Promise<ImportBatch>;
61
+ /**
62
+ * Worker step: claims one queued or running batch and imports up to `chunk`
63
+ * rows. Progress is committed per row, so a crash resumes after the last one.
64
+ */
65
+ process(chunk?: number): Promise<{
66
+ id: number;
67
+ processed: number;
68
+ created: number;
69
+ failed: number;
70
+ } | null>;
71
+ private safeTargets;
72
+ private owned;
73
+ private present;
74
+ }
@@ -0,0 +1,333 @@
1
+ import Papa from 'papaparse';
2
+ import { subject } from '@casl/ability';
3
+ import { buildAbility } from '../auth/ability.js';
4
+ import { KitError } from '../admin/errors.js';
5
+ import { notifyWithTemplate } from '../core/message_templates.js';
6
+ export const IMPORT_ROW_LIMIT = 5000;
7
+ const COLUMN_LIMIT = 100;
8
+ const ERROR_LIMIT = 200;
9
+ const FILE_LIMIT = 5 * 1024 * 1024;
10
+ const UNSUPPORTED = new Set(['hasMany', 'attachment', 'json']);
11
+ const normalize = (value) => value
12
+ .trim()
13
+ .toLowerCase()
14
+ .replace(/[\s_-]+/g, '');
15
+ /** Converts one CSV cell to the field's stored representation, or throws a readable error. */
16
+ export function importCell(field, raw, lookups) {
17
+ const value = raw.trim();
18
+ if (value === '')
19
+ return null;
20
+ const latin = value.replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 0x0660));
21
+ switch (field.type) {
22
+ case 'integer':
23
+ case 'belongsTo':
24
+ case 'user': {
25
+ if (!/^-?\d+$/.test(latin))
26
+ throw new Error('رقم صحيح مطلوب');
27
+ return Number(latin);
28
+ }
29
+ case 'money': {
30
+ const plain = latin.replace(/[,٬\s]/g, '').replace('٫', '.');
31
+ if (!/^-?\d+(\.\d{1,2})?$/.test(plain))
32
+ throw new Error('مبلغ غير صالح');
33
+ const [whole, fraction = ''] = plain.replace('-', '').split('.');
34
+ const minor = BigInt(whole) * 100n + BigInt(fraction.padEnd(2, '0') || '0');
35
+ return String(plain.startsWith('-') ? -minor : minor);
36
+ }
37
+ case 'boolean': {
38
+ const truthy = ['1', 'true', 'yes', 'نعم', 'صح'];
39
+ const falsy = ['0', 'false', 'no', 'لا', 'خطأ'];
40
+ if (truthy.includes(value.toLowerCase()))
41
+ return true;
42
+ if (falsy.includes(value.toLowerCase()))
43
+ return false;
44
+ throw new Error('قيمة نعم/لا غير صالحة');
45
+ }
46
+ case 'date': {
47
+ const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(latin);
48
+ const local = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(latin);
49
+ const [y, m, d] = iso
50
+ ? [iso[1], iso[2], iso[3]]
51
+ : local
52
+ ? [local[3], local[2].padStart(2, '0'), local[1].padStart(2, '0')]
53
+ : [];
54
+ const date = y ? new Date(`${y}-${m}-${d}T00:00:00Z`) : null;
55
+ if (!date || Number.isNaN(date.getTime()) || date.getUTCDate() !== Number(d))
56
+ throw new Error('تاريخ غير صالح (YYYY-MM-DD أو DD/MM/YYYY)');
57
+ return `${y}-${m}-${d}`;
58
+ }
59
+ case 'datetime': {
60
+ const date = new Date(latin);
61
+ if (Number.isNaN(date.getTime()))
62
+ throw new Error('تاريخ ووقت غير صالحين');
63
+ return date.toISOString();
64
+ }
65
+ case 'lookup': {
66
+ const key = lookups.get(normalize(value));
67
+ if (!key)
68
+ throw new Error('قيمة غير موجودة في القائمة');
69
+ return key;
70
+ }
71
+ default:
72
+ return value;
73
+ }
74
+ }
75
+ /**
76
+ * CSV imports in batches: the upload is parsed and stored, the user maps columns
77
+ * to writable fields, and a worker saves each row through ResourceService with
78
+ * the importing user's current permissions. Row failures never stop the batch.
79
+ */
80
+ export class ImportBatches {
81
+ db;
82
+ registry;
83
+ resources;
84
+ actors;
85
+ constructor(db, registry, resources, actors) {
86
+ this.db = db;
87
+ this.registry = registry;
88
+ this.resources = resources;
89
+ this.actors = actors;
90
+ }
91
+ /** Writable, importable fields for this actor. */
92
+ targets(name, actor) {
93
+ const resource = this.registry.get(name);
94
+ const ability = buildAbility(actor.rules, this.registry.all());
95
+ if (!resource.actions.includes('create') || !ability.can('create', name))
96
+ throw new KitError(403, 'E_FORBIDDEN', 'ليس لديك صلاحية الإضافة إلى هذا الكيان');
97
+ const fields = resource.form
98
+ .filter((key) => {
99
+ const field = resource.fields[key];
100
+ return (!UNSUPPORTED.has(field.type) &&
101
+ !field.sequence &&
102
+ actor.permissionLevel >=
103
+ Math.max(field.permissionLevel ?? 0, resource.hidden?.includes(key) ? 1 : 0) &&
104
+ ability.can('create', subject(name, {}), key));
105
+ })
106
+ .map((key) => ({
107
+ key,
108
+ label: resource.fields[key].label.ar,
109
+ type: resource.fields[key].type,
110
+ required: Boolean(resource.fields[key].required),
111
+ }));
112
+ // An inherited scope takes the unit from the parent row instead (#45).
113
+ if (resource.scoped && !resource.scope)
114
+ fields.push({
115
+ key: 'orgUnitId',
116
+ label: 'الوحدة التنظيمية (رقم)',
117
+ type: 'integer',
118
+ required: true,
119
+ });
120
+ return fields;
121
+ }
122
+ async create(name, actor, input) {
123
+ const targets = this.targets(name, actor);
124
+ const fileName = typeof input.fileName === 'string' ? input.fileName.slice(0, 200) : 'import.csv';
125
+ if (typeof input.content !== 'string' || !input.content.trim())
126
+ throw new KitError(422, 'E_IMPORT_FILE', 'الملف فارغ');
127
+ if (Buffer.byteLength(input.content) > FILE_LIMIT)
128
+ throw new KitError(422, 'E_IMPORT_FILE', 'حجم الملف يتجاوز 5 ميغابايت');
129
+ const parsed = Papa.parse(input.content.replace(/^\uFEFF/, ''), {
130
+ skipEmptyLines: 'greedy',
131
+ });
132
+ if (parsed.errors.some((error) => error.type !== 'Delimiter'))
133
+ throw new KitError(422, 'E_IMPORT_FILE', `تعذر قراءة CSV: ${parsed.errors[0].message}`);
134
+ const [headers = [], ...rows] = parsed.data;
135
+ if (!headers.length || headers.length > COLUMN_LIMIT)
136
+ throw new KitError(422, 'E_IMPORT_FILE', 'يجب أن يحتوي الملف صف عناوين (100 عمود كحد أقصى)');
137
+ if (!rows.length || rows.length > IMPORT_ROW_LIMIT)
138
+ throw new KitError(422, 'E_IMPORT_ROWS', `عدد الصفوف بين 1 و${IMPORT_ROW_LIMIT}`);
139
+ const mapping = {};
140
+ headers.forEach((header, index) => {
141
+ const target = targets.find((field) => [field.key, field.label].some((alias) => normalize(alias) === normalize(header)));
142
+ if (target && !Object.values(mapping).includes(target.key))
143
+ mapping[String(index)] = target.key;
144
+ });
145
+ const [row] = await this.db('import_batches')
146
+ .insert({
147
+ resource: name,
148
+ user_id: actor.id,
149
+ file_name: fileName,
150
+ status: 'mapping',
151
+ headers: JSON.stringify(headers),
152
+ mapping: JSON.stringify(mapping),
153
+ rows: JSON.stringify(rows.map((cells) => cells.slice(0, headers.length))),
154
+ total: rows.length,
155
+ })
156
+ .returning('*');
157
+ return this.present(row, targets);
158
+ }
159
+ async show(id, actor) {
160
+ const row = await this.owned(id, actor);
161
+ return this.present(row, this.safeTargets(row.resource, actor));
162
+ }
163
+ async list(actor) {
164
+ const rows = await this.db('import_batches')
165
+ .where('user_id', actor.id)
166
+ .orderBy('id', 'desc')
167
+ .limit(50)
168
+ .select('id', 'resource', 'file_name', 'status', 'headers', 'mapping', 'total', 'processed', 'created', 'failed', 'errors', 'created_at', 'finished_at');
169
+ return rows.map((row) => this.present({ ...row, rows: [] }, []));
170
+ }
171
+ /** Stores the column mapping and queues the batch for the worker. */
172
+ async start(id, actor, mapping) {
173
+ const row = await this.owned(id, actor);
174
+ if (row.status !== 'mapping')
175
+ throw new KitError(409, 'E_IMPORT_STATE', 'بدأت معالجة هذه الدفعة بالفعل');
176
+ const targets = this.targets(row.resource, actor);
177
+ if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping))
178
+ throw new KitError(422, 'E_IMPORT_MAPPING', 'مطابقة الأعمدة غير صالحة');
179
+ const clean = {};
180
+ const used = new Set();
181
+ for (const [column, key] of Object.entries(mapping)) {
182
+ if (key === '' || key === null || key === undefined)
183
+ continue;
184
+ const index = Number(column);
185
+ if (!Number.isInteger(index) || index < 0 || index >= row.headers.length)
186
+ throw new KitError(422, 'E_IMPORT_MAPPING', 'عمود غير موجود');
187
+ if (typeof key !== 'string' || !targets.some((target) => target.key === key) || used.has(key))
188
+ throw new KitError(422, 'E_IMPORT_MAPPING', `حقل غير قابل للاستيراد أو مكرر: ${String(key)}`);
189
+ used.add(key);
190
+ clean[String(index)] = key;
191
+ }
192
+ const missing = targets.filter((target) => target.required && !used.has(target.key));
193
+ if (missing.length)
194
+ throw new KitError(422, 'E_IMPORT_MAPPING', `حقول مطلوبة بلا عمود: ${missing.map((target) => target.label).join('، ')}`);
195
+ await this.db('import_batches')
196
+ .where('id', row.id)
197
+ .update({ mapping: JSON.stringify(clean), status: 'queued' });
198
+ return this.show(row.id, actor);
199
+ }
200
+ /**
201
+ * Worker step: claims one queued or running batch and imports up to `chunk`
202
+ * rows. Progress is committed per row, so a crash resumes after the last one.
203
+ */
204
+ async process(chunk = 200) {
205
+ const batch = await this.db.transaction(async (trx) => {
206
+ const row = await trx('import_batches')
207
+ .whereIn('status', ['queued', 'running'])
208
+ .orderBy('id')
209
+ .forUpdate()
210
+ .skipLocked()
211
+ .first();
212
+ if (!row)
213
+ return null;
214
+ await trx('import_batches')
215
+ .where('id', row.id)
216
+ .update({ status: 'running', started_at: row.started_at ?? trx.fn.now() });
217
+ return row;
218
+ });
219
+ if (!batch)
220
+ return null;
221
+ const actor = await this.actors.load(Number(batch.user_id));
222
+ const resource = this.registry.get(batch.resource);
223
+ const lookupGroups = new Map();
224
+ for (const [key, field] of Object.entries(resource.fields)) {
225
+ if (field.type !== 'lookup')
226
+ continue;
227
+ const rows = await this.db('lookups')
228
+ .where({ group: field.group, active: true })
229
+ .select('key', 'label_ar', 'label_en');
230
+ const map = new Map();
231
+ for (const entry of rows)
232
+ for (const name of [entry.key, entry.label_ar, entry.label_en])
233
+ map.set(normalize(String(name)), entry.key);
234
+ lookupGroups.set(key, map);
235
+ }
236
+ const mapping = batch.mapping;
237
+ const rows = batch.rows;
238
+ let { processed, created, failed } = batch;
239
+ const errors = [...batch.errors];
240
+ const end = Math.min(rows.length, processed + chunk);
241
+ for (let index = processed; index < end; index++) {
242
+ const cells = rows[index];
243
+ const input = {};
244
+ try {
245
+ for (const [column, key] of Object.entries(mapping)) {
246
+ const raw = cells[Number(column)] ?? '';
247
+ if (key === 'orgUnitId') {
248
+ input.orgUnitId = raw.trim() === '' ? null : Number(raw.trim());
249
+ continue;
250
+ }
251
+ try {
252
+ input[key] = importCell(resource.fields[key], raw, lookupGroups.get(key) ?? new Map());
253
+ }
254
+ catch (error) {
255
+ throw new Error(`${resource.fields[key].label.ar}: ${error.message}`);
256
+ }
257
+ }
258
+ await this.resources.save(batch.resource, actor, input);
259
+ created++;
260
+ }
261
+ catch (error) {
262
+ failed++;
263
+ if (errors.length < ERROR_LIMIT)
264
+ errors.push({
265
+ row: index + 2,
266
+ message: error instanceof KitError || error instanceof Error
267
+ ? error.message.slice(0, 300)
268
+ : 'خطأ غير معروف',
269
+ });
270
+ }
271
+ processed = index + 1;
272
+ await this.db('import_batches')
273
+ .where('id', batch.id)
274
+ .update({ processed, created, failed, errors: JSON.stringify(errors) });
275
+ }
276
+ if (processed >= rows.length) {
277
+ await this.db.transaction(async (trx) => {
278
+ await trx('import_batches')
279
+ .where('id', batch.id)
280
+ // Row data is only needed while importing; the log keeps counts and errors.
281
+ .update({ status: 'done', finished_at: trx.fn.now(), rows: JSON.stringify([]) });
282
+ await notifyWithTemplate(trx, Number(batch.user_id), 'import.finished', {
283
+ resource: resource.label.ar,
284
+ created,
285
+ failed,
286
+ });
287
+ });
288
+ }
289
+ return { id: Number(batch.id), processed, created, failed };
290
+ }
291
+ safeTargets(name, actor) {
292
+ try {
293
+ return this.targets(name, actor);
294
+ }
295
+ catch {
296
+ return [];
297
+ }
298
+ }
299
+ async owned(id, actor) {
300
+ const batchId = Number(id);
301
+ if (!Number.isSafeInteger(batchId) || batchId <= 0)
302
+ throw new KitError(404, 'E_IMPORT_NOT_FOUND', 'الدفعة غير موجودة');
303
+ const row = await this.db('import_batches').where({ id: batchId, user_id: actor.id }).first();
304
+ if (!row)
305
+ throw new KitError(404, 'E_IMPORT_NOT_FOUND', 'الدفعة غير موجودة');
306
+ return row;
307
+ }
308
+ present(row, targets) {
309
+ let label = String(row.resource);
310
+ try {
311
+ label = this.registry.get(row.resource).label.ar;
312
+ }
313
+ catch { }
314
+ return {
315
+ id: Number(row.id),
316
+ resource: String(row.resource),
317
+ resourceLabel: label,
318
+ fileName: String(row.file_name),
319
+ status: row.status,
320
+ headers: row.headers ?? [],
321
+ mapping: row.mapping ?? {},
322
+ sample: (row.rows ?? []).slice(0, 5),
323
+ total: Number(row.total),
324
+ processed: Number(row.processed ?? 0),
325
+ created: Number(row.created ?? 0),
326
+ failed: Number(row.failed ?? 0),
327
+ errors: row.errors ?? [],
328
+ createdAt: new Date(row.created_at).toISOString(),
329
+ finishedAt: row.finished_at ? new Date(row.finished_at).toISOString() : null,
330
+ targets,
331
+ };
332
+ }
333
+ }
@@ -0,0 +1,94 @@
1
+ import type { Knex } from 'knex';
2
+ import type { SecretBox } from './webhooks.js';
3
+ export type InboundSource = {
4
+ id: number;
5
+ key: string;
6
+ name: string;
7
+ algorithm: 'sha256' | 'sha512';
8
+ signatureHeader: string;
9
+ signaturePrefix: string;
10
+ eventHeader: string;
11
+ deliveryHeader: string;
12
+ /** Refuse a body already received from this source, whatever its delivery id or event. */
13
+ dedupeBody: boolean;
14
+ active: boolean;
15
+ createdAt: string;
16
+ lastReceivedAt: string | null;
17
+ /** The path senders post to. */
18
+ path: string;
19
+ };
20
+ export type InboundSourceInput = {
21
+ key?: unknown;
22
+ name?: unknown;
23
+ algorithm?: unknown;
24
+ signatureHeader?: unknown;
25
+ signaturePrefix?: unknown;
26
+ eventHeader?: unknown;
27
+ deliveryHeader?: unknown;
28
+ dedupeBody?: unknown;
29
+ active?: unknown;
30
+ };
31
+ export type InboundDelivery = {
32
+ id: string;
33
+ deliveryId: string;
34
+ event: string;
35
+ eventName: string;
36
+ dispatches: number;
37
+ receivedAt: string;
38
+ };
39
+ export type InboundReceipt = {
40
+ delivery: string;
41
+ event: string;
42
+ duplicate: boolean;
43
+ };
44
+ /** Inbound payloads above this size are refused before verification. */
45
+ export declare const INBOUND_BODY_LIMIT: number;
46
+ /** Stored deliveries older than this are removed by pruneDeliveries(). */
47
+ export declare const INBOUND_RETENTION_DAYS = 90;
48
+ /** The domain event an inbound delivery raises: inbound.<source>.<event>. */
49
+ export declare function inboundEventName(source: string, event: string): string;
50
+ /**
51
+ * Signed inbound webhooks (#30). Administrators create a source per sender (for example
52
+ * a Git host) and give it the generated secret. POST /webhooks/in/<key> verifies an
53
+ * HMAC over the raw body in constant time (X-Hub-Signature-256 by default), stores each
54
+ * delivery once by the sender's delivery id and raises inbound.<key>.<event> through the
55
+ * outbox, so module listeners run idempotently with retries and a delivery history.
56
+ */
57
+ export declare class InboundWebhooks {
58
+ private db;
59
+ private secrets;
60
+ constructor(db: Knex, secrets: SecretBox);
61
+ list(): Promise<InboundSource[]>;
62
+ /** Creates a source and returns its secret once; it is never shown again. */
63
+ create(actorId: number, input: InboundSourceInput): Promise<{
64
+ source: InboundSource;
65
+ secret: string;
66
+ }>;
67
+ /**
68
+ * Updates the fields given (name, algorithm, headers, body deduplication, state); fields
69
+ * left out keep their stored values. The key and secret stay.
70
+ */
71
+ update(id: number, input: InboundSourceInput): Promise<void>;
72
+ /** Issues a new secret; the old one stops verifying at once. */
73
+ rotate(id: number): Promise<string>;
74
+ remove(id: number): Promise<void>;
75
+ /**
76
+ * Verifies and records one delivery. Unknown or inactive sources and bad signatures
77
+ * are refused without storing anything; a repeated delivery id is acknowledged once.
78
+ */
79
+ receive(key: string, request: {
80
+ headers: Record<string, string | string[] | undefined>;
81
+ body: string | Buffer;
82
+ }): Promise<InboundReceipt>;
83
+ deliveries(sourceId: number, limit?: number): Promise<InboundDelivery[]>;
84
+ /**
85
+ * Raises a stored delivery again as a new domain event, for listeners that failed or
86
+ * were added later. Listeners must be idempotent on the delivery id.
87
+ */
88
+ redispatch(deliveryId: string): Promise<void>;
89
+ /** Removes stored deliveries older than the retention period (90 days by default). */
90
+ pruneDeliveries(days?: number): Promise<number>;
91
+ private envelope;
92
+ private validate;
93
+ private present;
94
+ }