@friggframework/core 2.0.0--canary.703.12b980b.0 → 2.0.0--canary.707.df80bfd.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CLAUDE.md +28 -0
  3. package/README.md +105 -0
  4. package/application/commands/integration-commands.js +20 -10
  5. package/application/commands/scheduler-commands.js +11 -7
  6. package/application/commands/user-commands.js +17 -5
  7. package/application/index.js +6 -2
  8. package/core/Worker.js +31 -7
  9. package/core/create-handler.js +9 -0
  10. package/credential/repositories/credential-repository-documentdb.js +34 -0
  11. package/credential/repositories/credential-repository-interface.js +15 -0
  12. package/credential/repositories/credential-repository-mongo.js +23 -0
  13. package/credential/repositories/credential-repository-postgres.js +28 -0
  14. package/credential/use-cases/credential-operations-for-user.js +202 -0
  15. package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
  16. package/database/config.js +48 -24
  17. package/database/documentdb-encryption-service.js +9 -22
  18. package/database/documentdb-utils.js +2 -2
  19. package/database/encryption/README.md +78 -14
  20. package/database/encryption/documentdb-encryption-service.md +78 -1
  21. package/database/encryption/encryption-config.js +219 -0
  22. package/database/encryption/encryption-schema-registry.js +121 -24
  23. package/database/encryption/field-encryption-service.js +69 -5
  24. package/database/encryption/integration-mapping-encryption.js +1 -1
  25. package/database/encryption/test-support/fake-ciphertext.js +15 -0
  26. package/database/organization-where.js +18 -0
  27. package/database/prisma.js +15 -38
  28. package/database/unique-violation.js +25 -0
  29. package/database/use-cases/check-encryption-health-use-case.js +43 -52
  30. package/errors/index.js +0 -4
  31. package/extensions/bootstrap-runner.js +43 -0
  32. package/extensions/extension-loader.js +230 -0
  33. package/extensions/index.js +44 -0
  34. package/extensions/initialize-app.js +123 -0
  35. package/extensions/prisma-composer.js +270 -0
  36. package/extensions/route-mounter.js +103 -0
  37. package/extensions/schema-composer.js +188 -0
  38. package/generated/prisma-mongodb/edge.js +33 -42
  39. package/generated/prisma-mongodb/index-browser.js +30 -39
  40. package/generated/prisma-mongodb/index.d.ts +3187 -2005
  41. package/generated/prisma-mongodb/index.js +33 -42
  42. package/generated/prisma-mongodb/package.json +1 -1
  43. package/generated/prisma-mongodb/schema.prisma +86 -59
  44. package/generated/prisma-mongodb/wasm.js +33 -42
  45. package/generated/prisma-postgresql/edge.js +33 -42
  46. package/generated/prisma-postgresql/index-browser.js +30 -39
  47. package/generated/prisma-postgresql/index.d.ts +3380 -2177
  48. package/generated/prisma-postgresql/index.js +33 -42
  49. package/generated/prisma-postgresql/package.json +1 -1
  50. package/generated/prisma-postgresql/schema.prisma +81 -57
  51. package/generated/prisma-postgresql/wasm.js +33 -42
  52. package/handlers/WEBHOOKS.md +0 -29
  53. package/handlers/app-definition-loader.js +128 -19
  54. package/handlers/app-handler-helpers.js +27 -8
  55. package/handlers/backend-utils.js +31 -25
  56. package/handlers/integration-event-dispatcher.js +3 -7
  57. package/handlers/middleware/admin-auth.js +14 -1
  58. package/handlers/routers/auth.js +46 -10
  59. package/handlers/routers/health.js +13 -4
  60. package/handlers/routers/integration-defined-routers.js +144 -92
  61. package/handlers/routers/integration-webhook-routers.js +100 -50
  62. package/handlers/workers/integration-defined-workers.js +5 -2
  63. package/index.js +66 -16
  64. package/infrastructure/scheduler/index.js +19 -6
  65. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  66. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  67. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  68. package/integrations/index.js +1 -14
  69. package/integrations/integration-base.js +2 -163
  70. package/integrations/integration-router.js +132 -102
  71. package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
  72. package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
  73. package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
  74. package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
  75. package/integrations/repositories/integration-mapping-repository.js +0 -29
  76. package/integrations/repositories/integration-not-found.js +14 -0
  77. package/integrations/repositories/integration-repository-documentdb.js +37 -9
  78. package/integrations/repositories/integration-repository-interface.js +17 -4
  79. package/integrations/repositories/integration-repository-mongo.js +64 -96
  80. package/integrations/repositories/integration-repository-postgres.js +63 -81
  81. package/integrations/repositories/process-repository-mongo.js +1 -1
  82. package/integrations/schedule-config.js +80 -0
  83. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  84. package/integrations/use-cases/create-integration.js +56 -31
  85. package/integrations/use-cases/delete-integration-for-user.js +4 -4
  86. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  87. package/integrations/use-cases/get-integration-for-user.js +2 -4
  88. package/integrations/use-cases/get-integration-instance.js +4 -4
  89. package/integrations/use-cases/get-integrations-for-user.js +79 -39
  90. package/integrations/use-cases/get-possible-integrations.js +2 -7
  91. package/integrations/use-cases/index.js +7 -1
  92. package/integrations/use-cases/integration-operations-for-user.js +256 -0
  93. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  94. package/integrations/use-cases/update-integration.js +4 -4
  95. package/integrations/utils/map-integration-dto.js +43 -14
  96. package/management-api/create-management-api-router.js +49 -0
  97. package/management-api/error-response.js +116 -0
  98. package/management-api/management-api-config.js +151 -0
  99. package/management-api/meta-router.js +76 -0
  100. package/management-api/openapi/build-openapi.js +144 -0
  101. package/management-api/openapi/components.js +415 -0
  102. package/management-api/openapi/v1-operations.js +83 -0
  103. package/management-api/openapi/v2-operations.js +264 -0
  104. package/management-api/route-registry.js +411 -0
  105. package/management-api/test-support/http-client.js +39 -0
  106. package/management-api/use-cases/get-management-api-meta.js +57 -0
  107. package/management-api/v1-deprecation.js +120 -0
  108. package/management-api/v2/create-v2-router.js +85 -0
  109. package/management-api/v2/dependencies.js +281 -0
  110. package/management-api/v2/dto.js +143 -0
  111. package/management-api/v2/handlers/authorization.js +94 -0
  112. package/management-api/v2/handlers/credentials.js +33 -0
  113. package/management-api/v2/handlers/entities.js +77 -0
  114. package/management-api/v2/handlers/integrations.js +128 -0
  115. package/management-api/v2/handlers/proxy.js +34 -0
  116. package/management-api/v2/request.js +85 -0
  117. package/management-api/v2/test-support/fake-dependencies.js +103 -0
  118. package/modules/domain/authorization-session.js +144 -0
  119. package/modules/entity-scope.js +15 -0
  120. package/modules/entity-type-registry.js +311 -0
  121. package/modules/index.js +14 -0
  122. package/modules/module-factory.js +12 -0
  123. package/modules/module.js +53 -2
  124. package/modules/proxy/proxy-bases.js +169 -0
  125. package/modules/proxy/proxy-errors.js +31 -0
  126. package/modules/proxy/proxy-path.js +183 -0
  127. package/modules/proxy/proxy-policy.js +277 -0
  128. package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
  129. package/modules/repositories/authorization-session-repository-factory.js +31 -0
  130. package/modules/repositories/authorization-session-repository-interface.js +47 -0
  131. package/modules/repositories/authorization-session-repository-prisma.js +87 -0
  132. package/modules/repositories/entity-not-found.js +18 -0
  133. package/modules/repositories/module-repository-documentdb.js +51 -6
  134. package/modules/repositories/module-repository-interface.js +19 -2
  135. package/modules/repositories/module-repository-mongo.js +67 -74
  136. package/modules/repositories/module-repository-postgres.js +66 -74
  137. package/modules/repositories/module-repository.js +2 -1
  138. package/modules/requester/proxy-request.js +245 -0
  139. package/modules/requester/requester.js +35 -5
  140. package/modules/scope-config.js +192 -0
  141. package/modules/scope-policy.js +112 -0
  142. package/modules/scope-resolution.js +288 -0
  143. package/modules/scope-visibility.js +247 -0
  144. package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
  145. package/modules/tests/doubles/ownership-matching.js +18 -0
  146. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  147. package/modules/use-cases/create-entity-from-credential.js +113 -0
  148. package/modules/use-cases/entity-operations-for-user.js +162 -0
  149. package/modules/use-cases/entity-types.js +139 -0
  150. package/modules/use-cases/execute-entity-proxy-request.js +331 -0
  151. package/modules/use-cases/get-authorization-requirements.js +100 -0
  152. package/modules/use-cases/get-entities-for-user.js +26 -13
  153. package/modules/use-cases/get-entity-module-for-user.js +113 -0
  154. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  155. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  156. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  157. package/modules/use-cases/get-module.js +10 -4
  158. package/modules/use-cases/manage-access.js +21 -0
  159. package/modules/use-cases/multi-step-authorization.js +329 -0
  160. package/modules/use-cases/process-authorization-callback.js +53 -19
  161. package/modules/use-cases/refresh-entity-options.js +10 -4
  162. package/modules/use-cases/test-module-auth.js +10 -4
  163. package/package.json +6 -7
  164. package/prisma-mongodb/schema.prisma +86 -59
  165. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
  166. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  167. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  168. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  169. package/prisma-postgresql/schema.prisma +81 -57
  170. package/providers/index.js +23 -0
  171. package/providers/resolve-provider.js +93 -0
  172. package/providers/runtime-detection.js +124 -0
  173. package/queues/index.js +25 -0
  174. package/queues/providers/index.js +11 -0
  175. package/queues/providers/netlify-background-provider.js +150 -0
  176. package/queues/providers/qstash-queue-provider.js +219 -0
  177. package/queues/providers/sqs-queue-provider.js +70 -0
  178. package/queues/queue-client-interface.js +55 -0
  179. package/queues/queue-message-auth.js +193 -0
  180. package/queues/queue-provider-factory.js +101 -0
  181. package/queues/queue-provider.js +65 -0
  182. package/queues/queuer-util.js +2 -9
  183. package/queues/sqs-queue-client.js +81 -0
  184. package/types/index.d.ts +2 -0
  185. package/types/modules/index.d.ts +214 -0
  186. package/types/user/index.d.ts +134 -0
  187. package/user/adopter-jwt-config.js +177 -0
  188. package/user/adopter-jwt-errors.js +59 -0
  189. package/user/adopter-token-verifier.js +122 -0
  190. package/user/app-user-helpers.js +97 -0
  191. package/user/create-user-authentication.js +64 -0
  192. package/user/membership.js +51 -0
  193. package/user/repositories/organization-membership-record.js +19 -0
  194. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  195. package/user/repositories/organization-membership-repository-factory.js +42 -0
  196. package/user/repositories/organization-membership-repository-interface.js +57 -0
  197. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  198. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  199. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  200. package/user/repositories/user-repository-documentdb.js +27 -2
  201. package/user/repositories/user-repository-interface.js +18 -0
  202. package/user/repositories/user-repository-mongo.js +24 -0
  203. package/user/repositories/user-repository-postgres.js +18 -0
  204. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  205. package/user/use-cases/authenticate-user.js +7 -1
  206. package/user/use-cases/ensure-organization-membership.js +92 -0
  207. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  208. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  209. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  210. package/user/user.js +64 -1
  211. package/errors/mapping-already-exists-error.js +0 -14
  212. package/handlers/workers/integration-triggers.js +0 -25
  213. package/infrastructure/scheduler/recurrence.js +0 -204
  214. package/integrations/define/DESIGN.md +0 -122
  215. package/integrations/define/config.js +0 -98
  216. package/integrations/define/define-app.js +0 -66
  217. package/integrations/define/define-extension.js +0 -154
  218. package/integrations/define/define-integration.js +0 -335
  219. package/integrations/define/examples/deal-alerts.js +0 -140
  220. package/integrations/define/extension-binding.js +0 -174
  221. package/integrations/define/index.js +0 -11
  222. package/integrations/define/json-schema-lite.js +0 -68
  223. package/integrations/define/subscriptions.js +0 -186
  224. package/integrations/define/tests/fixtures/hubspot.js +0 -109
  225. package/integrations/define/tests/fixtures/slack.js +0 -26
  226. package/integrations/define/use.js +0 -39
  227. package/integrations/definitions/README.md +0 -278
  228. package/integrations/definitions/canonical.js +0 -11
  229. package/integrations/definitions/capabilities/AUDIT.md +0 -480
  230. package/integrations/definitions/capabilities/acmeerp.capabilities.json +0 -633
  231. package/integrations/definitions/capabilities/asana.capabilities.json +0 -279
  232. package/integrations/definitions/capabilities/calendly.capabilities.json +0 -385
  233. package/integrations/definitions/capabilities/docusign.capabilities.json +0 -314
  234. package/integrations/definitions/capabilities/fathom.capabilities.json +0 -238
  235. package/integrations/definitions/capabilities/github.capabilities.json +0 -585
  236. package/integrations/definitions/capabilities/hubspot.capabilities.json +0 -822
  237. package/integrations/definitions/capabilities/jira.capabilities.json +0 -391
  238. package/integrations/definitions/capabilities/klaviyo.capabilities.json +0 -503
  239. package/integrations/definitions/capabilities/linear.capabilities.json +0 -378
  240. package/integrations/definitions/capabilities/netsuite.capabilities.json +0 -535
  241. package/integrations/definitions/capabilities/notion.capabilities.json +0 -208
  242. package/integrations/definitions/capabilities/phone.capabilities.json +0 -511
  243. package/integrations/definitions/capabilities/pipedrive.capabilities.json +0 -430
  244. package/integrations/definitions/capabilities/quickbooks.capabilities.json +0 -635
  245. package/integrations/definitions/capabilities/salesforce.capabilities.json +0 -568
  246. package/integrations/definitions/capabilities/shopify.capabilities.json +0 -1128
  247. package/integrations/definitions/capabilities/slack.capabilities.json +0 -373
  248. package/integrations/definitions/capabilities/stripe.capabilities.json +0 -630
  249. package/integrations/definitions/capabilities/webflow.capabilities.json +0 -196
  250. package/integrations/definitions/capabilities/zendesk.capabilities.json +0 -422
  251. package/integrations/definitions/compile.js +0 -80
  252. package/integrations/definitions/config-form.js +0 -137
  253. package/integrations/definitions/examples/acme-erp-quickbooks.definition.json +0 -467
  254. package/integrations/definitions/examples/asana-slack.definition.json +0 -219
  255. package/integrations/definitions/examples/calendly-salesforce.definition.json +0 -459
  256. package/integrations/definitions/examples/docusign-salesforce.definition.json +0 -305
  257. package/integrations/definitions/examples/fathom-hubspot-notion.definition.json +0 -575
  258. package/integrations/definitions/examples/github-linear.definition.json +0 -322
  259. package/integrations/definitions/examples/hubspot-slack.definition.json +0 -427
  260. package/integrations/definitions/examples/klaviyo-hubspot.definition.json +0 -336
  261. package/integrations/definitions/examples/phone-hubspot.definition.json +0 -633
  262. package/integrations/definitions/examples/pipedrive-hubspot.definition.json +0 -136
  263. package/integrations/definitions/examples/pipedrive-phone.definition.json +0 -340
  264. package/integrations/definitions/examples/records/phone-hubspot.definition-version.json +0 -652
  265. package/integrations/definitions/examples/records/phone-hubspot.integration.json +0 -86
  266. package/integrations/definitions/examples/records/phone-hubspot.mappings.json +0 -39
  267. package/integrations/definitions/examples/records/phone-hubspot.workflow-run.json +0 -62
  268. package/integrations/definitions/examples/shopify-klaviyo.definition.json +0 -488
  269. package/integrations/definitions/examples/shopify-netsuite.definition.json +0 -622
  270. package/integrations/definitions/examples/stripe-quickbooks.definition.json +0 -476
  271. package/integrations/definitions/examples/webflow-hubspot-slack.definition.json +0 -262
  272. package/integrations/definitions/examples/zendesk-jira.definition.json +0 -604
  273. package/integrations/definitions/expressions.js +0 -229
  274. package/integrations/definitions/index.js +0 -48
  275. package/integrations/definitions/integration-runtime.js +0 -779
  276. package/integrations/definitions/interpreter.js +0 -104
  277. package/integrations/definitions/manifests.js +0 -177
  278. package/integrations/definitions/renderers/common.js +0 -67
  279. package/integrations/definitions/renderers/html.js +0 -129
  280. package/integrations/definitions/renderers/hubspot.js +0 -59
  281. package/integrations/definitions/renderers/preview.js +0 -154
  282. package/integrations/definitions/renderers/slack.js +0 -146
  283. package/integrations/definitions/repositories/definition-version-repository.js +0 -67
  284. package/integrations/definitions/repositories/workflow-run-repository.js +0 -80
  285. package/integrations/definitions/router.js +0 -170
  286. package/integrations/definitions/runtime.js +0 -268
  287. package/integrations/definitions/schema/capability-manifest.v0.1.schema.json +0 -549
  288. package/integrations/definitions/schema/installation.v0.1.schema.json +0 -130
  289. package/integrations/definitions/schema/integration-definition.v0.1.schema.json +0 -909
  290. package/integrations/definitions/schema/records.md +0 -105
  291. package/integrations/definitions/schema-check.js +0 -58
  292. package/integrations/definitions/scripts/check-manifests.js +0 -220
  293. package/integrations/definitions/scripts/validate.js +0 -66
  294. package/integrations/definitions/steps.js +0 -123
  295. package/integrations/definitions/sync-engine.js +0 -297
  296. package/integrations/definitions/tests/helpers-fake-crm.js +0 -49
  297. package/integrations/definitions/transforms.js +0 -91
  298. package/integrations/definitions/use-cases/definition-use-cases.js +0 -245
  299. package/integrations/definitions/validate.js +0 -436
  300. package/integrations/definitions/versioning.js +0 -176
  301. package/integrations/definitions/webhooks.js +0 -119
  302. package/integrations/integration-types.js +0 -73
  303. package/integrations/triggers.js +0 -244
  304. package/integrations/use-cases/run-trigger-tick.js +0 -155
  305. package/integrations/use-cases/run-trigger.js +0 -77
  306. package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +0 -66
  307. package/prisma-postgresql/migrations/20261019000000_workflow_run_waits/migration.sql +0 -7
  308. package/prisma-postgresql/migrations/20261019120000_definition_workspace/migration.sql +0 -5
@@ -1,104 +0,0 @@
1
- /**
2
- * Runs a list of steps against a context. Pure apart from the ports it is given:
3
- * ports.call(moduleKey, capability, args) a module capability (the runtime maps it to Api.<method>)
4
- * ports.links.find / ports.links.upsert the link + identity store (IntegrationMapping rows)
5
- * ports.runCode(ref, args) registered code steps (optional)
6
- *
7
- * Every step leaves a trace entry: { id, use, status, ms, error? }. A step's result is stored at $.steps.<id>.
8
- * onError per step: 'fail' (default: throw, so the queue retries the whole run), 'continue' (result null), 'stop'.
9
- *
10
- * A pausing step (delay, approval) ends the run for now with status 'waiting': the caller saves a checkpoint (the
11
- * step's index, $.steps and $.vars) and later calls execute again with startAt = index + 1 and the wait step's
12
- * result in ctx.steps. Pausing steps run only at the top level, so an index is the whole position.
13
- */
14
- const { resolve, evaluate } = require('./expressions');
15
- const { BUILTINS, FOREACH_HARD_CAP, PAUSING, isBuiltin, splitCapability } = require('./steps');
16
-
17
- class StopRun extends Error { constructor(reason) { super(reason); this.name = 'StopRun'; this.reason = reason; } }
18
- class WaitRun extends Error { constructor(index, id, wait) { super(`waiting at ${id}`); this.name = 'WaitRun'; Object.assign(this, { index, id, wait }); } }
19
-
20
- async function runSteps(steps, env, path = 'steps', startAt = 0) {
21
- const { ctx, trace } = env;
22
- for (let i = startAt; i < (steps || []).length; i++) {
23
- const step = steps[i];
24
- const id = step.id || `${step.use}#${i}`;
25
- const at = `${path}[${i}]`;
26
- if (step.when && !evaluate(step.when, ctx)) { trace.push({ id, use: step.use, at, status: 'skipped' }); continue; }
27
- const t0 = Date.now();
28
- try {
29
- let out;
30
- if (step.use === 'foreach') {
31
- const all = resolve(step.with?.over, ctx) ?? [];
32
- if (!Array.isArray(all)) throw new Error('foreach: `over` is not an array');
33
- const max = Math.min(step.with?.max || 100, FOREACH_HARD_CAP);
34
- const saved = { item: ctx.item, index: ctx.index };
35
- // `where` is checked per item, before the cap, so max bounds the work actually done.
36
- const items = [];
37
- for (const [n, item] of all.entries()) {
38
- ctx.item = item; ctx.index = n;
39
- if (!step.with?.where || evaluate(step.with.where, ctx)) items.push(item);
40
- }
41
- Object.assign(ctx, saved);
42
- const collect = step.with && 'collect' in step.with;
43
- out = [];
44
- for (const [n, item] of items.slice(0, max).entries()) {
45
- ctx.item = item; ctx.index = n;
46
- await runSteps(step.do || [], env, `${at}.do#${n}`);
47
- out.push(collect
48
- ? resolve(step.with.collect, ctx)
49
- : Object.fromEntries((step.do || []).filter((s) => s.id).map((s) => [s.id, ctx.steps[s.id]])));
50
- Object.assign(ctx, saved);
51
- }
52
- if (items.length > max) trace.push({ id, use: 'foreach', at, status: 'truncated', note: `${items.length - max} items over max ${max}` });
53
- } else if (step.use === 'branch') {
54
- const hit = (step.cases || []).find((c) => evaluate(c.when, ctx));
55
- await runSteps(hit ? hit.do : step.else || [], env, `${at}.${hit ? 'case' : 'else'}`);
56
- out = { matched: hit ? hit.when : null };
57
- } else if (isBuiltin(step.use)) {
58
- if (PAUSING.has(step.use) && path !== 'steps') throw new Error(`${step.use} can only be a top-level workflow step`);
59
- out = await BUILTINS[step.use].run(resolve(step.with || {}, ctx), { ...env, now: ctx.now });
60
- if (out && out.wait) {
61
- if (!env.canWait) throw new Error(`${step.use} can only be a top-level workflow step`);
62
- trace.push({ id, use: step.use, at, status: 'waiting', ms: Date.now() - t0, until: out.wait.until });
63
- throw new WaitRun(i, id, out.wait);
64
- }
65
- if (out && out.stop) { ctx.steps[id] = out; trace.push({ id, use: step.use, at, status: 'stopped', ms: Date.now() - t0, reason: out.reason }); throw new StopRun(out.reason); }
66
- } else {
67
- const cap = splitCapability(step.use);
68
- if (!cap) throw new Error(`Unknown step "${step.use}"`);
69
- out = await env.ports.call(cap.module, cap.capability, resolve(step.with || {}, ctx));
70
- }
71
- ctx.steps[id] = out === undefined ? null : out;
72
- trace.push({ id, use: step.use, at, status: 'ok', ms: Date.now() - t0 });
73
- } catch (e) {
74
- if (e instanceof StopRun || e instanceof WaitRun) throw e;
75
- const policy = step.onError || 'fail';
76
- trace.push({ id, use: step.use, at, status: 'error', ms: Date.now() - t0, error: String(e && e.message || e), policy });
77
- if (policy === 'continue') { ctx.steps[id] = null; continue; }
78
- if (policy === 'stop') throw new StopRun(`step ${id} failed`);
79
- throw e;
80
- }
81
- }
82
- }
83
-
84
- /**
85
- * Run steps and report the outcome instead of throwing for a normal stop or pause.
86
- * @param {object} p
87
- * @param {boolean} [p.canWait] a workflow run (pausing steps allowed); queries and lifecycle steps cannot pause
88
- * @param {number} [p.startAt] resume: the index of the first step to run (ctx.steps/ctx.vars carry the earlier ones)
89
- * @returns {Promise<{status:'completed'|'stopped'|'waiting', reason?, steps, vars, trace, wait?: {index, id, ...}}>}
90
- */
91
- async function execute(steps, { def, installation, ctx, ports, canWait = false, startAt = 0 }) {
92
- const env = { def, installation, ports, canWait, trace: [], ctx: { steps: {}, vars: {}, ...ctx } };
93
- try {
94
- await runSteps(steps, env, 'steps', startAt);
95
- return { status: 'completed', steps: env.ctx.steps, vars: env.ctx.vars, trace: env.trace };
96
- } catch (e) {
97
- if (e instanceof StopRun) return { status: 'stopped', reason: e.reason, steps: env.ctx.steps, vars: env.ctx.vars, trace: env.trace };
98
- if (e instanceof WaitRun) return { status: 'waiting', wait: { index: e.index, id: e.id, ...e.wait }, steps: env.ctx.steps, vars: env.ctx.vars, trace: env.trace };
99
- e.trace = env.trace;
100
- throw e;
101
- }
102
- }
103
-
104
- module.exports = { execute, runSteps, StopRun, WaitRun };
@@ -1,177 +0,0 @@
1
- /**
2
- * Validate a capability manifest (v0.1): the module author's promise of what a definition may call. Definitions are
3
- * validated against manifests, so a broken manifest would let a broken definition through; this checks the manifest
4
- * itself.
5
- *
6
- * @param {object} m capabilities/<module>.capabilities.json
7
- * @returns {{ valid: boolean, errors: Array<{path:string,message:string}> }}
8
- */
9
- const { templateTags, parseCondition, FILTERS } = require('./expressions');
10
- const { eventsIn } = require('./webhooks');
11
-
12
- const PATH_RE = /^\$\.[A-Za-z_][\w-]*(\.[\w-]+|\[\d+\])*$/;
13
- const KINDS = new Set(['read', 'search', 'write', 'delete', 'subscribe']); // schema/capability-manifest.v0.1.schema.json
14
- const READS = new Set(['read', 'search']);
15
-
16
- function validateManifest(m) {
17
- const errors = [];
18
- const err = (path, message) => errors.push({ path, message });
19
- if (!m || typeof m !== 'object') return { valid: false, errors: [{ path: '', message: 'Manifest must be an object' }] };
20
- if (!/^[a-z][a-z0-9-]*$/.test(m.module || '')) err('module', 'Lowercase module name');
21
- if (!m.label) err('label', 'Required');
22
-
23
- const objects = m.objects || {};
24
- for (const [o, def] of Object.entries(objects)) {
25
- const names = new Set();
26
- (def.fields || []).forEach((f, i) => {
27
- if (!f?.name) err(`objects.${o}.fields[${i}]`, 'Field needs a name');
28
- else if (names.has(f.name)) err(`objects.${o}.fields[${i}]`, `Duplicate field "${f.name}"`);
29
- else names.add(f.name);
30
- // An enum lists its values, or names one of this module's option lists when they differ per account
31
- // (HubSpot deal stages are per portal).
32
- if (f?.type === 'enum' && !Array.isArray(f.values) && !(typeof f.values === 'string' && m.options?.[f.values])) err(`objects.${o}.fields[${i}]`, 'An enum lists its values, or names an option list of this module');
33
- });
34
- }
35
-
36
- const caps = m.capabilities || {};
37
- for (const [name, c] of Object.entries(caps)) {
38
- const p = `capabilities.${name}`;
39
- if (!/^[A-Za-z][\w]*\.[A-Za-z][\w]*$/.test(name)) err(p, 'Capability names are "<object>.<verb>"');
40
- if (!KINDS.has(c?.kind)) err(`${p}.kind`, [...KINDS].join(', '));
41
- if (!/^[A-Za-z_$][\w$]*$/.test(c?.method || '')) err(`${p}.method`, 'Name of a method on the module\'s Api class');
42
- for (const [arg, spec] of Object.entries(c?.input || {})) if (!spec?.type) err(`${p}.input.${arg}`, 'Input needs a type');
43
- if (c?.status !== undefined && c.status !== 'unimplemented') err(`${p}.status`, 'Only "unimplemented" (the module\'s Api has no such method)');
44
- if (c?.args !== undefined) {
45
- if (!Array.isArray(c.args)) err(`${p}.args`, 'An ordered list: input names, "$rest", or { value }');
46
- else {
47
- const named = new Set();
48
- c.args.forEach((a, i) => {
49
- if (typeof a === 'string' && a !== '$rest') {
50
- if (!(a in (c.input || {}))) err(`${p}.args[${i}]`, `"${a}" is not an input of this capability`);
51
- else if (named.has(a)) err(`${p}.args[${i}]`, `"${a}" is passed twice`);
52
- named.add(a);
53
- } else if (a === '$rest') { if (c.args.indexOf('$rest') !== i) err(`${p}.args[${i}]`, '"$rest" appears once'); }
54
- else if (!(a && typeof a === 'object' && 'value' in a)) err(`${p}.args[${i}]`, 'An input name, "$rest" or { value }');
55
- });
56
- // An input that is never passed would be accepted and silently dropped.
57
- if (!c.args.includes('$rest')) for (const k of Object.keys(c.input || {})) if (!named.has(k)) err(`${p}.args`, `Input "${k}" is declared but never passed to ${c.method}; list it in args or use "$rest"`);
58
- }
59
- }
60
- }
61
-
62
- // How a sync reads and writes an object (sync-engine.js). Each operation names a capability of the right kind on
63
- // that object; list says how pages work; a write may wrap the mapped record in one argument (body).
64
- const OPS = { list: READS, get: READS, search: new Set(['search']), create: new Set(['write']), update: new Set(['write']) };
65
- const ARG_RE = /^[A-Za-z_][\w]*$/;
66
- for (const [o, def] of Object.entries(objects)) {
67
- const sy = def.sync;
68
- if (!sy) continue;
69
- const p = `objects.${o}.sync`;
70
- for (const [op, kinds] of Object.entries(OPS)) {
71
- const spec = sy[op];
72
- if (spec === undefined) continue;
73
- const c = caps[spec?.capability];
74
- if (!c) { err(`${p}.${op}.capability`, `"${spec?.capability}" is not a capability of this module`); continue; }
75
- if (spec.capability.split('.')[0] !== o) err(`${p}.${op}.capability`, `${spec.capability} belongs to another object`);
76
- if (!kinds.has(c.kind)) err(`${p}.${op}.capability`, `${op} needs a ${[...kinds].join(' or ')} capability; ${spec.capability} is ${c.kind}`);
77
- for (const argKey of ['id', 'body', 'cursor', 'limit', 'since']) {
78
- const arg = spec[argKey];
79
- if (arg === undefined) continue;
80
- if (!ARG_RE.test(arg)) err(`${p}.${op}.${argKey}`, 'The name of one argument');
81
- else if (!(arg in (c.input || {}))) err(`${p}.${op}.${argKey}`, `${spec.capability} takes no argument "${arg}"`);
82
- }
83
- }
84
- if (sy.get && !sy.get.id) err(`${p}.get.id`, 'Which argument carries the record id');
85
- if (sy.update && !sy.update.id) err(`${p}.update.id`, 'Which argument carries the record id');
86
- if (sy.list) {
87
- const mode = sy.list.paging || 'cursor';
88
- if (!['cursor', 'offset', 'page'].includes(mode)) err(`${p}.list.paging`, 'cursor (the next cursor is in the response), offset or page');
89
- if (mode === 'cursor' && sy.list.cursor && !sy.list.next) err(`${p}.list.next`, 'Where the response puts the next cursor');
90
- if (mode !== 'cursor' && !sy.list.limit) err(`${p}.list.limit`, `${mode} paging needs the page size argument`);
91
- }
92
- for (const k of ['id', 'updatedAt']) if (sy[k] !== undefined && typeof sy[k] !== 'string') err(`${p}.${k}`, 'A path in the record');
93
- }
94
-
95
- for (const [topic, e] of Object.entries(m.events || {})) {
96
- const p = `events.${topic}`;
97
- if (e.object && !objects[e.object]) err(`${p}.object`, `"${e.object}" is not an object of this module`);
98
- if (e.recordId !== undefined && !objects[e.object]?.sync?.get) err(`${p}.recordId`, 'An event that carries only an id needs its object\'s sync.get to fetch the record');
99
- if (e.payload && (e.delivery || 'webhook') === 'webhook' && m.webhook) {
100
- // The example is a real delivery (body, plus headers when the topic is in one): it must route to this event
101
- // through the same code a live webhook goes through, and its record path must find something.
102
- const found = eventsIn(m, { body: e.payload, headers: e.headers }).find((x) => x.topic === topic);
103
- if (!found) err(`${p}.payload`, `The example delivery does not route to "${topic}" (check webhook.topic and this event's match)`);
104
- else if (e.record && found.record === undefined) err(`${p}.record`, `"${e.record}" finds nothing in the example event`);
105
- } else if (e.record && e.payload) {
106
- let cur = e.payload;
107
- for (const seg of e.record.split(/\.|(?=\[)/)) { const i = /^\[(\d+)\]$/.exec(seg); cur = cur == null ? undefined : i ? cur[Number(i[1])] : cur[seg]; }
108
- if (cur === undefined) err(`${p}.record`, `"${e.record}" is not a path in the example payload`);
109
- }
110
- }
111
-
112
- // An option list: a read capability, its inputs, and how each item becomes { label, value } (one reference, or
113
- // a template). A string that mixes "$.x" into text renders the path literally, so it is refused.
114
- const checkItemExpr = (v, p) => {
115
- if (typeof v !== 'string') { err(p, 'A reference ($.item.id) or a template ({{item.name}})'); return; }
116
- if (!PATH_RE.test(v) && v.includes('$.')) err(p, `"${v}" mixes a $. reference into text, which renders literally; use {{item.x}} for every part`);
117
- for (const t of templateTags(v)) {
118
- if (t.path.split(/\.|\[/)[0] !== 'item') err(p, `{{${t.path}}} — an option label can only read item`);
119
- for (const f of t.filters) if (!FILTERS[f.name]) err(p, `Unknown template filter "${f.name}"`);
120
- }
121
- if (PATH_RE.test(v) && !v.startsWith('$.item')) err(p, 'An option value can only read $.item');
122
- };
123
- for (const [name, o] of Object.entries(m.options || {})) {
124
- const p = `options.${name}`;
125
- const c = caps[o?.capability];
126
- if (!c) err(`${p}.capability`, `"${o?.capability}" is not a capability of this module`);
127
- else if (!READS.has(c.kind)) err(`${p}.capability`, `An option list reads; ${o.capability} is a ${c.kind} capability`);
128
- checkItemExpr(o?.label, `${p}.label`);
129
- checkItemExpr(o?.value, `${p}.value`);
130
- for (const [input, spec] of Object.entries(o?.inputs || {})) {
131
- if (c && !(input in (c.input || {}))) err(`${p}.inputs.${input}`, `${o.capability} takes no argument "${input}"`);
132
- if (!spec?.type) err(`${p}.inputs.${input}`, 'Input needs a type');
133
- }
134
- if (c) for (const [arg, spec] of Object.entries(c.input || {})) if (spec.required && !(arg in (o.inputs || {}))) err(`${p}.inputs`, `${o.capability} requires "${arg}", so the option list must declare it as an input`);
135
- }
136
-
137
- // Webhook delivery: how a POST from this provider becomes module events (webhooks.js). Templates may read body,
138
- // headers (lower-case), query, item (one event of a batch) and parent (the outer item of a two-level batch).
139
- const w = m.webhook;
140
- const webhookEvents = Object.entries(m.events || {}).filter(([, e]) => (e.delivery || 'webhook') === 'webhook');
141
- if (webhookEvents.length && !w) err('webhook', 'This module has webhook events, so it needs a webhook block saying where the topic is');
142
- if (w) {
143
- if (!['installation', 'app'].includes(w.scope)) err('webhook.scope', 'installation (a URL per installation) or app (one URL for every account)');
144
- const tpl = (v, p, roots = ['body', 'headers', 'query', 'item', 'parent']) => {
145
- for (const t of (Array.isArray(v) ? v : [v])) {
146
- if (typeof t !== 'string') { err(p, 'A template string, or a list of them'); continue; }
147
- for (const tag of templateTags(t)) {
148
- if (!roots.includes(tag.path.split(/\.|\[/)[0])) err(p, `{{${tag.path}}}: a webhook template can read ${roots.join(', ')}`);
149
- for (const f of tag.filters) if (!FILTERS[f.name]) err(p, `Unknown template filter "${f.name}"`);
150
- }
151
- }
152
- };
153
- if (!w.topic) err('webhook.topic', 'Required: where the provider puts the event type');
154
- else tpl(w.topic, 'webhook.topic');
155
- if (w.eventId) tpl(w.eventId, 'webhook.eventId'); else err('webhook.eventId', 'Required: the id that makes a redelivery a no-op');
156
- if (w.scope === 'app' && !w.account) err('webhook.account', 'An app-level webhook needs account: which connected account an event is about');
157
- if (w.account) tpl(w.account, 'webhook.account');
158
- const batch = w.batch == null ? [] : Array.isArray(w.batch) ? w.batch : [w.batch];
159
- if (batch.length > 2 || batch.some((b) => typeof b !== 'string')) err('webhook.batch', 'A path to the array of events, or [outer, inner] for two levels');
160
- if (w.status !== undefined && w.status !== 'unimplemented') err('webhook.status', 'Only "unimplemented" (the module\'s Api has no verify method)');
161
- if (w.verify != null && !/^[A-Za-z_$][\w$]*$/.test(w.verify)) err('webhook.verify', 'Name of an Api method that checks the signature');
162
- if (w.handshake) {
163
- try { parseCondition(w.handshake.when); } catch (e) { err('webhook.handshake.when', e.message); }
164
- if (w.handshake.status != null && !Number.isInteger(w.handshake.status)) err('webhook.handshake.status', 'An HTTP status code');
165
- }
166
- if (w.unwrap != null && typeof w.unwrap !== 'string') err('webhook.unwrap', 'The name of a form field holding the JSON body');
167
- }
168
- for (const [topic, e] of webhookEvents) {
169
- if (e.match != null && typeof e.match !== 'string') err(`events.${topic}.match`, 'The provider\'s topic string for this event');
170
- if (e.when) { try { parseCondition(e.when); } catch (x) { err(`events.${topic}.when`, x.message); } }
171
- }
172
-
173
- for (const pl of m.ui?.placements || []) if (typeof pl !== 'string') err('ui.placements', 'Placements are strings');
174
- return { valid: errors.length === 0, errors };
175
- }
176
-
177
- module.exports = { validateManifest };
@@ -1,67 +0,0 @@
1
- /**
2
- * What every renderer shares: coercion of view props (they come from record data, so any type can arrive), the one
3
- * URL rule, truncation that never leaves half an entity behind, and the button <-> Slack round trip.
4
- */
5
-
6
- /** A view prop as text. Objects are JSON, null/undefined are empty: a renderer never prints "[object Object]" or "undefined". */
7
- function str(v) {
8
- if (v == null) return '';
9
- if (typeof v === 'string') return v;
10
- if (typeof v === 'object') { try { return JSON.stringify(v); } catch (_) { return ''; } }
11
- return String(v);
12
- }
13
-
14
- /**
15
- * Only http(s) links leave a preview or a card. Record data decides these URLs ("{{data.proposal.recording}}"), and a
16
- * `javascript:` or `data:` URL in a vendor field must not become a clickable thing in someone's HubSpot or Slack.
17
- * @returns {string|null}
18
- */
19
- function safeUrl(u) {
20
- const s = str(u).trim();
21
- if (!/^https?:\/\/[^\s<>"']+$/i.test(s)) return null;
22
- try { new URL(s); } catch (_) { return null; }
23
- return s;
24
- }
25
-
26
- /**
27
- * Cut to at most `max` characters, ending in the marker. Never leaves a dangling half of a Slack entity ("&am"), which
28
- * would render as literal junk instead of the character it stood for.
29
- */
30
- function truncate(s, max, marker = '…') {
31
- s = String(s);
32
- if (s.length <= max) return s;
33
- let cut = s.slice(0, Math.max(0, max - marker.length));
34
- cut = cut.replace(/&(?:[a-z]{1,4}|#\d{0,6}|#x[0-9a-f]{0,5})?$/i, '');
35
- return cut + marker;
36
- }
37
-
38
- /** Badge tone (`neutral`, or a Jira statusCategory key like `done`) -> one of four. Unknown is neutral, never an error. */
39
- function toneOf(tone) {
40
- const t = str(tone).toLowerCase();
41
- if (['positive', 'success', 'good', 'done', 'green', 'complete', 'completed'].includes(t)) return 'positive';
42
- if (['warning', 'caution', 'indeterminate', 'yellow', 'in_progress', 'pending'].includes(t)) return 'warning';
43
- if (['critical', 'danger', 'error', 'bad', 'red', 'failed'].includes(t)) return 'critical';
44
- return 'neutral';
45
- }
46
-
47
- const ACTION_VALUE_MAX = 2000; // Slack's cap on a button's value
48
-
49
- /**
50
- * A userAction button's identity as a Slack button value: which installation, which workflow, which surface it was on.
51
- * `needsInput` marks a button whose workflow asks for values first (a Slack button cannot, so the handler opens a form).
52
- */
53
- function encodeAction({ integrationId, workflow, surface, needsInput }) {
54
- const value = JSON.stringify({ ...(integrationId ? { integrationId: String(integrationId) } : {}), workflow, surface, ...(needsInput ? { needsInput: true } : {}) });
55
- return value.length <= ACTION_VALUE_MAX ? value : null;
56
- }
57
-
58
- /** The inverse, for the handler that receives a click. null for anything that is not one of ours. */
59
- function decodeAction(value) {
60
- let v;
61
- try { v = JSON.parse(value); } catch (_) { return null; }
62
- if (!v || typeof v !== 'object' || typeof v.workflow !== 'string' || !v.workflow || typeof v.surface !== 'string') return null;
63
- if (v.integrationId !== undefined && typeof v.integrationId !== 'string') return null;
64
- return { ...(v.integrationId ? { integrationId: v.integrationId } : {}), workflow: v.workflow, surface: v.surface, ...(v.needsInput ? { needsInput: true } : {}) };
65
- }
66
-
67
- module.exports = { str, safeUrl, truncate, toneOf, encodeAction, decodeAction, ACTION_VALUE_MAX };
@@ -1,129 +0,0 @@
1
- /**
2
- * Safe, self-contained HTML for a preview pane. Every piece of data goes through `esc`, links are http(s) only, styles
3
- * are inline attributes and there is no script, no `<style>`, no event handler and no external resource, so a pane can
4
- * show a vendor's record next to the definition's chrome without that record being able to do anything.
5
- */
6
- const { str, safeUrl, toneOf } = require('./common');
7
-
8
- const esc = (v) => str(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
9
- const FONT = "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif";
10
-
11
- /** How each host frames what it shows. `generic` is for hosts with no look of their own (a phone app, Zendesk, Shopify…). */
12
- const HOSTS = {
13
- hubspot: { label: 'HubSpot card', frame: `${FONT};background:#fff;border:1px solid #cbd6e2;border-radius:3px;max-width:420px;color:#33475b;font-size:14px;box-shadow:0 1px 2px rgba(0,0,0,.08)`, head: 'background:#f5f8fa;border-bottom:1px solid #cbd6e2;padding:8px 14px;font-size:12px;color:#516f90;font-weight:600', body: 'padding:14px', accent: '#ff7a59' },
14
- slack: { label: 'Slack message', frame: `${FONT};background:#fff;border:1px solid #e0e0e0;border-radius:8px;max-width:560px;color:#1d1c1d;font-size:15px`, head: 'padding:8px 14px 0;font-size:13px;color:#616061', body: 'padding:6px 14px 12px', accent: '#4a154b' },
15
- generic: { label: 'Preview', frame: `${FONT};background:#fff;border:1px solid #d9dde3;border-radius:6px;max-width:480px;color:#1f2937;font-size:14px`, head: 'background:#f3f4f6;border-bottom:1px solid #d9dde3;padding:8px 14px;font-size:12px;color:#6b7280;font-weight:600', body: 'padding:14px', accent: '#2563eb' },
16
- };
17
- const hostOf = (h) => HOSTS[h] || HOSTS.generic;
18
- const TONE = { positive: 'background:#e6f4ea;color:#1e6b34', warning: 'background:#fff4d6;color:#8a5a00', critical: 'background:#fde8e6;color:#a4261b', neutral: 'background:#eceff3;color:#44505f' };
19
-
20
- /** The view tree as HTML, without the host frame. */
21
- function viewToHtmlBody(view, host = 'generic') {
22
- const H = hostOf(host);
23
- const go = (v) => {
24
- if (!v || typeof v !== 'object') return '';
25
- const p = v.props || {};
26
- const kids = () => (v.children || []).map(go).join('');
27
- switch (v.component) {
28
- case 'Stack': return `<div style="display:flex;flex-direction:column;gap:10px">${kids()}</div>`;
29
- case 'Heading': return `<h3 style="margin:0;font-size:16px;font-weight:700">${esc(p.text)}</h3>`;
30
- case 'Text': return `<p style="margin:0;line-height:1.4">${esc(p.text)}</p>`;
31
- case 'KeyValue': return `<dl style="margin:0;display:grid;grid-template-columns:max-content 1fr;gap:4px 14px">${(Array.isArray(p.items) ? p.items : []).map((it) => `<dt style="color:#6b7280">${esc(it?.label)}</dt><dd style="margin:0">${esc(it?.value) || '—'}</dd>`).join('')}</dl>`;
32
- case 'List': return `<ul style="margin:0;padding-left:18px">${(Array.isArray(p.items) ? p.items : []).map((it) => `<li>${esc(it?.title)}${it?.meta ? ` <span style="color:#6b7280;font-size:12px">${esc(it.meta)}</span>` : ''}</li>`).join('')}</ul>`;
33
- case 'Table': return `<table style="border-collapse:collapse;width:100%"><thead><tr>${(Array.isArray(p.columns) ? p.columns : []).map((c) => `<th style="text-align:left;border-bottom:1px solid #d9dde3;padding:4px 8px;font-size:12px;color:#6b7280">${esc(c)}</th>`).join('')}</tr></thead><tbody>${(Array.isArray(p.rows) ? p.rows : []).map((r) => `<tr>${(Array.isArray(r) ? r : []).map((c) => `<td style="border-bottom:1px solid #eef0f3;padding:4px 8px">${esc(c)}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
34
- case 'Badge': return `<span style="display:inline-block;align-self:flex-start;padding:2px 8px;border-radius:10px;font-size:12px;font-weight:600;${TONE[toneOf(p.tone)]}">${esc(p.text)}</span>`;
35
- case 'Empty': return `<p style="margin:0;color:#6b7280;font-style:italic">${esc(p.text)}</p>`;
36
- case 'Divider': return '<hr style="border:0;border-top:1px solid #e5e7eb;margin:2px 0">';
37
- case 'Button': return buttonHtml({ label: p.label, action: p.action, primary: p.style === 'primary', accent: H.accent });
38
- default: return kids();
39
- }
40
- };
41
- return go(view);
42
- }
43
-
44
- /** A button in a preview does nothing (no script), but says what it would do: a link for open_url, a `data-` marker for a workflow. */
45
- function buttonHtml({ label, action, primary, accent }) {
46
- const style = `display:inline-block;align-self:flex-start;padding:6px 14px;border-radius:4px;font-size:14px;font-weight:600;text-decoration:none;cursor:default;${primary ? `background:${accent};color:#fff;border:1px solid ${accent}` : `background:#fff;color:${accent};border:1px solid ${accent}`}`;
47
- if (action?.type === 'open_url') {
48
- const url = safeUrl(action.url);
49
- if (url) return `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer" style="${style}">${esc(label || 'Open')}</a>`;
50
- }
51
- const wf = action?.type === 'userAction' ? ` data-workflow="${esc(action.workflow)}" title="${esc(`Runs the "${action.workflow}" workflow`)}"` : '';
52
- return `<button type="button"${wf} style="${style}">${esc(label || 'Open')}</button>`;
53
- }
54
-
55
- /** The framed preview: a head line saying what this is and where it appears, then the body. */
56
- function frame(host, title, bodyHtml) {
57
- const H = hostOf(host);
58
- return `<div data-preview-host="${esc(host in HOSTS ? host : 'generic')}" style="${H.frame}"><div style="${H.head}">${esc(title || H.label)}</div><div style="${H.body}">${bodyHtml}</div></div>`;
59
- }
60
-
61
- /**
62
- * @param {object|null} view
63
- * @param {{ host?: string, title?: string }} [opts]
64
- */
65
- function viewToHtml(view, { host = 'generic', title } = {}) {
66
- const body = view ? viewToHtmlBody(view, host) : `<p style="margin:0;color:#6b7280;font-style:italic">Nothing to show for this record.</p>`;
67
- return frame(host, title, body);
68
- }
69
-
70
- // ---- Slack: mrkdwn and Block Kit as HTML, so a posted message previews as it will look, not as a data dump ----
71
-
72
- /** Slack mrkdwn (already Slack-encoded: `&amp;`, `<url|label>`) to HTML. Only `* _ ~ \`` and links are interpreted. */
73
- function mrkdwnToHtml(text) {
74
- const parts = str(text).split(/(<[^<>\n]*>)/);
75
- const out = parts.map((seg) => {
76
- const tok = /^<([^<>\n]*)>$/.exec(seg);
77
- if (tok) {
78
- // the target is Slack-encoded like the rest (`&amp;` in a query string): decode it once, then esc() it as an attribute
79
- const [rawTarget, label] = tok[1].split('|');
80
- const target = rawTarget.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
81
- if (/^https?:\/\//i.test(target)) { const url = safeUrl(target); return url ? `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer" style="color:#1264a3">${esc(label || target)}</a>` : esc(label || ''); }
82
- if (target.startsWith('!')) return `<span style="background:#fff3cd">@${esc(label || target.slice(1).split('^')[0])}</span>`;
83
- if (target.startsWith('@')) return `<span style="background:#e8f5fa;color:#1264a3">@${esc(label || target.slice(1))}</span>`;
84
- if (target.startsWith('#')) return `<span style="color:#1264a3">#${esc(label || target.slice(1))}</span>`;
85
- return esc(label || '');
86
- }
87
- // decode Slack's three entities once, then escape for HTML, then emphasis on the escaped text
88
- const raw = seg.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
89
- return esc(raw)
90
- .replace(/`([^`\n]+)`/g, '<code style="background:#f4f4f4;border:1px solid #e0e0e0;border-radius:3px;padding:0 3px">$1</code>')
91
- .replace(/(^|[\s(])\*([^*\n]+)\*(?=$|[\s).,!?:;])/g, '$1<strong>$2</strong>')
92
- .replace(/(^|[\s(])_([^_\n]+)_(?=$|[\s).,!?:;])/g, '$1<em>$2</em>')
93
- .replace(/(^|[\s(])~([^~\n]+)~(?=$|[\s).,!?:;])/g, '$1<del>$2</del>');
94
- }).join('');
95
- return out.replace(/\n/g, '<br>');
96
- }
97
-
98
- const textOf = (t) => (t && t.type === 'mrkdwn' ? mrkdwnToHtml(t.text) : esc(t?.text));
99
-
100
- function slackBlocksToHtml(blocks) {
101
- return (Array.isArray(blocks) ? blocks : []).map((b) => {
102
- switch (b?.type) {
103
- case 'header': return `<div style="font-size:18px;font-weight:700;margin:6px 0">${esc(b.text?.text)}</div>`;
104
- case 'section': return `<div style="margin:4px 0">${b.text ? `<div>${textOf(b.text)}</div>` : ''}${Array.isArray(b.fields) ? `<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px 14px;margin-top:4px">${b.fields.map((f) => `<div>${textOf(f)}</div>`).join('')}</div>` : ''}</div>`;
105
- case 'context': return `<div style="color:#616061;font-size:12px;margin:4px 0">${(b.elements || []).map(textOf).join(' ')}</div>`;
106
- case 'divider': return '<hr style="border:0;border-top:1px solid #e0e0e0;margin:8px 0">';
107
- case 'actions': return `<div style="display:flex;flex-wrap:wrap;gap:8px;margin:6px 0">${(b.elements || []).map((e) => (e.url
108
- ? buttonHtml({ label: e.text?.text, action: { type: 'open_url', url: e.url }, accent: '#1264a3' })
109
- : buttonHtml({ label: e.text?.text, action: { type: 'userAction', workflow: safeWorkflow(e.value) }, primary: e.style === 'primary', accent: e.style === 'primary' ? '#007a5a' : '#1264a3' }))).join('')}</div>`;
110
- default: return `<div style="color:#616061;font-size:12px">(${esc(b?.type || 'unknown')} block)</div>`;
111
- }
112
- }).join('');
113
- }
114
- function safeWorkflow(value) { try { const w = JSON.parse(value)?.workflow; return typeof w === 'string' ? w : ''; } catch (_) { return ''; } }
115
-
116
- /** A Slack message as the channel will show it: who posts it, where, then the blocks (or the text). */
117
- function slackMessageToHtml({ channel, text, blocks, appName = 'Your integration' }) {
118
- const to = channel ? esc(channel) : '<em>(channel not chosen yet)</em>';
119
- const body = Array.isArray(blocks) && blocks.length ? slackBlocksToHtml(blocks) : `<div>${mrkdwnToHtml(text)}</div>`;
120
- return `<div data-preview-host="slack" style="${HOSTS.slack.frame}"><div style="${HOSTS.slack.head}"><strong style="color:#1d1c1d">${esc(appName)}</strong> <span style="background:#e8e8e8;border-radius:3px;padding:0 4px;font-size:11px">APP</span> to ${to}</div><div style="${HOSTS.slack.body}">${body}</div></div>`;
121
- }
122
-
123
- /** Any other outbound call: what it is and what it carries. The values are escaped, and objects are shown as JSON. */
124
- function callToHtml({ module, capability, args }) {
125
- const rows = Object.entries(args || {}).map(([k, v]) => `<dt style="color:#6b7280">${esc(k)}</dt><dd style="margin:0;white-space:pre-wrap;word-break:break-word">${esc(typeof v === 'string' ? v : JSON.stringify(v, null, 2))}</dd>`).join('');
126
- return frame('generic', `Would call ${module} · ${capability}`, `<dl style="margin:0;display:grid;grid-template-columns:max-content 1fr;gap:4px 14px">${rows}</dl>`);
127
- }
128
-
129
- module.exports = { viewToHtml, viewToHtmlBody, slackBlocksToHtml, slackMessageToHtml, mrkdwnToHtml, callToHtml, esc, HOSTS };
@@ -1,59 +0,0 @@
1
- /**
2
- * Resolved view tree -> a JSON description of HubSpot UI-extension components. Names and prop values were checked
3
- * against @hubspot/ui-extensions 0.17.1 (its shared.synced/types/components/*.d.ts). Data, not React: a thin card
4
- * deployed once maps `{ type, props, children }` onto `React.createElement(components[type], props, ...children)`, so a
5
- * changed card is a publish, not a card deploy.
6
- *
7
- * A node is `{ type, props?, children?, action? }`; a child is a string (text) or a node. Strings are text, never markup.
8
- * `props` holds only real component props, passed straight through. `action` is OURS, not a HubSpot prop: the card must
9
- * not spread it onto the component, but turn it into `onClick={() => runServerless(...)}` that posts it to the actions
10
- * endpoint (a userAction button), because a UI extension cannot call out except through a serverless function.
11
- * An `open_url` button needs no action: it is the real `href` prop, which the host opens in a new tab.
12
- */
13
- const { str, safeUrl, toneOf } = require('./common');
14
-
15
- /** The component names this emits, so the card can fail closed on one it does not know. */
16
- const HUBSPOT_COMPONENTS = ['Flex', 'Heading', 'Text', 'List', 'Tag', 'Button', 'EmptyState', 'DescriptionList', 'DescriptionListItem', 'Divider', 'Table', 'TableHead', 'TableBody', 'TableRow', 'TableHeader', 'TableCell'];
17
- const TAG_VARIANT = { positive: 'success', warning: 'warning', critical: 'error', neutral: 'default' };
18
-
19
- const node = (type, props, children, action) => ({ type, ...(props && Object.keys(props).length ? { props } : {}), ...(action ? { action } : {}), ...(children && children.length ? { children } : {}) });
20
-
21
- /**
22
- * @param {object|null} view
23
- * @param {{ integrationId?: string, surface?: string }} [opts] carried on a button's `action`, so the card can post it back
24
- * @returns {object|null}
25
- */
26
- function viewToHubSpot(view, { integrationId, surface } = {}) {
27
- const go = (v) => {
28
- if (!v || typeof v !== 'object') return null;
29
- const p = v.props || {};
30
- const kids = () => (v.children || []).map(go).filter(Boolean);
31
- switch (v.component) {
32
- case 'Stack': return node('Flex', { direction: 'column', gap: 'sm' }, kids());
33
- case 'Heading': return node('Heading', {}, [str(p.text)]);
34
- case 'Text': return node('Text', {}, [str(p.text)]);
35
- case 'KeyValue': return node('DescriptionList', { direction: 'row' }, (Array.isArray(p.items) ? p.items : []).map((it) => node('DescriptionListItem', { label: str(it?.label) }, [node('Text', {}, [str(it?.value) || '—'])])));
36
- case 'List': return node('List', { variant: 'unordered' }, (Array.isArray(p.items) ? p.items : []).map((it) => node('Flex', { direction: 'column', gap: 'flush' }, [node('Text', { format: { fontWeight: 'demibold' } }, [str(it?.title)]), ...(it?.meta ? [node('Text', { variant: 'microcopy' }, [str(it.meta)])] : [])])));
37
- case 'Table': return node('Table', {}, [
38
- node('TableHead', {}, [node('TableRow', {}, (Array.isArray(p.columns) ? p.columns : []).map((c) => node('TableHeader', {}, [str(c)])))]),
39
- node('TableBody', {}, (Array.isArray(p.rows) ? p.rows : []).map((r) => node('TableRow', {}, (Array.isArray(r) ? r : []).map((c) => node('TableCell', {}, [str(c)]))))),
40
- ]);
41
- case 'Badge': return node('Tag', { variant: TAG_VARIANT[toneOf(p.tone)] }, [str(p.text)]);
42
- case 'Empty': return node('EmptyState', { layout: 'vertical' }, [node('Text', {}, [str(p.text)])]);
43
- case 'Divider': return node('Divider', { distance: 'sm' });
44
- case 'Button': {
45
- const a = p.action;
46
- const variant = p.style === 'primary' ? 'primary' : p.style === 'danger' ? 'destructive' : 'secondary';
47
- let action;
48
- if (a?.type === 'userAction' && typeof a.workflow === 'string') action = { type: 'userAction', workflow: a.workflow, surface, ...(integrationId ? { integrationId: String(integrationId) } : {}), ...(a.input ? { input: a.input } : {}) };
49
- const href = a?.type === 'open_url' ? safeUrl(a.url) : null;
50
- // A button with no usable action still renders (disabled), so the card matches the definition.
51
- return node('Button', { variant, ...(href ? { href } : action ? {} : { disabled: true }) }, [str(p.label) || 'Open'], action);
52
- }
53
- default: return kids().length === 1 ? kids()[0] : node('Flex', { direction: 'column', gap: 'sm' }, kids());
54
- }
55
- };
56
- return go(view);
57
- }
58
-
59
- module.exports = { viewToHubSpot, HUBSPOT_COMPONENTS };