@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
|
@@ -1,1181 +1,1181 @@
|
|
|
1
|
-
# Module 4: Webhook Patterns
|
|
2
|
-
|
|
3
|
-
> **Learning Objective:** Master webhook integration patterns for event-driven architectures, including request parsing, response formatting, and error handling.
|
|
4
|
-
>
|
|
5
|
-
> **Level:** Intermediate
|
|
6
|
-
|
|
7
|
-
## Table of Contents
|
|
8
|
-
|
|
9
|
-
1. [What are Webhooks?](#what-are-webhooks)
|
|
10
|
-
2. [Webhook Types and Patterns](#webhook-types-and-patterns)
|
|
11
|
-
3. [SDK Webhook Components](#sdk-webhook-components)
|
|
12
|
-
4. [Pattern 1: JSON Request/Response](#pattern-1-json-requestresponse)
|
|
13
|
-
5. [Pattern 2: XML Request/Response](#pattern-2-xml-requestresponse)
|
|
14
|
-
6. `Pattern 3: Mixed Format (XML→JSON)`
|
|
15
|
-
7. [Versori Webhook Response Handling](#versori-webhook-response-handling)
|
|
16
|
-
8. [Webhook Security](#webhook-security)
|
|
17
|
-
9. [Error Handling and Retry Logic](#error-handling-and-retry-logic)
|
|
18
|
-
10. [Complete Examples](#complete-examples)
|
|
19
|
-
11. [Next Steps](#next-steps)
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## What are Webhooks?
|
|
24
|
-
|
|
25
|
-
**Webhooks** are HTTP endpoints that receive event notifications from external systems in real-time.
|
|
26
|
-
|
|
27
|
-
### Webhook Flow
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
External System Your Webhook Fluent Commerce
|
|
31
|
-
│ │ │
|
|
32
|
-
│ ① Event occurs │ │
|
|
33
|
-
│ (order placed) │ │
|
|
34
|
-
│ │ │
|
|
35
|
-
│ ② HTTP POST ────────>│ │
|
|
36
|
-
│ Payload (XML/JSON)│ │
|
|
37
|
-
│ │ │
|
|
38
|
-
│ │ ③ Parse payload │
|
|
39
|
-
│ │ ④ Transform data │
|
|
40
|
-
│ │ │
|
|
41
|
-
│ │ ⑤ GraphQL mutation ──>│
|
|
42
|
-
│ │ │
|
|
43
|
-
│ │<── ⑥ Response │
|
|
44
|
-
│ │ │
|
|
45
|
-
│<── ⑦ 200 OK │ │
|
|
46
|
-
│ Success message │ │
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Key Characteristics
|
|
50
|
-
|
|
51
|
-
| Characteristic | Description | Example |
|
|
52
|
-
| ---------------- | ---------------------------- | ---------------------- |
|
|
53
|
-
| **Push-Based** | External system initiates | SFCC pushes order |
|
|
54
|
-
| **Event-Driven** | Triggered by specific events | Order created, shipped |
|
|
55
|
-
| **Synchronous** | Caller waits for response | Return 200 OK or 500 |
|
|
56
|
-
| **Real-Time** | Immediate processing | < 5 second response |
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## Webhook Types and Patterns
|
|
61
|
-
|
|
62
|
-
### By Input Format
|
|
63
|
-
|
|
64
|
-
| Type | Content-Type | Example Source | SDK Parser |
|
|
65
|
-
| ------------- | ----------------------------------- | --------------- | ------------------ |
|
|
66
|
-
| **JSON** | `application/json` | Shopify, Stripe | Native JSON.parse |
|
|
67
|
-
| **XML** | `application/xml` | SFCC, Radial | `XMLParserService` |
|
|
68
|
-
| **Form Data** | `application/x-www-form-urlencoded` | Older systems | URLSearchParams |
|
|
69
|
-
| **Multipart** | `multipart/form-data` | File uploads | Not supported |
|
|
70
|
-
|
|
71
|
-
### By Response Format
|
|
72
|
-
|
|
73
|
-
| Type | Content-Type | Use Case | Pattern |
|
|
74
|
-
| -------------- | ------------------ | --------------- | --------------- |
|
|
75
|
-
| **JSON** | `application/json` | API responses | Default |
|
|
76
|
-
| **XML** | `application/xml` | SOAP/EDI | Custom Response |
|
|
77
|
-
| **HTML** | `text/html` | Browser display | Custom Response |
|
|
78
|
-
| **Plain Text** | `text/plain` | Simple status | Custom Response |
|
|
79
|
-
|
|
80
|
-
### By Processing Pattern
|
|
81
|
-
|
|
82
|
-
| Pattern | Latency | Complexity | Example |
|
|
83
|
-
| ------------------- | ------------------ | ---------- | ------------------------------ |
|
|
84
|
-
| **Immediate** | < 2 sec | Low | Status update |
|
|
85
|
-
| **Transformation** | 2-5 sec | Medium | XML → GraphQL |
|
|
86
|
-
| **Lookup & Create** | 3-10 sec | High | Customer lookup + order create |
|
|
87
|
-
| **Async** | Return immediately | High | Queue for later processing |
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## SDK Webhook Components
|
|
92
|
-
|
|
93
|
-
### Component 1: `XMLParserService`
|
|
94
|
-
|
|
95
|
-
**Purpose**: Parse XML payloads (SFCC, Radial, SOAP)
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
import { XMLParserService } from '@fluentcommerce/fc-connect-sdk';
|
|
99
|
-
|
|
100
|
-
const parser = new XMLParserService({
|
|
101
|
-
ignoreAttributes: false, // Keep XML attributes
|
|
102
|
-
attributeNamePrefix: '@', // Prefix for attributes
|
|
103
|
-
textNodeName: '_', // Text content key
|
|
104
|
-
parseAttributeValue: true, // Parse attribute values
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Parse XML string
|
|
108
|
-
const xmlData = await parser.parse(xmlString);
|
|
109
|
-
|
|
110
|
-
// Parse with path resolution
|
|
111
|
-
const value = await parser.parseWithPath(xmlString, 'order.items.item[0].sku');
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Component 2: `GraphQLMutationMapper`
|
|
115
|
-
|
|
116
|
-
**Purpose**: Transform XML/JSON to GraphQL mutations
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
import { GraphQLMutationMapper } from '@fluentcommerce/fc-connect-sdk';
|
|
120
|
-
|
|
121
|
-
const mapper = new GraphQLMutationMapper(mappingConfig, logger, { fluentClient: client });
|
|
122
|
-
|
|
123
|
-
// Process with nodes (extract nested/escaped XML)
|
|
124
|
-
const result = await mapper.mapWithNodes(sourceData, customResolvers, context);
|
|
125
|
-
|
|
126
|
-
// Check success (errors are returned, not thrown)
|
|
127
|
-
if (!result.success) {
|
|
128
|
-
console.error('Mapping failed:', result.errors);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Execute (query is auto-generated in result)
|
|
133
|
-
const data = await client.graphql({
|
|
134
|
-
query: result.query,
|
|
135
|
-
variables: result.variables // ✅ Use variables (wrapped if fields pattern)
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
**Key features**:
|
|
140
|
-
|
|
141
|
-
- **Nodes**: Extract nested XML before mapping
|
|
142
|
-
- **Path Resolution**: Navigate complex XML/JSON structures
|
|
143
|
-
- **Custom Resolvers**: Apply business logic (async supported)
|
|
144
|
-
- **Validation**: Required fields and types
|
|
145
|
-
|
|
146
|
-
### Component 3: `createClient()`
|
|
147
|
-
|
|
148
|
-
**Purpose**: Create authenticated Fluent client
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
import { createClient } from '@fluentcommerce/fc-connect-sdk';
|
|
152
|
-
|
|
153
|
-
// Versori platform
|
|
154
|
-
const client = await createClient({
|
|
155
|
-
connection: connections.fluent_commerce,
|
|
156
|
-
logger: log,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Standalone
|
|
160
|
-
const client = await createClient({
|
|
161
|
-
config: {
|
|
162
|
-
baseUrl: 'https://api.fluentcommerce.com',
|
|
163
|
-
clientId: process.env.FLUENT_CLIENT_ID,
|
|
164
|
-
clientSecret: process.env.FLUENT_CLIENT_SECRET,
|
|
165
|
-
retailerId: process.env.FLUENT_RETAILER_ID,
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Component 4: Versori Webhook Utilities
|
|
171
|
-
|
|
172
|
-
**Available in Versori platform**:
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
import { webhook, fn, http } from '@versori/run';
|
|
176
|
-
|
|
177
|
-
// Create webhook endpoint
|
|
178
|
-
export const orderWebhook = webhook('order-create', async ctx => {
|
|
179
|
-
const orderData = ctx.request.body;
|
|
180
|
-
// Process order
|
|
181
|
-
return { success: true };
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// With custom response handlers
|
|
185
|
-
export const xmlWebhook = webhook('xml-endpoint', {
|
|
186
|
-
response: {
|
|
187
|
-
mode: 'sync',
|
|
188
|
-
onSuccess: ctx =>
|
|
189
|
-
new Response(ctx.data, {
|
|
190
|
-
status: 200,
|
|
191
|
-
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
192
|
-
}),
|
|
193
|
-
},
|
|
194
|
-
}).then(
|
|
195
|
-
fn('process', async ctx => {
|
|
196
|
-
// Process data
|
|
197
|
-
return '<response>Success</response>';
|
|
198
|
-
})
|
|
199
|
-
);
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## Pattern 1: JSON Request/Response
|
|
205
|
-
|
|
206
|
-
### Use Case
|
|
207
|
-
|
|
208
|
-
**Modern APIs** (Shopify, Stripe, custom REST APIs) sending JSON webhooks.
|
|
209
|
-
|
|
210
|
-
### Implementation
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
/**
|
|
214
|
-
* JSON Webhook: Shopify Order → Fluent Order
|
|
215
|
-
*
|
|
216
|
-
* Input: application/json
|
|
217
|
-
* Output: application/json
|
|
218
|
-
*/
|
|
219
|
-
|
|
220
|
-
import { createClient, UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
|
|
221
|
-
|
|
222
|
-
export default async function shopifyOrderWebhook(activation: any, log: any, connections: any) {
|
|
223
|
-
try {
|
|
224
|
-
// Step 1: Extract JSON payload (Versori auto-parses)
|
|
225
|
-
const shopifyOrder = activation.body;
|
|
226
|
-
|
|
227
|
-
log.info('Received Shopify order', {
|
|
228
|
-
orderId: shopifyOrder.id,
|
|
229
|
-
orderNumber: shopifyOrder.order_number,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Step 2: Validate payload
|
|
233
|
-
if (!shopifyOrder.id || !shopifyOrder.line_items) {
|
|
234
|
-
throw new Error('Invalid Shopify order payload');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Step 3: Create Fluent client
|
|
238
|
-
const client = await createClient({
|
|
239
|
-
connection: connections.fluent_commerce,
|
|
240
|
-
logger: log,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Step 4: Map Shopify → Fluent
|
|
244
|
-
const mapper = new UniversalMapper(
|
|
245
|
-
{
|
|
246
|
-
fields: {
|
|
247
|
-
ref: { source: 'order_number', required: true },
|
|
248
|
-
type: { value: 'HD' },
|
|
249
|
-
totalPrice: { source: 'total_price', resolver: 'sdk.parseFloat' },
|
|
250
|
-
currency: { source: 'currency' },
|
|
251
|
-
'retailer.id': { value: '2' },
|
|
252
|
-
'customer.id': { resolver: 'custom.lookupCustomer' },
|
|
253
|
-
'items[]': {
|
|
254
|
-
source: 'line_items',
|
|
255
|
-
fields: {
|
|
256
|
-
ref: { source: '$.id', resolver: 'sdk.toString' },
|
|
257
|
-
productRef: { source: '$.sku', required: true },
|
|
258
|
-
quantity: { source: '$.quantity', resolver: 'sdk.parseInt' },
|
|
259
|
-
price: { source: '$.price', resolver: 'sdk.parseFloat' },
|
|
260
|
-
currency: { source: '^.currency' },
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
customResolvers: {
|
|
267
|
-
'custom.lookupCustomer': async (value, data, config, helpers) => {
|
|
268
|
-
const email = data.customer?.email;
|
|
269
|
-
if (!email) return null;
|
|
270
|
-
|
|
271
|
-
// Query for customer
|
|
272
|
-
const result = await helpers.fluentClient.graphql({
|
|
273
|
-
query: `query GetCustomer($email: String) {
|
|
274
|
-
customers(primaryEmail: $email, first: 1) {
|
|
275
|
-
edges { node { id } }
|
|
276
|
-
}
|
|
277
|
-
}`,
|
|
278
|
-
variables: { email },
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
return result?.customers?.edges?.[0]?.node?.id || null;
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
}
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
// map() returns GraphQLPayload { query, variables } - ready to execute!
|
|
288
|
-
const mapResult = await mapper.map(shopifyOrder, {
|
|
289
|
-
fluentClient: client,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// Step 5: Create order in Fluent (mapResult already has query + variables)
|
|
293
|
-
const order = await client.graphql(mapResult);
|
|
294
|
-
|
|
295
|
-
log.info('Order created in Fluent', {
|
|
296
|
-
fluentOrderId: order.id,
|
|
297
|
-
fluentOrderRef: order.ref,
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// Step 6: Return JSON response
|
|
301
|
-
return {
|
|
302
|
-
status: 200,
|
|
303
|
-
body: {
|
|
304
|
-
success: true,
|
|
305
|
-
shopifyOrderId: shopifyOrder.id,
|
|
306
|
-
fluentOrderId: order.id,
|
|
307
|
-
fluentOrderRef: order.ref,
|
|
308
|
-
},
|
|
309
|
-
};
|
|
310
|
-
} catch (error: any) {
|
|
311
|
-
log.error('Shopify webhook failed', error);
|
|
312
|
-
|
|
313
|
-
return {
|
|
314
|
-
status: 500,
|
|
315
|
-
body: {
|
|
316
|
-
success: false,
|
|
317
|
-
error: error.message,
|
|
318
|
-
},
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### Example Payload
|
|
325
|
-
|
|
326
|
-
**Input** (Shopify webhook):
|
|
327
|
-
|
|
328
|
-
```json
|
|
329
|
-
{
|
|
330
|
-
"id": 12345678,
|
|
331
|
-
"order_number": "SO-1001",
|
|
332
|
-
"total_price": "159.98",
|
|
333
|
-
"currency": "USD",
|
|
334
|
-
"customer": {
|
|
335
|
-
"email": "customer@example.com"
|
|
336
|
-
},
|
|
337
|
-
"line_items": [
|
|
338
|
-
{
|
|
339
|
-
"id": 111,
|
|
340
|
-
"sku": "SKU-001",
|
|
341
|
-
"quantity": 2,
|
|
342
|
-
"price": "79.99"
|
|
343
|
-
}
|
|
344
|
-
]
|
|
345
|
-
}
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
**Output**:
|
|
349
|
-
|
|
350
|
-
```json
|
|
351
|
-
{
|
|
352
|
-
"success": true,
|
|
353
|
-
"shopifyOrderId": 12345678,
|
|
354
|
-
"fluentOrderId": "1234",
|
|
355
|
-
"fluentOrderRef": "SO-1001"
|
|
356
|
-
}
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
---
|
|
360
|
-
|
|
361
|
-
## Pattern 2: XML Request/Response
|
|
362
|
-
|
|
363
|
-
### Use Case
|
|
364
|
-
|
|
365
|
-
**Older systems** (SFCC, Radial, SOAP) sending XML webhooks and expecting XML responses.
|
|
366
|
-
|
|
367
|
-
### Implementation
|
|
368
|
-
|
|
369
|
-
See complete implementation: [XML Order Ingestion](../../../01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md)
|
|
370
|
-
|
|
371
|
-
### Quick Example
|
|
372
|
-
|
|
373
|
-
```typescript
|
|
374
|
-
/**
|
|
375
|
-
* XML Webhook: SFCC Order → Fluent Order → XML Response
|
|
376
|
-
*
|
|
377
|
-
* Input: application/xml
|
|
378
|
-
* Output: application/xml
|
|
379
|
-
*/
|
|
380
|
-
|
|
381
|
-
import {
|
|
382
|
-
createClient,
|
|
383
|
-
GraphQLMutationMapper,
|
|
384
|
-
XMLParserService,
|
|
385
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
386
|
-
import { webhook, fn } from '@versori/run';
|
|
387
|
-
import { XMLBuilder } from 'fast-xml-parser';
|
|
388
|
-
|
|
389
|
-
export const sfccOrderWebhook = webhook('sfcc-order', {
|
|
390
|
-
response: {
|
|
391
|
-
mode: 'sync',
|
|
392
|
-
// CRITICAL: Custom response handler for XML (not JSON)
|
|
393
|
-
onSuccess: ctx =>
|
|
394
|
-
new Response(ctx.data, {
|
|
395
|
-
status: 200,
|
|
396
|
-
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
397
|
-
}),
|
|
398
|
-
onError: ctx =>
|
|
399
|
-
new Response(ctx.data, {
|
|
400
|
-
status: 500,
|
|
401
|
-
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
402
|
-
}),
|
|
403
|
-
},
|
|
404
|
-
})
|
|
405
|
-
.then(
|
|
406
|
-
fn('process-order', async ctx => {
|
|
407
|
-
const { body } = ctx.request;
|
|
408
|
-
const { log, connections } = ctx;
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
// Parse XML (Versori pre-parses, but you can re-parse if needed)
|
|
412
|
-
const parser = new XMLParserService();
|
|
413
|
-
const orderData = typeof body === 'string' ? await parser.parse(body) : body;
|
|
414
|
-
|
|
415
|
-
// Create client
|
|
416
|
-
const client = await createClient({
|
|
417
|
-
connection: connections.fluent_commerce,
|
|
418
|
-
logger: log,
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// Map XML → GraphQL
|
|
422
|
-
const mapper = new GraphQLMutationMapper(mappingConfig, log, { fluentClient: client });
|
|
423
|
-
const result = await mapper.mapWithNodes(orderData, customResolvers, {
|
|
424
|
-
fluentClient: client,
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
if (!result.success) {
|
|
428
|
-
throw new Error(`Mapping failed: ${result.errors?.join(', ')}`);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Execute mutation (query is auto-generated in result)
|
|
432
|
-
const order = await client.graphql({
|
|
433
|
-
query: result.query,
|
|
434
|
-
variables: result.variables, // ✅ Use variables (wrapped if fields pattern)
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
// Build XML response
|
|
438
|
-
const builder = new XMLBuilder({
|
|
439
|
-
ignoreAttributes: false,
|
|
440
|
-
attributeNamePrefix: '@',
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
const xmlResponse = builder.build({
|
|
444
|
-
OrderResponse: {
|
|
445
|
-
'@status': 'success',
|
|
446
|
-
OrderId: order.id,
|
|
447
|
-
OrderRef: order.ref,
|
|
448
|
-
Message: 'Order created successfully',
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
return `<?xml version="1.0" encoding="UTF-8"?>\n${xmlResponse}`;
|
|
453
|
-
} catch (error: any) {
|
|
454
|
-
log.error('SFCC order processing failed', error);
|
|
455
|
-
|
|
456
|
-
// Build XML error response
|
|
457
|
-
const builder = new XMLBuilder({ ignoreAttributes: false });
|
|
458
|
-
const xmlError = builder.build({
|
|
459
|
-
OrderResponse: {
|
|
460
|
-
'@status': 'error',
|
|
461
|
-
ErrorMessage: error.message,
|
|
462
|
-
},
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
throw new Error(`<?xml version="1.0" encoding="UTF-8"?>\n${xmlError}`);
|
|
466
|
-
}
|
|
467
|
-
})
|
|
468
|
-
)
|
|
469
|
-
.catch(({ data }) => {
|
|
470
|
-
// Return error XML (already formatted in catch block)
|
|
471
|
-
return data;
|
|
472
|
-
});
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
**Why custom response handler?**
|
|
476
|
-
|
|
477
|
-
Versori's default behavior JSON-encodes all responses. For XML, you must use `Response` objects with proper Content-Type.
|
|
478
|
-
|
|
479
|
-
See: [Versori Webhook Response Patterns](../../../04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md#critical-non-json-response-handlers-xml-html-csv)
|
|
480
|
-
|
|
481
|
-
---
|
|
482
|
-
|
|
483
|
-
## Pattern 3: Mixed Format (XML→JSON)
|
|
484
|
-
|
|
485
|
-
### Use Case
|
|
486
|
-
|
|
487
|
-
**Older system input, modern output**: Receive XML webhook, return JSON response.
|
|
488
|
-
|
|
489
|
-
### Implementation
|
|
490
|
-
|
|
491
|
-
```typescript
|
|
492
|
-
/**
|
|
493
|
-
* Mixed Format Webhook: XML Input → JSON Output
|
|
494
|
-
*
|
|
495
|
-
* Input: application/xml
|
|
496
|
-
* Output: application/json
|
|
497
|
-
*/
|
|
498
|
-
|
|
499
|
-
import {
|
|
500
|
-
createClient,
|
|
501
|
-
GraphQLMutationMapper,
|
|
502
|
-
XMLParserService,
|
|
503
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
504
|
-
|
|
505
|
-
export default async function mixedFormatWebhook(activation: any, log: any, connections: any) {
|
|
506
|
-
try {
|
|
507
|
-
// Parse XML input
|
|
508
|
-
const parser = new XMLParserService();
|
|
509
|
-
const xmlData =
|
|
510
|
-
typeof activation.body === 'string' ? await parser.parse(activation.body) : activation.body;
|
|
511
|
-
|
|
512
|
-
log.info('Parsed XML payload', { rootKeys: Object.keys(xmlData) });
|
|
513
|
-
|
|
514
|
-
// Create client
|
|
515
|
-
const client = await createClient({
|
|
516
|
-
connection: connections.fluent_commerce,
|
|
517
|
-
logger: log,
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
// Map XML → GraphQL
|
|
521
|
-
const mapper = new GraphQLMutationMapper(mappingConfig, log, { fluentClient: client });
|
|
522
|
-
const result = await mapper.mapWithNodes(xmlData, customResolvers, {
|
|
523
|
-
fluentClient: client,
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
if (!result.success) {
|
|
527
|
-
throw new Error(`Mapping failed: ${result.errors?.join(', ')}`);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Execute mutation (query is auto-generated in result)
|
|
531
|
-
const order = await client.graphql({
|
|
532
|
-
query: result.query,
|
|
533
|
-
variables: result.variables, // ✅ Use variables (wrapped if fields pattern)
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// Return JSON response (default Versori behavior)
|
|
537
|
-
return {
|
|
538
|
-
status: 200,
|
|
539
|
-
body: {
|
|
540
|
-
success: true,
|
|
541
|
-
orderId: order.id,
|
|
542
|
-
orderRef: order.ref,
|
|
543
|
-
message: 'Order created successfully',
|
|
544
|
-
},
|
|
545
|
-
};
|
|
546
|
-
} catch (error: any) {
|
|
547
|
-
log.error('Webhook processing failed', error);
|
|
548
|
-
|
|
549
|
-
return {
|
|
550
|
-
status: 500,
|
|
551
|
-
body: {
|
|
552
|
-
success: false,
|
|
553
|
-
error: error.message,
|
|
554
|
-
},
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
---
|
|
561
|
-
|
|
562
|
-
## Versori Webhook Response Handling
|
|
563
|
-
|
|
564
|
-
### Critical Pattern: Non-JSON Responses
|
|
565
|
-
|
|
566
|
-
**Problem**: Versori default handlers JSON-encode all responses.
|
|
567
|
-
|
|
568
|
-
**Solution**: Use custom `onSuccess`/`onError` handlers with `Response` objects.
|
|
569
|
-
|
|
570
|
-
### XML Response Pattern
|
|
571
|
-
|
|
572
|
-
```typescript
|
|
573
|
-
export const xmlEndpoint = webhook('xml', {
|
|
574
|
-
response: {
|
|
575
|
-
mode: 'sync',
|
|
576
|
-
onSuccess: ctx =>
|
|
577
|
-
new Response(ctx.data, {
|
|
578
|
-
status: 200,
|
|
579
|
-
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
580
|
-
}),
|
|
581
|
-
onError: ctx =>
|
|
582
|
-
new Response(ctx.data, {
|
|
583
|
-
status: 500,
|
|
584
|
-
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
585
|
-
}),
|
|
586
|
-
},
|
|
587
|
-
})
|
|
588
|
-
.then(fn('gen', () => '<?xml version="1.0"?><response>Success</response>'))
|
|
589
|
-
.catch(({ data }) => '<error>Failed</error>');
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
### HTML Response Pattern
|
|
593
|
-
|
|
594
|
-
```typescript
|
|
595
|
-
export const htmlEndpoint = webhook('html', {
|
|
596
|
-
response: {
|
|
597
|
-
mode: 'sync',
|
|
598
|
-
onSuccess: ctx =>
|
|
599
|
-
new Response(ctx.data, {
|
|
600
|
-
status: 200,
|
|
601
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
602
|
-
}),
|
|
603
|
-
},
|
|
604
|
-
}).then(fn('gen', () => '<!DOCTYPE html><html><body>OK</body></html>'));
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
### CSV Download Pattern
|
|
608
|
-
|
|
609
|
-
```typescript
|
|
610
|
-
export const csvEndpoint = webhook('csv', {
|
|
611
|
-
response: {
|
|
612
|
-
mode: 'sync',
|
|
613
|
-
onSuccess: ctx =>
|
|
614
|
-
new Response(ctx.data, {
|
|
615
|
-
status: 200,
|
|
616
|
-
headers: {
|
|
617
|
-
'Content-Type': 'text/csv; charset=utf-8',
|
|
618
|
-
'Content-Disposition': 'attachment; filename="data.csv"',
|
|
619
|
-
},
|
|
620
|
-
}),
|
|
621
|
-
},
|
|
622
|
-
}).then(fn('gen', () => 'ID,Name,Value\n1,Item,100'));
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
**Complete guide**: [Versori Webhook Response Patterns](../../../04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md#critical-non-json-response-handlers-xml-html-csv)
|
|
626
|
-
|
|
627
|
-
---
|
|
628
|
-
|
|
629
|
-
## Webhook Security
|
|
630
|
-
|
|
631
|
-
### Pattern 1: Fluent Commerce Webhook Validation
|
|
632
|
-
|
|
633
|
-
⚠️ **CRITICAL LIMITATION:** The SDK's `validateWebhook()` method **ONLY works with Fluent Commerce Rubix webhooks**. Do not use for Shopify, GitHub, Stripe, or other third-party webhooks. See Pattern 3 for those systems.
|
|
634
|
-
|
|
635
|
-
**Use FluentClient.validateWebhook()** for cryptographic signature validation of **Fluent Commerce webhooks only**.
|
|
636
|
-
|
|
637
|
-
The SDK provides built-in webhook validation using RSA-based algorithms (SHA512withRSA, MD5withRSA).
|
|
638
|
-
|
|
639
|
-
```typescript
|
|
640
|
-
import { createClient } from '@fluentcommerce/fc-connect-sdk';
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* SDK Webhook Validation - Fluent Commerce Only
|
|
644
|
-
*
|
|
645
|
-
* Supports: SHA512withRSA (fluent-signature), MD5withRSA (flex.signature)
|
|
646
|
-
* Does NOT support: Shopify, GitHub, Stripe, or other third-party webhooks
|
|
647
|
-
*/
|
|
648
|
-
export default async function secureFluentWebhook(activation: any, log: any, connections: any) {
|
|
649
|
-
try {
|
|
650
|
-
// Create client with public key
|
|
651
|
-
const client = await createClient({
|
|
652
|
-
connection: connections.fluent_commerce,
|
|
653
|
-
logger: log,
|
|
654
|
-
publicKey: process.env.FLUENT_WEBHOOK_PUBLIC_KEY, // Required for webhook validation
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
// Get raw body (CRITICAL: Must be raw string, not parsed JSON)
|
|
658
|
-
const rawBody = activation.rawBody || JSON.stringify(activation.body);
|
|
659
|
-
const payload = typeof activation.body === 'string' ? JSON.parse(activation.body) : activation.body;
|
|
660
|
-
|
|
661
|
-
// Extract signature from Fluent Commerce headers
|
|
662
|
-
const signature = activation.headers['fluent-signature'] || activation.headers['flex.signature'];
|
|
663
|
-
|
|
664
|
-
if (!signature) {
|
|
665
|
-
log.error('Missing Fluent Commerce signature header');
|
|
666
|
-
return { status: 401, body: { error: 'Unauthorized' } };
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Validate webhook signature using SDK (Fluent Commerce only)
|
|
670
|
-
const isValid = await client.validateWebhook(payload, signature, rawBody);
|
|
671
|
-
|
|
672
|
-
if (!isValid) {
|
|
673
|
-
log.error('Invalid Fluent Commerce webhook signature');
|
|
674
|
-
return { status: 401, body: { error: 'Unauthorized' } };
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
log.info('Fluent Commerce webhook signature validated successfully');
|
|
678
|
-
|
|
679
|
-
// Process webhook
|
|
680
|
-
const result = await processFluentWebhook(payload, client, log);
|
|
681
|
-
|
|
682
|
-
return {
|
|
683
|
-
status: 200,
|
|
684
|
-
body: {
|
|
685
|
-
success: true,
|
|
686
|
-
...result,
|
|
687
|
-
},
|
|
688
|
-
};
|
|
689
|
-
} catch (error: any) {
|
|
690
|
-
log.error('Webhook validation or processing failed', error);
|
|
691
|
-
|
|
692
|
-
return {
|
|
693
|
-
status: 500,
|
|
694
|
-
body: {
|
|
695
|
-
success: false,
|
|
696
|
-
error: error.message,
|
|
697
|
-
},
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
### Fluent Commerce Signature Headers
|
|
704
|
-
|
|
705
|
-
The SDK automatically detects the signature algorithm from headers:
|
|
706
|
-
|
|
707
|
-
| Header | Algorithm | Status |
|
|
708
|
-
|--------|-----------|--------|
|
|
709
|
-
| `fluent-signature` | SHA512withRSA | **Recommended** |
|
|
710
|
-
| `x-fluent-signature` | SHA512withRSA | Supported |
|
|
711
|
-
| `flex.signature` | MD5withRSA | **Older algorithm** |
|
|
712
|
-
| `flex-signature` | MD5withRSA | Supported |
|
|
713
|
-
|
|
714
|
-
```typescript
|
|
715
|
-
// Auto-detection (recommended)
|
|
716
|
-
const signature = headers['fluent-signature'] || headers['flex.signature'];
|
|
717
|
-
const isValid = await client.validateWebhook(payload, signature, rawBody);
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
### Pattern 2: IP Allowlist (All Webhook Types)
|
|
721
|
-
|
|
722
|
-
**Restrict webhook sources** to known IP addresses - works for Fluent Commerce, Shopify, GitHub, Stripe, etc.
|
|
723
|
-
|
|
724
|
-
```typescript
|
|
725
|
-
const ALLOWED_IPS = ['192.168.1.1', '10.0.0.0/8'];
|
|
726
|
-
|
|
727
|
-
function isIPAllowed(ip: string): boolean {
|
|
728
|
-
return ALLOWED_IPS.some(allowed => {
|
|
729
|
-
if (allowed.includes('/')) {
|
|
730
|
-
// CIDR range check (use ip-cidr library)
|
|
731
|
-
return true; // Simplified
|
|
732
|
-
}
|
|
733
|
-
return ip === allowed;
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Usage
|
|
738
|
-
export default async function ipRestrictedWebhook(activation: any, log: any) {
|
|
739
|
-
const clientIP = activation.headers['x-forwarded-for'] || activation.ip;
|
|
740
|
-
|
|
741
|
-
if (!isIPAllowed(clientIP)) {
|
|
742
|
-
log.warn('Blocked request from unauthorized IP', { ip: clientIP });
|
|
743
|
-
return { status: 403, body: { error: 'Forbidden' } };
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Process webhook
|
|
747
|
-
}
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
### Pattern 3: Third-Party Webhook Validation (Shopify, GitHub, Stripe)
|
|
751
|
-
|
|
752
|
-
⚠️ **REQUIRED FOR THIRD-PARTY WEBHOOKS:** The SDK's `validateWebhook()` does NOT support HMAC algorithms used by Shopify, GitHub, Stripe. You must implement manual HMAC validation for these systems.
|
|
753
|
-
|
|
754
|
-
#### Shopify Webhook Validation (HMAC-SHA256, base64)
|
|
755
|
-
|
|
756
|
-
```typescript
|
|
757
|
-
import crypto from 'crypto';
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* Validate Shopify webhook signature
|
|
761
|
-
* Algorithm: HMAC-SHA256 with base64 encoding
|
|
762
|
-
*/
|
|
763
|
-
function validateShopifyWebhook(rawBody: string, signature: string, secret: string): boolean {
|
|
764
|
-
const hash = crypto
|
|
765
|
-
.createHmac('sha256', secret)
|
|
766
|
-
.update(rawBody, 'utf8')
|
|
767
|
-
.digest('base64');
|
|
768
|
-
|
|
769
|
-
return hash === signature;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// Usage
|
|
773
|
-
export default async function shopifyWebhook(activation: any, log: any) {
|
|
774
|
-
const rawBody = activation.rawBody || JSON.stringify(activation.body);
|
|
775
|
-
const signature = activation.headers['x-shopify-hmac-sha256'];
|
|
776
|
-
const secret = process.env.SHOPIFY_WEBHOOK_SECRET!;
|
|
777
|
-
|
|
778
|
-
if (!validateShopifyWebhook(rawBody, signature, secret)) {
|
|
779
|
-
log.error('Invalid Shopify webhook signature');
|
|
780
|
-
return { status: 401, body: { error: 'Unauthorized' } };
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// Process webhook
|
|
784
|
-
const order = await processShopifyOrder(activation.body);
|
|
785
|
-
return { status: 200, body: { success: true, orderId: order.id } };
|
|
786
|
-
}
|
|
787
|
-
```
|
|
788
|
-
|
|
789
|
-
#### GitHub Webhook Validation (HMAC-SHA256, hex with prefix)
|
|
790
|
-
|
|
791
|
-
```typescript
|
|
792
|
-
import crypto from 'crypto';
|
|
793
|
-
|
|
794
|
-
/**
|
|
795
|
-
* Validate GitHub webhook signature
|
|
796
|
-
* Algorithm: HMAC-SHA256 with hex encoding and sha256= prefix
|
|
797
|
-
*/
|
|
798
|
-
function validateGitHubWebhook(rawBody: string, signature: string, secret: string): boolean {
|
|
799
|
-
const hash = crypto
|
|
800
|
-
.createHmac('sha256', secret)
|
|
801
|
-
.update(rawBody, 'utf8')
|
|
802
|
-
.digest('hex');
|
|
803
|
-
|
|
804
|
-
const expected = `sha256=${hash}`;
|
|
805
|
-
return expected === signature;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Usage
|
|
809
|
-
export default async function githubWebhook(activation: any, log: any) {
|
|
810
|
-
const rawBody = activation.rawBody || JSON.stringify(activation.body);
|
|
811
|
-
const signature = activation.headers['x-hub-signature-256'];
|
|
812
|
-
const secret = process.env.GITHUB_WEBHOOK_SECRET!;
|
|
813
|
-
|
|
814
|
-
if (!validateGitHubWebhook(rawBody, signature, secret)) {
|
|
815
|
-
log.error('Invalid GitHub webhook signature');
|
|
816
|
-
return { status: 401, body: { error: 'Unauthorized' } };
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// Process webhook
|
|
820
|
-
const result = await processGitHubEvent(activation.body);
|
|
821
|
-
return { status: 200, body: { success: true } };
|
|
822
|
-
}
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
#### Stripe Webhook Validation (HMAC-SHA256 with timestamp)
|
|
826
|
-
|
|
827
|
-
```typescript
|
|
828
|
-
import crypto from 'crypto';
|
|
829
|
-
|
|
830
|
-
/**
|
|
831
|
-
* Validate Stripe webhook signature
|
|
832
|
-
* Algorithm: HMAC-SHA256 with timestamp verification to prevent replay attacks
|
|
833
|
-
*/
|
|
834
|
-
function validateStripeWebhook(
|
|
835
|
-
rawBody: string,
|
|
836
|
-
signature: string,
|
|
837
|
-
secret: string,
|
|
838
|
-
tolerance: number = 300 // 5 minutes
|
|
839
|
-
): boolean {
|
|
840
|
-
// Parse signature header: t=timestamp,v1=signature
|
|
841
|
-
const parts = signature.split(',');
|
|
842
|
-
const timestamp = parts.find(p => p.startsWith('t='))?.split('=')[1];
|
|
843
|
-
const sig = parts.find(p => p.startsWith('v1='))?.split('=')[1];
|
|
844
|
-
|
|
845
|
-
if (!timestamp || !sig) return false;
|
|
846
|
-
|
|
847
|
-
// Check timestamp tolerance (prevent replay attacks)
|
|
848
|
-
const now = Math.floor(Date.now() / 1000);
|
|
849
|
-
if (Math.abs(now - parseInt(timestamp)) > tolerance) {
|
|
850
|
-
return false;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Verify signature
|
|
854
|
-
const payload = `${timestamp}.${rawBody}`;
|
|
855
|
-
const hash = crypto
|
|
856
|
-
.createHmac('sha256', secret)
|
|
857
|
-
.update(payload, 'utf8')
|
|
858
|
-
.digest('hex');
|
|
859
|
-
|
|
860
|
-
return hash === sig;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
// Usage
|
|
864
|
-
export default async function stripeWebhook(activation: any, log: any) {
|
|
865
|
-
const rawBody = activation.rawBody || JSON.stringify(activation.body);
|
|
866
|
-
const signature = activation.headers['stripe-signature'];
|
|
867
|
-
const secret = process.env.STRIPE_WEBHOOK_SECRET!;
|
|
868
|
-
|
|
869
|
-
if (!validateStripeWebhook(rawBody, signature, secret)) {
|
|
870
|
-
log.error('Invalid Stripe webhook signature');
|
|
871
|
-
return { status: 401, body: { error: 'Unauthorized' } };
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Process webhook
|
|
875
|
-
const result = await processStripeEvent(activation.body);
|
|
876
|
-
return { status: 200, body: { success: true } };
|
|
877
|
-
}
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
#### Third-Party Webhook Algorithms
|
|
881
|
-
|
|
882
|
-
| Provider | Algorithm | Encoding | Header | Notes |
|
|
883
|
-
|----------|-----------|----------|--------|-------|
|
|
884
|
-
| **Shopify** | HMAC-SHA256 | base64 | `x-shopify-hmac-sha256` | Simple HMAC |
|
|
885
|
-
| **GitHub** | HMAC-SHA256 | hex | `x-hub-signature-256` | Prefixed with `sha256=` |
|
|
886
|
-
| **Stripe** | HMAC-SHA256 | hex | `stripe-signature` | Includes timestamp (replay protection) |
|
|
887
|
-
| **PayPal** | HMAC-SHA256 | base64 | `paypal-transmission-sig` | Multiple verification params |
|
|
888
|
-
|
|
889
|
-
### Error Handling for Validation
|
|
890
|
-
|
|
891
|
-
```typescript
|
|
892
|
-
// Fluent Commerce webhook validation error handling
|
|
893
|
-
try {
|
|
894
|
-
const isValid = await client.validateWebhook(payload, signature, rawBody);
|
|
895
|
-
|
|
896
|
-
if (!isValid) {
|
|
897
|
-
log.error('Fluent Commerce webhook signature validation failed', {
|
|
898
|
-
header: signature ? 'present' : 'missing',
|
|
899
|
-
algorithm: signature?.includes('fluent-signature') ? 'SHA512withRSA' : 'MD5withRSA',
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
return {
|
|
903
|
-
status: 401,
|
|
904
|
-
body: {
|
|
905
|
-
error: 'Unauthorized',
|
|
906
|
-
message: 'Invalid webhook signature',
|
|
907
|
-
},
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// Process webhook
|
|
912
|
-
} catch (error: any) {
|
|
913
|
-
log.error('Webhook validation error', error);
|
|
914
|
-
|
|
915
|
-
return {
|
|
916
|
-
status: 500,
|
|
917
|
-
body: {
|
|
918
|
-
error: 'Internal error',
|
|
919
|
-
message: error.message,
|
|
920
|
-
},
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// Third-party webhook validation error handling (Shopify example)
|
|
925
|
-
try {
|
|
926
|
-
const signature = headers['x-shopify-hmac-sha256'];
|
|
927
|
-
const secret = process.env.SHOPIFY_WEBHOOK_SECRET;
|
|
928
|
-
|
|
929
|
-
if (!signature || !secret) {
|
|
930
|
-
log.error('Missing Shopify webhook credentials');
|
|
931
|
-
return { status: 401, body: { error: 'Unauthorized' } };
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const isValid = validateShopifyWebhook(rawBody, signature, secret);
|
|
935
|
-
|
|
936
|
-
if (!isValid) {
|
|
937
|
-
log.error('Shopify webhook signature validation failed');
|
|
938
|
-
return { status: 401, body: { error: 'Unauthorized' } };
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
// Process webhook
|
|
942
|
-
} catch (error: any) {
|
|
943
|
-
log.error('Shopify webhook validation error', error);
|
|
944
|
-
return { status: 500, body: { error: 'Internal error' } };
|
|
945
|
-
}
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
### Pattern 5: API Key Authentication
|
|
950
|
-
|
|
951
|
-
**Require API key** in headers (alternative to signature validation).
|
|
952
|
-
|
|
953
|
-
```typescript
|
|
954
|
-
export default async function apiKeyWebhook(activation: any, log: any) {
|
|
955
|
-
const apiKey = activation.headers['x-api-key'];
|
|
956
|
-
const expectedKey = process.env.WEBHOOK_API_KEY!;
|
|
957
|
-
|
|
958
|
-
if (!apiKey || apiKey !== expectedKey) {
|
|
959
|
-
log.error('Missing or invalid API key');
|
|
960
|
-
return { status: 401, body: { error: 'Unauthorized' } };
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// Process webhook
|
|
964
|
-
}
|
|
965
|
-
```
|
|
966
|
-
|
|
967
|
-
---
|
|
968
|
-
|
|
969
|
-
## Error Handling and Retry Logic
|
|
970
|
-
|
|
971
|
-
### HTTP Status Codes
|
|
972
|
-
|
|
973
|
-
| Status | Meaning | Retry? | Example |
|
|
974
|
-
| ------- | -------------------------- | ------ | ------------------------- |
|
|
975
|
-
| **200** | Success | No | Order created |
|
|
976
|
-
| **400** | Bad Request (client error) | No | Invalid payload |
|
|
977
|
-
| **401** | Unauthorized | No | Missing/invalid signature |
|
|
978
|
-
| **422** | Unprocessable Entity | No | Validation failed |
|
|
979
|
-
| **429** | Rate Limit | Yes | Too many requests |
|
|
980
|
-
| **500** | Server Error | Yes | Fluent API down |
|
|
981
|
-
| **503** | Service Unavailable | Yes | Temporary outage |
|
|
982
|
-
|
|
983
|
-
### Error Response Pattern
|
|
984
|
-
|
|
985
|
-
```typescript
|
|
986
|
-
try {
|
|
987
|
-
// Process webhook
|
|
988
|
-
|
|
989
|
-
return {
|
|
990
|
-
status: 200,
|
|
991
|
-
body: {
|
|
992
|
-
success: true,
|
|
993
|
-
orderId: order.id,
|
|
994
|
-
},
|
|
995
|
-
};
|
|
996
|
-
} catch (error: any) {
|
|
997
|
-
// Categorize error
|
|
998
|
-
if (error.message.includes('required field')) {
|
|
999
|
-
// Validation error - don't retry
|
|
1000
|
-
return {
|
|
1001
|
-
status: 400,
|
|
1002
|
-
body: {
|
|
1003
|
-
success: false,
|
|
1004
|
-
error: 'Validation failed',
|
|
1005
|
-
details: error.message,
|
|
1006
|
-
},
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
if (error.message.includes('rate limit')) {
|
|
1011
|
-
// Rate limit - retry after delay
|
|
1012
|
-
return {
|
|
1013
|
-
status: 429,
|
|
1014
|
-
body: {
|
|
1015
|
-
success: false,
|
|
1016
|
-
error: 'Rate limit exceeded',
|
|
1017
|
-
retryAfter: 60,
|
|
1018
|
-
},
|
|
1019
|
-
};
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// Unknown error - retryable
|
|
1023
|
-
return {
|
|
1024
|
-
status: 500,
|
|
1025
|
-
body: {
|
|
1026
|
-
success: false,
|
|
1027
|
-
error: 'Internal server error',
|
|
1028
|
-
retryable: true,
|
|
1029
|
-
},
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
```
|
|
1033
|
-
|
|
1034
|
-
### Idempotency Pattern
|
|
1035
|
-
|
|
1036
|
-
**Prevent duplicate processing** of same webhook.
|
|
1037
|
-
|
|
1038
|
-
```typescript
|
|
1039
|
-
import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1040
|
-
|
|
1041
|
-
export default async function idempotentWebhook(activation: any, log: any) {
|
|
1042
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
1043
|
-
const stateService = new StateService(logger);
|
|
1044
|
-
|
|
1045
|
-
// Use webhook ID or order ID as idempotency key
|
|
1046
|
-
const idempotencyKey = `webhook:${activation.body.id}`;
|
|
1047
|
-
|
|
1048
|
-
// Check if already processed
|
|
1049
|
-
const processed = await stateService.getState(idempotencyKey);
|
|
1050
|
-
|
|
1051
|
-
if (processed) {
|
|
1052
|
-
log.info('Webhook already processed', { id: activation.body.id });
|
|
1053
|
-
return {
|
|
1054
|
-
status: 200,
|
|
1055
|
-
body: {
|
|
1056
|
-
success: true,
|
|
1057
|
-
message: 'Already processed',
|
|
1058
|
-
orderId: processed.orderId,
|
|
1059
|
-
},
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
// Process webhook
|
|
1064
|
-
const order = await processOrder(activation.body);
|
|
1065
|
-
|
|
1066
|
-
// Mark as processed
|
|
1067
|
-
await stateService.setState(idempotencyKey, {
|
|
1068
|
-
orderId: order.id,
|
|
1069
|
-
processedAt: new Date().toISOString(),
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
return {
|
|
1073
|
-
status: 200,
|
|
1074
|
-
body: {
|
|
1075
|
-
success: true,
|
|
1076
|
-
orderId: order.id,
|
|
1077
|
-
},
|
|
1078
|
-
};
|
|
1079
|
-
}
|
|
1080
|
-
```
|
|
1081
|
-
|
|
1082
|
-
---
|
|
1083
|
-
|
|
1084
|
-
## Complete Examples
|
|
1085
|
-
|
|
1086
|
-
### Example 1: Shopify Order Webhook (JSON)
|
|
1087
|
-
|
|
1088
|
-
Complete production-ready Shopify integration with:
|
|
1089
|
-
|
|
1090
|
-
- JSON parsing
|
|
1091
|
-
- Customer lookup/create
|
|
1092
|
-
- Order creation
|
|
1093
|
-
- Error handling
|
|
1094
|
-
|
|
1095
|
-
See: [Common Patterns](../examples/common-patterns.ts)
|
|
1096
|
-
|
|
1097
|
-
### Example 2: SFCC Order Webhook (XML)
|
|
1098
|
-
|
|
1099
|
-
Complete production-ready SFCC integration with:
|
|
1100
|
-
|
|
1101
|
-
- XML parsing with nodes
|
|
1102
|
-
- GraphQL mutation mapping
|
|
1103
|
-
- Custom resolvers
|
|
1104
|
-
- XML response formatting
|
|
1105
|
-
|
|
1106
|
-
See: [XML Order Ingestion](../../../01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md)
|
|
1107
|
-
|
|
1108
|
-
### Example 3: Inventory Webhook (JSON)
|
|
1109
|
-
|
|
1110
|
-
Simple inventory update webhook:
|
|
1111
|
-
|
|
1112
|
-
- Single SKU update
|
|
1113
|
-
- Direct GraphQL mutation
|
|
1114
|
-
- Fast response (< 2 sec)
|
|
1115
|
-
|
|
1116
|
-
```typescript
|
|
1117
|
-
import { createClient } from '@fluentcommerce/fc-connect-sdk';
|
|
1118
|
-
|
|
1119
|
-
export default async function inventoryWebhook(activation: any, log: any, connections: any) {
|
|
1120
|
-
const { sku, location, quantity } = activation.body;
|
|
1121
|
-
|
|
1122
|
-
const client = await createClient({
|
|
1123
|
-
connection: connections.fluent_commerce,
|
|
1124
|
-
logger: log,
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
const result = await client.graphql({
|
|
1128
|
-
query: `
|
|
1129
|
-
mutation UpdateInventory($input: UpdateInventoryQuantityInput!) {
|
|
1130
|
-
updateInventoryQuantity(input: $input) {
|
|
1131
|
-
id
|
|
1132
|
-
ref
|
|
1133
|
-
onHand
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
`,
|
|
1137
|
-
variables: {
|
|
1138
|
-
input: {
|
|
1139
|
-
ref: `${sku}-${location}`,
|
|
1140
|
-
onHand: quantity,
|
|
1141
|
-
},
|
|
1142
|
-
},
|
|
1143
|
-
});
|
|
1144
|
-
|
|
1145
|
-
return {
|
|
1146
|
-
status: 200,
|
|
1147
|
-
body: {
|
|
1148
|
-
success: true,
|
|
1149
|
-
inventoryId: result.id,
|
|
1150
|
-
newQuantity: result.onHand,
|
|
1151
|
-
},
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
```
|
|
1155
|
-
|
|
1156
|
-
---
|
|
1157
|
-
|
|
1158
|
-
## Next Steps
|
|
1159
|
-
|
|
1160
|
-
Now that you understand webhook patterns, you're ready to learn comprehensive error handling and resilience strategies!
|
|
1161
|
-
|
|
1162
|
-
**Continue to:** [Module 5: Error Handling →](./integration-patterns-05-error-handling.md)
|
|
1163
|
-
|
|
1164
|
-
Or explore:
|
|
1165
|
-
|
|
1166
|
-
- [Complete Example: XML Order Webhook](../../../01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md)
|
|
1167
|
-
- [Versori Webhook Response Patterns](../../../04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md#critical-non-json-response-handlers-xml-html-csv)
|
|
1168
|
-
- [GraphQL Mutation Mapper API](../../../02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md)
|
|
1169
|
-
|
|
1170
|
-
---
|
|
1171
|
-
|
|
1172
|
-
## Additional Resources
|
|
1173
|
-
|
|
1174
|
-
- [Versori Webhook Documentation](https://docs.versori.com/)
|
|
1175
|
-
- [XMLParserService API Reference](../../../02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md)
|
|
1176
|
-
- [GraphQL Mutation Mapper Guide](../../../02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md)
|
|
1177
|
-
- [Webhook Security Best Practices](../../../02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md)
|
|
1178
|
-
|
|
1179
|
-
---
|
|
1180
|
-
|
|
1181
|
-
[← Back to Index](../integration-patterns-readme.md) | [Previous: Delta Sync →](./integration-patterns-03-delta-sync.md) | [Next: Error Handling →](./integration-patterns-05-error-handling.md)
|
|
1
|
+
# Module 4: Webhook Patterns
|
|
2
|
+
|
|
3
|
+
> **Learning Objective:** Master webhook integration patterns for event-driven architectures, including request parsing, response formatting, and error handling.
|
|
4
|
+
>
|
|
5
|
+
> **Level:** Intermediate
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [What are Webhooks?](#what-are-webhooks)
|
|
10
|
+
2. [Webhook Types and Patterns](#webhook-types-and-patterns)
|
|
11
|
+
3. [SDK Webhook Components](#sdk-webhook-components)
|
|
12
|
+
4. [Pattern 1: JSON Request/Response](#pattern-1-json-requestresponse)
|
|
13
|
+
5. [Pattern 2: XML Request/Response](#pattern-2-xml-requestresponse)
|
|
14
|
+
6. `Pattern 3: Mixed Format (XML→JSON)`
|
|
15
|
+
7. [Versori Webhook Response Handling](#versori-webhook-response-handling)
|
|
16
|
+
8. [Webhook Security](#webhook-security)
|
|
17
|
+
9. [Error Handling and Retry Logic](#error-handling-and-retry-logic)
|
|
18
|
+
10. [Complete Examples](#complete-examples)
|
|
19
|
+
11. [Next Steps](#next-steps)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## What are Webhooks?
|
|
24
|
+
|
|
25
|
+
**Webhooks** are HTTP endpoints that receive event notifications from external systems in real-time.
|
|
26
|
+
|
|
27
|
+
### Webhook Flow
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
External System Your Webhook Fluent Commerce
|
|
31
|
+
│ │ │
|
|
32
|
+
│ ① Event occurs │ │
|
|
33
|
+
│ (order placed) │ │
|
|
34
|
+
│ │ │
|
|
35
|
+
│ ② HTTP POST ────────>│ │
|
|
36
|
+
│ Payload (XML/JSON)│ │
|
|
37
|
+
│ │ │
|
|
38
|
+
│ │ ③ Parse payload │
|
|
39
|
+
│ │ ④ Transform data │
|
|
40
|
+
│ │ │
|
|
41
|
+
│ │ ⑤ GraphQL mutation ──>│
|
|
42
|
+
│ │ │
|
|
43
|
+
│ │<── ⑥ Response │
|
|
44
|
+
│ │ │
|
|
45
|
+
│<── ⑦ 200 OK │ │
|
|
46
|
+
│ Success message │ │
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Key Characteristics
|
|
50
|
+
|
|
51
|
+
| Characteristic | Description | Example |
|
|
52
|
+
| ---------------- | ---------------------------- | ---------------------- |
|
|
53
|
+
| **Push-Based** | External system initiates | SFCC pushes order |
|
|
54
|
+
| **Event-Driven** | Triggered by specific events | Order created, shipped |
|
|
55
|
+
| **Synchronous** | Caller waits for response | Return 200 OK or 500 |
|
|
56
|
+
| **Real-Time** | Immediate processing | < 5 second response |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Webhook Types and Patterns
|
|
61
|
+
|
|
62
|
+
### By Input Format
|
|
63
|
+
|
|
64
|
+
| Type | Content-Type | Example Source | SDK Parser |
|
|
65
|
+
| ------------- | ----------------------------------- | --------------- | ------------------ |
|
|
66
|
+
| **JSON** | `application/json` | Shopify, Stripe | Native JSON.parse |
|
|
67
|
+
| **XML** | `application/xml` | SFCC, Radial | `XMLParserService` |
|
|
68
|
+
| **Form Data** | `application/x-www-form-urlencoded` | Older systems | URLSearchParams |
|
|
69
|
+
| **Multipart** | `multipart/form-data` | File uploads | Not supported |
|
|
70
|
+
|
|
71
|
+
### By Response Format
|
|
72
|
+
|
|
73
|
+
| Type | Content-Type | Use Case | Pattern |
|
|
74
|
+
| -------------- | ------------------ | --------------- | --------------- |
|
|
75
|
+
| **JSON** | `application/json` | API responses | Default |
|
|
76
|
+
| **XML** | `application/xml` | SOAP/EDI | Custom Response |
|
|
77
|
+
| **HTML** | `text/html` | Browser display | Custom Response |
|
|
78
|
+
| **Plain Text** | `text/plain` | Simple status | Custom Response |
|
|
79
|
+
|
|
80
|
+
### By Processing Pattern
|
|
81
|
+
|
|
82
|
+
| Pattern | Latency | Complexity | Example |
|
|
83
|
+
| ------------------- | ------------------ | ---------- | ------------------------------ |
|
|
84
|
+
| **Immediate** | < 2 sec | Low | Status update |
|
|
85
|
+
| **Transformation** | 2-5 sec | Medium | XML → GraphQL |
|
|
86
|
+
| **Lookup & Create** | 3-10 sec | High | Customer lookup + order create |
|
|
87
|
+
| **Async** | Return immediately | High | Queue for later processing |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## SDK Webhook Components
|
|
92
|
+
|
|
93
|
+
### Component 1: `XMLParserService`
|
|
94
|
+
|
|
95
|
+
**Purpose**: Parse XML payloads (SFCC, Radial, SOAP)
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { XMLParserService } from '@fluentcommerce/fc-connect-sdk';
|
|
99
|
+
|
|
100
|
+
const parser = new XMLParserService({
|
|
101
|
+
ignoreAttributes: false, // Keep XML attributes
|
|
102
|
+
attributeNamePrefix: '@', // Prefix for attributes
|
|
103
|
+
textNodeName: '_', // Text content key
|
|
104
|
+
parseAttributeValue: true, // Parse attribute values
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Parse XML string
|
|
108
|
+
const xmlData = await parser.parse(xmlString);
|
|
109
|
+
|
|
110
|
+
// Parse with path resolution
|
|
111
|
+
const value = await parser.parseWithPath(xmlString, 'order.items.item[0].sku');
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Component 2: `GraphQLMutationMapper`
|
|
115
|
+
|
|
116
|
+
**Purpose**: Transform XML/JSON to GraphQL mutations
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { GraphQLMutationMapper } from '@fluentcommerce/fc-connect-sdk';
|
|
120
|
+
|
|
121
|
+
const mapper = new GraphQLMutationMapper(mappingConfig, logger, { fluentClient: client });
|
|
122
|
+
|
|
123
|
+
// Process with nodes (extract nested/escaped XML)
|
|
124
|
+
const result = await mapper.mapWithNodes(sourceData, customResolvers, context);
|
|
125
|
+
|
|
126
|
+
// Check success (errors are returned, not thrown)
|
|
127
|
+
if (!result.success) {
|
|
128
|
+
console.error('Mapping failed:', result.errors);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Execute (query is auto-generated in result)
|
|
133
|
+
const data = await client.graphql({
|
|
134
|
+
query: result.query,
|
|
135
|
+
variables: result.variables // ✅ Use variables (wrapped if fields pattern)
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Key features**:
|
|
140
|
+
|
|
141
|
+
- **Nodes**: Extract nested XML before mapping
|
|
142
|
+
- **Path Resolution**: Navigate complex XML/JSON structures
|
|
143
|
+
- **Custom Resolvers**: Apply business logic (async supported)
|
|
144
|
+
- **Validation**: Required fields and types
|
|
145
|
+
|
|
146
|
+
### Component 3: `createClient()`
|
|
147
|
+
|
|
148
|
+
**Purpose**: Create authenticated Fluent client
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { createClient } from '@fluentcommerce/fc-connect-sdk';
|
|
152
|
+
|
|
153
|
+
// Versori platform
|
|
154
|
+
const client = await createClient({
|
|
155
|
+
connection: connections.fluent_commerce,
|
|
156
|
+
logger: log,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Standalone
|
|
160
|
+
const client = await createClient({
|
|
161
|
+
config: {
|
|
162
|
+
baseUrl: 'https://api.fluentcommerce.com',
|
|
163
|
+
clientId: process.env.FLUENT_CLIENT_ID,
|
|
164
|
+
clientSecret: process.env.FLUENT_CLIENT_SECRET,
|
|
165
|
+
retailerId: process.env.FLUENT_RETAILER_ID,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Component 4: Versori Webhook Utilities
|
|
171
|
+
|
|
172
|
+
**Available in Versori platform**:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { webhook, fn, http } from '@versori/run';
|
|
176
|
+
|
|
177
|
+
// Create webhook endpoint
|
|
178
|
+
export const orderWebhook = webhook('order-create', async ctx => {
|
|
179
|
+
const orderData = ctx.request.body;
|
|
180
|
+
// Process order
|
|
181
|
+
return { success: true };
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// With custom response handlers
|
|
185
|
+
export const xmlWebhook = webhook('xml-endpoint', {
|
|
186
|
+
response: {
|
|
187
|
+
mode: 'sync',
|
|
188
|
+
onSuccess: ctx =>
|
|
189
|
+
new Response(ctx.data, {
|
|
190
|
+
status: 200,
|
|
191
|
+
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
192
|
+
}),
|
|
193
|
+
},
|
|
194
|
+
}).then(
|
|
195
|
+
fn('process', async ctx => {
|
|
196
|
+
// Process data
|
|
197
|
+
return '<response>Success</response>';
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Pattern 1: JSON Request/Response
|
|
205
|
+
|
|
206
|
+
### Use Case
|
|
207
|
+
|
|
208
|
+
**Modern APIs** (Shopify, Stripe, custom REST APIs) sending JSON webhooks.
|
|
209
|
+
|
|
210
|
+
### Implementation
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
/**
|
|
214
|
+
* JSON Webhook: Shopify Order → Fluent Order
|
|
215
|
+
*
|
|
216
|
+
* Input: application/json
|
|
217
|
+
* Output: application/json
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
import { createClient, UniversalMapper } from '@fluentcommerce/fc-connect-sdk';
|
|
221
|
+
|
|
222
|
+
export default async function shopifyOrderWebhook(activation: any, log: any, connections: any) {
|
|
223
|
+
try {
|
|
224
|
+
// Step 1: Extract JSON payload (Versori auto-parses)
|
|
225
|
+
const shopifyOrder = activation.body;
|
|
226
|
+
|
|
227
|
+
log.info('Received Shopify order', {
|
|
228
|
+
orderId: shopifyOrder.id,
|
|
229
|
+
orderNumber: shopifyOrder.order_number,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Step 2: Validate payload
|
|
233
|
+
if (!shopifyOrder.id || !shopifyOrder.line_items) {
|
|
234
|
+
throw new Error('Invalid Shopify order payload');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Step 3: Create Fluent client
|
|
238
|
+
const client = await createClient({
|
|
239
|
+
connection: connections.fluent_commerce,
|
|
240
|
+
logger: log,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Step 4: Map Shopify → Fluent
|
|
244
|
+
const mapper = new UniversalMapper(
|
|
245
|
+
{
|
|
246
|
+
fields: {
|
|
247
|
+
ref: { source: 'order_number', required: true },
|
|
248
|
+
type: { value: 'HD' },
|
|
249
|
+
totalPrice: { source: 'total_price', resolver: 'sdk.parseFloat' },
|
|
250
|
+
currency: { source: 'currency' },
|
|
251
|
+
'retailer.id': { value: '2' },
|
|
252
|
+
'customer.id': { resolver: 'custom.lookupCustomer' },
|
|
253
|
+
'items[]': {
|
|
254
|
+
source: 'line_items',
|
|
255
|
+
fields: {
|
|
256
|
+
ref: { source: '$.id', resolver: 'sdk.toString' },
|
|
257
|
+
productRef: { source: '$.sku', required: true },
|
|
258
|
+
quantity: { source: '$.quantity', resolver: 'sdk.parseInt' },
|
|
259
|
+
price: { source: '$.price', resolver: 'sdk.parseFloat' },
|
|
260
|
+
currency: { source: '^.currency' },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
customResolvers: {
|
|
267
|
+
'custom.lookupCustomer': async (value, data, config, helpers) => {
|
|
268
|
+
const email = data.customer?.email;
|
|
269
|
+
if (!email) return null;
|
|
270
|
+
|
|
271
|
+
// Query for customer
|
|
272
|
+
const result = await helpers.fluentClient.graphql({
|
|
273
|
+
query: `query GetCustomer($email: String) {
|
|
274
|
+
customers(primaryEmail: $email, first: 1) {
|
|
275
|
+
edges { node { id } }
|
|
276
|
+
}
|
|
277
|
+
}`,
|
|
278
|
+
variables: { email },
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return result?.customers?.edges?.[0]?.node?.id || null;
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// map() returns GraphQLPayload { query, variables } - ready to execute!
|
|
288
|
+
const mapResult = await mapper.map(shopifyOrder, {
|
|
289
|
+
fluentClient: client,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Step 5: Create order in Fluent (mapResult already has query + variables)
|
|
293
|
+
const order = await client.graphql(mapResult);
|
|
294
|
+
|
|
295
|
+
log.info('Order created in Fluent', {
|
|
296
|
+
fluentOrderId: order.id,
|
|
297
|
+
fluentOrderRef: order.ref,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Step 6: Return JSON response
|
|
301
|
+
return {
|
|
302
|
+
status: 200,
|
|
303
|
+
body: {
|
|
304
|
+
success: true,
|
|
305
|
+
shopifyOrderId: shopifyOrder.id,
|
|
306
|
+
fluentOrderId: order.id,
|
|
307
|
+
fluentOrderRef: order.ref,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
} catch (error: any) {
|
|
311
|
+
log.error('Shopify webhook failed', error);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
status: 500,
|
|
315
|
+
body: {
|
|
316
|
+
success: false,
|
|
317
|
+
error: error.message,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Example Payload
|
|
325
|
+
|
|
326
|
+
**Input** (Shopify webhook):
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"id": 12345678,
|
|
331
|
+
"order_number": "SO-1001",
|
|
332
|
+
"total_price": "159.98",
|
|
333
|
+
"currency": "USD",
|
|
334
|
+
"customer": {
|
|
335
|
+
"email": "customer@example.com"
|
|
336
|
+
},
|
|
337
|
+
"line_items": [
|
|
338
|
+
{
|
|
339
|
+
"id": 111,
|
|
340
|
+
"sku": "SKU-001",
|
|
341
|
+
"quantity": 2,
|
|
342
|
+
"price": "79.99"
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Output**:
|
|
349
|
+
|
|
350
|
+
```json
|
|
351
|
+
{
|
|
352
|
+
"success": true,
|
|
353
|
+
"shopifyOrderId": 12345678,
|
|
354
|
+
"fluentOrderId": "1234",
|
|
355
|
+
"fluentOrderRef": "SO-1001"
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Pattern 2: XML Request/Response
|
|
362
|
+
|
|
363
|
+
### Use Case
|
|
364
|
+
|
|
365
|
+
**Older systems** (SFCC, Radial, SOAP) sending XML webhooks and expecting XML responses.
|
|
366
|
+
|
|
367
|
+
### Implementation
|
|
368
|
+
|
|
369
|
+
See complete implementation: [XML Order Ingestion](../../../01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md)
|
|
370
|
+
|
|
371
|
+
### Quick Example
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
/**
|
|
375
|
+
* XML Webhook: SFCC Order → Fluent Order → XML Response
|
|
376
|
+
*
|
|
377
|
+
* Input: application/xml
|
|
378
|
+
* Output: application/xml
|
|
379
|
+
*/
|
|
380
|
+
|
|
381
|
+
import {
|
|
382
|
+
createClient,
|
|
383
|
+
GraphQLMutationMapper,
|
|
384
|
+
XMLParserService,
|
|
385
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
386
|
+
import { webhook, fn } from '@versori/run';
|
|
387
|
+
import { XMLBuilder } from 'fast-xml-parser';
|
|
388
|
+
|
|
389
|
+
export const sfccOrderWebhook = webhook('sfcc-order', {
|
|
390
|
+
response: {
|
|
391
|
+
mode: 'sync',
|
|
392
|
+
// CRITICAL: Custom response handler for XML (not JSON)
|
|
393
|
+
onSuccess: ctx =>
|
|
394
|
+
new Response(ctx.data, {
|
|
395
|
+
status: 200,
|
|
396
|
+
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
397
|
+
}),
|
|
398
|
+
onError: ctx =>
|
|
399
|
+
new Response(ctx.data, {
|
|
400
|
+
status: 500,
|
|
401
|
+
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
402
|
+
}),
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
.then(
|
|
406
|
+
fn('process-order', async ctx => {
|
|
407
|
+
const { body } = ctx.request;
|
|
408
|
+
const { log, connections } = ctx;
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
// Parse XML (Versori pre-parses, but you can re-parse if needed)
|
|
412
|
+
const parser = new XMLParserService();
|
|
413
|
+
const orderData = typeof body === 'string' ? await parser.parse(body) : body;
|
|
414
|
+
|
|
415
|
+
// Create client
|
|
416
|
+
const client = await createClient({
|
|
417
|
+
connection: connections.fluent_commerce,
|
|
418
|
+
logger: log,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Map XML → GraphQL
|
|
422
|
+
const mapper = new GraphQLMutationMapper(mappingConfig, log, { fluentClient: client });
|
|
423
|
+
const result = await mapper.mapWithNodes(orderData, customResolvers, {
|
|
424
|
+
fluentClient: client,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (!result.success) {
|
|
428
|
+
throw new Error(`Mapping failed: ${result.errors?.join(', ')}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Execute mutation (query is auto-generated in result)
|
|
432
|
+
const order = await client.graphql({
|
|
433
|
+
query: result.query,
|
|
434
|
+
variables: result.variables, // ✅ Use variables (wrapped if fields pattern)
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Build XML response
|
|
438
|
+
const builder = new XMLBuilder({
|
|
439
|
+
ignoreAttributes: false,
|
|
440
|
+
attributeNamePrefix: '@',
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const xmlResponse = builder.build({
|
|
444
|
+
OrderResponse: {
|
|
445
|
+
'@status': 'success',
|
|
446
|
+
OrderId: order.id,
|
|
447
|
+
OrderRef: order.ref,
|
|
448
|
+
Message: 'Order created successfully',
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n${xmlResponse}`;
|
|
453
|
+
} catch (error: any) {
|
|
454
|
+
log.error('SFCC order processing failed', error);
|
|
455
|
+
|
|
456
|
+
// Build XML error response
|
|
457
|
+
const builder = new XMLBuilder({ ignoreAttributes: false });
|
|
458
|
+
const xmlError = builder.build({
|
|
459
|
+
OrderResponse: {
|
|
460
|
+
'@status': 'error',
|
|
461
|
+
ErrorMessage: error.message,
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
throw new Error(`<?xml version="1.0" encoding="UTF-8"?>\n${xmlError}`);
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
)
|
|
469
|
+
.catch(({ data }) => {
|
|
470
|
+
// Return error XML (already formatted in catch block)
|
|
471
|
+
return data;
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**Why custom response handler?**
|
|
476
|
+
|
|
477
|
+
Versori's default behavior JSON-encodes all responses. For XML, you must use `Response` objects with proper Content-Type.
|
|
478
|
+
|
|
479
|
+
See: [Versori Webhook Response Patterns](../../../04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md#critical-non-json-response-handlers-xml-html-csv)
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Pattern 3: Mixed Format (XML→JSON)
|
|
484
|
+
|
|
485
|
+
### Use Case
|
|
486
|
+
|
|
487
|
+
**Older system input, modern output**: Receive XML webhook, return JSON response.
|
|
488
|
+
|
|
489
|
+
### Implementation
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
/**
|
|
493
|
+
* Mixed Format Webhook: XML Input → JSON Output
|
|
494
|
+
*
|
|
495
|
+
* Input: application/xml
|
|
496
|
+
* Output: application/json
|
|
497
|
+
*/
|
|
498
|
+
|
|
499
|
+
import {
|
|
500
|
+
createClient,
|
|
501
|
+
GraphQLMutationMapper,
|
|
502
|
+
XMLParserService,
|
|
503
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
504
|
+
|
|
505
|
+
export default async function mixedFormatWebhook(activation: any, log: any, connections: any) {
|
|
506
|
+
try {
|
|
507
|
+
// Parse XML input
|
|
508
|
+
const parser = new XMLParserService();
|
|
509
|
+
const xmlData =
|
|
510
|
+
typeof activation.body === 'string' ? await parser.parse(activation.body) : activation.body;
|
|
511
|
+
|
|
512
|
+
log.info('Parsed XML payload', { rootKeys: Object.keys(xmlData) });
|
|
513
|
+
|
|
514
|
+
// Create client
|
|
515
|
+
const client = await createClient({
|
|
516
|
+
connection: connections.fluent_commerce,
|
|
517
|
+
logger: log,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Map XML → GraphQL
|
|
521
|
+
const mapper = new GraphQLMutationMapper(mappingConfig, log, { fluentClient: client });
|
|
522
|
+
const result = await mapper.mapWithNodes(xmlData, customResolvers, {
|
|
523
|
+
fluentClient: client,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
if (!result.success) {
|
|
527
|
+
throw new Error(`Mapping failed: ${result.errors?.join(', ')}`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Execute mutation (query is auto-generated in result)
|
|
531
|
+
const order = await client.graphql({
|
|
532
|
+
query: result.query,
|
|
533
|
+
variables: result.variables, // ✅ Use variables (wrapped if fields pattern)
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Return JSON response (default Versori behavior)
|
|
537
|
+
return {
|
|
538
|
+
status: 200,
|
|
539
|
+
body: {
|
|
540
|
+
success: true,
|
|
541
|
+
orderId: order.id,
|
|
542
|
+
orderRef: order.ref,
|
|
543
|
+
message: 'Order created successfully',
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
} catch (error: any) {
|
|
547
|
+
log.error('Webhook processing failed', error);
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
status: 500,
|
|
551
|
+
body: {
|
|
552
|
+
success: false,
|
|
553
|
+
error: error.message,
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## Versori Webhook Response Handling
|
|
563
|
+
|
|
564
|
+
### Critical Pattern: Non-JSON Responses
|
|
565
|
+
|
|
566
|
+
**Problem**: Versori default handlers JSON-encode all responses.
|
|
567
|
+
|
|
568
|
+
**Solution**: Use custom `onSuccess`/`onError` handlers with `Response` objects.
|
|
569
|
+
|
|
570
|
+
### XML Response Pattern
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
export const xmlEndpoint = webhook('xml', {
|
|
574
|
+
response: {
|
|
575
|
+
mode: 'sync',
|
|
576
|
+
onSuccess: ctx =>
|
|
577
|
+
new Response(ctx.data, {
|
|
578
|
+
status: 200,
|
|
579
|
+
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
580
|
+
}),
|
|
581
|
+
onError: ctx =>
|
|
582
|
+
new Response(ctx.data, {
|
|
583
|
+
status: 500,
|
|
584
|
+
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
585
|
+
}),
|
|
586
|
+
},
|
|
587
|
+
})
|
|
588
|
+
.then(fn('gen', () => '<?xml version="1.0"?><response>Success</response>'))
|
|
589
|
+
.catch(({ data }) => '<error>Failed</error>');
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### HTML Response Pattern
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
export const htmlEndpoint = webhook('html', {
|
|
596
|
+
response: {
|
|
597
|
+
mode: 'sync',
|
|
598
|
+
onSuccess: ctx =>
|
|
599
|
+
new Response(ctx.data, {
|
|
600
|
+
status: 200,
|
|
601
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
602
|
+
}),
|
|
603
|
+
},
|
|
604
|
+
}).then(fn('gen', () => '<!DOCTYPE html><html><body>OK</body></html>'));
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### CSV Download Pattern
|
|
608
|
+
|
|
609
|
+
```typescript
|
|
610
|
+
export const csvEndpoint = webhook('csv', {
|
|
611
|
+
response: {
|
|
612
|
+
mode: 'sync',
|
|
613
|
+
onSuccess: ctx =>
|
|
614
|
+
new Response(ctx.data, {
|
|
615
|
+
status: 200,
|
|
616
|
+
headers: {
|
|
617
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
618
|
+
'Content-Disposition': 'attachment; filename="data.csv"',
|
|
619
|
+
},
|
|
620
|
+
}),
|
|
621
|
+
},
|
|
622
|
+
}).then(fn('gen', () => 'ID,Name,Value\n1,Item,100'));
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Complete guide**: [Versori Webhook Response Patterns](../../../04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md#critical-non-json-response-handlers-xml-html-csv)
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## Webhook Security
|
|
630
|
+
|
|
631
|
+
### Pattern 1: Fluent Commerce Webhook Validation
|
|
632
|
+
|
|
633
|
+
⚠️ **CRITICAL LIMITATION:** The SDK's `validateWebhook()` method **ONLY works with Fluent Commerce Rubix webhooks**. Do not use for Shopify, GitHub, Stripe, or other third-party webhooks. See Pattern 3 for those systems.
|
|
634
|
+
|
|
635
|
+
**Use FluentClient.validateWebhook()** for cryptographic signature validation of **Fluent Commerce webhooks only**.
|
|
636
|
+
|
|
637
|
+
The SDK provides built-in webhook validation using RSA-based algorithms (SHA512withRSA, MD5withRSA).
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
import { createClient } from '@fluentcommerce/fc-connect-sdk';
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* SDK Webhook Validation - Fluent Commerce Only
|
|
644
|
+
*
|
|
645
|
+
* Supports: SHA512withRSA (fluent-signature), MD5withRSA (flex.signature)
|
|
646
|
+
* Does NOT support: Shopify, GitHub, Stripe, or other third-party webhooks
|
|
647
|
+
*/
|
|
648
|
+
export default async function secureFluentWebhook(activation: any, log: any, connections: any) {
|
|
649
|
+
try {
|
|
650
|
+
// Create client with public key
|
|
651
|
+
const client = await createClient({
|
|
652
|
+
connection: connections.fluent_commerce,
|
|
653
|
+
logger: log,
|
|
654
|
+
publicKey: process.env.FLUENT_WEBHOOK_PUBLIC_KEY, // Required for webhook validation
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Get raw body (CRITICAL: Must be raw string, not parsed JSON)
|
|
658
|
+
const rawBody = activation.rawBody || JSON.stringify(activation.body);
|
|
659
|
+
const payload = typeof activation.body === 'string' ? JSON.parse(activation.body) : activation.body;
|
|
660
|
+
|
|
661
|
+
// Extract signature from Fluent Commerce headers
|
|
662
|
+
const signature = activation.headers['fluent-signature'] || activation.headers['flex.signature'];
|
|
663
|
+
|
|
664
|
+
if (!signature) {
|
|
665
|
+
log.error('Missing Fluent Commerce signature header');
|
|
666
|
+
return { status: 401, body: { error: 'Unauthorized' } };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Validate webhook signature using SDK (Fluent Commerce only)
|
|
670
|
+
const isValid = await client.validateWebhook(payload, signature, rawBody);
|
|
671
|
+
|
|
672
|
+
if (!isValid) {
|
|
673
|
+
log.error('Invalid Fluent Commerce webhook signature');
|
|
674
|
+
return { status: 401, body: { error: 'Unauthorized' } };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
log.info('Fluent Commerce webhook signature validated successfully');
|
|
678
|
+
|
|
679
|
+
// Process webhook
|
|
680
|
+
const result = await processFluentWebhook(payload, client, log);
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
status: 200,
|
|
684
|
+
body: {
|
|
685
|
+
success: true,
|
|
686
|
+
...result,
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
} catch (error: any) {
|
|
690
|
+
log.error('Webhook validation or processing failed', error);
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
status: 500,
|
|
694
|
+
body: {
|
|
695
|
+
success: false,
|
|
696
|
+
error: error.message,
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Fluent Commerce Signature Headers
|
|
704
|
+
|
|
705
|
+
The SDK automatically detects the signature algorithm from headers:
|
|
706
|
+
|
|
707
|
+
| Header | Algorithm | Status |
|
|
708
|
+
|--------|-----------|--------|
|
|
709
|
+
| `fluent-signature` | SHA512withRSA | **Recommended** |
|
|
710
|
+
| `x-fluent-signature` | SHA512withRSA | Supported |
|
|
711
|
+
| `flex.signature` | MD5withRSA | **Older algorithm** |
|
|
712
|
+
| `flex-signature` | MD5withRSA | Supported |
|
|
713
|
+
|
|
714
|
+
```typescript
|
|
715
|
+
// Auto-detection (recommended)
|
|
716
|
+
const signature = headers['fluent-signature'] || headers['flex.signature'];
|
|
717
|
+
const isValid = await client.validateWebhook(payload, signature, rawBody);
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### Pattern 2: IP Allowlist (All Webhook Types)
|
|
721
|
+
|
|
722
|
+
**Restrict webhook sources** to known IP addresses - works for Fluent Commerce, Shopify, GitHub, Stripe, etc.
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
const ALLOWED_IPS = ['192.168.1.1', '10.0.0.0/8'];
|
|
726
|
+
|
|
727
|
+
function isIPAllowed(ip: string): boolean {
|
|
728
|
+
return ALLOWED_IPS.some(allowed => {
|
|
729
|
+
if (allowed.includes('/')) {
|
|
730
|
+
// CIDR range check (use ip-cidr library)
|
|
731
|
+
return true; // Simplified
|
|
732
|
+
}
|
|
733
|
+
return ip === allowed;
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Usage
|
|
738
|
+
export default async function ipRestrictedWebhook(activation: any, log: any) {
|
|
739
|
+
const clientIP = activation.headers['x-forwarded-for'] || activation.ip;
|
|
740
|
+
|
|
741
|
+
if (!isIPAllowed(clientIP)) {
|
|
742
|
+
log.warn('Blocked request from unauthorized IP', { ip: clientIP });
|
|
743
|
+
return { status: 403, body: { error: 'Forbidden' } };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Process webhook
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### Pattern 3: Third-Party Webhook Validation (Shopify, GitHub, Stripe)
|
|
751
|
+
|
|
752
|
+
⚠️ **REQUIRED FOR THIRD-PARTY WEBHOOKS:** The SDK's `validateWebhook()` does NOT support HMAC algorithms used by Shopify, GitHub, Stripe. You must implement manual HMAC validation for these systems.
|
|
753
|
+
|
|
754
|
+
#### Shopify Webhook Validation (HMAC-SHA256, base64)
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
import crypto from 'crypto';
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Validate Shopify webhook signature
|
|
761
|
+
* Algorithm: HMAC-SHA256 with base64 encoding
|
|
762
|
+
*/
|
|
763
|
+
function validateShopifyWebhook(rawBody: string, signature: string, secret: string): boolean {
|
|
764
|
+
const hash = crypto
|
|
765
|
+
.createHmac('sha256', secret)
|
|
766
|
+
.update(rawBody, 'utf8')
|
|
767
|
+
.digest('base64');
|
|
768
|
+
|
|
769
|
+
return hash === signature;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Usage
|
|
773
|
+
export default async function shopifyWebhook(activation: any, log: any) {
|
|
774
|
+
const rawBody = activation.rawBody || JSON.stringify(activation.body);
|
|
775
|
+
const signature = activation.headers['x-shopify-hmac-sha256'];
|
|
776
|
+
const secret = process.env.SHOPIFY_WEBHOOK_SECRET!;
|
|
777
|
+
|
|
778
|
+
if (!validateShopifyWebhook(rawBody, signature, secret)) {
|
|
779
|
+
log.error('Invalid Shopify webhook signature');
|
|
780
|
+
return { status: 401, body: { error: 'Unauthorized' } };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Process webhook
|
|
784
|
+
const order = await processShopifyOrder(activation.body);
|
|
785
|
+
return { status: 200, body: { success: true, orderId: order.id } };
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
#### GitHub Webhook Validation (HMAC-SHA256, hex with prefix)
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
import crypto from 'crypto';
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Validate GitHub webhook signature
|
|
796
|
+
* Algorithm: HMAC-SHA256 with hex encoding and sha256= prefix
|
|
797
|
+
*/
|
|
798
|
+
function validateGitHubWebhook(rawBody: string, signature: string, secret: string): boolean {
|
|
799
|
+
const hash = crypto
|
|
800
|
+
.createHmac('sha256', secret)
|
|
801
|
+
.update(rawBody, 'utf8')
|
|
802
|
+
.digest('hex');
|
|
803
|
+
|
|
804
|
+
const expected = `sha256=${hash}`;
|
|
805
|
+
return expected === signature;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Usage
|
|
809
|
+
export default async function githubWebhook(activation: any, log: any) {
|
|
810
|
+
const rawBody = activation.rawBody || JSON.stringify(activation.body);
|
|
811
|
+
const signature = activation.headers['x-hub-signature-256'];
|
|
812
|
+
const secret = process.env.GITHUB_WEBHOOK_SECRET!;
|
|
813
|
+
|
|
814
|
+
if (!validateGitHubWebhook(rawBody, signature, secret)) {
|
|
815
|
+
log.error('Invalid GitHub webhook signature');
|
|
816
|
+
return { status: 401, body: { error: 'Unauthorized' } };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Process webhook
|
|
820
|
+
const result = await processGitHubEvent(activation.body);
|
|
821
|
+
return { status: 200, body: { success: true } };
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
#### Stripe Webhook Validation (HMAC-SHA256 with timestamp)
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
import crypto from 'crypto';
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Validate Stripe webhook signature
|
|
832
|
+
* Algorithm: HMAC-SHA256 with timestamp verification to prevent replay attacks
|
|
833
|
+
*/
|
|
834
|
+
function validateStripeWebhook(
|
|
835
|
+
rawBody: string,
|
|
836
|
+
signature: string,
|
|
837
|
+
secret: string,
|
|
838
|
+
tolerance: number = 300 // 5 minutes
|
|
839
|
+
): boolean {
|
|
840
|
+
// Parse signature header: t=timestamp,v1=signature
|
|
841
|
+
const parts = signature.split(',');
|
|
842
|
+
const timestamp = parts.find(p => p.startsWith('t='))?.split('=')[1];
|
|
843
|
+
const sig = parts.find(p => p.startsWith('v1='))?.split('=')[1];
|
|
844
|
+
|
|
845
|
+
if (!timestamp || !sig) return false;
|
|
846
|
+
|
|
847
|
+
// Check timestamp tolerance (prevent replay attacks)
|
|
848
|
+
const now = Math.floor(Date.now() / 1000);
|
|
849
|
+
if (Math.abs(now - parseInt(timestamp)) > tolerance) {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Verify signature
|
|
854
|
+
const payload = `${timestamp}.${rawBody}`;
|
|
855
|
+
const hash = crypto
|
|
856
|
+
.createHmac('sha256', secret)
|
|
857
|
+
.update(payload, 'utf8')
|
|
858
|
+
.digest('hex');
|
|
859
|
+
|
|
860
|
+
return hash === sig;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Usage
|
|
864
|
+
export default async function stripeWebhook(activation: any, log: any) {
|
|
865
|
+
const rawBody = activation.rawBody || JSON.stringify(activation.body);
|
|
866
|
+
const signature = activation.headers['stripe-signature'];
|
|
867
|
+
const secret = process.env.STRIPE_WEBHOOK_SECRET!;
|
|
868
|
+
|
|
869
|
+
if (!validateStripeWebhook(rawBody, signature, secret)) {
|
|
870
|
+
log.error('Invalid Stripe webhook signature');
|
|
871
|
+
return { status: 401, body: { error: 'Unauthorized' } };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Process webhook
|
|
875
|
+
const result = await processStripeEvent(activation.body);
|
|
876
|
+
return { status: 200, body: { success: true } };
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
#### Third-Party Webhook Algorithms
|
|
881
|
+
|
|
882
|
+
| Provider | Algorithm | Encoding | Header | Notes |
|
|
883
|
+
|----------|-----------|----------|--------|-------|
|
|
884
|
+
| **Shopify** | HMAC-SHA256 | base64 | `x-shopify-hmac-sha256` | Simple HMAC |
|
|
885
|
+
| **GitHub** | HMAC-SHA256 | hex | `x-hub-signature-256` | Prefixed with `sha256=` |
|
|
886
|
+
| **Stripe** | HMAC-SHA256 | hex | `stripe-signature` | Includes timestamp (replay protection) |
|
|
887
|
+
| **PayPal** | HMAC-SHA256 | base64 | `paypal-transmission-sig` | Multiple verification params |
|
|
888
|
+
|
|
889
|
+
### Error Handling for Validation
|
|
890
|
+
|
|
891
|
+
```typescript
|
|
892
|
+
// Fluent Commerce webhook validation error handling
|
|
893
|
+
try {
|
|
894
|
+
const isValid = await client.validateWebhook(payload, signature, rawBody);
|
|
895
|
+
|
|
896
|
+
if (!isValid) {
|
|
897
|
+
log.error('Fluent Commerce webhook signature validation failed', {
|
|
898
|
+
header: signature ? 'present' : 'missing',
|
|
899
|
+
algorithm: signature?.includes('fluent-signature') ? 'SHA512withRSA' : 'MD5withRSA',
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
status: 401,
|
|
904
|
+
body: {
|
|
905
|
+
error: 'Unauthorized',
|
|
906
|
+
message: 'Invalid webhook signature',
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Process webhook
|
|
912
|
+
} catch (error: any) {
|
|
913
|
+
log.error('Webhook validation error', error);
|
|
914
|
+
|
|
915
|
+
return {
|
|
916
|
+
status: 500,
|
|
917
|
+
body: {
|
|
918
|
+
error: 'Internal error',
|
|
919
|
+
message: error.message,
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Third-party webhook validation error handling (Shopify example)
|
|
925
|
+
try {
|
|
926
|
+
const signature = headers['x-shopify-hmac-sha256'];
|
|
927
|
+
const secret = process.env.SHOPIFY_WEBHOOK_SECRET;
|
|
928
|
+
|
|
929
|
+
if (!signature || !secret) {
|
|
930
|
+
log.error('Missing Shopify webhook credentials');
|
|
931
|
+
return { status: 401, body: { error: 'Unauthorized' } };
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const isValid = validateShopifyWebhook(rawBody, signature, secret);
|
|
935
|
+
|
|
936
|
+
if (!isValid) {
|
|
937
|
+
log.error('Shopify webhook signature validation failed');
|
|
938
|
+
return { status: 401, body: { error: 'Unauthorized' } };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Process webhook
|
|
942
|
+
} catch (error: any) {
|
|
943
|
+
log.error('Shopify webhook validation error', error);
|
|
944
|
+
return { status: 500, body: { error: 'Internal error' } };
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
### Pattern 5: API Key Authentication
|
|
950
|
+
|
|
951
|
+
**Require API key** in headers (alternative to signature validation).
|
|
952
|
+
|
|
953
|
+
```typescript
|
|
954
|
+
export default async function apiKeyWebhook(activation: any, log: any) {
|
|
955
|
+
const apiKey = activation.headers['x-api-key'];
|
|
956
|
+
const expectedKey = process.env.WEBHOOK_API_KEY!;
|
|
957
|
+
|
|
958
|
+
if (!apiKey || apiKey !== expectedKey) {
|
|
959
|
+
log.error('Missing or invalid API key');
|
|
960
|
+
return { status: 401, body: { error: 'Unauthorized' } };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Process webhook
|
|
964
|
+
}
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
---
|
|
968
|
+
|
|
969
|
+
## Error Handling and Retry Logic
|
|
970
|
+
|
|
971
|
+
### HTTP Status Codes
|
|
972
|
+
|
|
973
|
+
| Status | Meaning | Retry? | Example |
|
|
974
|
+
| ------- | -------------------------- | ------ | ------------------------- |
|
|
975
|
+
| **200** | Success | No | Order created |
|
|
976
|
+
| **400** | Bad Request (client error) | No | Invalid payload |
|
|
977
|
+
| **401** | Unauthorized | No | Missing/invalid signature |
|
|
978
|
+
| **422** | Unprocessable Entity | No | Validation failed |
|
|
979
|
+
| **429** | Rate Limit | Yes | Too many requests |
|
|
980
|
+
| **500** | Server Error | Yes | Fluent API down |
|
|
981
|
+
| **503** | Service Unavailable | Yes | Temporary outage |
|
|
982
|
+
|
|
983
|
+
### Error Response Pattern
|
|
984
|
+
|
|
985
|
+
```typescript
|
|
986
|
+
try {
|
|
987
|
+
// Process webhook
|
|
988
|
+
|
|
989
|
+
return {
|
|
990
|
+
status: 200,
|
|
991
|
+
body: {
|
|
992
|
+
success: true,
|
|
993
|
+
orderId: order.id,
|
|
994
|
+
},
|
|
995
|
+
};
|
|
996
|
+
} catch (error: any) {
|
|
997
|
+
// Categorize error
|
|
998
|
+
if (error.message.includes('required field')) {
|
|
999
|
+
// Validation error - don't retry
|
|
1000
|
+
return {
|
|
1001
|
+
status: 400,
|
|
1002
|
+
body: {
|
|
1003
|
+
success: false,
|
|
1004
|
+
error: 'Validation failed',
|
|
1005
|
+
details: error.message,
|
|
1006
|
+
},
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (error.message.includes('rate limit')) {
|
|
1011
|
+
// Rate limit - retry after delay
|
|
1012
|
+
return {
|
|
1013
|
+
status: 429,
|
|
1014
|
+
body: {
|
|
1015
|
+
success: false,
|
|
1016
|
+
error: 'Rate limit exceeded',
|
|
1017
|
+
retryAfter: 60,
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Unknown error - retryable
|
|
1023
|
+
return {
|
|
1024
|
+
status: 500,
|
|
1025
|
+
body: {
|
|
1026
|
+
success: false,
|
|
1027
|
+
error: 'Internal server error',
|
|
1028
|
+
retryable: true,
|
|
1029
|
+
},
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
### Idempotency Pattern
|
|
1035
|
+
|
|
1036
|
+
**Prevent duplicate processing** of same webhook.
|
|
1037
|
+
|
|
1038
|
+
```typescript
|
|
1039
|
+
import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
1040
|
+
|
|
1041
|
+
export default async function idempotentWebhook(activation: any, log: any) {
|
|
1042
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
1043
|
+
const stateService = new StateService(logger);
|
|
1044
|
+
|
|
1045
|
+
// Use webhook ID or order ID as idempotency key
|
|
1046
|
+
const idempotencyKey = `webhook:${activation.body.id}`;
|
|
1047
|
+
|
|
1048
|
+
// Check if already processed
|
|
1049
|
+
const processed = await stateService.getState(idempotencyKey);
|
|
1050
|
+
|
|
1051
|
+
if (processed) {
|
|
1052
|
+
log.info('Webhook already processed', { id: activation.body.id });
|
|
1053
|
+
return {
|
|
1054
|
+
status: 200,
|
|
1055
|
+
body: {
|
|
1056
|
+
success: true,
|
|
1057
|
+
message: 'Already processed',
|
|
1058
|
+
orderId: processed.orderId,
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Process webhook
|
|
1064
|
+
const order = await processOrder(activation.body);
|
|
1065
|
+
|
|
1066
|
+
// Mark as processed
|
|
1067
|
+
await stateService.setState(idempotencyKey, {
|
|
1068
|
+
orderId: order.id,
|
|
1069
|
+
processedAt: new Date().toISOString(),
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
return {
|
|
1073
|
+
status: 200,
|
|
1074
|
+
body: {
|
|
1075
|
+
success: true,
|
|
1076
|
+
orderId: order.id,
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
1084
|
+
## Complete Examples
|
|
1085
|
+
|
|
1086
|
+
### Example 1: Shopify Order Webhook (JSON)
|
|
1087
|
+
|
|
1088
|
+
Complete production-ready Shopify integration with:
|
|
1089
|
+
|
|
1090
|
+
- JSON parsing
|
|
1091
|
+
- Customer lookup/create
|
|
1092
|
+
- Order creation
|
|
1093
|
+
- Error handling
|
|
1094
|
+
|
|
1095
|
+
See: [Common Patterns](../examples/common-patterns.ts)
|
|
1096
|
+
|
|
1097
|
+
### Example 2: SFCC Order Webhook (XML)
|
|
1098
|
+
|
|
1099
|
+
Complete production-ready SFCC integration with:
|
|
1100
|
+
|
|
1101
|
+
- XML parsing with nodes
|
|
1102
|
+
- GraphQL mutation mapping
|
|
1103
|
+
- Custom resolvers
|
|
1104
|
+
- XML response formatting
|
|
1105
|
+
|
|
1106
|
+
See: [XML Order Ingestion](../../../01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md)
|
|
1107
|
+
|
|
1108
|
+
### Example 3: Inventory Webhook (JSON)
|
|
1109
|
+
|
|
1110
|
+
Simple inventory update webhook:
|
|
1111
|
+
|
|
1112
|
+
- Single SKU update
|
|
1113
|
+
- Direct GraphQL mutation
|
|
1114
|
+
- Fast response (< 2 sec)
|
|
1115
|
+
|
|
1116
|
+
```typescript
|
|
1117
|
+
import { createClient } from '@fluentcommerce/fc-connect-sdk';
|
|
1118
|
+
|
|
1119
|
+
export default async function inventoryWebhook(activation: any, log: any, connections: any) {
|
|
1120
|
+
const { sku, location, quantity } = activation.body;
|
|
1121
|
+
|
|
1122
|
+
const client = await createClient({
|
|
1123
|
+
connection: connections.fluent_commerce,
|
|
1124
|
+
logger: log,
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
const result = await client.graphql({
|
|
1128
|
+
query: `
|
|
1129
|
+
mutation UpdateInventory($input: UpdateInventoryQuantityInput!) {
|
|
1130
|
+
updateInventoryQuantity(input: $input) {
|
|
1131
|
+
id
|
|
1132
|
+
ref
|
|
1133
|
+
onHand
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
`,
|
|
1137
|
+
variables: {
|
|
1138
|
+
input: {
|
|
1139
|
+
ref: `${sku}-${location}`,
|
|
1140
|
+
onHand: quantity,
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
return {
|
|
1146
|
+
status: 200,
|
|
1147
|
+
body: {
|
|
1148
|
+
success: true,
|
|
1149
|
+
inventoryId: result.id,
|
|
1150
|
+
newQuantity: result.onHand,
|
|
1151
|
+
},
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
---
|
|
1157
|
+
|
|
1158
|
+
## Next Steps
|
|
1159
|
+
|
|
1160
|
+
Now that you understand webhook patterns, you're ready to learn comprehensive error handling and resilience strategies!
|
|
1161
|
+
|
|
1162
|
+
**Continue to:** [Module 5: Error Handling →](./integration-patterns-05-error-handling.md)
|
|
1163
|
+
|
|
1164
|
+
Or explore:
|
|
1165
|
+
|
|
1166
|
+
- [Complete Example: XML Order Webhook](../../../01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md)
|
|
1167
|
+
- [Versori Webhook Response Patterns](../../../04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md#critical-non-json-response-handlers-xml-html-csv)
|
|
1168
|
+
- [GraphQL Mutation Mapper API](../../../02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md)
|
|
1169
|
+
|
|
1170
|
+
---
|
|
1171
|
+
|
|
1172
|
+
## Additional Resources
|
|
1173
|
+
|
|
1174
|
+
- [Versori Webhook Documentation](https://docs.versori.com/)
|
|
1175
|
+
- [XMLParserService API Reference](../../../02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md)
|
|
1176
|
+
- [GraphQL Mutation Mapper Guide](../../../02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md)
|
|
1177
|
+
- [Webhook Security Best Practices](../../../02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md)
|
|
1178
|
+
|
|
1179
|
+
---
|
|
1180
|
+
|
|
1181
|
+
[← Back to Index](../integration-patterns-readme.md) | [Previous: Delta Sync →](./integration-patterns-03-delta-sync.md) | [Next: Error Handling →](./integration-patterns-05-error-handling.md)
|