spree_api 5.6.0.rc3 → 5.6.0.rc4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1029d16c5aff295ae773e0c605d1c2003157c4efcfdc5ac5f4ef6edf073b0d68
4
- data.tar.gz: 8778b2415f90f19df1f7631ff6df0f4f2508ec8ba7d4116f81f76fecf81dae49
3
+ metadata.gz: ca50a0fac8c0d346157c3d42a2969414d5f4e8a392aae87176cb02069c828e21
4
+ data.tar.gz: dc56eff71e891ef304710a9a2f2bfc7c72be17a50d0a24ba1335b137ea817057
5
5
  SHA512:
6
- metadata.gz: d7f0f2b4eabc6fc302f18b8cdd76d3213360ba927f3a1f5690253e7443baf4778464c1a3c395f1fa29827f1a2acdb66849e6738bc1af7e6fd0f9a910bcf09ecd
7
- data.tar.gz: 8db65610da6e5ff6bc2dd736050c19d6c9624f64e48ba56b85d0c6bcc935f3ba1a00ff2080643b35cda5c7d05e0c83f3510c75c09926e483ae4b15c93bdc39a5
6
+ metadata.gz: e1097d160a66eac8cee1e1b9169f06781c4105ddb01fce2bf560bcf0c94aeb7e0f947bd36675e17e7b6532af02c47999d242f23fb8359abaa866d3257775afd0
7
+ data.tar.gz: a48b3317819fd03b05d71b016a7856b5c1a8da5d238080964dd9980b0eef328f2a712aed8a903efffde9a86ee1c902903495aee339d01e4a5e216fae0886ffb7
@@ -6,41 +6,87 @@ module Spree
6
6
  # read channel context without threading it through method args.
7
7
  #
8
8
  # Resolution order:
9
- # 1. +X-Spree-Channel+ header value matched against +channels.code+ —
9
+ # 1. The authenticated publishable key's channel binding
10
+ # (+Spree::ApiKey#channel+) — server-assigned, cannot be overridden;
11
+ # a +X-Spree-Channel+ header naming a different channel is a 422
12
+ # 2. +X-Spree-Channel+ header value matched against +channels.code+ —
10
13
  # or, if it looks like a prefixed ID (+ch_…+), against +channels.id+
11
14
  # — scoped to the current store
12
- # 2. +current_store.default_channel+
15
+ # 3. +current_store.default_channel+
13
16
  #
14
17
  # The concern is a no-op if no channel matches — callers fall back to
15
18
  # +Spree::Current.channel+'s store-default behavior.
19
+ #
20
+ # IMPORTANT: key-bound resolution requires +authenticate_api_key!+ to run
21
+ # BEFORE +set_current_channel+ — both Store API branches order their
22
+ # callbacks accordingly (prepended in Store::BaseController, inherited
23
+ # ahead of the include in Store::ResourceController).
16
24
  module ChannelResolution
17
25
  extend ActiveSupport::Concern
18
26
 
19
27
  CHANNEL_HEADER = 'X-Spree-Channel'.freeze
20
28
 
21
29
  included do
30
+ # Prepended: LocaleAndCurrency's set_locale/set_currency callbacks
31
+ # (registered up in V3::BaseController) consult Spree::Current's
32
+ # fallback chain, which must derive from the REQUEST's store — not
33
+ # the Store.default fallback — before they run.
34
+ prepend_before_action :set_current_store_context
22
35
  before_action :set_current_channel
23
36
  end
24
37
 
25
38
  protected
26
39
 
27
40
  def current_channel
28
- @current_channel ||= channel_from_header || Spree::Current.channel
41
+ @current_channel ||= channel_from_api_key || channel_from_header || Spree::Current.channel
29
42
  end
30
43
 
31
44
  private
32
45
 
33
- # Only write to Spree::Current when the header resolves a specific
34
- # channel. The store-default fallback is handled lazily by
46
+ # Only write to Spree::Current when the key or header resolves a
47
+ # specific channel. The store-default fallback is handled lazily by
35
48
  # +Spree::Current.channel+ itself, which avoids one query per
36
- # header-less API request.
49
+ # unbound, header-less API request.
50
+ def set_current_store_context
51
+ Spree::Current.store = current_store
52
+ end
53
+
37
54
  def set_current_channel
55
+ bound = channel_from_api_key
56
+
57
+ if bound
58
+ header = channel_from_header
59
+ if header && header.id != bound.id
60
+ render_error(
61
+ code: ErrorHandler::ERROR_CODES[:channel_mismatch],
62
+ message: Spree.t('api.errors.channel_mismatch', default: 'The requested channel does not match the channel this API key is bound to'),
63
+ status: :unprocessable_entity
64
+ )
65
+ return
66
+ end
67
+
68
+ unless bound.active?
69
+ render_error(
70
+ code: ErrorHandler::ERROR_CODES[:channel_inactive],
71
+ message: Spree.t('api.errors.channel_inactive', default: 'The channel this API key is bound to is not active'),
72
+ status: :forbidden
73
+ )
74
+ return
75
+ end
76
+ end
77
+
38
78
  # Memoize so a later +current_channel+ call (e.g. the storefront gate,
39
- # serializer params) reuses this row instead of re-querying the header.
40
- @current_channel = channel_from_header
79
+ # serializer params) reuses this row instead of re-querying.
80
+ @current_channel = bound || channel_from_header
41
81
  Spree::Current.channel = @current_channel if @current_channel
42
82
  end
43
83
 
84
+ def channel_from_api_key
85
+ return nil unless respond_to?(:current_api_key, true)
86
+
87
+ current_api_key&.channel
88
+ end
89
+
44
90
  def channel_from_header
45
91
  value = request.headers[CHANNEL_HEADER].presence
46
92
  return nil if value.blank?
@@ -21,6 +21,10 @@ module Spree
21
21
  record_not_found: 'record_not_found',
22
22
  resource_invalid: 'resource_invalid',
23
23
 
24
+ # Channel errors
25
+ channel_mismatch: 'channel_mismatch',
26
+ channel_inactive: 'channel_inactive',
27
+
24
28
  # Cart errors
25
29
  cart_not_found: 'cart_not_found',
26
30
  cart_cannot_transition: 'cart_cannot_transition',
@@ -87,14 +87,14 @@ module Spree
87
87
  end
88
88
  end
89
89
 
90
- # `key_type` and `scopes` are create-only (scope immutability lives on
91
- # Spree::ApiKey); update is limited to the human-facing `name`. Stripping
92
- # them here keeps them out of mass assignment so a rename returns 200
93
- # rather than 422.
90
+ # `key_type`, `scopes`, and `channel_id` are create-only (immutability
91
+ # lives on Spree::ApiKey); update is limited to the human-facing `name`.
92
+ # Stripping them here keeps them out of mass assignment so a rename
93
+ # returns 200 rather than 422.
94
94
  def permitted_params
95
95
  return params.permit(:name) if action_name == 'update'
96
96
 
97
- params.permit(:name, :key_type, scopes: [])
97
+ params.permit(:name, :key_type, :channel_id, scopes: [])
98
98
  end
99
99
 
100
100
  private
@@ -14,8 +14,13 @@ module Spree
14
14
  # +allow_guest_storefront_access!+.
15
15
  include Spree::Api::V3::StorefrontGating
16
16
 
17
- # Require publishable API key for all Store API requests
18
- before_action :authenticate_api_key!
17
+ # Require publishable API key for all Store API requests. Prepended so
18
+ # the key is authenticated BEFORE ChannelResolution's
19
+ # +set_current_channel+ and the StorefrontGating 401 guard run —
20
+ # a channel-bound key must resolve its channel ahead of the gate, and
21
+ # this matches Store::ResourceController, where +authenticate_request!+
22
+ # is inherited ahead of both concerns.
23
+ prepend_before_action :authenticate_api_key!
19
24
  end
20
25
  end
21
26
  end
@@ -141,8 +141,13 @@ module Spree
141
141
  Spree.api.cart_serializer
142
142
  end
143
143
 
144
+ # Channel-scoped: a storefront surface lists only its own channel's
145
+ # carts — without this, a blended DTC + wholesale storefront leaks
146
+ # the user's wholesale cart into the DTC surface (and vice versa).
147
+ # Explicit by-ID lookups (show/update) stay cross-channel so a shared
148
+ # checkout can load any cart the caller is authorized for.
144
149
  def scope
145
- current_store.carts.where(user: current_user).order(updated_at: :desc)
150
+ current_store.carts.where(user: current_user, channel: current_channel).order(updated_at: :desc)
146
151
  end
147
152
 
148
153
  private
@@ -0,0 +1,31 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ # Exposes the channel context the request resolved to (bound key →
6
+ # X-Spree-Channel header → store default) so a storefront can read its
7
+ # own identity and access posture. Deliberately a singular resource:
8
+ # the Store API never enumerates a store's channels — that would leak
9
+ # the existence of gated surfaces (wholesale, POS) to anyone holding
10
+ # the publishable key.
11
+ class ChannelController < Store::BaseController
12
+ # A gated storefront must be able to read "this channel requires
13
+ # login" before authentication to render a sign-in wall instead of
14
+ # an error page.
15
+ allow_guest_storefront_access!
16
+
17
+ # GET /api/v3/store/channel
18
+ def show
19
+ render json: serialize_resource(current_channel)
20
+ end
21
+
22
+ protected
23
+
24
+ def serializer_class
25
+ Spree.api.channel_serializer
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -9,6 +9,18 @@ module Spree
9
9
  include Spree::Api::V3::ChannelResolution
10
10
  include Spree::Api::V3::StorefrontGating
11
11
 
12
+ # The inherited +set_parent+/+set_resource+ callbacks were registered
13
+ # before the two concerns above, so they'd run resource lookups ahead
14
+ # of channel resolution (wrong channel scoping for X-Spree-Channel /
15
+ # key-bound requests) and ahead of the login gate (guests could probe
16
+ # resource existence on a gated channel via 404s). Re-register them so
17
+ # lookups always run inside the resolved channel context, behind the
18
+ # gate.
19
+ skip_before_action :set_parent
20
+ skip_before_action :set_resource
21
+ before_action :set_parent
22
+ before_action :set_resource, only: [:show, :update, :destroy]
23
+
12
24
  protected
13
25
 
14
26
  def authenticate_request!
@@ -13,10 +13,15 @@ module Spree
13
13
 
14
14
  # Accept optional second argument for backward compatibility with jobs
15
15
  # enqueued before this change was deployed.
16
- def perform(delivery_id, _deprecated_secret_key = nil)
16
+ #
17
+ # `payload_secrets` carries credentials withheld from the persisted payload
18
+ # so the outgoing request body stays complete. See
19
+ # {Spree::WebhookPayloadRedaction}.
20
+ def perform(delivery_id, _deprecated_secret_key = nil, payload_secrets: nil)
17
21
  delivery = Spree::WebhookDelivery.find_by(id: delivery_id)
18
22
  return if delivery.nil?
19
23
 
24
+ delivery.payload_secrets = payload_secrets
20
25
  secret_key = delivery.webhook_endpoint.secret_key
21
26
  Spree::Webhooks::DeliverWebhook.call(delivery: delivery, secret_key: secret_key)
22
27
  end
@@ -18,12 +18,17 @@ module Spree
18
18
  scopes: [:string, multi: true],
19
19
  revoked_at: [:string, nullable: true],
20
20
  last_used_at: [:string, nullable: true],
21
- created_by_email: [:string, nullable: true]
21
+ created_by_email: [:string, nullable: true],
22
+ channel_id: [:string, nullable: true]
22
23
 
23
24
  attributes :name, :key_type, :token_prefix, :scopes,
24
25
  created_at: :iso8601, updated_at: :iso8601,
25
26
  revoked_at: :iso8601, last_used_at: :iso8601
26
27
 
28
+ attribute :channel_id do |key|
29
+ key.channel&.prefixed_id
30
+ end
31
+
27
32
  # Returned only on the create response — `plaintext_token` is held in
28
33
  # memory on the model after `generate_token` and is never persisted
29
34
  # for secret keys, so we serialize it whenever it's available rather
@@ -5,12 +5,11 @@ module Spree
5
5
  # Serializes Spree::CustomerGroup for the admin pickers
6
6
  # (e.g. promotion rule, customer-group filters). Surfaces only
7
7
  # what the admin UI needs to display + select rows.
8
- class CustomerGroupSerializer < V3::BaseSerializer
9
- typelize name: :string,
10
- description: 'string | null',
8
+ class CustomerGroupSerializer < V3::CustomerGroupSerializer
9
+ typelize description: 'string | null',
11
10
  customers_count: :number
12
11
 
13
- attributes :name, :description, :customers_count,
12
+ attributes :description, :customers_count,
14
13
  created_at: :iso8601, updated_at: :iso8601
15
14
 
16
15
  # Members are paginated separately via `/customers?customer_group_id_in=…`
@@ -25,10 +25,15 @@ module Spree
25
25
 
26
26
  attributes :event_name, :event_id, :response_code, :execution_time,
27
27
  :error_type, :request_errors, :response_body, :success,
28
- :payload,
29
28
  created_at: :iso8601, updated_at: :iso8601,
30
29
  delivered_at: :iso8601
31
30
 
31
+ # Redacted again on read: deliveries written before payload redaction
32
+ # shipped still hold live credentials in the column.
33
+ attribute :payload do |delivery|
34
+ Spree::WebhookPayloadRedaction.split(delivery.payload).first
35
+ end
36
+
32
37
  attribute :webhook_endpoint_id do |delivery|
33
38
  delivery.webhook_endpoint&.prefixed_id
34
39
  end
@@ -5,7 +5,7 @@ module Spree
5
5
  # Pre-purchase cart data with checkout progression info
6
6
  class CartSerializer < BaseSerializer
7
7
  typelize number: :string, current_step: :string, completed_steps: 'string[]', token: :string, email: [:string, nullable: true],
8
- customer_note: [:string, nullable: true], market_id: [:string, nullable: true],
8
+ customer_note: [:string, nullable: true], market_id: [:string, nullable: true], channel_id: [:string, nullable: true],
9
9
  currency: :string, locale: [:string, nullable: true], total_quantity: :number,
10
10
  requirements: 'Array<{step: string, field: string, message: string}>',
11
11
  item_total: [:string, nullable: true], display_item_total: [:string, nullable: true],
@@ -34,6 +34,13 @@ module Spree
34
34
  order.market&.prefixed_id
35
35
  end
36
36
 
37
+ # Which surface this cart belongs to — lets a multi-channel storefront
38
+ # verify a cookie-persisted cart against its own channel before
39
+ # adopting it (see the wholesale reference storefront's cart guard).
40
+ attribute :channel_id do |order|
41
+ order.channel&.prefixed_id
42
+ end
43
+
37
44
  attributes :number, :token, :email, :customer_note,
38
45
  :currency, :locale, :total_quantity, :warnings
39
46
 
@@ -5,9 +5,17 @@ module Spree
5
5
  typelize name: :string,
6
6
  code: :string,
7
7
  active: :boolean,
8
- default: :boolean
8
+ default: :boolean,
9
+ storefront_access: :string,
10
+ guest_checkout: :boolean
9
11
 
10
12
  attributes :name, :code, :active, :default
13
+
14
+ # Resolved (channel → store fallback) values — what a client should act
15
+ # on, as opposed to the raw nullable preferences on the admin serializer.
16
+ attribute :storefront_access, &:resolved_storefront_access
17
+
18
+ attribute :guest_checkout, &:resolved_guest_checkout
11
19
  end
12
20
  end
13
21
  end
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ # Minimal customer-group shape for the Store API. Exposed on
5
+ # `customers/me` so a storefront can branch on membership (e.g. an
6
+ # approved wholesale buyer) — pricing itself always resolves
7
+ # server-side.
8
+ class CustomerGroupSerializer < BaseSerializer
9
+ typelize name: :string
10
+
11
+ attributes :name
12
+ end
13
+ end
14
+ end
15
+ end
@@ -37,6 +37,14 @@ module Spree
37
37
  store = params&.dig(:store) || Spree::Current.store
38
38
  user.newsletter_subscriber(store)
39
39
  end
40
+
41
+ # Membership signal for storefront branching (e.g. wholesale approval);
42
+ # scoped to the request store (params first — Spree::Current.store falls
43
+ # back to the DEFAULT store, wrong on sibling stores' domains) so other
44
+ # stores' memberships never leak.
45
+ many :customer_groups,
46
+ proc { |groups, params| groups.for_store(params&.dig(:store) || Spree::Current.store) },
47
+ resource: proc { Spree.api.customer_group_serializer }
40
48
  end
41
49
  end
42
50
  end
@@ -56,7 +56,7 @@ module Spree
56
56
  'X-Spree-Webhook-Timestamp' => webhook_timestamp.to_s,
57
57
  'X-Spree-Webhook-Event' => @delivery.event_name
58
58
  }
59
- body = @delivery.payload.to_json
59
+ body = payload_json
60
60
  http_options = { open_timeout: TIMEOUT, read_timeout: TIMEOUT, verify_mode: ssl_verify_mode }
61
61
 
62
62
  # SSRF protection is disabled in development so webhooks can reach
@@ -77,10 +77,14 @@ module Spree
77
77
  end
78
78
 
79
79
  def generate_signature
80
- payload_json = @delivery.payload.to_json
81
80
  OpenSSL::HMAC.hexdigest('SHA256', @secret_key, "#{webhook_timestamp}.#{payload_json}")
82
81
  end
83
82
 
83
+ # Memoized so the signature is computed over exactly the bytes sent.
84
+ def payload_json
85
+ @payload_json ||= @delivery.deliverable_payload.to_json
86
+ end
87
+
84
88
  def webhook_timestamp
85
89
  @webhook_timestamp ||= Time.current.to_i
86
90
  end
@@ -56,13 +56,16 @@ module Spree
56
56
  )
57
57
  end
58
58
 
59
+ # Live credentials go over the wire but never into the delivery log.
60
+ persisted_payload, secrets = Spree::WebhookPayloadRedaction.split(payload)
61
+
59
62
  delivery = endpoint.webhook_deliveries.create!(
60
63
  event_name: event.name,
61
64
  event_id: event.id,
62
- payload: payload
65
+ payload: persisted_payload
63
66
  )
64
67
 
65
- Spree::WebhookDeliveryJob.perform_later(delivery.id)
68
+ Spree::WebhookDeliveryJob.perform_later(delivery.id, payload_secrets: secrets)
66
69
  rescue ActiveRecord::RecordNotUnique
67
70
  # Race condition: another thread already created this delivery — safe to ignore
68
71
  rescue StandardError => e
@@ -10,6 +10,8 @@ en:
10
10
  invalid_taxonomy_id: Invalid taxonomy id.
11
11
  must_specify_api_key: You must specify an API key.
12
12
  errors:
13
+ channel_inactive: The channel this API key is bound to is not active.
14
+ channel_mismatch: The requested channel does not match the channel this API key is bound to.
13
15
  guest_checkout_not_allowed: You must be signed in to complete checkout.
14
16
  storefront_login_required: Authentication required to access this store.
15
17
  negative_quantity: quantity is negative
data/config/routes.rb CHANGED
@@ -20,6 +20,9 @@ Spree::Core::Engine.add_routes do
20
20
  resources :currencies, only: [:index]
21
21
  resources :locales, only: [:index]
22
22
 
23
+ # Current channel context (never a list — see Store::ChannelController)
24
+ resource :channel, only: [:show], controller: 'channel'
25
+
23
26
  # Catalog
24
27
  resources :products, only: [:index, :show] do
25
28
  collection do
@@ -112,6 +112,7 @@ module Spree
112
112
  fulfillment_serializer: 'Spree::Api::V3::FulfillmentSerializer',
113
113
  address_serializer: 'Spree::Api::V3::AddressSerializer',
114
114
  customer_serializer: 'Spree::Api::V3::CustomerSerializer',
115
+ customer_group_serializer: 'Spree::Api::V3::CustomerGroupSerializer',
115
116
  country_serializer: 'Spree::Api::V3::CountrySerializer',
116
117
  channel_serializer: 'Spree::Api::V3::ChannelSerializer',
117
118
  product_publication_serializer: 'Spree::Api::V3::ProductPublicationSerializer',
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.0.rc3
4
+ version: 5.6.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-19 00:00:00.000000000 Z
11
+ date: 2026-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rswag-specs
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 5.6.0.rc3
75
+ version: 5.6.0.rc4
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
- version: 5.6.0.rc3
82
+ version: 5.6.0.rc4
83
83
  description: Spree's API
84
84
  email:
85
85
  - hello@spreecommerce.org
@@ -189,6 +189,7 @@ files:
189
189
  - app/controllers/spree/api/v3/store/carts/store_credits_controller.rb
190
190
  - app/controllers/spree/api/v3/store/carts_controller.rb
191
191
  - app/controllers/spree/api/v3/store/categories_controller.rb
192
+ - app/controllers/spree/api/v3/store/channel_controller.rb
192
193
  - app/controllers/spree/api/v3/store/countries_controller.rb
193
194
  - app/controllers/spree/api/v3/store/currencies_controller.rb
194
195
  - app/controllers/spree/api/v3/store/customer/addresses_controller.rb
@@ -291,6 +292,7 @@ files:
291
292
  - app/serializers/spree/api/v3/credit_card_serializer.rb
292
293
  - app/serializers/spree/api/v3/currency_serializer.rb
293
294
  - app/serializers/spree/api/v3/custom_field_serializer.rb
295
+ - app/serializers/spree/api/v3/customer_group_serializer.rb
294
296
  - app/serializers/spree/api/v3/customer_return_serializer.rb
295
297
  - app/serializers/spree/api/v3/customer_serializer.rb
296
298
  - app/serializers/spree/api/v3/delivery_method_serializer.rb
@@ -380,9 +382,9 @@ licenses:
380
382
  - BSD-3-Clause
381
383
  metadata:
382
384
  bug_tracker_uri: https://github.com/spree/spree/issues
383
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc3
385
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc4
384
386
  documentation_uri: https://docs.spreecommerce.org/
385
- source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc3
387
+ source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc4
386
388
  post_install_message:
387
389
  rdoc_options: []
388
390
  require_paths: