@deuriib/better-dgii-ecf 0.0.0-stage → 0.0.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/dist/index.cjs ADDED
@@ -0,0 +1,1337 @@
1
+ 'use strict';
2
+
3
+ var api = require('better-auth/api');
4
+ var error = require('@better-auth/core/error');
5
+ var dgiiEcf$1 = require('@deuriib/dgii-ecf');
6
+ var errorCodes = require('@better-auth/core/utils/error-codes');
7
+ var crypto = require('crypto');
8
+ var z = require('zod');
9
+
10
+ function _interopNamespace(e) {
11
+ if (e && e.__esModule) return e;
12
+ var n = Object.create(null);
13
+ if (e) {
14
+ Object.keys(e).forEach(function (k) {
15
+ if (k !== 'default') {
16
+ var d = Object.getOwnPropertyDescriptor(e, k);
17
+ Object.defineProperty(n, k, d.get ? d : {
18
+ enumerable: true,
19
+ get: function () { return e[k]; }
20
+ });
21
+ }
22
+ });
23
+ }
24
+ n.default = e;
25
+ return Object.freeze(n);
26
+ }
27
+
28
+ var z__namespace = /*#__PURE__*/_interopNamespace(z);
29
+
30
+ // src/client-provider.ts
31
+ var DGII_ECF_ERROR_CODES = errorCodes.defineErrorCodes({
32
+ DGII_SESSION_REQUIRED: "A session is required to call a DGII e-CF endpoint",
33
+ DGII_TAXPAYER_ID_REQUIRED: "No taxpayer id (RNC) could be resolved: send `taxPayerId`, set the organization field, or configure `resolveTaxPayerId`",
34
+ DGII_TAXPAYER_ID_INVALID: "The taxpayer id is not a valid DGII RNC (9 digits) or c\xE9dula (11 digits)",
35
+ DGII_TAXPAYER_ID_MISMATCH: "The signed document was issued under a taxpayer id other than the one resolved for this request",
36
+ DGII_TAXPAYER_ID_FORBIDDEN: "This session is not allowed to act on behalf of the requested taxpayer id",
37
+ DGII_ORGANIZATION_NOT_FOUND: "The organization in scope was not found",
38
+ DGII_ORGANIZATION_REQUIRED: "This request needs an organization: send `organizationId` or select an active organization",
39
+ DGII_NOT_A_MEMBER: "The session user is not a member of the organization in scope",
40
+ DGII_INSUFFICIENT_ROLE: "The session user does not hold a role allowed to perform this DGII operation",
41
+ DGII_CREDENTIALS_UNAVAILABLE: "No DGII credentials could be resolved for this taxpayer id",
42
+ DGII_CLIENT_NOT_CONFIGURED: "The plugin was configured without `credentials` or `getClient`, so it cannot reach DGII",
43
+ DGII_STATUS_API_KEY_REQUIRED: "The DGII status service needs an API key: configure `status.apiKey`",
44
+ DGII_DOCUMENT_UNREADABLE: "The signed document carries no issuer taxpayer id (RNCEmisor / RncEmisor)",
45
+ DGII_SIGNING_FAILED: "The document could not be signed with the organization key",
46
+ DGII_REQUEST_FAILED: "The DGII request failed",
47
+ DGII_SUBMISSION_NOT_FOUND: "No stored submission matches this id",
48
+ DGII_PERSISTENCE_UNAVAILABLE: "Submission persistence is disabled, so this operation is unavailable",
49
+ DGII_IDEMPOTENCY_IN_FLIGHT: "A submission with this idempotency key is still in flight or has an unknown outcome; reconcile it before retrying"
50
+ });
51
+ var STATUS_BY_DGII_CODE = {
52
+ INVALID_XML: "BAD_REQUEST",
53
+ SIGNING: "BAD_REQUEST",
54
+ VALIDATION: "BAD_REQUEST",
55
+ CERTIFICATE: "INTERNAL_SERVER_ERROR",
56
+ IDEMPOTENCY: "CONFLICT",
57
+ AUTH: "BAD_GATEWAY",
58
+ HTTP: "BAD_GATEWAY",
59
+ TIMEOUT: "BAD_GATEWAY",
60
+ CIRCUIT_OPEN: "BAD_GATEWAY",
61
+ UNKNOWN: "BAD_GATEWAY"
62
+ };
63
+ var toFailureDetail = (error) => ({
64
+ code: error.code,
65
+ status: error.status,
66
+ statusText: error.statusText,
67
+ resource: error.resource,
68
+ method: error.method,
69
+ mensajes: error.mensajes
70
+ });
71
+ var badRequest = (error, message) => new api.APIError("BAD_REQUEST", {
72
+ message: message ?? error.message,
73
+ code: error.code
74
+ });
75
+ var forbidden = (error, message) => new api.APIError("FORBIDDEN", {
76
+ message: message ?? error.message,
77
+ code: error.code
78
+ });
79
+ var conflict = (error, message) => new api.APIError("CONFLICT", {
80
+ message: message ?? error.message,
81
+ code: error.code
82
+ });
83
+ var serverError = (error, message) => new api.APIError("INTERNAL_SERVER_ERROR", {
84
+ message: message ?? error.message,
85
+ code: error.code
86
+ });
87
+ var toApiError = (error) => new api.APIError(STATUS_BY_DGII_CODE[error.code], {
88
+ message: error.message,
89
+ code: DGII_ECF_ERROR_CODES.DGII_REQUEST_FAILED.code,
90
+ dgii: toFailureDetail(error)
91
+ });
92
+
93
+ // src/client-provider.ts
94
+ var DEFAULT_MAX_SIZE = 50;
95
+ var DEFAULT_TTL_MS = 10 * 60 * 1e3;
96
+ var cacheKey = (scope) => `${scope.environment}\0${scope.taxPayerId}`;
97
+ var unavailable = (taxPayerId) => new api.APIError("INTERNAL_SERVER_ERROR", {
98
+ message: `No DGII credentials were resolved for taxpayer ${taxPayerId}`,
99
+ code: DGII_ECF_ERROR_CODES.DGII_CREDENTIALS_UNAVAILABLE.code
100
+ });
101
+ var createClientProvider = (options, now = Date.now) => {
102
+ const getClient = options.getClient;
103
+ if (getClient !== void 0) {
104
+ return {
105
+ async get(scope) {
106
+ const client = await getClient(scope);
107
+ if (client === null) {
108
+ throw unavailable(scope.taxPayerId);
109
+ }
110
+ return client;
111
+ },
112
+ invalidate() {
113
+ },
114
+ size: 0,
115
+ constructsClients: false
116
+ };
117
+ }
118
+ const resolveCredentials = options.credentials;
119
+ if (resolveCredentials === void 0) {
120
+ throw new error.BetterAuthError(
121
+ "[better-dgii-ecf] Configure `credentials` or `getClient` \u2014 the plugin cannot reach DGII without one of them."
122
+ );
123
+ }
124
+ const maxSize = Math.max(1, options.clientCache?.maxSize ?? DEFAULT_MAX_SIZE);
125
+ const ttlMs = Math.max(0, options.clientCache?.ttlMs ?? DEFAULT_TTL_MS);
126
+ const cache = /* @__PURE__ */ new Map();
127
+ const evictExpired = (at) => {
128
+ for (const [key, entry] of cache) {
129
+ if (entry.expiresAt <= at) {
130
+ cache.delete(key);
131
+ }
132
+ }
133
+ };
134
+ return {
135
+ async get(scope) {
136
+ const at = now();
137
+ evictExpired(at);
138
+ const key = cacheKey(scope);
139
+ const cached = cache.get(key);
140
+ if (cached !== void 0 && cached.expiresAt > at) {
141
+ cache.delete(key);
142
+ cache.set(key, cached);
143
+ return cached.client;
144
+ }
145
+ const material = await resolveCredentials(scope);
146
+ if (material === null) {
147
+ throw unavailable(scope.taxPayerId);
148
+ }
149
+ if (material.key === void 0 || material.cert === void 0) {
150
+ throw new api.APIError("INTERNAL_SERVER_ERROR", {
151
+ message: `The credentials resolved for taxpayer ${scope.taxPayerId} carry no key/cert PEM pair`,
152
+ code: DGII_ECF_ERROR_CODES.DGII_CREDENTIALS_UNAVAILABLE.code
153
+ });
154
+ }
155
+ const statusApiKey = options.status?.apiKey;
156
+ const client = new dgiiEcf$1.ECF(material, scope.environment, {
157
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
158
+ ...statusApiKey === void 0 ? {} : { statusApiKey }
159
+ });
160
+ if (ttlMs > 0) {
161
+ while (cache.size >= maxSize) {
162
+ const oldest = cache.keys().next();
163
+ if (oldest.done === true) {
164
+ break;
165
+ }
166
+ cache.delete(oldest.value);
167
+ }
168
+ cache.set(key, { client, expiresAt: at + ttlMs });
169
+ }
170
+ return client;
171
+ },
172
+ invalidate(taxPayerId) {
173
+ if (taxPayerId === void 0) {
174
+ cache.clear();
175
+ return;
176
+ }
177
+ for (const key of cache.keys()) {
178
+ if (key.endsWith(`\0${taxPayerId}`)) {
179
+ cache.delete(key);
180
+ }
181
+ }
182
+ },
183
+ get size() {
184
+ return cache.size;
185
+ },
186
+ constructsClients: true
187
+ };
188
+ };
189
+ var SUBMISSION_MODEL = "dgiiEcfSubmission";
190
+ var DEFAULT_MAX_RESPONSE_LENGTH = 8192;
191
+ var hashDocument = (xml) => crypto.createHash("sha256").update(xml, "utf8").digest("hex");
192
+ var serializeResponse = (value, maxLength) => {
193
+ if (value === void 0 || value === null) {
194
+ return null;
195
+ }
196
+ let serialized;
197
+ try {
198
+ serialized = JSON.stringify(value);
199
+ } catch {
200
+ return null;
201
+ }
202
+ if (serialized === void 0) {
203
+ return null;
204
+ }
205
+ return serialized.length > maxLength ? serialized.slice(0, maxLength) : serialized;
206
+ };
207
+ var disabledStore = {
208
+ enabled: false,
209
+ async findReplay() {
210
+ return null;
211
+ },
212
+ async begin() {
213
+ return null;
214
+ },
215
+ async complete() {
216
+ },
217
+ async list() {
218
+ throw new api.APIError("NOT_IMPLEMENTED", {
219
+ message: DGII_ECF_ERROR_CODES.DGII_PERSISTENCE_UNAVAILABLE.message,
220
+ code: DGII_ECF_ERROR_CODES.DGII_PERSISTENCE_UNAVAILABLE.code
221
+ });
222
+ }
223
+ };
224
+ var createSubmissionStore = (ctx, options) => {
225
+ if (options?.enabled === false) {
226
+ return disabledStore;
227
+ }
228
+ const adapter = ctx.context.adapter;
229
+ const storeResponses = options?.storeResponses ?? true;
230
+ const maxResponseLength = options?.maxResponseLength ?? DEFAULT_MAX_RESPONSE_LENGTH;
231
+ return {
232
+ enabled: true,
233
+ async findReplay(taxPayerId, idempotencyKey) {
234
+ const rows = await adapter.findMany({
235
+ model: SUBMISSION_MODEL,
236
+ where: [
237
+ { field: "taxPayerId", value: taxPayerId },
238
+ { field: "idempotencyKey", value: idempotencyKey }
239
+ ],
240
+ limit: 1,
241
+ sortBy: { field: "createdAt", direction: "desc" }
242
+ });
243
+ return rows[0] ?? null;
244
+ },
245
+ async begin(input) {
246
+ const created = await adapter.create({
247
+ model: SUBMISSION_MODEL,
248
+ data: {
249
+ organizationId: input.organizationId,
250
+ taxPayerId: input.taxPayerId,
251
+ userId: input.userId,
252
+ environment: input.environment,
253
+ operation: input.operation,
254
+ status: "pending",
255
+ idempotencyKey: input.idempotencyKey,
256
+ encf: input.encf,
257
+ trackId: null,
258
+ documentHash: input.documentHash,
259
+ errorCode: null,
260
+ response: null
261
+ }
262
+ });
263
+ return created.id;
264
+ },
265
+ async complete(id, outcome) {
266
+ if (id === null) {
267
+ return;
268
+ }
269
+ await adapter.update({
270
+ model: SUBMISSION_MODEL,
271
+ where: [{ field: "id", value: id }],
272
+ update: {
273
+ status: outcome.status,
274
+ trackId: outcome.trackId ?? null,
275
+ errorCode: outcome.errorCode ?? null,
276
+ response: storeResponses ? serializeResponse(outcome.response, maxResponseLength) : null
277
+ }
278
+ });
279
+ },
280
+ async list({ organizationId, taxPayerId, limit }) {
281
+ const where = [];
282
+ if (organizationId !== null) {
283
+ where.push({ field: "organizationId", value: organizationId });
284
+ }
285
+ if (taxPayerId !== null) {
286
+ where.push({ field: "taxPayerId", value: taxPayerId });
287
+ }
288
+ return adapter.findMany({
289
+ model: SUBMISSION_MODEL,
290
+ ...where.length > 0 ? { where } : {},
291
+ limit,
292
+ sortBy: { field: "createdAt", direction: "desc" }
293
+ });
294
+ }
295
+ };
296
+ };
297
+ var planReplay = (record) => {
298
+ if (record === null) {
299
+ return { kind: "proceed" };
300
+ }
301
+ if (record.status === "succeeded") {
302
+ return { kind: "replay", record };
303
+ }
304
+ if (record.status === "pending") {
305
+ return { kind: "conflict" };
306
+ }
307
+ return { kind: "proceed" };
308
+ };
309
+ var parseStoredResponse = (record) => {
310
+ const raw = record.response;
311
+ if (typeof raw !== "string" || raw === "") {
312
+ return null;
313
+ }
314
+ try {
315
+ return JSON.parse(raw);
316
+ } catch {
317
+ return null;
318
+ }
319
+ };
320
+
321
+ // src/tax-payer.ts
322
+ var SEPARATORS = /[\s.\-–—]/g;
323
+ var SHAPE = /^(?:\d{9}|\d{11})$/;
324
+ var normalizeTaxPayerId = (value) => value.replace(SEPARATORS, "").trim();
325
+ var isTaxPayerIdShape = (value) => SHAPE.test(value);
326
+ var TaxPayerIdError = class extends Error {
327
+ reason;
328
+ constructor(reason, message) {
329
+ super(message);
330
+ this.name = "TaxPayerIdError";
331
+ this.reason = reason;
332
+ }
333
+ };
334
+ var applyTaxPayerIdValidation = async (raw, validation) => {
335
+ const normalized = normalizeTaxPayerId(raw);
336
+ if (normalized === "") {
337
+ throw new TaxPayerIdError("shape", "The taxpayer id is empty");
338
+ }
339
+ if (validation === "off") {
340
+ return normalized;
341
+ }
342
+ if (validation === "format") {
343
+ if (!isTaxPayerIdShape(normalized)) {
344
+ throw new TaxPayerIdError(
345
+ "shape",
346
+ `"${raw}" is not a DGII RNC (9 digits) or c\xE9dula (11 digits)`
347
+ );
348
+ }
349
+ return normalized;
350
+ }
351
+ const verdict = await validation(normalized);
352
+ if (verdict === false) {
353
+ throw new TaxPayerIdError("custom", `"${raw}" was rejected by the validator`);
354
+ }
355
+ if (typeof verdict === "string") {
356
+ return normalizeTaxPayerId(verdict);
357
+ }
358
+ return normalized;
359
+ };
360
+
361
+ // src/resolution.ts
362
+ var ORGANIZATION_MODEL = "organization";
363
+ var MEMBER_MODEL = "member";
364
+ var DEFAULT_ROLES = ["owner", "admin"];
365
+ var hasOrganizationPlugin = (ctx) => (ctx.context.options.plugins ?? []).some((plugin) => plugin.id === "organization");
366
+ var readOrganizationField = (row, field) => {
367
+ const raw = row[field] ?? row.taxPayerId ?? row.tax_payer_id;
368
+ if (typeof raw !== "string") {
369
+ return null;
370
+ }
371
+ const normalized = normalizeTaxPayerId(raw);
372
+ return normalized === "" ? null : normalized;
373
+ };
374
+ var readActiveOrganizationId = (session) => {
375
+ const raw = session.activeOrganizationId;
376
+ return typeof raw === "string" && raw !== "" ? raw : null;
377
+ };
378
+ var rolesOf = (member) => String(member.role ?? "").split(",").map((role) => role.trim()).filter((role) => role !== "");
379
+ var environmentOf = async (options, scope) => {
380
+ const configured = options.environment ?? dgiiEcf$1.Environment.Dev;
381
+ if (typeof configured === "function") {
382
+ return configured({
383
+ taxPayerId: scope.taxPayerId,
384
+ organizationId: scope.organizationId,
385
+ userId: scope.user.id
386
+ });
387
+ }
388
+ return configured;
389
+ };
390
+ var resolveRequestScope = async ({
391
+ ctx,
392
+ options,
393
+ input
394
+ }) => {
395
+ const authenticated = ctx.context.session;
396
+ if (authenticated === null) {
397
+ throw new api.APIError("UNAUTHORIZED", {
398
+ message: DGII_ECF_ERROR_CODES.DGII_SESSION_REQUIRED.message,
399
+ code: DGII_ECF_ERROR_CODES.DGII_SESSION_REQUIRED.code
400
+ });
401
+ }
402
+ const { user, session } = authenticated;
403
+ const withOrganization = hasOrganizationPlugin(ctx);
404
+ const organizationId = input.organizationId ?? (withOrganization ? readActiveOrganizationId(session) : null);
405
+ let organization = null;
406
+ let organizationTaxPayerId = null;
407
+ if (withOrganization && organizationId !== null) {
408
+ organization = await ctx.context.adapter.findOne({
409
+ model: ORGANIZATION_MODEL,
410
+ where: [{ field: "id", value: organizationId }]
411
+ });
412
+ if (organization === null) {
413
+ throw new api.APIError("NOT_FOUND", {
414
+ message: DGII_ECF_ERROR_CODES.DGII_ORGANIZATION_NOT_FOUND.message,
415
+ code: DGII_ECF_ERROR_CODES.DGII_ORGANIZATION_NOT_FOUND.code
416
+ });
417
+ }
418
+ organizationTaxPayerId = readOrganizationField(
419
+ organization,
420
+ options.organization?.field ?? "taxPayerId"
421
+ );
422
+ }
423
+ const validation = options.validateTaxPayerId ?? "format";
424
+ const toCanonical = async (raw) => {
425
+ try {
426
+ return await applyTaxPayerIdValidation(raw, validation);
427
+ } catch (error) {
428
+ if (error instanceof TaxPayerIdError) {
429
+ throw badRequest(DGII_ECF_ERROR_CODES.DGII_TAXPAYER_ID_INVALID, error.message);
430
+ }
431
+ throw error;
432
+ }
433
+ };
434
+ let taxPayerId = null;
435
+ let source = "request";
436
+ if (input.taxPayerId !== void 0) {
437
+ taxPayerId = await toCanonical(input.taxPayerId);
438
+ } else if (organizationTaxPayerId !== null) {
439
+ taxPayerId = await toCanonical(organizationTaxPayerId);
440
+ source = "organization";
441
+ } else {
442
+ const resolver = options.resolveTaxPayerId;
443
+ const fromResolver = resolver === void 0 ? null : await resolver({ user, session, organizationId });
444
+ if (fromResolver !== null) {
445
+ taxPayerId = await toCanonical(fromResolver);
446
+ source = "resolver";
447
+ }
448
+ }
449
+ if (taxPayerId === null) {
450
+ throw badRequest(DGII_ECF_ERROR_CODES.DGII_TAXPAYER_ID_REQUIRED);
451
+ }
452
+ await authorize({
453
+ ctx,
454
+ options,
455
+ user,
456
+ session,
457
+ taxPayerId,
458
+ organizationId: withOrganization ? organizationId : null,
459
+ organization,
460
+ organizationTaxPayerId,
461
+ source,
462
+ withOrganization
463
+ });
464
+ const resolved = {
465
+ taxPayerId,
466
+ organizationId: withOrganization ? organizationId : null,
467
+ organizationTaxPayerId,
468
+ source,
469
+ userId: user.id,
470
+ user,
471
+ session
472
+ };
473
+ return { ...resolved, environment: await environmentOf(options, resolved) };
474
+ };
475
+ var authorize = async (input) => {
476
+ const custom = input.options.authorize;
477
+ if (custom !== void 0) {
478
+ const allowed = await custom({
479
+ user: input.user,
480
+ session: input.session,
481
+ taxPayerId: input.taxPayerId,
482
+ organizationId: input.organizationId,
483
+ organizationTaxPayerId: input.organizationTaxPayerId,
484
+ source: input.source
485
+ });
486
+ if (!allowed) {
487
+ throw forbidden(DGII_ECF_ERROR_CODES.DGII_TAXPAYER_ID_FORBIDDEN);
488
+ }
489
+ return;
490
+ }
491
+ if (!input.withOrganization) {
492
+ return;
493
+ }
494
+ if (input.organizationId === null || input.organization === null) {
495
+ if (input.source === "request" && input.options.allowUntrustedTaxPayerId === true) {
496
+ return;
497
+ }
498
+ if (input.source === "resolver") {
499
+ return;
500
+ }
501
+ throw badRequest(DGII_ECF_ERROR_CODES.DGII_ORGANIZATION_REQUIRED);
502
+ }
503
+ const member = await input.ctx.context.adapter.findOne({
504
+ model: MEMBER_MODEL,
505
+ where: [
506
+ { field: "organizationId", value: input.organizationId },
507
+ { field: "userId", value: input.user.id }
508
+ ]
509
+ });
510
+ if (member === null) {
511
+ throw forbidden(DGII_ECF_ERROR_CODES.DGII_NOT_A_MEMBER);
512
+ }
513
+ const allowedRoles = input.options.roles ?? DEFAULT_ROLES;
514
+ const memberRoles = rolesOf(member);
515
+ if (!memberRoles.some((role) => allowedRoles.includes(role))) {
516
+ throw forbidden(
517
+ DGII_ECF_ERROR_CODES.DGII_INSUFFICIENT_ROLE,
518
+ `${DGII_ECF_ERROR_CODES.DGII_INSUFFICIENT_ROLE.message} (required one of: ${allowedRoles.join(", ")})`
519
+ );
520
+ }
521
+ if (input.organizationTaxPayerId !== null && input.organizationTaxPayerId !== input.taxPayerId) {
522
+ throw forbidden(
523
+ DGII_ECF_ERROR_CODES.DGII_TAXPAYER_ID_FORBIDDEN,
524
+ `Organization ${input.organizationId} issues under ${input.organizationTaxPayerId}, not ${input.taxPayerId}`
525
+ );
526
+ }
527
+ };
528
+
529
+ // src/xml.ts
530
+ var PATTERN_CACHE = /* @__PURE__ */ new Map();
531
+ var ATTRIBUTE_SPAN = `(?:[^>"']|"[^"]*"|'[^']*')*`;
532
+ var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
533
+ var elementPattern = (tag) => {
534
+ const cached = PATTERN_CACHE.get(tag);
535
+ if (cached !== void 0) {
536
+ return cached;
537
+ }
538
+ const name = escapeRegExp(tag);
539
+ const prefix = "(?:[\\w.-]+:)?";
540
+ const pattern = new RegExp(
541
+ `<${prefix}${name}${ATTRIBUTE_SPAN}>([\\s\\S]*?)<\\/${prefix}${name}\\s*>`
542
+ );
543
+ PATTERN_CACHE.set(tag, pattern);
544
+ return pattern;
545
+ };
546
+ var skipNonElement = (xml, open) => {
547
+ if (xml.startsWith("<!--", open)) {
548
+ const end = xml.indexOf("-->", open + 4);
549
+ return end === -1 ? xml.length : end + 3;
550
+ }
551
+ if (xml.startsWith("<?", open)) {
552
+ const end = xml.indexOf("?>", open + 2);
553
+ return end === -1 ? xml.length : end + 2;
554
+ }
555
+ if (xml.startsWith("<!", open)) {
556
+ let depth = 0;
557
+ for (let cursor = open + 2; cursor < xml.length; cursor += 1) {
558
+ const character = xml[cursor];
559
+ if (character === "[") {
560
+ depth += 1;
561
+ } else if (character === "]") {
562
+ depth = Math.max(0, depth - 1);
563
+ } else if (character === ">" && depth === 0) {
564
+ return cursor + 1;
565
+ }
566
+ }
567
+ return xml.length;
568
+ }
569
+ return -1;
570
+ };
571
+ var withoutNonElementMarkup = (xml) => {
572
+ const parts = [];
573
+ let index = 0;
574
+ for (; ; ) {
575
+ const open = xml.indexOf("<", index);
576
+ if (open === -1) {
577
+ parts.push(xml.slice(index));
578
+ return parts.join("");
579
+ }
580
+ parts.push(xml.slice(index, open));
581
+ const end = skipNonElement(xml, open);
582
+ if (end === -1) {
583
+ parts.push("<");
584
+ index = open + 1;
585
+ continue;
586
+ }
587
+ index = end;
588
+ }
589
+ };
590
+ var elementText = (xml, tag) => {
591
+ const text = elementPattern(tag).exec(withoutNonElementMarkup(xml))?.[1]?.trim();
592
+ return text === void 0 || text === "" ? void 0 : text;
593
+ };
594
+ var ROOT_PATTERN = /<\s*(?:[A-Za-z_][\w.-]*:)?([A-Za-z_][\w.-]*)[\s/>]/;
595
+ var rootElementName = (xml) => ROOT_PATTERN.exec(withoutNonElementMarkup(xml))?.[1];
596
+ var ISSUER_TAGS = ["RNCEmisor", "RncEmisor"];
597
+ var issuerTaxPayerId = (xml) => {
598
+ for (const tag of ISSUER_TAGS) {
599
+ const value = elementText(xml, tag);
600
+ if (value !== void 0) {
601
+ return value;
602
+ }
603
+ }
604
+ return void 0;
605
+ };
606
+
607
+ // src/endpoints/shared.ts
608
+ var prepare = async (deps, ctx, input) => {
609
+ const scope = await resolveRequestScope({ ctx, options: deps.options, input });
610
+ const client = await deps.provider.get(scope);
611
+ return { scope, client };
612
+ };
613
+ var unwrap = (result) => {
614
+ if (result.isErr()) {
615
+ throw toApiError(result.error);
616
+ }
617
+ return result.value;
618
+ };
619
+ var assertIssuer = (xml, taxPayerId) => {
620
+ const issuer = issuerTaxPayerId(xml);
621
+ if (issuer === void 0) {
622
+ throw badRequest(DGII_ECF_ERROR_CODES.DGII_DOCUMENT_UNREADABLE);
623
+ }
624
+ if (normalizeTaxPayerId(issuer) !== taxPayerId) {
625
+ throw forbidden(
626
+ DGII_ECF_ERROR_CODES.DGII_TAXPAYER_ID_MISMATCH,
627
+ `The document is issued by ${issuer} but this request acts for ${taxPayerId}`
628
+ );
629
+ }
630
+ };
631
+ var signDocument = async (deps, scope, xml) => {
632
+ const resolver = deps.options.credentials;
633
+ if (resolver === void 0) {
634
+ throw serverError(
635
+ DGII_ECF_ERROR_CODES.DGII_CREDENTIALS_UNAVAILABLE,
636
+ "Server-side signing needs the `credentials` resolver; `getClient` hands the key to the host."
637
+ );
638
+ }
639
+ const material = await resolver(scope);
640
+ if (material === null) {
641
+ throw serverError(
642
+ DGII_ECF_ERROR_CODES.DGII_CREDENTIALS_UNAVAILABLE,
643
+ `No credentials were resolved for taxpayer ${scope.taxPayerId}`
644
+ );
645
+ }
646
+ const { key, cert } = material;
647
+ if (key === void 0 || cert === void 0) {
648
+ throw serverError(
649
+ DGII_ECF_ERROR_CODES.DGII_CREDENTIALS_UNAVAILABLE,
650
+ "The resolved credentials carry no `key`/`cert` PEM pair, so the document cannot be signed"
651
+ );
652
+ }
653
+ const signature = new dgiiEcf$1.Signature(key, cert);
654
+ const root = rootElementName(xml);
655
+ const signed = root === void 0 ? signature.signXml(xml) : signature.signXml(xml, root);
656
+ if (signed.isErr()) {
657
+ throw new api.APIError("BAD_REQUEST", {
658
+ message: signed.error.message,
659
+ code: DGII_ECF_ERROR_CODES.DGII_SIGNING_FAILED.code,
660
+ dgii: { code: signed.error.code }
661
+ });
662
+ }
663
+ return signed.value;
664
+ };
665
+ var documentEncf = (xml) => elementText(xml, "eNCF") ?? elementText(xml, "SecuenciaeNCFDesde") ?? elementText(xml, "RangoNCF") ?? null;
666
+ var trackIdOf = (value) => {
667
+ if (typeof value !== "object" || value === null) {
668
+ return null;
669
+ }
670
+ const candidate = value.trackId;
671
+ return typeof candidate === "string" && candidate !== "" ? candidate : null;
672
+ };
673
+ var runSubmission = async ({
674
+ ctx,
675
+ deps,
676
+ scope,
677
+ operation,
678
+ xml,
679
+ idempotencyKey,
680
+ send
681
+ }) => {
682
+ const store = createSubmissionStore(ctx, deps.options.persistence);
683
+ const documentHash = hashDocument(xml);
684
+ const replayKey = idempotencyKey ?? documentHash;
685
+ if (store.enabled) {
686
+ const existing = await store.findReplay(scope.taxPayerId, replayKey);
687
+ const decision = planReplay(existing);
688
+ if (decision.kind === "conflict") {
689
+ throw conflict(
690
+ DGII_ECF_ERROR_CODES.DGII_IDEMPOTENCY_IN_FLIGHT,
691
+ `A submission for this document is still in flight (record ${existing?.id ?? "unknown"}). Reconcile it by e-NCF before retrying.`
692
+ );
693
+ }
694
+ if (decision.kind === "replay") {
695
+ return {
696
+ result: parseStoredResponse(decision.record),
697
+ replayed: true,
698
+ submissionId: decision.record.id
699
+ };
700
+ }
701
+ }
702
+ const submissionId = await store.begin({
703
+ organizationId: scope.organizationId,
704
+ taxPayerId: scope.taxPayerId,
705
+ userId: scope.userId,
706
+ environment: scope.environment,
707
+ operation,
708
+ idempotencyKey: replayKey,
709
+ encf: documentEncf(xml),
710
+ documentHash
711
+ });
712
+ let result;
713
+ try {
714
+ result = unwrap(await send());
715
+ } catch (error) {
716
+ const errorCode = error instanceof api.APIError ? String(error.body?.code ?? error.status) : error instanceof Error ? error.name : "UNKNOWN";
717
+ await store.complete(submissionId, { status: "failed", errorCode });
718
+ throw error;
719
+ }
720
+ await store.complete(submissionId, {
721
+ status: "succeeded",
722
+ trackId: trackIdOf(result),
723
+ response: result
724
+ });
725
+ return { result, replayed: false, submissionId };
726
+ };
727
+ var scopeEcho = (scope) => ({
728
+ taxPayerId: scope.taxPayerId,
729
+ organizationId: scope.organizationId,
730
+ environment: scope.environment
731
+ });
732
+
733
+ // src/endpoints/documents.ts
734
+ var scopeShape = {
735
+ taxPayerId: z__namespace.string().min(1).optional().meta({
736
+ description: "Taxpayer id (RNC / c\xE9dula) to act for. Overrides the organization field for this request only."
737
+ }),
738
+ organizationId: z__namespace.string().min(1).optional().meta({
739
+ description: "Organization to act for. Defaults to the session active organization."
740
+ })
741
+ };
742
+ var documentShape = {
743
+ ...scopeShape,
744
+ xml: z__namespace.string().min(1).meta({ description: "The DGII document, as XML." }),
745
+ fileName: z__namespace.string().min(1).max(255).optional().meta({
746
+ description: "Multipart file name sent to DGII. Defaults to the document e-NCF."
747
+ }),
748
+ sign: z__namespace.boolean().optional().meta({
749
+ description: "Sign the XML server-side with the organization key before transmitting. Requires the `credentials` resolver."
750
+ }),
751
+ idempotencyKey: z__namespace.string().min(1).max(200).optional().meta({
752
+ description: "Replay guard key. Defaults to the SHA-256 of the document, so retrying the same document never double-issues."
753
+ })
754
+ };
755
+ var fileNameFor = (xml, fallback) => {
756
+ const encf = documentEncf(xml);
757
+ return encf === null ? `${fallback}.xml` : `${encf}.xml`;
758
+ };
759
+ var sendDocument = (deps) => api.createAuthEndpoint(
760
+ "/dgii-ecf/documents",
761
+ {
762
+ method: "POST",
763
+ body: z__namespace.object({
764
+ ...documentShape,
765
+ buyerHost: z__namespace.string().url().optional().meta({
766
+ description: "Deliver directly to the buyer Sender\u2194Receiver host instead of DGII."
767
+ })
768
+ }),
769
+ use: [api.sessionMiddleware]
770
+ },
771
+ async (ctx) => {
772
+ const { scope, client } = await prepare(deps, ctx, ctx.body);
773
+ const xml = ctx.body.sign === true ? await signDocument(deps, scope, ctx.body.xml) : ctx.body.xml;
774
+ assertIssuer(xml, scope.taxPayerId);
775
+ const outcome = await runSubmission({
776
+ ctx,
777
+ deps,
778
+ scope,
779
+ operation: "sendDocument",
780
+ xml,
781
+ idempotencyKey: ctx.body.idempotencyKey,
782
+ send: () => client.sendDocument(
783
+ xml,
784
+ ctx.body.fileName ?? fileNameFor(xml, "document"),
785
+ ctx.body.buyerHost
786
+ )
787
+ });
788
+ return ctx.json({
789
+ ...scopeEcho(scope),
790
+ submissionId: outcome.submissionId,
791
+ replayed: outcome.replayed,
792
+ result: outcome.result
793
+ });
794
+ }
795
+ );
796
+ var sendElectronicDocument = (deps) => api.createAuthEndpoint(
797
+ "/dgii-ecf/documents/electronic",
798
+ {
799
+ method: "POST",
800
+ body: z__namespace.object({
801
+ ...documentShape,
802
+ buyerHost: z__namespace.string().url().optional()
803
+ }),
804
+ use: [api.sessionMiddleware]
805
+ },
806
+ async (ctx) => {
807
+ const { scope, client } = await prepare(deps, ctx, ctx.body);
808
+ const xml = ctx.body.sign === true ? await signDocument(deps, scope, ctx.body.xml) : ctx.body.xml;
809
+ assertIssuer(xml, scope.taxPayerId);
810
+ const outcome = await runSubmission({
811
+ ctx,
812
+ deps,
813
+ scope,
814
+ operation: "sendElectronicDocument",
815
+ xml,
816
+ idempotencyKey: ctx.body.idempotencyKey,
817
+ send: () => client.sendElectronicDocument(
818
+ xml,
819
+ ctx.body.fileName ?? fileNameFor(xml, "ecf"),
820
+ ctx.body.buyerHost
821
+ )
822
+ });
823
+ return ctx.json({
824
+ ...scopeEcho(scope),
825
+ submissionId: outcome.submissionId,
826
+ replayed: outcome.replayed,
827
+ result: outcome.result
828
+ });
829
+ }
830
+ );
831
+ var sendSummary = (deps) => api.createAuthEndpoint(
832
+ "/dgii-ecf/documents/summary",
833
+ {
834
+ method: "POST",
835
+ body: z__namespace.object(documentShape),
836
+ use: [api.sessionMiddleware]
837
+ },
838
+ async (ctx) => {
839
+ const { scope, client } = await prepare(deps, ctx, ctx.body);
840
+ const xml = ctx.body.sign === true ? await signDocument(deps, scope, ctx.body.xml) : ctx.body.xml;
841
+ assertIssuer(xml, scope.taxPayerId);
842
+ const outcome = await runSubmission({
843
+ ctx,
844
+ deps,
845
+ scope,
846
+ operation: "sendSummary",
847
+ xml,
848
+ idempotencyKey: ctx.body.idempotencyKey,
849
+ send: () => client.sendSummary(xml, ctx.body.fileName ?? fileNameFor(xml, "summary"))
850
+ });
851
+ return ctx.json({
852
+ ...scopeEcho(scope),
853
+ submissionId: outcome.submissionId,
854
+ replayed: outcome.replayed,
855
+ result: outcome.result
856
+ });
857
+ }
858
+ );
859
+ var sendCommercialApproval = (deps) => api.createAuthEndpoint(
860
+ "/dgii-ecf/documents/commercial-approval",
861
+ {
862
+ method: "POST",
863
+ body: z__namespace.object({
864
+ ...documentShape,
865
+ buyerHost: z__namespace.string().url().optional().meta({
866
+ description: "Deliver the approval directly to the issuer host."
867
+ })
868
+ }),
869
+ use: [api.sessionMiddleware]
870
+ },
871
+ async (ctx) => {
872
+ const { scope, client } = await prepare(deps, ctx, ctx.body);
873
+ const xml = ctx.body.sign === true ? await signDocument(deps, scope, ctx.body.xml) : ctx.body.xml;
874
+ assertIssuer(xml, scope.taxPayerId);
875
+ const buyerHost = ctx.body.buyerHost;
876
+ const outcome = await runSubmission({
877
+ ctx,
878
+ deps,
879
+ scope,
880
+ operation: "sendCommercialApproval",
881
+ xml,
882
+ idempotencyKey: ctx.body.idempotencyKey,
883
+ send: () => {
884
+ const fileName = ctx.body.fileName ?? fileNameFor(xml, "approval");
885
+ return buyerHost === void 0 ? client.sendCommercialApproval(xml, fileName) : client.sendCommercialApproval(xml, fileName, buyerHost);
886
+ }
887
+ });
888
+ return ctx.json({
889
+ ...scopeEcho(scope),
890
+ submissionId: outcome.submissionId,
891
+ replayed: outcome.replayed,
892
+ result: outcome.result
893
+ });
894
+ }
895
+ );
896
+ var voidENCF = (deps) => api.createAuthEndpoint(
897
+ "/dgii-ecf/documents/void-range",
898
+ {
899
+ method: "POST",
900
+ body: z__namespace.object(documentShape),
901
+ use: [api.sessionMiddleware]
902
+ },
903
+ async (ctx) => {
904
+ const { scope, client } = await prepare(deps, ctx, ctx.body);
905
+ const xml = ctx.body.sign === true ? await signDocument(deps, scope, ctx.body.xml) : ctx.body.xml;
906
+ assertIssuer(xml, scope.taxPayerId);
907
+ const outcome = await runSubmission({
908
+ ctx,
909
+ deps,
910
+ scope,
911
+ operation: "voidENCF",
912
+ xml,
913
+ idempotencyKey: ctx.body.idempotencyKey,
914
+ send: () => client.voidENCF(xml, ctx.body.fileName ?? fileNameFor(xml, "void-range"))
915
+ });
916
+ return ctx.json({
917
+ ...scopeEcho(scope),
918
+ submissionId: outcome.submissionId,
919
+ replayed: outcome.replayed,
920
+ result: outcome.result
921
+ });
922
+ }
923
+ );
924
+ var reconcileSubmission = (deps) => api.createAuthEndpoint(
925
+ "/dgii-ecf/documents/reconcile",
926
+ {
927
+ method: "POST",
928
+ body: z__namespace.object({
929
+ ...scopeShape,
930
+ xml: z__namespace.string().min(1).meta({ description: "The signed document, as XML." })
931
+ }),
932
+ use: [api.sessionMiddleware]
933
+ },
934
+ async (ctx) => {
935
+ const { scope, client } = await prepare(deps, ctx, ctx.body);
936
+ assertIssuer(ctx.body.xml, scope.taxPayerId);
937
+ const result = unwrap(await client.reconcileSubmission(ctx.body.xml));
938
+ return ctx.json({ ...scopeEcho(scope), result });
939
+ }
940
+ );
941
+ var scopeShape2 = {
942
+ taxPayerId: z__namespace.string().min(1).optional().meta({
943
+ description: "Taxpayer id (RNC / c\xE9dula) to act for. Overrides the organization field."
944
+ }),
945
+ organizationId: z__namespace.string().min(1).optional().meta({
946
+ description: "Organization to act for. Defaults to the session active organization."
947
+ })
948
+ };
949
+ var trackStatus = (deps) => api.createAuthEndpoint(
950
+ "/dgii-ecf/tracks",
951
+ {
952
+ method: "GET",
953
+ query: z__namespace.object({
954
+ ...scopeShape2,
955
+ trackId: z__namespace.string().min(1).meta({ description: "DGII track id." })
956
+ }),
957
+ use: [api.sessionMiddleware]
958
+ },
959
+ async (ctx) => {
960
+ const { scope, client } = await prepare(deps, ctx, ctx.query);
961
+ const result = unwrap(await client.statusTrackId(ctx.query.trackId));
962
+ return ctx.json({ ...scopeEcho(scope), result });
963
+ }
964
+ );
965
+ var inquiryStatus = (deps) => api.createAuthEndpoint(
966
+ "/dgii-ecf/inquiries/status",
967
+ {
968
+ method: "POST",
969
+ body: z__namespace.object({
970
+ ...scopeShape2,
971
+ ncf: z__namespace.string().min(1).meta({ description: "The e-NCF to look up." }),
972
+ buyerTaxPayerId: z__namespace.string().min(1).optional().meta({ description: "Buyer RNC, when DGII needs it to disambiguate." }),
973
+ securityCode: z__namespace.string().min(1).optional().meta({ description: "Six-digit security code of the document." })
974
+ }),
975
+ use: [api.sessionMiddleware]
976
+ },
977
+ async (ctx) => {
978
+ const { scope, client } = await prepare(deps, ctx, ctx.body);
979
+ const result = unwrap(
980
+ await client.inquiryStatus(
981
+ scope.taxPayerId,
982
+ ctx.body.ncf,
983
+ ctx.body.buyerTaxPayerId,
984
+ ctx.body.securityCode
985
+ )
986
+ );
987
+ return ctx.json({ ...scopeEcho(scope), result });
988
+ }
989
+ );
990
+ var trackStatuses = (deps) => api.createAuthEndpoint(
991
+ "/dgii-ecf/inquiries/tracks",
992
+ {
993
+ method: "POST",
994
+ body: z__namespace.object({
995
+ ...scopeShape2,
996
+ encf: z__namespace.string().min(1).meta({ description: "The e-NCF to list tracks for." })
997
+ }),
998
+ use: [api.sessionMiddleware]
999
+ },
1000
+ async (ctx) => {
1001
+ const { scope, client } = await prepare(deps, ctx, ctx.body);
1002
+ const result = unwrap(
1003
+ await client.trackStatuses(scope.taxPayerId, ctx.body.encf)
1004
+ );
1005
+ return ctx.json({ ...scopeEcho(scope), result });
1006
+ }
1007
+ );
1008
+ var summaryInvoiceInquiry = (deps) => api.createAuthEndpoint(
1009
+ "/dgii-ecf/inquiries/summary",
1010
+ {
1011
+ method: "POST",
1012
+ body: z__namespace.object({
1013
+ ...scopeShape2,
1014
+ encf: z__namespace.string().min(1).meta({ description: "The RFCE e-NCF to look up." }),
1015
+ securityCode: z__namespace.string().min(1).meta({ description: "Six-digit security code of the summary invoice." })
1016
+ }),
1017
+ use: [api.sessionMiddleware]
1018
+ },
1019
+ async (ctx) => {
1020
+ const { scope, client } = await prepare(deps, ctx, ctx.body);
1021
+ const result = unwrap(
1022
+ await client.getSummaryInvoiceInquiry(
1023
+ scope.taxPayerId,
1024
+ ctx.body.encf,
1025
+ ctx.body.securityCode
1026
+ )
1027
+ );
1028
+ return ctx.json({ ...scopeEcho(scope), result });
1029
+ }
1030
+ );
1031
+ var customerDirectory = (deps) => api.createAuthEndpoint(
1032
+ "/dgii-ecf/directories/customers",
1033
+ {
1034
+ method: "GET",
1035
+ query: z__namespace.object({
1036
+ ...scopeShape2,
1037
+ rnc: z__namespace.string().min(1).optional().meta({
1038
+ description: "RNC to look up. Defaults to the taxpayer this request acts for."
1039
+ })
1040
+ }),
1041
+ use: [api.sessionMiddleware]
1042
+ },
1043
+ async (ctx) => {
1044
+ const { scope, client } = await prepare(deps, ctx, ctx.query);
1045
+ const result = unwrap(
1046
+ await client.getCustomerDirectory(ctx.query.rnc ?? scope.taxPayerId)
1047
+ );
1048
+ return ctx.json({ ...scopeEcho(scope), result: result ?? [] });
1049
+ }
1050
+ );
1051
+ var directoryListing = (deps) => api.createAuthEndpoint(
1052
+ "/dgii-ecf/directories/all",
1053
+ {
1054
+ method: "GET",
1055
+ query: z__namespace.object(scopeShape2),
1056
+ use: [api.sessionMiddleware]
1057
+ },
1058
+ async (ctx) => {
1059
+ const { scope, client } = await prepare(deps, ctx, ctx.query);
1060
+ const result = unwrap(await client.getDirectoryListing());
1061
+ return ctx.json({ ...scopeEcho(scope), result });
1062
+ }
1063
+ );
1064
+ var scopeShape3 = {
1065
+ taxPayerId: z__namespace.string().min(1).optional().meta({
1066
+ description: "Taxpayer id (RNC / c\xE9dula) to act for. Overrides the organization field."
1067
+ }),
1068
+ organizationId: z__namespace.string().min(1).optional().meta({
1069
+ description: "Organization to act for. Defaults to the session active organization."
1070
+ })
1071
+ };
1072
+ var STATUS_ENVIRONMENTS = {
1073
+ precertification: dgiiEcf$1.StatusEnvironment.PreCertification,
1074
+ production: dgiiEcf$1.StatusEnvironment.Production,
1075
+ certification: dgiiEcf$1.StatusEnvironment.Certification
1076
+ };
1077
+ var requireStatusKey = (deps) => {
1078
+ if (deps.provider.constructsClients && deps.options.status?.apiKey === void 0) {
1079
+ throw serverError(DGII_ECF_ERROR_CODES.DGII_STATUS_API_KEY_REQUIRED);
1080
+ }
1081
+ };
1082
+ var authenticate = (deps) => api.createAuthEndpoint(
1083
+ "/dgii-ecf/authenticate",
1084
+ {
1085
+ method: "POST",
1086
+ body: z__namespace.object({
1087
+ ...scopeShape3,
1088
+ buyerHost: z__namespace.string().url().optional().meta({ description: "Authenticate against a buyer Sender\u2194Receiver host." })
1089
+ }),
1090
+ use: [api.sessionMiddleware]
1091
+ },
1092
+ async (ctx) => {
1093
+ const { scope, client } = await prepare(deps, ctx, ctx.body);
1094
+ const token = unwrap(await client.authenticate(ctx.body.buyerHost));
1095
+ return ctx.json({
1096
+ ...scopeEcho(scope),
1097
+ issuedAt: token.expedido,
1098
+ expiresAt: token.expira
1099
+ });
1100
+ }
1101
+ );
1102
+ var serviceStatus = (deps) => api.createAuthEndpoint(
1103
+ "/dgii-ecf/service-status",
1104
+ {
1105
+ method: "GET",
1106
+ query: z__namespace.object(scopeShape3),
1107
+ use: [api.sessionMiddleware]
1108
+ },
1109
+ async (ctx) => {
1110
+ requireStatusKey(deps);
1111
+ const { scope, client } = await prepare(deps, ctx, ctx.query);
1112
+ const services = unwrap(await client.getServiceStatus());
1113
+ return ctx.json({ ...scopeEcho(scope), services });
1114
+ }
1115
+ );
1116
+ var maintenanceWindows = (deps) => api.createAuthEndpoint(
1117
+ "/dgii-ecf/maintenance-windows",
1118
+ {
1119
+ method: "GET",
1120
+ query: z__namespace.object(scopeShape3),
1121
+ use: [api.sessionMiddleware]
1122
+ },
1123
+ async (ctx) => {
1124
+ requireStatusKey(deps);
1125
+ const { scope, client } = await prepare(deps, ctx, ctx.query);
1126
+ const windows = unwrap(await client.getMaintenanceWindows());
1127
+ return ctx.json({ ...scopeEcho(scope), windows });
1128
+ }
1129
+ );
1130
+ var serviceAvailability = (deps) => api.createAuthEndpoint(
1131
+ "/dgii-ecf/service-availability",
1132
+ {
1133
+ method: "GET",
1134
+ query: z__namespace.object({
1135
+ ...scopeShape3,
1136
+ environment: z__namespace.enum(["precertification", "production", "certification"]).meta({ description: "DGII environment to query." })
1137
+ }),
1138
+ use: [api.sessionMiddleware]
1139
+ },
1140
+ async (ctx) => {
1141
+ requireStatusKey(deps);
1142
+ const { scope, client } = await prepare(deps, ctx, ctx.query);
1143
+ const result = unwrap(
1144
+ await client.checkServiceAvailability(
1145
+ STATUS_ENVIRONMENTS[ctx.query.environment]
1146
+ )
1147
+ );
1148
+ return ctx.json({
1149
+ ...scopeEcho(scope),
1150
+ environmentChecked: ctx.query.environment,
1151
+ estado: result.estado
1152
+ });
1153
+ }
1154
+ );
1155
+ var scopeShape4 = {
1156
+ taxPayerId: z__namespace.string().min(1).optional().meta({
1157
+ description: "Taxpayer id (RNC / c\xE9dula) to act for. Overrides the organization field."
1158
+ }),
1159
+ organizationId: z__namespace.string().min(1).optional().meta({
1160
+ description: "Organization to act for. Defaults to the session active organization."
1161
+ })
1162
+ };
1163
+ var resolveTaxPayer = (deps) => api.createAuthEndpoint(
1164
+ "/dgii-ecf/taxpayer",
1165
+ {
1166
+ method: "GET",
1167
+ query: z__namespace.object(scopeShape4),
1168
+ use: [api.sessionMiddleware]
1169
+ },
1170
+ async (ctx) => {
1171
+ const { scope } = await prepare(deps, ctx, ctx.query);
1172
+ return ctx.json({
1173
+ taxPayerId: scope.taxPayerId,
1174
+ organizationId: scope.organizationId,
1175
+ organizationTaxPayerId: scope.organizationTaxPayerId,
1176
+ source: scope.source,
1177
+ environment: scope.environment
1178
+ });
1179
+ }
1180
+ );
1181
+ var listSubmissions = (deps) => api.createAuthEndpoint(
1182
+ "/dgii-ecf/submissions",
1183
+ {
1184
+ method: "GET",
1185
+ query: z__namespace.object({
1186
+ ...scopeShape4,
1187
+ limit: z__namespace.coerce.number().int().min(1).max(100).optional().meta({
1188
+ description: "Maximum number of records to return. Defaults to 20."
1189
+ }),
1190
+ status: z__namespace.enum(["pending", "succeeded", "failed"]).optional().meta({ description: "Filter by record status." })
1191
+ }),
1192
+ use: [api.sessionMiddleware]
1193
+ },
1194
+ async (ctx) => {
1195
+ const { scope } = await prepare(deps, ctx, ctx.query);
1196
+ const store = createSubmissionStore(ctx, deps.options.persistence);
1197
+ const records = await store.list({
1198
+ organizationId: scope.organizationId,
1199
+ taxPayerId: scope.organizationId === null ? scope.taxPayerId : null,
1200
+ limit: Math.min(ctx.query.limit ?? 20, 100)
1201
+ });
1202
+ const filtered = ctx.query.status === void 0 ? records : records.filter((record) => record.status === ctx.query.status);
1203
+ return ctx.json({
1204
+ ...scopeEcho(scope),
1205
+ submissions: filtered.map((record) => ({
1206
+ id: record.id,
1207
+ operation: record.operation,
1208
+ status: record.status,
1209
+ taxPayerId: record.taxPayerId,
1210
+ organizationId: record.organizationId ?? null,
1211
+ environment: record.environment,
1212
+ encf: record.encf ?? null,
1213
+ trackId: record.trackId ?? null,
1214
+ documentHash: record.documentHash ?? null,
1215
+ errorCode: record.errorCode ?? null,
1216
+ createdAt: record.createdAt ?? null
1217
+ }))
1218
+ });
1219
+ }
1220
+ );
1221
+
1222
+ // src/plugin.ts
1223
+ var readonlyTaxPayerIdField = () => ({
1224
+ type: "string",
1225
+ required: false,
1226
+ input: false,
1227
+ returned: true,
1228
+ unique: false
1229
+ });
1230
+ var dgiiEcfOrganizationSchema = (field = "taxPayerId") => ({
1231
+ additionalFields: {
1232
+ [field]: {
1233
+ type: "string",
1234
+ required: false,
1235
+ // Writable: this fragment is declared by the organization plugin, which
1236
+ // is what makes `organization.create` / `organization.update` accept it.
1237
+ input: true,
1238
+ returned: true,
1239
+ unique: false
1240
+ }
1241
+ }
1242
+ });
1243
+ var dgiiEcf = (options = {}) => {
1244
+ const provider = createClientProvider(options);
1245
+ const deps = { options, provider };
1246
+ const schema = {
1247
+ [SUBMISSION_MODEL]: {
1248
+ modelName: SUBMISSION_MODEL,
1249
+ fields: {
1250
+ organizationId: { type: "string", required: false },
1251
+ taxPayerId: { type: "string", required: true },
1252
+ userId: { type: "string", required: true },
1253
+ environment: { type: "string", required: true },
1254
+ operation: { type: "string", required: true },
1255
+ status: { type: "string", required: true },
1256
+ idempotencyKey: { type: "string", required: false },
1257
+ encf: { type: "string", required: false },
1258
+ trackId: { type: "string", required: false },
1259
+ documentHash: { type: "string", required: false },
1260
+ errorCode: { type: "string", required: false },
1261
+ response: { type: "string", required: false },
1262
+ // Better Auth does not add timestamps implicitly: a plugin table that
1263
+ // wants them declares them. `createdAt` orders the replay lookup and the
1264
+ // audit listing; `updatedAt` records when a submission was closed.
1265
+ createdAt: { type: "date", required: true, defaultValue: () => /* @__PURE__ */ new Date() },
1266
+ updatedAt: { type: "date", required: false, onUpdate: () => /* @__PURE__ */ new Date() }
1267
+ },
1268
+ indexes: [
1269
+ // The replay lookup: taxpayer + key must be a cheap read.
1270
+ { fields: ["taxPayerId", "idempotencyKey"] },
1271
+ // The audit listing: "everything this organization submitted".
1272
+ { fields: ["organizationId", "createdAt"] }
1273
+ ]
1274
+ }
1275
+ };
1276
+ if (options.organization?.extendSchema === true) {
1277
+ schema.organization = {
1278
+ fields: {
1279
+ [options.organization.field ?? "taxPayerId"]: readonlyTaxPayerIdField()
1280
+ }
1281
+ };
1282
+ }
1283
+ const plugin = {
1284
+ id: "dgii-ecf",
1285
+ $ERROR_CODES: DGII_ECF_ERROR_CODES,
1286
+ schema,
1287
+ endpoints: {
1288
+ // Identity
1289
+ dgiiEcfResolveTaxPayer: resolveTaxPayer(deps),
1290
+ // Session / token
1291
+ dgiiEcfAuthenticate: authenticate(deps),
1292
+ // Documents
1293
+ dgiiEcfSendDocument: sendDocument(deps),
1294
+ dgiiEcfSendElectronicDocument: sendElectronicDocument(deps),
1295
+ dgiiEcfSendSummary: sendSummary(deps),
1296
+ dgiiEcfSendCommercialApproval: sendCommercialApproval(deps),
1297
+ dgiiEcfVoidENCF: voidENCF(deps),
1298
+ dgiiEcfReconcileSubmission: reconcileSubmission(deps),
1299
+ // Queries
1300
+ dgiiEcfTrackStatus: trackStatus(deps),
1301
+ dgiiEcfInquiryStatus: inquiryStatus(deps),
1302
+ dgiiEcfTrackStatuses: trackStatuses(deps),
1303
+ dgiiEcfSummaryInvoiceInquiry: summaryInvoiceInquiry(deps),
1304
+ dgiiEcfCustomerDirectory: customerDirectory(deps),
1305
+ dgiiEcfDirectoryListing: directoryListing(deps),
1306
+ // DGII operations
1307
+ dgiiEcfServiceStatus: serviceStatus(deps),
1308
+ dgiiEcfMaintenanceWindows: maintenanceWindows(deps),
1309
+ dgiiEcfServiceAvailability: serviceAvailability(deps),
1310
+ // Audit
1311
+ dgiiEcfListSubmissions: listSubmissions(deps)
1312
+ },
1313
+ rateLimit: [
1314
+ {
1315
+ // Fiscal transmissions are metered by DGII and expensive to retry, so
1316
+ // the whole surface gets a conservative per-window budget.
1317
+ window: 60,
1318
+ max: 60,
1319
+ pathMatcher: (path) => path.startsWith("/dgii-ecf")
1320
+ }
1321
+ ]
1322
+ };
1323
+ return Object.assign(plugin, { clientProvider: provider });
1324
+ };
1325
+
1326
+ Object.defineProperty(exports, "Environment", {
1327
+ enumerable: true,
1328
+ get: function () { return dgiiEcf$1.Environment; }
1329
+ });
1330
+ exports.DGII_ECF_ERROR_CODES = DGII_ECF_ERROR_CODES;
1331
+ exports.SUBMISSION_MODEL = SUBMISSION_MODEL;
1332
+ exports.dgiiEcf = dgiiEcf;
1333
+ exports.dgiiEcfOrganizationSchema = dgiiEcfOrganizationSchema;
1334
+ exports.isTaxPayerIdShape = isTaxPayerIdShape;
1335
+ exports.normalizeTaxPayerId = normalizeTaxPayerId;
1336
+ //# sourceMappingURL=index.cjs.map
1337
+ //# sourceMappingURL=index.cjs.map