@open-mercato/tillio 0.7.1-develop.7176.1.d229567a50

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 (135) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/build.mjs +7 -0
  3. package/dist/index.js +5 -0
  4. package/dist/index.js.map +7 -0
  5. package/dist/modules/tillio/__integration__/TC-PHONE-001-pull-readiness.spec.js +17 -0
  6. package/dist/modules/tillio/__integration__/TC-PHONE-001-pull-readiness.spec.js.map +7 -0
  7. package/dist/modules/tillio/__integration__/TC-PHONE-002-pull-blocked.spec.js +27 -0
  8. package/dist/modules/tillio/__integration__/TC-PHONE-002-pull-blocked.spec.js.map +7 -0
  9. package/dist/modules/tillio/__integration__/TC-PHONE-003-operators.spec.js +47 -0
  10. package/dist/modules/tillio/__integration__/TC-PHONE-003-operators.spec.js.map +7 -0
  11. package/dist/modules/tillio/__integration__/TC-PHONE-004-pull-widget.spec.js +11 -0
  12. package/dist/modules/tillio/__integration__/TC-PHONE-004-pull-widget.spec.js.map +7 -0
  13. package/dist/modules/tillio/__integration__/TC-PHONE-005-operators-widget.spec.js +13 -0
  14. package/dist/modules/tillio/__integration__/TC-PHONE-005-operators-widget.spec.js.map +7 -0
  15. package/dist/modules/tillio/acl.js +9 -0
  16. package/dist/modules/tillio/acl.js.map +7 -0
  17. package/dist/modules/tillio/api/operators/[id]/route.js +78 -0
  18. package/dist/modules/tillio/api/operators/[id]/route.js.map +7 -0
  19. package/dist/modules/tillio/api/operators/route.js +171 -0
  20. package/dist/modules/tillio/api/operators/route.js.map +7 -0
  21. package/dist/modules/tillio/api/pull/route.js +180 -0
  22. package/dist/modules/tillio/api/pull/route.js.map +7 -0
  23. package/dist/modules/tillio/cli.js +108 -0
  24. package/dist/modules/tillio/cli.js.map +7 -0
  25. package/dist/modules/tillio/di.js +16 -0
  26. package/dist/modules/tillio/di.js.map +7 -0
  27. package/dist/modules/tillio/index.js +12 -0
  28. package/dist/modules/tillio/index.js.map +7 -0
  29. package/dist/modules/tillio/integration.js +62 -0
  30. package/dist/modules/tillio/integration.js.map +7 -0
  31. package/dist/modules/tillio/lib/adapter.js +107 -0
  32. package/dist/modules/tillio/lib/adapter.js.map +7 -0
  33. package/dist/modules/tillio/lib/client.js +249 -0
  34. package/dist/modules/tillio/lib/client.js.map +7 -0
  35. package/dist/modules/tillio/lib/environment.js +34 -0
  36. package/dist/modules/tillio/lib/environment.js.map +7 -0
  37. package/dist/modules/tillio/lib/error-codes.js +59 -0
  38. package/dist/modules/tillio/lib/error-codes.js.map +7 -0
  39. package/dist/modules/tillio/lib/errors.js +12 -0
  40. package/dist/modules/tillio/lib/errors.js.map +7 -0
  41. package/dist/modules/tillio/lib/health.js +41 -0
  42. package/dist/modules/tillio/lib/health.js.map +7 -0
  43. package/dist/modules/tillio/lib/locking.js +18 -0
  44. package/dist/modules/tillio/lib/locking.js.map +7 -0
  45. package/dist/modules/tillio/lib/normalizer.js +134 -0
  46. package/dist/modules/tillio/lib/normalizer.js.map +7 -0
  47. package/dist/modules/tillio/lib/operators-store.js +62 -0
  48. package/dist/modules/tillio/lib/operators-store.js.map +7 -0
  49. package/dist/modules/tillio/lib/operators.js +168 -0
  50. package/dist/modules/tillio/lib/operators.js.map +7 -0
  51. package/dist/modules/tillio/lib/preset.js +122 -0
  52. package/dist/modules/tillio/lib/preset.js.map +7 -0
  53. package/dist/modules/tillio/lib/pull-job.js +265 -0
  54. package/dist/modules/tillio/lib/pull-job.js.map +7 -0
  55. package/dist/modules/tillio/lib/pull-readiness.js +36 -0
  56. package/dist/modules/tillio/lib/pull-readiness.js.map +7 -0
  57. package/dist/modules/tillio/lib/queue.js +22 -0
  58. package/dist/modules/tillio/lib/queue.js.map +7 -0
  59. package/dist/modules/tillio/lib/tz.js +85 -0
  60. package/dist/modules/tillio/lib/tz.js.map +7 -0
  61. package/dist/modules/tillio/lib/url-guard.js +24 -0
  62. package/dist/modules/tillio/lib/url-guard.js.map +7 -0
  63. package/dist/modules/tillio/setup.js +45 -0
  64. package/dist/modules/tillio/setup.js.map +7 -0
  65. package/dist/modules/tillio/widgets/injection/operators-config/widget.client.js +208 -0
  66. package/dist/modules/tillio/widgets/injection/operators-config/widget.client.js.map +7 -0
  67. package/dist/modules/tillio/widgets/injection/operators-config/widget.js +17 -0
  68. package/dist/modules/tillio/widgets/injection/operators-config/widget.js.map +7 -0
  69. package/dist/modules/tillio/widgets/injection/pull-calls/widget.client.js +148 -0
  70. package/dist/modules/tillio/widgets/injection/pull-calls/widget.client.js.map +7 -0
  71. package/dist/modules/tillio/widgets/injection/pull-calls/widget.js +17 -0
  72. package/dist/modules/tillio/widgets/injection/pull-calls/widget.js.map +7 -0
  73. package/dist/modules/tillio/widgets/injection-table.js +21 -0
  74. package/dist/modules/tillio/widgets/injection-table.js.map +7 -0
  75. package/dist/modules/tillio/workers/tillio-pull.js +17 -0
  76. package/dist/modules/tillio/workers/tillio-pull.js.map +7 -0
  77. package/jest.config.cjs +35 -0
  78. package/package.json +92 -0
  79. package/src/index.ts +1 -0
  80. package/src/modules/tillio/__integration__/TC-PHONE-001-pull-readiness.spec.ts +38 -0
  81. package/src/modules/tillio/__integration__/TC-PHONE-002-pull-blocked.spec.ts +44 -0
  82. package/src/modules/tillio/__integration__/TC-PHONE-003-operators.spec.ts +76 -0
  83. package/src/modules/tillio/__integration__/TC-PHONE-004-pull-widget.spec.ts +22 -0
  84. package/src/modules/tillio/__integration__/TC-PHONE-005-operators-widget.spec.ts +27 -0
  85. package/src/modules/tillio/__tests__/adapter-fetch-calls.test.ts +148 -0
  86. package/src/modules/tillio/__tests__/client.test.ts +203 -0
  87. package/src/modules/tillio/__tests__/health.test.ts +122 -0
  88. package/src/modules/tillio/__tests__/normalizer.test.ts +143 -0
  89. package/src/modules/tillio/__tests__/operators-routes.test.ts +100 -0
  90. package/src/modules/tillio/__tests__/operators-store.test.ts +104 -0
  91. package/src/modules/tillio/__tests__/operators.test.ts +250 -0
  92. package/src/modules/tillio/__tests__/preset.test.ts +337 -0
  93. package/src/modules/tillio/__tests__/pull-body.test.ts +43 -0
  94. package/src/modules/tillio/__tests__/pull-job.test.ts +250 -0
  95. package/src/modules/tillio/__tests__/pull-readiness.test.ts +143 -0
  96. package/src/modules/tillio/__tests__/pull-route.test.ts +148 -0
  97. package/src/modules/tillio/__tests__/tz.test.ts +53 -0
  98. package/src/modules/tillio/__tests__/url-guard.test.ts +46 -0
  99. package/src/modules/tillio/acl.ts +5 -0
  100. package/src/modules/tillio/api/operators/[id]/route.ts +90 -0
  101. package/src/modules/tillio/api/operators/route.ts +191 -0
  102. package/src/modules/tillio/api/pull/route.ts +213 -0
  103. package/src/modules/tillio/cli.ts +125 -0
  104. package/src/modules/tillio/di.ts +16 -0
  105. package/src/modules/tillio/i18n/de.json +73 -0
  106. package/src/modules/tillio/i18n/en.json +73 -0
  107. package/src/modules/tillio/i18n/es.json +73 -0
  108. package/src/modules/tillio/i18n/ko.json +73 -0
  109. package/src/modules/tillio/i18n/pl.json +73 -0
  110. package/src/modules/tillio/index.ts +8 -0
  111. package/src/modules/tillio/integration.ts +58 -0
  112. package/src/modules/tillio/lib/adapter.ts +135 -0
  113. package/src/modules/tillio/lib/client.ts +329 -0
  114. package/src/modules/tillio/lib/environment.ts +53 -0
  115. package/src/modules/tillio/lib/error-codes.ts +67 -0
  116. package/src/modules/tillio/lib/errors.ts +11 -0
  117. package/src/modules/tillio/lib/health.ts +57 -0
  118. package/src/modules/tillio/lib/locking.ts +30 -0
  119. package/src/modules/tillio/lib/normalizer.ts +184 -0
  120. package/src/modules/tillio/lib/operators-store.ts +77 -0
  121. package/src/modules/tillio/lib/operators.ts +269 -0
  122. package/src/modules/tillio/lib/preset.ts +226 -0
  123. package/src/modules/tillio/lib/pull-job.ts +390 -0
  124. package/src/modules/tillio/lib/pull-readiness.ts +77 -0
  125. package/src/modules/tillio/lib/queue.ts +33 -0
  126. package/src/modules/tillio/lib/tz.ts +121 -0
  127. package/src/modules/tillio/lib/url-guard.ts +34 -0
  128. package/src/modules/tillio/setup.ts +49 -0
  129. package/src/modules/tillio/widgets/injection/operators-config/widget.client.tsx +304 -0
  130. package/src/modules/tillio/widgets/injection/operators-config/widget.ts +16 -0
  131. package/src/modules/tillio/widgets/injection/pull-calls/widget.client.tsx +214 -0
  132. package/src/modules/tillio/widgets/injection/pull-calls/widget.ts +16 -0
  133. package/src/modules/tillio/widgets/injection-table.ts +19 -0
  134. package/src/modules/tillio/workers/tillio-pull.ts +18 -0
  135. package/tsconfig.json +9 -0
@@ -0,0 +1,180 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { createLogger } from "@open-mercato/shared/lib/logger";
4
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
6
+ import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
7
+ import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
8
+ import { PHONE_CALL_RESOURCE_KIND } from "@open-mercato/shared/modules/phone_calls/types";
9
+ import { TILLIO_PROVIDER_KEY } from "../../integration.js";
10
+ import {
11
+ resolvePullContext,
12
+ TILLIO_PULL_JOB_TYPE
13
+ } from "../../lib/pull-job.js";
14
+ import { blockerSection, PULL_BLOCKER_MESSAGES } from "../../lib/pull-readiness.js";
15
+ import { getTillioQueue, TILLIO_PULL_QUEUE } from "../../lib/queue.js";
16
+ import { createTillioLock, tillioPullLockKey } from "../../lib/locking.js";
17
+ const logger = createLogger("tillio").child({ component: "pull-route" });
18
+ const daySchema = z.iso.date();
19
+ const pullBodySchema = z.object({
20
+ from: daySchema,
21
+ to: daySchema,
22
+ cursor: z.string().trim().min(1).optional(),
23
+ limit: z.number().int().positive().max(1e3).optional()
24
+ }).refine((body) => body.from <= body.to, { message: "The start day must not be after the end day." });
25
+ const metadata = {
26
+ GET: { requireAuth: true, requireFeatures: ["phone_calls.manage", "integrations.manage"] },
27
+ POST: { requireAuth: true, requireFeatures: ["phone_calls.manage", "integrations.manage"] }
28
+ };
29
+ const openApi = {
30
+ GET: {
31
+ tags: ["Tillio"],
32
+ summary: "Report whether Tillio is ready to pull phone calls"
33
+ },
34
+ POST: {
35
+ tags: ["Tillio"],
36
+ summary: "Queue a Tillio phone-call pull and return its progress job id",
37
+ responses: [
38
+ { status: 202, description: "Pull queued; returns { progressJobId }" },
39
+ { status: 400, description: "Invalid day range" },
40
+ { status: 401, description: "Unauthorized" },
41
+ { status: 409, description: "Tillio is not ready to pull (environment or operator blocker)" },
42
+ { status: 429, description: "A pull is already running for this scope" }
43
+ ]
44
+ }
45
+ };
46
+ async function GET(req) {
47
+ const auth = await getAuthFromRequest(req);
48
+ if (!auth?.tenantId || !auth.orgId) {
49
+ return NextResponse.json({ ok: false, code: "unauthorized", message: "Unauthorized" }, { status: 401 });
50
+ }
51
+ const container = await createRequestContainer();
52
+ const credentialsService = container.resolve("integrationCredentialsService");
53
+ const em = container.resolve("em");
54
+ const scope = { organizationId: auth.orgId, tenantId: auth.tenantId };
55
+ const { readiness, environment, operator } = await resolvePullContext(credentialsService, em, scope);
56
+ return NextResponse.json({
57
+ ok: true,
58
+ ...readiness,
59
+ // The dialog states which zone the picked days are read in, and that zone is per
60
+ // environment, so it comes from here rather than from a string baked into the widget.
61
+ timeZone: environment?.timeZone ?? null,
62
+ operatorId: operator?.id ?? null,
63
+ plugin: operator?.plugin ?? null
64
+ });
65
+ }
66
+ async function POST(req) {
67
+ const auth = await getAuthFromRequest(req);
68
+ if (!auth?.tenantId || !auth.orgId) {
69
+ return NextResponse.json({ ok: false, code: "unauthorized", message: "Unauthorized" }, { status: 401 });
70
+ }
71
+ const parsedBody = pullBodySchema.safeParse(await readJsonSafe(req));
72
+ if (!parsedBody.success) {
73
+ return NextResponse.json({ ok: false, code: "invalid_payload", message: "Invalid pull payload" }, { status: 400 });
74
+ }
75
+ const body = parsedBody.data;
76
+ const container = await createRequestContainer();
77
+ const credentialsService = container.resolve("integrationCredentialsService");
78
+ const progressService = container.resolve("progressService");
79
+ const em = container.resolve("em");
80
+ const scope = { organizationId: auth.orgId, tenantId: auth.tenantId };
81
+ const progressContext = {
82
+ tenantId: auth.tenantId,
83
+ organizationId: auth.orgId,
84
+ userId: auth.sub ?? null
85
+ };
86
+ const { readiness } = await resolvePullContext(credentialsService, em, scope);
87
+ if (readiness.blocker) {
88
+ return NextResponse.json(
89
+ {
90
+ ok: false,
91
+ code: readiness.blocker,
92
+ section: blockerSection(readiness.blocker),
93
+ message: PULL_BLOCKER_MESSAGES[readiness.blocker]
94
+ },
95
+ { status: 409 }
96
+ );
97
+ }
98
+ const guarded = await runRouteMutationGuards({
99
+ container,
100
+ req,
101
+ auth: {
102
+ userId: auth.sub,
103
+ tenantId: auth.tenantId,
104
+ organizationId: auth.orgId
105
+ },
106
+ input: {
107
+ resourceKind: PHONE_CALL_RESOURCE_KIND,
108
+ operation: "custom",
109
+ mutationPayload: { providerKey: TILLIO_PROVIDER_KEY, from: body.from, to: body.to }
110
+ }
111
+ });
112
+ if (!guarded.ok) return guarded.response;
113
+ const guardedBody = pullBodySchema.safeParse({
114
+ ...body,
115
+ ...typeof guarded.modifiedPayload?.from === "string" ? { from: guarded.modifiedPayload.from } : {},
116
+ ...typeof guarded.modifiedPayload?.to === "string" ? { to: guarded.modifiedPayload.to } : {}
117
+ });
118
+ if (!guardedBody.success) {
119
+ return NextResponse.json(
120
+ { ok: false, code: "invalid_payload", message: "A guard rewrote the pull range into an invalid one" },
121
+ { status: 400 }
122
+ );
123
+ }
124
+ const range = guardedBody.data;
125
+ const withLock = createTillioLock(em, tillioPullLockKey(scope));
126
+ return withLock(async () => {
127
+ const active = await progressService.getActiveJobs(progressContext);
128
+ if (active.some((job) => job.jobType === TILLIO_PULL_JOB_TYPE)) {
129
+ return NextResponse.json(
130
+ { ok: false, code: "pull_already_running", section: "operator", message: "A Tillio pull is already running." },
131
+ { status: 429 }
132
+ );
133
+ }
134
+ const progressJob = await progressService.createJob(
135
+ {
136
+ jobType: TILLIO_PULL_JOB_TYPE,
137
+ name: "Pull calls from Tillio",
138
+ description: `Pulling Tillio calls from ${range.from} to ${range.to}`,
139
+ cancellable: true,
140
+ meta: {
141
+ resourceKind: PHONE_CALL_RESOURCE_KIND,
142
+ providerKey: TILLIO_PROVIDER_KEY,
143
+ from: range.from,
144
+ to: range.to
145
+ }
146
+ },
147
+ progressContext
148
+ );
149
+ const payload = {
150
+ progressJobId: progressJob.id,
151
+ scope: { tenantId: scope.tenantId, organizationId: scope.organizationId, userId: auth.sub ?? null },
152
+ from: range.from,
153
+ to: range.to,
154
+ cursor: range.cursor ?? null,
155
+ limit: range.limit ?? null
156
+ };
157
+ try {
158
+ await getTillioQueue(TILLIO_PULL_QUEUE).enqueue(payload);
159
+ } catch (err) {
160
+ logger.error("could not enqueue the Tillio pull", { progressJobId: progressJob.id, err });
161
+ await progressService.failJob(progressJob.id, { errorMessage: "The pull could not be handed to the queue." }, progressContext).catch((failErr) => {
162
+ logger.error("could not fail the orphaned pull job", { progressJobId: progressJob.id, err: failErr });
163
+ });
164
+ return NextResponse.json(
165
+ { ok: false, code: "pull_failed", section: "operator", message: "Could not queue the Tillio pull." },
166
+ { status: 500 }
167
+ );
168
+ }
169
+ await guarded.runAfterSuccess();
170
+ return NextResponse.json({ ok: true, progressJobId: progressJob.id }, { status: 202 });
171
+ });
172
+ }
173
+ export {
174
+ GET,
175
+ POST,
176
+ metadata,
177
+ openApi,
178
+ pullBodySchema
179
+ };
180
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/tillio/api/pull/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\nimport { PHONE_CALL_RESOURCE_KIND } from '@open-mercato/shared/modules/phone_calls/types'\nimport { TILLIO_PROVIDER_KEY } from '../../integration'\nimport {\n resolvePullContext,\n TILLIO_PULL_JOB_TYPE,\n type TillioPullJobPayload,\n} from '../../lib/pull-job'\nimport type { TillioCredentialsService } from '../../lib/operators-store'\nimport { blockerSection, PULL_BLOCKER_MESSAGES } from '../../lib/pull-readiness'\nimport { getTillioQueue, TILLIO_PULL_QUEUE } from '../../lib/queue'\nimport { createTillioLock, tillioPullLockKey } from '../../lib/locking'\n\nconst logger = createLogger('tillio').child({ component: 'pull-route' })\n\nconst daySchema = z.iso.date()\n\nexport const pullBodySchema = z\n .object({\n from: daySchema,\n to: daySchema,\n cursor: z.string().trim().min(1).optional(),\n limit: z.number().int().positive().max(1000).optional(),\n })\n .refine((body) => body.from <= body.to, { message: 'The start day must not be after the end day.' })\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['phone_calls.manage', 'integrations.manage'] },\n POST: { requireAuth: true, requireFeatures: ['phone_calls.manage', 'integrations.manage'] },\n}\n\nexport const openApi = {\n GET: {\n tags: ['Tillio'],\n summary: 'Report whether Tillio is ready to pull phone calls',\n },\n POST: {\n tags: ['Tillio'],\n summary: 'Queue a Tillio phone-call pull and return its progress job id',\n responses: [\n { status: 202, description: 'Pull queued; returns { progressJobId }' },\n { status: 400, description: 'Invalid day range' },\n { status: 401, description: 'Unauthorized' },\n { status: 409, description: 'Tillio is not ready to pull (environment or operator blocker)' },\n { status: 429, description: 'A pull is already running for this scope' },\n ],\n },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ ok: false, code: 'unauthorized', message: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const credentialsService = container.resolve('integrationCredentialsService') as TillioCredentialsService\n const em = container.resolve('em') as EntityManager\n const scope: IntegrationScope = { organizationId: auth.orgId, tenantId: auth.tenantId }\n\n const { readiness, environment, operator } = await resolvePullContext(credentialsService, em, scope)\n\n return NextResponse.json({\n ok: true,\n ...readiness,\n // The dialog states which zone the picked days are read in, and that zone is per\n // environment, so it comes from here rather than from a string baked into the widget.\n timeZone: environment?.timeZone ?? null,\n operatorId: operator?.id ?? null,\n plugin: operator?.plugin ?? null,\n })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ ok: false, code: 'unauthorized', message: 'Unauthorized' }, { status: 401 })\n }\n\n const parsedBody = pullBodySchema.safeParse(await readJsonSafe(req))\n if (!parsedBody.success) {\n return NextResponse.json({ ok: false, code: 'invalid_payload', message: 'Invalid pull payload' }, { status: 400 })\n }\n const body = parsedBody.data\n\n const container = await createRequestContainer()\n const credentialsService = container.resolve('integrationCredentialsService') as TillioCredentialsService\n const progressService = container.resolve('progressService') as ProgressService\n const em = container.resolve('em') as EntityManager\n const scope: IntegrationScope = { organizationId: auth.orgId, tenantId: auth.tenantId }\n const progressContext = {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? null,\n }\n\n // Checked here as well as in the worker so an unconfigured provider answers immediately\n // instead of parking a job that can only fail.\n const { readiness } = await resolvePullContext(credentialsService, em, scope)\n if (readiness.blocker) {\n return NextResponse.json(\n {\n ok: false,\n code: readiness.blocker,\n section: blockerSection(readiness.blocker),\n message: PULL_BLOCKER_MESSAGES[readiness.blocker],\n },\n { status: 409 },\n )\n }\n\n const guarded = await runRouteMutationGuards({\n container,\n req,\n auth: {\n userId: auth.sub,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n },\n input: {\n resourceKind: PHONE_CALL_RESOURCE_KIND,\n operation: 'custom',\n mutationPayload: { providerKey: TILLIO_PROVIDER_KEY, from: body.from, to: body.to },\n },\n })\n if (!guarded.ok) return guarded.response\n\n // A guard may narrow the window it was shown, so the job runs on what the guard returned\n // rather than on what the caller asked for. The rewrite comes from another module, so it\n // goes back through the same schema before it reaches the queue.\n const guardedBody = pullBodySchema.safeParse({\n ...body,\n ...(typeof guarded.modifiedPayload?.from === 'string' ? { from: guarded.modifiedPayload.from } : {}),\n ...(typeof guarded.modifiedPayload?.to === 'string' ? { to: guarded.modifiedPayload.to } : {}),\n })\n if (!guardedBody.success) {\n return NextResponse.json(\n { ok: false, code: 'invalid_payload', message: 'A guard rewrote the pull range into an invalid one' },\n { status: 400 },\n )\n }\n const range = guardedBody.data\n\n // The 429 below is a documented contract, so the check and the create happen under a lock\n // rather than merely close together. Two requests arriving at once would otherwise both see\n // no active job and both sweep the provider.\n const withLock = createTillioLock(em, tillioPullLockKey(scope))\n\n return withLock(async () => {\n const active = await progressService.getActiveJobs(progressContext)\n if (active.some((job) => job.jobType === TILLIO_PULL_JOB_TYPE)) {\n return NextResponse.json(\n { ok: false, code: 'pull_already_running', section: 'operator', message: 'A Tillio pull is already running.' },\n { status: 429 },\n )\n }\n\n const progressJob = await progressService.createJob(\n {\n jobType: TILLIO_PULL_JOB_TYPE,\n name: 'Pull calls from Tillio',\n description: `Pulling Tillio calls from ${range.from} to ${range.to}`,\n cancellable: true,\n meta: {\n resourceKind: PHONE_CALL_RESOURCE_KIND,\n providerKey: TILLIO_PROVIDER_KEY,\n from: range.from,\n to: range.to,\n },\n },\n progressContext,\n )\n\n const payload: TillioPullJobPayload = {\n progressJobId: progressJob.id,\n scope: { tenantId: scope.tenantId, organizationId: scope.organizationId, userId: auth.sub ?? null },\n from: range.from,\n to: range.to,\n cursor: range.cursor ?? null,\n limit: range.limit ?? null,\n }\n try {\n await getTillioQueue(TILLIO_PULL_QUEUE).enqueue(payload as unknown as Record<string, unknown>)\n } catch (err) {\n // The job row exists but nothing will ever advance it, and the active-job check above\n // would answer 429 to every later pull until the stale sweep clears it. One unreachable\n // queue would lock the tenant out of pulling.\n logger.error('could not enqueue the Tillio pull', { progressJobId: progressJob.id, err })\n await progressService\n .failJob(progressJob.id, { errorMessage: 'The pull could not be handed to the queue.' }, progressContext)\n .catch((failErr: unknown) => {\n logger.error('could not fail the orphaned pull job', { progressJobId: progressJob.id, err: failErr })\n })\n return NextResponse.json(\n { ok: false, code: 'pull_failed', section: 'operator', message: 'Could not queue the Tillio pull.' },\n { status: 500 },\n )\n }\n\n await guarded.runAfterSuccess()\n\n return NextResponse.json({ ok: true, progressJobId: progressJob.id }, { status: 202 })\n })\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,108 @@
1
+ import { createInterface } from "node:readline";
2
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
3
+ import { applyTillioEnvPreset, TILLIO_ENV_VARS } from "./lib/preset.js";
4
+ import { createTillioLock, tillioOperatorLockKey } from "./lib/locking.js";
5
+ const USAGE = [
6
+ "Usage: yarn mercato tillio configure-from-env --tenant <tenantId> --org <organizationId> [--force]",
7
+ `Required env: ${TILLIO_ENV_VARS.apiUrl}, ${TILLIO_ENV_VARS.apiKey}`,
8
+ `Optional env: ${TILLIO_ENV_VARS.ringostatKey} (attaches the operator), ${TILLIO_ENV_VARS.force} (overwrites existing credentials), ${TILLIO_ENV_VARS.replaceOperator} (answers the replacement prompt for unattended runs)`
9
+ ].join("\n");
10
+ async function confirmOperatorReplacement(operator) {
11
+ if (!process.stdin.isTTY) return false;
12
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
13
+ try {
14
+ console.log(`[tillio] The environment variables point at a different Tillio instance than the stored ones.`);
15
+ console.log(`[tillio] Operator ${operator.id} (${operator.plugin}) is attached to the current one and will be revoked before the switch.`);
16
+ const answer = await new Promise((resolve) => {
17
+ rl.question("[tillio] Detach it and attach a new one from env? [y/N] ", (value) => resolve(value.trim().toLowerCase()));
18
+ });
19
+ return answer === "y" || answer === "yes";
20
+ } finally {
21
+ rl.close();
22
+ }
23
+ }
24
+ function parseArgs(rest) {
25
+ const args = {};
26
+ for (let index = 0; index < rest.length; index += 1) {
27
+ const arg = rest[index];
28
+ if (!arg || !arg.startsWith("--")) continue;
29
+ const [key, inlineValue] = arg.replace(/^--/, "").split("=");
30
+ if (inlineValue !== void 0) {
31
+ args[key] = inlineValue;
32
+ continue;
33
+ }
34
+ const next = rest[index + 1];
35
+ if (next && !next.startsWith("--")) {
36
+ args[key] = next;
37
+ index += 1;
38
+ continue;
39
+ }
40
+ args[key] = true;
41
+ }
42
+ return args;
43
+ }
44
+ function readScopeArg(args, keys) {
45
+ for (const key of keys) {
46
+ const value = args[key];
47
+ if (typeof value === "string" && value.trim()) return value.trim();
48
+ }
49
+ return null;
50
+ }
51
+ const configureFromEnv = {
52
+ command: "configure-from-env",
53
+ async run(rest) {
54
+ const args = parseArgs(rest);
55
+ const tenantId = readScopeArg(args, ["tenantId", "tenant"]);
56
+ const organizationId = readScopeArg(args, ["organizationId", "orgId", "org"]);
57
+ const force = args.force === true;
58
+ if (!tenantId || !organizationId) {
59
+ console.log(USAGE);
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+ const container = await createRequestContainer();
64
+ try {
65
+ console.log(`[tillio] Configuring tenant ${tenantId}, organization ${organizationId}${force ? " (force)" : ""}.`);
66
+ const result = await applyTillioEnvPreset({
67
+ credentialsService: container.resolve("integrationCredentialsService"),
68
+ integrationStateService: container.resolve("integrationStateService"),
69
+ integrationHealthService: container.resolve("integrationHealthService"),
70
+ integrationLogService: container.resolve("integrationLogService"),
71
+ scope: { tenantId, organizationId },
72
+ force,
73
+ confirmOperatorReplacement,
74
+ withLock: createTillioLock(
75
+ container.resolve("em"),
76
+ tillioOperatorLockKey({ tenantId, organizationId })
77
+ )
78
+ });
79
+ if (result.status === "skipped") {
80
+ console.log(`[tillio] Skipped: ${result.reason}`);
81
+ return;
82
+ }
83
+ if (result.status === "blocked") {
84
+ console.error(`[tillio] Blocked: ${result.reason}`);
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+ console.log(`[tillio] Credentials ${result.credentialsAction}, health check reported ${result.health}, operator ${result.operator}.`);
89
+ if (result.health !== "healthy") {
90
+ console.log("[tillio] Fix the environment, then rerun this command to finish provisioning.");
91
+ } else if (result.operator === "not-requested") {
92
+ console.log(`[tillio] Set ${TILLIO_ENV_VARS.ringostatKey} and rerun to attach the operator, or attach it in the UI.`);
93
+ }
94
+ } catch (error) {
95
+ console.error(`[tillio] ${error instanceof Error ? error.message : "Failed to apply the env preset"}`);
96
+ process.exitCode = 1;
97
+ } finally {
98
+ const disposable = container;
99
+ if (typeof disposable.dispose === "function") await disposable.dispose();
100
+ }
101
+ }
102
+ };
103
+ const commands = [configureFromEnv];
104
+ var cli_default = commands;
105
+ export {
106
+ cli_default as default
107
+ };
108
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/tillio/cli.ts"],
4
+ "sourcesContent": ["import { createInterface } from 'node:readline'\nimport type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { applyTillioEnvPreset, TILLIO_ENV_VARS } from './lib/preset'\nimport { createTillioLock, tillioOperatorLockKey } from './lib/locking'\nimport type { TillioCredentialsService, TillioOperatorRecord } from './lib/operators-store'\n\nconst USAGE = [\n 'Usage: yarn mercato tillio configure-from-env --tenant <tenantId> --org <organizationId> [--force]',\n `Required env: ${TILLIO_ENV_VARS.apiUrl}, ${TILLIO_ENV_VARS.apiKey}`,\n `Optional env: ${TILLIO_ENV_VARS.ringostatKey} (attaches the operator), ${TILLIO_ENV_VARS.force} (overwrites existing credentials), ${TILLIO_ENV_VARS.replaceOperator} (answers the replacement prompt for unattended runs)`,\n].join('\\n')\n\nasync function confirmOperatorReplacement(operator: TillioOperatorRecord): Promise<boolean> {\n if (!process.stdin.isTTY) return false\n\n const rl = createInterface({ input: process.stdin, output: process.stdout })\n try {\n console.log(`[tillio] The environment variables point at a different Tillio instance than the stored ones.`)\n console.log(`[tillio] Operator ${operator.id} (${operator.plugin}) is attached to the current one and will be revoked before the switch.`)\n const answer = await new Promise<string>((resolve) => {\n rl.question('[tillio] Detach it and attach a new one from env? [y/N] ', (value) => resolve(value.trim().toLowerCase()))\n })\n return answer === 'y' || answer === 'yes'\n } finally {\n rl.close()\n }\n}\n\nfunction parseArgs(rest: string[]): Record<string, string | boolean> {\n const args: Record<string, string | boolean> = {}\n\n for (let index = 0; index < rest.length; index += 1) {\n const arg = rest[index]\n if (!arg || !arg.startsWith('--')) continue\n\n const [key, inlineValue] = arg.replace(/^--/, '').split('=')\n if (inlineValue !== undefined) {\n args[key] = inlineValue\n continue\n }\n\n const next = rest[index + 1]\n if (next && !next.startsWith('--')) {\n args[key] = next\n index += 1\n continue\n }\n\n args[key] = true\n }\n\n return args\n}\n\nfunction readScopeArg(args: Record<string, string | boolean>, keys: string[]): string | null {\n for (const key of keys) {\n const value = args[key]\n if (typeof value === 'string' && value.trim()) return value.trim()\n }\n return null\n}\n\nconst configureFromEnv: ModuleCli = {\n command: 'configure-from-env',\n async run(rest) {\n const args = parseArgs(rest)\n const tenantId = readScopeArg(args, ['tenantId', 'tenant'])\n const organizationId = readScopeArg(args, ['organizationId', 'orgId', 'org'])\n const force = args.force === true\n\n if (!tenantId || !organizationId) {\n console.log(USAGE)\n process.exitCode = 1\n return\n }\n\n const container = await createRequestContainer()\n try {\n console.log(`[tillio] Configuring tenant ${tenantId}, organization ${organizationId}${force ? ' (force)' : ''}.`)\n\n const result = await applyTillioEnvPreset({\n credentialsService: container.resolve('integrationCredentialsService') as TillioCredentialsService,\n integrationStateService: container.resolve('integrationStateService'),\n integrationHealthService: container.resolve('integrationHealthService'),\n integrationLogService: container.resolve('integrationLogService'),\n scope: { tenantId, organizationId },\n force,\n confirmOperatorReplacement,\n withLock: createTillioLock(\n container.resolve('em') as EntityManager,\n tillioOperatorLockKey({ tenantId, organizationId }),\n ),\n })\n\n if (result.status === 'skipped') {\n console.log(`[tillio] Skipped: ${result.reason}`)\n return\n }\n if (result.status === 'blocked') {\n console.error(`[tillio] Blocked: ${result.reason}`)\n process.exitCode = 1\n return\n }\n\n console.log(`[tillio] Credentials ${result.credentialsAction}, health check reported ${result.health}, operator ${result.operator}.`)\n if (result.health !== 'healthy') {\n console.log('[tillio] Fix the environment, then rerun this command to finish provisioning.')\n } else if (result.operator === 'not-requested') {\n console.log(`[tillio] Set ${TILLIO_ENV_VARS.ringostatKey} and rerun to attach the operator, or attach it in the UI.`)\n }\n } catch (error) {\n console.error(`[tillio] ${error instanceof Error ? error.message : 'Failed to apply the env preset'}`)\n process.exitCode = 1\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') await disposable.dispose()\n }\n },\n}\n\nconst commands: ModuleCli[] = [configureFromEnv]\n\nexport default commands\n"],
5
+ "mappings": "AAAA,SAAS,uBAAuB;AAEhC,SAAS,8BAA8B;AAEvC,SAAS,sBAAsB,uBAAuB;AACtD,SAAS,kBAAkB,6BAA6B;AAGxD,MAAM,QAAQ;AAAA,EACZ;AAAA,EACA,iBAAiB,gBAAgB,MAAM,KAAK,gBAAgB,MAAM;AAAA,EAClE,iBAAiB,gBAAgB,YAAY,6BAA6B,gBAAgB,KAAK,uCAAuC,gBAAgB,eAAe;AACvK,EAAE,KAAK,IAAI;AAEX,eAAe,2BAA2B,UAAkD;AAC1F,MAAI,CAAC,QAAQ,MAAM,MAAO,QAAO;AAEjC,QAAM,KAAK,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AAC3E,MAAI;AACF,YAAQ,IAAI,+FAA+F;AAC3G,YAAQ,IAAI,qBAAqB,SAAS,EAAE,KAAK,SAAS,MAAM,yEAAyE;AACzI,UAAM,SAAS,MAAM,IAAI,QAAgB,CAAC,YAAY;AACpD,SAAG,SAAS,4DAA4D,CAAC,UAAU,QAAQ,MAAM,KAAK,EAAE,YAAY,CAAC,CAAC;AAAA,IACxH,CAAC;AACD,WAAO,WAAW,OAAO,WAAW;AAAA,EACtC,UAAE;AACA,OAAG,MAAM;AAAA,EACX;AACF;AAEA,SAAS,UAAU,MAAkD;AACnE,QAAM,OAAyC,CAAC;AAEhD,WAAS,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;AACnD,UAAM,MAAM,KAAK,KAAK;AACtB,QAAI,CAAC,OAAO,CAAC,IAAI,WAAW,IAAI,EAAG;AAEnC,UAAM,CAAC,KAAK,WAAW,IAAI,IAAI,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG;AAC3D,QAAI,gBAAgB,QAAW;AAC7B,WAAK,GAAG,IAAI;AACZ;AAAA,IACF;AAEA,UAAM,OAAO,KAAK,QAAQ,CAAC;AAC3B,QAAI,QAAQ,CAAC,KAAK,WAAW,IAAI,GAAG;AAClC,WAAK,GAAG,IAAI;AACZ,eAAS;AACT;AAAA,IACF;AAEA,SAAK,GAAG,IAAI;AAAA,EACd;AAEA,SAAO;AACT;AAEA,SAAS,aAAa,MAAwC,MAA+B;AAC3F,aAAW,OAAO,MAAM;AACtB,UAAM,QAAQ,KAAK,GAAG;AACtB,QAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAG,QAAO,MAAM,KAAK;AAAA,EACnE;AACA,SAAO;AACT;AAEA,MAAM,mBAA8B;AAAA,EAClC,SAAS;AAAA,EACT,MAAM,IAAI,MAAM;AACd,UAAM,OAAO,UAAU,IAAI;AAC3B,UAAM,WAAW,aAAa,MAAM,CAAC,YAAY,QAAQ,CAAC;AAC1D,UAAM,iBAAiB,aAAa,MAAM,CAAC,kBAAkB,SAAS,KAAK,CAAC;AAC5E,UAAM,QAAQ,KAAK,UAAU;AAE7B,QAAI,CAAC,YAAY,CAAC,gBAAgB;AAChC,cAAQ,IAAI,KAAK;AACjB,cAAQ,WAAW;AACnB;AAAA,IACF;AAEA,UAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAI;AACF,cAAQ,IAAI,+BAA+B,QAAQ,kBAAkB,cAAc,GAAG,QAAQ,aAAa,EAAE,GAAG;AAEhH,YAAM,SAAS,MAAM,qBAAqB;AAAA,QACxC,oBAAoB,UAAU,QAAQ,+BAA+B;AAAA,QACrE,yBAAyB,UAAU,QAAQ,yBAAyB;AAAA,QACpE,0BAA0B,UAAU,QAAQ,0BAA0B;AAAA,QACtE,uBAAuB,UAAU,QAAQ,uBAAuB;AAAA,QAChE,OAAO,EAAE,UAAU,eAAe;AAAA,QAClC;AAAA,QACA;AAAA,QACA,UAAU;AAAA,UACR,UAAU,QAAQ,IAAI;AAAA,UACtB,sBAAsB,EAAE,UAAU,eAAe,CAAC;AAAA,QACpD;AAAA,MACF,CAAC;AAED,UAAI,OAAO,WAAW,WAAW;AAC/B,gBAAQ,IAAI,qBAAqB,OAAO,MAAM,EAAE;AAChD;AAAA,MACF;AACA,UAAI,OAAO,WAAW,WAAW;AAC/B,gBAAQ,MAAM,qBAAqB,OAAO,MAAM,EAAE;AAClD,gBAAQ,WAAW;AACnB;AAAA,MACF;AAEA,cAAQ,IAAI,wBAAwB,OAAO,iBAAiB,2BAA2B,OAAO,MAAM,cAAc,OAAO,QAAQ,GAAG;AACpI,UAAI,OAAO,WAAW,WAAW;AAC/B,gBAAQ,IAAI,+EAA+E;AAAA,MAC7F,WAAW,OAAO,aAAa,iBAAiB;AAC9C,gBAAQ,IAAI,gBAAgB,gBAAgB,YAAY,4DAA4D;AAAA,MACtH;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,YAAY,iBAAiB,QAAQ,MAAM,UAAU,gCAAgC,EAAE;AACrG,cAAQ,WAAW;AAAA,IACrB,UAAE;AACA,YAAM,aAAa;AACnB,UAAI,OAAO,WAAW,YAAY,WAAY,OAAM,WAAW,QAAQ;AAAA,IACzE;AAAA,EACF;AACF;AAEA,MAAM,WAAwB,CAAC,gBAAgB;AAE/C,IAAO,cAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,16 @@
1
+ import { asFunction } from "awilix";
2
+ import { registerPhoneCallProvider } from "@open-mercato/shared/modules/phone_calls/provider";
3
+ import { tillioAdapter } from "./lib/adapter.js";
4
+ import { createTillioEnvironmentHealthCheck } from "./lib/health.js";
5
+ function register(container) {
6
+ registerPhoneCallProvider(tillioAdapter);
7
+ container.register({
8
+ tillioEnvironmentHealthCheck: asFunction(
9
+ ({ integrationCredentialsService }) => createTillioEnvironmentHealthCheck({ credentialsService: integrationCredentialsService })
10
+ ).scoped().proxy()
11
+ });
12
+ }
13
+ export {
14
+ register
15
+ };
16
+ //# sourceMappingURL=di.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/tillio/di.ts"],
4
+ "sourcesContent": ["import { asFunction } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { registerPhoneCallProvider } from '@open-mercato/shared/modules/phone_calls/provider'\nimport { tillioAdapter } from './lib/adapter'\nimport { createTillioEnvironmentHealthCheck, type CredentialsServiceLike } from './lib/health'\n\nexport function register(container: AppContainer) {\n registerPhoneCallProvider(tillioAdapter)\n\n container.register({\n tillioEnvironmentHealthCheck: asFunction(\n ({ integrationCredentialsService }: { integrationCredentialsService: CredentialsServiceLike }) =>\n createTillioEnvironmentHealthCheck({ credentialsService: integrationCredentialsService }),\n ).scoped().proxy(),\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,iCAAiC;AAC1C,SAAS,qBAAqB;AAC9B,SAAS,0CAAuE;AAEzE,SAAS,SAAS,WAAyB;AAChD,4BAA0B,aAAa;AAEvC,YAAU,SAAS;AAAA,IACjB,8BAA8B;AAAA,MAC5B,CAAC,EAAE,8BAA8B,MAC/B,mCAAmC,EAAE,oBAAoB,8BAA8B,CAAC;AAAA,IAC5F,EAAE,OAAO,EAAE,MAAM;AAAA,EACnB,CAAC;AACH;",
6
+ "names": []
7
+ }
@@ -0,0 +1,12 @@
1
+ const metadata = {
2
+ id: "tillio",
3
+ title: "Tillio Telephony",
4
+ description: "Tillio VoIP provider for the phone_calls hub (Ringostat operator).",
5
+ requires: ["integrations", "phone_calls"]
6
+ };
7
+ import { features } from "./acl.js";
8
+ export {
9
+ features,
10
+ metadata
11
+ };
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/tillio/index.ts"],
4
+ "sourcesContent": ["export const metadata = {\n id: 'tillio',\n title: 'Tillio Telephony',\n description: 'Tillio VoIP provider for the phone_calls hub (Ringostat operator).',\n requires: ['integrations', 'phone_calls'],\n}\n\nexport { features } from './acl'\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AAAA,EACb,UAAU,CAAC,gBAAgB,aAAa;AAC1C;AAEA,SAAS,gBAAgB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,62 @@
1
+ import { buildIntegrationDetailWidgetSpotId } from "@open-mercato/shared/modules/integrations/types";
2
+ const TILLIO_INTEGRATION_ID = "tillio";
3
+ const TILLIO_PROVIDER_KEY = "tillio";
4
+ const tillioDetailWidgetSpotId = buildIntegrationDetailWidgetSpotId(TILLIO_INTEGRATION_ID);
5
+ const integration = {
6
+ id: TILLIO_INTEGRATION_ID,
7
+ title: "Tillio",
8
+ description: "VoIP bridge for the phone_calls hub. Configure the Tillio environment, then attach one Ringostat operator.",
9
+ category: "communication",
10
+ hub: "phone_calls",
11
+ providerKey: TILLIO_PROVIDER_KEY,
12
+ icon: "phone-call",
13
+ package: "@open-mercato/tillio",
14
+ version: "1.0.0",
15
+ author: "Open Mercato Team",
16
+ license: "MIT",
17
+ tags: ["tillio", "voip", "phone-calls", "telephony", "ringostat"],
18
+ detailPage: {
19
+ widgetSpotId: tillioDetailWidgetSpotId
20
+ },
21
+ credentials: {
22
+ fields: [
23
+ {
24
+ key: "apiUrl",
25
+ label: "Tillio API URL",
26
+ type: "url",
27
+ required: true,
28
+ placeholder: "https://your-tillio-instance.example.com",
29
+ helpText: "Base URL of your Tillio environment."
30
+ },
31
+ {
32
+ key: "apiKey",
33
+ label: "Tillio API Key",
34
+ type: "secret",
35
+ required: true,
36
+ helpText: "Global application key (X-Api-Key) for your Tillio environment."
37
+ },
38
+ {
39
+ key: "timeZone",
40
+ label: "Tillio time zone",
41
+ type: "text",
42
+ required: false,
43
+ placeholder: "Europe/Warsaw",
44
+ helpText: "IANA zone the instance reports call timestamps in. Leave empty for Europe/Warsaw."
45
+ }
46
+ ]
47
+ },
48
+ healthCheck: { service: "tillioEnvironmentHealthCheck" }
49
+ };
50
+ const integrations = [integration];
51
+ const bundles = [];
52
+ const bundle = void 0;
53
+ export {
54
+ TILLIO_INTEGRATION_ID,
55
+ TILLIO_PROVIDER_KEY,
56
+ bundle,
57
+ bundles,
58
+ integration,
59
+ integrations,
60
+ tillioDetailWidgetSpotId
61
+ };
62
+ //# sourceMappingURL=integration.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/tillio/integration.ts"],
4
+ "sourcesContent": ["import { buildIntegrationDetailWidgetSpotId, type IntegrationBundle, type IntegrationDefinition } from '@open-mercato/shared/modules/integrations/types'\n\n// Two names for one value on purpose: the integration registry keys credentials by the integration id\n// while the phone_calls hub keys calls by the provider key, and the two axes need not agree.\nexport const TILLIO_INTEGRATION_ID = 'tillio'\nexport const TILLIO_PROVIDER_KEY = 'tillio'\n\nexport const tillioDetailWidgetSpotId = buildIntegrationDetailWidgetSpotId(TILLIO_INTEGRATION_ID)\n\nexport const integration: IntegrationDefinition = {\n id: TILLIO_INTEGRATION_ID,\n title: 'Tillio',\n description: 'VoIP bridge for the phone_calls hub. Configure the Tillio environment, then attach one Ringostat operator.',\n category: 'communication',\n hub: 'phone_calls',\n providerKey: TILLIO_PROVIDER_KEY,\n icon: 'phone-call',\n package: '@open-mercato/tillio',\n version: '1.0.0',\n author: 'Open Mercato Team',\n license: 'MIT',\n tags: ['tillio', 'voip', 'phone-calls', 'telephony', 'ringostat'],\n detailPage: {\n widgetSpotId: tillioDetailWidgetSpotId,\n },\n credentials: {\n fields: [\n {\n key: 'apiUrl',\n label: 'Tillio API URL',\n type: 'url',\n required: true,\n placeholder: 'https://your-tillio-instance.example.com',\n helpText: 'Base URL of your Tillio environment.',\n },\n {\n key: 'apiKey',\n label: 'Tillio API Key',\n type: 'secret',\n required: true,\n helpText: 'Global application key (X-Api-Key) for your Tillio environment.',\n },\n {\n key: 'timeZone',\n label: 'Tillio time zone',\n type: 'text',\n required: false,\n placeholder: 'Europe/Warsaw',\n helpText: 'IANA zone the instance reports call timestamps in. Leave empty for Europe/Warsaw.',\n },\n ],\n },\n healthCheck: { service: 'tillioEnvironmentHealthCheck' },\n}\n\nexport const integrations: IntegrationDefinition[] = [integration]\nexport const bundles: IntegrationBundle[] = []\nexport const bundle: IntegrationBundle | undefined = undefined\n"],
5
+ "mappings": "AAAA,SAAS,0CAA8F;AAIhG,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAE5B,MAAM,2BAA2B,mCAAmC,qBAAqB;AAEzF,MAAM,cAAqC;AAAA,EAChD,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AAAA,EACb,UAAU;AAAA,EACV,KAAK;AAAA,EACL,aAAa;AAAA,EACb,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM,CAAC,UAAU,QAAQ,eAAe,aAAa,WAAW;AAAA,EAChE,YAAY;AAAA,IACV,cAAc;AAAA,EAChB;AAAA,EACA,aAAa;AAAA,IACX,QAAQ;AAAA,MACN;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,UAAU;AAAA,MACZ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,UAAU;AAAA,MACZ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAAA,EACA,aAAa,EAAE,SAAS,+BAA+B;AACzD;AAEO,MAAM,eAAwC,CAAC,WAAW;AAC1D,MAAM,UAA+B,CAAC;AACtC,MAAM,SAAwC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ import { TILLIO_PROVIDER_KEY } from "../integration.js";
3
+ import { TillioApiError } from "./errors.js";
4
+ import { createTillioClient } from "./client.js";
5
+ import { ENV_PROBE_TENANT_DOMAIN, environmentSchema } from "./environment.js";
6
+ import { normalizeTillioCall } from "./normalizer.js";
7
+ import { operatorPluginSchema } from "./operators-store.js";
8
+ import { formatTillioTimestamp, TILLIO_TIMEZONE } from "./tz.js";
9
+ const TILLIO_DEFAULT_BATCH_LIMIT = 500;
10
+ const pullCredentialsSchema = z.object({
11
+ apiUrl: z.string().trim().min(1),
12
+ apiKey: z.string().trim().min(1),
13
+ tenantSystemId: z.string().trim().min(1),
14
+ timeZone: z.string().trim().optional(),
15
+ operator: z.object({
16
+ id: z.string().trim().min(1),
17
+ plugin: operatorPluginSchema,
18
+ token: z.string().trim().min(1),
19
+ tenantDomain: z.string().trim().min(1)
20
+ })
21
+ });
22
+ function parseCursor(cursor) {
23
+ if (!cursor) return 1;
24
+ const parsed = Number(cursor);
25
+ if (!Number.isFinite(parsed) || parsed < 1) return 1;
26
+ return Math.trunc(parsed);
27
+ }
28
+ function createTillioAdapter(deps = {}) {
29
+ return {
30
+ providerKey: TILLIO_PROVIDER_KEY,
31
+ displayName: "Tillio",
32
+ async validateConnection(input) {
33
+ const parsed = environmentSchema.safeParse(input.credentials);
34
+ if (!parsed.success || !parsed.data.tenantSystemId) {
35
+ return { ok: false, message: "Tillio environment is not configured." };
36
+ }
37
+ try {
38
+ const client = createTillioClient({
39
+ apiUrl: parsed.data.apiUrl,
40
+ apiKey: parsed.data.apiKey,
41
+ tenantSystemId: parsed.data.tenantSystemId
42
+ }, deps);
43
+ await client.getPlugins(ENV_PROBE_TENANT_DOMAIN);
44
+ return { ok: true };
45
+ } catch (err) {
46
+ const message = err instanceof TillioApiError || err instanceof Error ? err.message : "Validation failed";
47
+ return { ok: false, message };
48
+ }
49
+ },
50
+ async fetchCalls(input) {
51
+ const credentials = pullCredentialsSchema.parse(input.credentials);
52
+ const { operator } = credentials;
53
+ const client = createTillioClient({
54
+ apiUrl: credentials.apiUrl,
55
+ apiKey: credentials.apiKey,
56
+ tenantSystemId: credentials.tenantSystemId
57
+ }, deps);
58
+ const timeZone = credentials.timeZone ?? TILLIO_TIMEZONE;
59
+ const from = input.from ? formatTillioTimestamp(input.from, timeZone) : void 0;
60
+ const to = input.to ? formatTillioTimestamp(input.to, timeZone) : void 0;
61
+ const limit = input.limit && input.limit > 0 ? Math.trunc(input.limit) : TILLIO_DEFAULT_BATCH_LIMIT;
62
+ const calls = [];
63
+ let page = parseCursor(input.cursor);
64
+ let nextCursor = null;
65
+ let anomaly = null;
66
+ let anomalyCursor = null;
67
+ while (true) {
68
+ const response = await client.getCalls({
69
+ tenantDomain: operator.tenantDomain,
70
+ token: operator.token,
71
+ from,
72
+ to,
73
+ page
74
+ });
75
+ for (const rawCall of response.calls) {
76
+ calls.push(normalizeTillioCall(rawCall, { operatorId: operator.id, plugin: operator.plugin, timeZone }));
77
+ }
78
+ const { pages, page: echoedPage } = response.pagination;
79
+ if (echoedPage !== page) {
80
+ anomaly = "page_not_echoed";
81
+ } else if (pages < 1) {
82
+ anomaly = "invalid_page_count";
83
+ } else if (page < pages && response.calls.length === 0) {
84
+ anomaly = "empty_page";
85
+ }
86
+ if (anomaly) {
87
+ anomalyCursor = String(page);
88
+ break;
89
+ }
90
+ if (page >= pages) break;
91
+ page += 1;
92
+ if (calls.length >= limit) {
93
+ nextCursor = String(page);
94
+ break;
95
+ }
96
+ }
97
+ return { calls, nextCursor, anomaly, anomalyCursor };
98
+ }
99
+ };
100
+ }
101
+ const tillioAdapter = createTillioAdapter();
102
+ export {
103
+ TILLIO_DEFAULT_BATCH_LIMIT,
104
+ createTillioAdapter,
105
+ tillioAdapter
106
+ };
107
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/tillio/lib/adapter.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { PhoneCallProviderAdapter } from '@open-mercato/shared/modules/phone_calls/provider'\nimport type {\n FetchPhoneCallsInput,\n NormalizedPhoneCall,\n NormalizedPhoneCallBatch,\n PhoneCallBatchAnomaly,\n ProviderValidationResult,\n ValidatePhoneCallProviderInput,\n} from '@open-mercato/shared/modules/phone_calls/types'\nimport { TILLIO_PROVIDER_KEY } from '../integration'\nimport { TillioApiError } from './errors'\nimport { createTillioClient, type TillioClientDeps } from './client'\nimport { ENV_PROBE_TENANT_DOMAIN, environmentSchema } from './environment'\nimport { normalizeTillioCall } from './normalizer'\nimport { operatorPluginSchema } from './operators-store'\nimport { formatTillioTimestamp, TILLIO_TIMEZONE } from './tz'\n\nexport const TILLIO_DEFAULT_BATCH_LIMIT = 500\n\nconst pullCredentialsSchema = z.object({\n apiUrl: z.string().trim().min(1),\n apiKey: z.string().trim().min(1),\n tenantSystemId: z.string().trim().min(1),\n timeZone: z.string().trim().optional(),\n operator: z.object({\n id: z.string().trim().min(1),\n plugin: operatorPluginSchema,\n token: z.string().trim().min(1),\n tenantDomain: z.string().trim().min(1),\n }),\n})\n\nexport type TillioPullCredentials = z.infer<typeof pullCredentialsSchema>\n\nfunction parseCursor(cursor: string | null | undefined): number {\n if (!cursor) return 1\n const parsed = Number(cursor)\n if (!Number.isFinite(parsed) || parsed < 1) return 1\n return Math.trunc(parsed)\n}\n\n// `deps` only carries the outbound-fetch test seams down to the client; production builds\n// the adapter without them and gets URL validation plus DNS pinning from `safeOutboundFetch`.\nexport function createTillioAdapter(deps: TillioClientDeps = {}): PhoneCallProviderAdapter {\n return {\n providerKey: TILLIO_PROVIDER_KEY,\n displayName: 'Tillio',\n\n async validateConnection(input: ValidatePhoneCallProviderInput): Promise<ProviderValidationResult> {\n const parsed = environmentSchema.safeParse(input.credentials)\n if (!parsed.success || !parsed.data.tenantSystemId) {\n return { ok: false, message: 'Tillio environment is not configured.' }\n }\n try {\n const client = createTillioClient({\n apiUrl: parsed.data.apiUrl,\n apiKey: parsed.data.apiKey,\n tenantSystemId: parsed.data.tenantSystemId,\n }, deps)\n await client.getPlugins(ENV_PROBE_TENANT_DOMAIN)\n return { ok: true }\n } catch (err) {\n const message = err instanceof TillioApiError || err instanceof Error ? err.message : 'Validation failed'\n return { ok: false, message }\n }\n },\n\n async fetchCalls(input: FetchPhoneCallsInput): Promise<NormalizedPhoneCallBatch> {\n const credentials = pullCredentialsSchema.parse(input.credentials)\n const { operator } = credentials\n const client = createTillioClient({\n apiUrl: credentials.apiUrl,\n apiKey: credentials.apiKey,\n tenantSystemId: credentials.tenantSystemId,\n }, deps)\n\n const timeZone = credentials.timeZone ?? TILLIO_TIMEZONE\n const from = input.from ? formatTillioTimestamp(input.from, timeZone) : undefined\n const to = input.to ? formatTillioTimestamp(input.to, timeZone) : undefined\n const limit = input.limit && input.limit > 0 ? Math.trunc(input.limit) : TILLIO_DEFAULT_BATCH_LIMIT\n\n const calls: NormalizedPhoneCall[] = []\n let page = parseCursor(input.cursor)\n let nextCursor: string | null = null\n let anomaly: PhoneCallBatchAnomaly | null = null\n let anomalyCursor: string | null = null\n\n while (true) {\n const response = await client.getCalls({\n tenantDomain: operator.tenantDomain,\n token: operator.token,\n from,\n to,\n page,\n })\n\n for (const rawCall of response.calls) {\n calls.push(normalizeTillioCall(rawCall, { operatorId: operator.id, plugin: operator.plugin, timeZone }))\n }\n\n // These stop a broken response from spinning the loop forever, and they are reported\n // rather than swallowed: a provider that ignores `page` answers one page and looks\n // finished, which would turn a partial sweep into a clean one.\n const { pages, page: echoedPage } = response.pagination\n if (echoedPage !== page) {\n anomaly = 'page_not_echoed'\n } else if (pages < 1) {\n anomaly = 'invalid_page_count'\n } else if (page < pages && response.calls.length === 0) {\n anomaly = 'empty_page'\n }\n if (anomaly) {\n anomalyCursor = String(page)\n break\n }\n\n // Reaching the last page is ordinary exhaustion, not an anomaly.\n if (page >= pages) break\n\n page += 1\n // The current page is always drained before stopping, so the cursor never\n // advances past calls we did not collect.\n if (calls.length >= limit) {\n nextCursor = String(page)\n break\n }\n }\n\n return { calls, nextCursor, anomaly, anomalyCursor }\n },\n }\n}\n\nexport const tillioAdapter: PhoneCallProviderAdapter = createTillioAdapter()\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }