Class: Parse::Webhooks

Inherits:
Object
  • Object
show all
Extended by:
Registration
Includes:
Client::Connectable
Defined in:
lib/parse/webhooks.rb,
lib/parse/webhooks/payload.rb,
lib/parse/webhooks/registration.rb,
lib/parse/webhooks/trigger_audit.rb,
lib/parse/webhooks/replay_protection.rb

Overview

Interface to the CloudCode webhooks API.

Defined Under Namespace

Modules: Registration, ReplayProtection Classes: Payload, ResponseError, TriggerAudit

Constant Summary collapse

AUTH_TRIGGERS =

The authentication-side triggers (local underscore form). These carry a +_User+ / +_Session+ as the payload object but are NOT object save/delete triggers: the router runs no ActiveModel save/create/destroy callbacks for them, and Parse Server ignores their response body.

%i[
  before_login after_login after_logout before_password_reset_request
].freeze
LIVE_QUERY_TRIGGERS =

The LiveQuery triggers (local underscore form). Connection-global or event-scoped; Parse Server ignores their response body. Delivered over an HTTP webhook only in a co-located single-process LiveQuery setup.

%i[before_connect before_subscribe after_event].freeze
NON_OBJECT_TRIGGERS =

Every trigger whose payload is not an object save/delete/find shape. Parse Server's webhook response handler resolves +{}+ for all of these (the body is ignored), so the router normalizes their handler result to a success no-op rather than serializing a returned object into the response.

(AUTH_TRIGGERS + LIVE_QUERY_TRIGGERS).freeze
REJECTABLE_NON_OBJECT_TRIGGERS =

The +before*+ subset of NON_OBJECT_TRIGGERS for which a handler can DENY the operation. Parse Server only treats an +error+ response as a rejection -- a +success:false+ body resolves and lets the login / connect / subscribe / reset proceed. So, mirroring the +before_save+ convention, the router converts a +false+ return from one of these into a ResponseError (which serializes to +error+). +error!+ works for any trigger; the +after*+ variants fire after the fact and cannot undo it.

%i[
  before_login before_password_reset_request before_connect before_subscribe
].freeze
HTTP_PARSE_WEBHOOK =

The name of the incoming env containing the webhook key.

"HTTP_X_PARSE_WEBHOOK_KEY"
HTTP_PARSE_APPLICATION_ID =

The name of the incoming env containing the application id key.

"HTTP_X_PARSE_APPLICATION_ID"
CONTENT_TYPE =

The content type that needs to be sent back to Parse server.

"application/json"

Constants included from Registration

Registration::ALLOWED_HOOKS

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Registration

register_functions!, register_triggers!, register_webhook!, remove_all_functions!, remove_all_triggers!

Methods included from Client::Connectable

#client

Class Attribute Details

.allow_private_webhook_urlsObject



538
539
540
541
# File 'lib/parse/webhooks.rb', line 538

def allow_private_webhook_urls
  return @allow_private_webhook_urls unless @allow_private_webhook_urls.nil?
  ENV["PARSE_WEBHOOK_ALLOW_PRIVATE_URLS"] == "true"
end

.allow_unauthenticatedObject



521
522
523
524
# File 'lib/parse/webhooks.rb', line 521

def allow_unauthenticated
  return @allow_unauthenticated unless @allow_unauthenticated.nil?
  ENV["PARSE_WEBHOOK_ALLOW_UNAUTHENTICATED"] == "true"
end

.keyString

Returns the configured webhook key if available. By default it will use the value of ENV['PARSE_SERVER_WEBHOOK_KEY'] if not configured.

Returns:



504
505
506
507
508
509
# File 'lib/parse/webhooks.rb', line 504

def key=(value)
  @key = value
  # Reset the warn-once flag so a deployment that configures the key
  # after startup gets a clean state if the key is later cleared.
  @missing_key_warned = nil
end

.loggingBoolean

Returns whether to print additional logging information. You may also set this to :debug for additional verbosity.

Returns:

  • (Boolean)

    whether to print additional logging information. You may also set this to :debug for additional verbosity.



142
143
144
# File 'lib/parse/webhooks.rb', line 142

def logging
  @logging
end

Class Method Details

.call(env) ⇒ Array

Standard Rack call method. This method processes an incoming cloud code webhook request from Parse Server, validates it and executes any registered handlers for it. The result of the handler for the matching webhook request is sent back to Parse Server. If the handler raises a ResponseError, it will return the proper error response.

Parameters:

  • env (Hash)

    the environment hash in a Rack request.

Returns:

Raises:

  • Parse::Webhooks::ResponseError whenever Object, ActiveModel::ValidationError



551
552
553
554
# File 'lib/parse/webhooks.rb', line 551

def call(env)
  # Thraed safety
  dup.call!(env)
end

.call_route(type, className, payload = nil) ⇒ Object

Calls the set of registered webhook trigger blocks or the specific function block. This method is usually called when an incoming request from Parse Server is received.

Parameters:

  • payload (Parse::Webhooks::Payload) (defaults to: nil)

    the payload object received from the server.

  • type (Symbol)

    The type of cloud code webhook to register. This can be any of the supported routes. These are :before_save, :after_save,

  • className (String)

    if type is not :function, then this registers a trigger for the given className. Otherwise, className is treated to be the function name to register with Parse server.

Returns:

  • (Object)

    the result of the trigger or function.



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/parse/webhooks.rb', line 309

def call_route(type, className, payload = nil)
  type = type.to_s.underscore.to_sym #support camelcase
  className = className.parse_class if className.respond_to?(:parse_class)
  className = className.to_s

  return unless routes[type].present? && routes[type][className].present?
  registry = routes[type][className]

  # Track the header-derived ruby_initiated flag on the payload so
  # user code can introspect it (`payload.ruby_initiated?`). For the
  # framework's own callback-deduplication logic below we use the
  # stricter `trusted_ruby_initiated`, which additionally requires the
  # master key. The X-Parse-Request-Id header is client-controllable,
  # so honoring `_RB_` alone would let any client send `_RB_attacker`
  # and trick the framework into skipping server-side callbacks.
  # Server-side Parse-Stack saves use the master key by default, so
  # the AND is a safe condition for legitimate Ruby-initiated traffic.
  if payload
    request_id = payload&.raw&.dig(:headers, "x-parse-request-id") ||
                 payload&.raw&.dig("headers", "x-parse-request-id") ||
                 payload&.raw&.dig(:headers, "X-Parse-Request-Id") ||
                 payload&.raw&.dig("headers", "X-Parse-Request-Id")
    ruby_initiated = request_id&.start_with?("_RB_") || false
    payload.instance_variable_set(:@ruby_initiated, ruby_initiated)
    trusted_ruby_initiated = ruby_initiated && (payload.master? == true)
  else
    ruby_initiated = false
    trusted_ruby_initiated = false
  end

  # Pre-block: apply declarative write protection (guard :field, :mode)
  # to the parse_object that the handler will receive. Running BEFORE
  # the handler block means trusted server-side writes performed inside
  # the block are preserved -- only client-supplied values for guarded
  # fields are reverted.
  #
  # Notably we do NOT gate this on ruby_initiated. That flag derives
  # from a client-controlled X-Parse-Request-Id header, so trusting it
  # to bypass write protection would allow a one-header attack. Master
  # key requests still bypass via the master:/payload.master? check.
  if type == :before_save && payload && payload.object?
    klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil
    if klass && klass.respond_to?(:field_guards) && klass.field_guards.any?
      pre_obj = payload.parse_object # memoized; the handler sees this same instance
      if pre_obj.respond_to?(:apply_field_guards!)
        pre_obj.apply_field_guards!(
          master: payload.master? || false,
          is_new: payload.original.blank?
        )
      end
    end
  end

  if registry.is_a?(Array)
    result = registry.map { |hook| invoke_handler(payload, hook) }.last
  else
    result = invoke_handler(payload, registry)
  end

  if result.is_a?(Parse::Object)
    # if it is a Parse::Object, we will call the registered ActiveModel callbacks
    if type == :before_save
      # returning false from the callback block only runs the before_* callback
      # Skip prepare_save! when this request is trusted-Ruby-initiated
      # (both `_RB_` header AND master key), since Parse-Stack already
      # ran ActiveModel before_save callbacks locally. A client-spoofed
      # `_RB_` without master falls through and runs them here.
      unless trusted_ruby_initiated
        before_save_result = result.run_before_save_callbacks
        # If a before_save callback halted the chain (returned false), reject the save.
        if before_save_result == false
          raise Parse::Webhooks::ResponseError, "Save halted by before_save callback"
        end
        # Parse Server exposes no separate beforeCreate trigger, so the
        # beforeSave hook is the single point at which before_create must
        # run for a client-initiated create. Run it AFTER before_save, for
        # new objects only -- matching ActiveModel order (before_save wraps
        # before_create) and mirroring the afterSave hook, which runs
        # after_create then after_save. `original.nil?` marks a create.
        if payload && payload.original.nil?
          create_result = result.run_before_create_callbacks
          if create_result == false
            raise Parse::Webhooks::ResponseError, "Save halted by before_create callback"
          end
        end
      end
      # For before_save, return the changes payload (what Parse Server expects)
      result = result.changes_payload
    elsif type == :before_delete
      result.run_callbacks(:destroy) { false }
      result = true
    end
  elsif type == :before_save && result == false
    # If webhook block returns false, halt the save by throwing an error
    raise Parse::Webhooks::ResponseError, "Save halted by before_save webhook"
  elsif type == :before_save && (result == true || result.nil?)
    # Open Source Parse server does not accept true results on before_save hooks.
    result = {}
  end

  # Auth- and LiveQuery-trigger dispatch (beforeLogin/afterLogin/
  # afterLogout/beforePasswordResetRequest, beforeConnect/beforeSubscribe/
  # afterEvent). Parse Server IGNORES the response body for all of these --
  # its webhook response handler resolves {} regardless -- so the ONLY way
  # a handler can affect the operation is the error path, and only for the
  # "before" variants (a login/connect/subscribe/reset can be denied; an
  # after_* fires after the fact and cannot be undone).
  #
  # Crucially, Parse Server treats only an {error} response as a rejection:
  # a {success:false} body RESOLVES and lets the operation proceed. So a
  # handler that returns `false` to "deny login" would silently allow it.
  # We mirror the before_save convention and convert that false into a
  # ResponseError (=> {error} => Parse Server denies). `error!` works for
  # any of them (the call! rescue converts it). Every other return value --
  # including a Parse::Object a handler happened to return (e.g. the _User
  # from beforeLogin) -- is normalized to a success no-op so we never
  # serialize an object into the response or the redacted request log.
  if NON_OBJECT_TRIGGERS.include?(type)
    if result == false && REJECTABLE_NON_OBJECT_TRIGGERS.include?(type)
      raise Parse::Webhooks::ResponseError, "#{type} rejected by webhook handler"
    end
    result = true
  end

  # Guard-injection: when a handler returns a Hash (or true/nil normalized
  # to {}) for a class with field_guards, Parse Server would otherwise
  # merge the response with the client's original payload and persist
  # the client-supplied values for guarded fields. Inject the pre-built
  # parse_object's changes_payload entries for any guarded field so the
  # response carries the appropriate revert (Delete op on create, prior
  # value on update). The Parse::Object return path already runs through
  # changes_payload on the same memoized instance and therefore needs no
  # extra injection.
  if type == :before_save && result.is_a?(Hash) && payload && payload.object?
    guard_klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil
    if guard_klass && guard_klass.respond_to?(:field_guards) && guard_klass.field_guards.any?
      pre_obj = payload.parse_object # same memoized instance the pre-block step mutated
      if pre_obj.respond_to?(:changes_payload)
        guard_payload = pre_obj.changes_payload
        field_map = guard_klass.respond_to?(:field_map) ? guard_klass.field_map : {}
        guard_klass.field_guards.each_key do |field|
          remote = (field_map[field.to_sym] || field).to_s
          result[remote] = guard_payload[remote] if guard_payload.key?(remote)
        end
      end
    end
  end

  if type == :after_save && payload&.parse_object.present? && payload.parse_object.is_a?(Parse::Object)
    # Handle after_save callbacks intelligently based on request origin.
    # For trusted-Ruby-initiated saves (both `_RB_` header AND master
    # key), Parse Stack's local `run_callbacks :save` will fire
    # after_create and after_save callbacks after the REST response
    # returns; firing them again here would double-fire any side
    # effect (e.g. an `after_save :send_email` would send two emails
    # per save). For everything else -- client-initiated saves, or a
    # spoofed `_RB_` from a non-master client -- Parse Stack never had
    # a chance to run callbacks, so we fire them here.
    #
    # The decision depends ONLY on request origin, never on what the
    # handler returned. Parse Server discards the afterSave response
    # body entirely (it resolves {success} even if the handler throws),
    # so a handler that returns the parse_object -- the recommended
    # before_save pattern, easy to copy by mistake -- must NOT silently
    # suppress these callbacks. We normalize the result to `true` below
    # so a returned object never leaks into the response or the log.
    is_new = payload.original.nil?
    unless trusted_ruby_initiated
      payload.parse_object.run_after_create_callbacks if is_new
      payload.parse_object.run_after_save_callbacks
    end
    result = true
  end

  result
end

.dispatch_deferred(env, payload)

This method returns an undefined value.

Run any Parse::Webhooks::Payload#after_response callbacks a handler registered, AFTER the response has been produced. Prefers the server's rack.after_reply hook (Puma / Unicorn), which fires once the response is flushed to the socket on the same worker thread; falls back to a detached thread when the server does not provide it (e.g. WEBrick). Each callback is isolated so one raising neither aborts the others nor reaches the client. No-op when nothing was deferred.

Parameters:



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/parse/webhooks.rb', line 270

def dispatch_deferred(env, payload)
  return if payload.nil? || !payload.respond_to?(:deferred_callbacks)
  callbacks = payload.deferred_callbacks
  return if callbacks.blank?

  runner = proc do
    callbacks.each do |cb|
      begin
        cb.call
      rescue => e
        warn "[Webhooks::after_response] deferred callback raised: #{e.class}: #{e.message}"
      end
    end
  end

  # Enqueueing must never break an otherwise-successful response: this runs
  # just before `response.finish`, so a raise here (a frozen after_reply
  # array, thread exhaustion) would discard the buffered reply and surface
  # as a 500. Failing to schedule deferred work degrades to "not run",
  # never to a failed response.
  begin
    after_reply = env.is_a?(Hash) ? env["rack.after_reply"] : nil
    if after_reply.respond_to?(:<<)
      after_reply << runner
    else
      Thread.new(&runner)
    end
  rescue => e
    warn "[Webhooks::after_response] could not schedule deferred work: #{e.class}: #{e.message}"
  end
  nil
end

.error(data = false) ⇒ Hash

Generates an error response for Parse Server.

Parameters:

  • data (Object) (defaults to: false)

    the data to send back with the error.

Returns:

  • (Hash)

    a error data payload



496
497
498
# File 'lib/parse/webhooks.rb', line 496

def error(data = false)
  { error: data }.to_json
end

.invoke_handler(payload, block) ⇒ Object

Evaluate a single registered handler block in the scope of the payload.

The block runs with self bound to the Payload, so a handler can call parse_object, params, error!, etc. directly -- exactly as it could under the historical payload.instance_exec(payload, &block) invocation. The difference is the return semantics:

  • return value returns value as the handler result (instead of the LocalJumpError: unexpected return that bare instance_exec raised when the block was defined inside a method).
  • The legacy idioms still work unchanged: the last expression's value, next value, and break value all return value, and raise propagates untouched (so error! / before_save rejections behave the same).

This is achieved by attaching the block as a singleton method on the per-request payload (so return gets method semantics) and removing it afterward. The payload is a per-request instance, so this neither leaks nor mutates shared state across threads.

Arity is matched to the old instance_exec(payload, ...) contract: a zero-arity block (do ... end / proc { }) is called with no args; a block that declares a parameter (do |payload| ... end) or a splat receives the payload.

Parameters:

  • payload (Parse::Webhooks::Payload)

    the request payload (becomes self).

  • block (Proc)

    the registered handler block.

Returns:

  • (Object)

    the handler's result value.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/parse/webhooks.rb', line 233

def invoke_handler(payload, block)
  name = :"__parse_webhook_handler_#{block.object_id}__"
  payload.define_singleton_method(name, &block)
  handler = payload.method(name)
  begin
    # Match the old `payload.instance_exec(payload, &block)` arity
    # leniency: a zero-arity block is called bare; otherwise it receives
    # the payload, plus a nil for each additional REQUIRED positional so a
    # block declaring `|payload, extra|` (or more) does not raise — under
    # instance_exec those surplus params were silently nil. `arity` is
    # negative for optional/splat params (e.g. -1 for `|*a|`, -2 for
    # `|a, *b|`); `~arity` gives the required count in that case.
    if handler.arity == 0
      handler.call
    else
      required = handler.arity.negative? ? ~handler.arity : handler.arity
      handler.call(payload, *Array.new([required - 1, 0].max))
    end
  ensure
    singleton = payload.singleton_class
    if singleton.method_defined?(name) || singleton.private_method_defined?(name)
      singleton.send(:remove_method, name)
    end
  end
end

.route(type, className) { ... } ⇒ OpenStruct

Internally registers a route for a specific webhook trigger or function. :before_delete, :after_delete or :function.

Parameters:

  • type (Symbol)

    The type of cloud code webhook to register. This can be any of the supported routes. These are :before_save, :after_save,

  • className (String)

    if type is not :function, then this registers a trigger for the given className. Otherwise, className is treated to be the function name to register with Parse server.

Yields:

  • the block that will handle of the webhook trigger or function.

Returns:

  • (OpenStruct)


163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/parse/webhooks.rb', line 163

def route(type, className, &block)
  type = type.to_s.underscore.to_sym #support camelcase
  if type != :function && className.respond_to?(:parse_class)
    className = className.parse_class
  end
  className = className.to_s
  # Parse Server has no beforeCreate/afterCreate webhook trigger; the
  # create variants are ActiveModel callbacks that run inside the
  # beforeSave/afterSave handler for new objects. Point callers there
  # rather than registering a route that can never fire.
  if type == :before_create || type == :after_create
    save = type == :before_create ? :before_save : :after_save
    raise ArgumentError,
          "There is no #{type} webhook. Register `webhook :#{save}` instead — " \
          "your #{type} ActiveModel callbacks run inside the #{save} handler " \
          "for new objects (registering #{save} enables BOTH the #{save} and " \
          "#{type} callbacks)."
  end
  if routes[type].nil? || block.respond_to?(:call) == false
    raise ArgumentError, "Invalid Webhook registration trigger #{type} #{className}"
  end

  # AfterSave/AfterDelete hooks support more than one
  if type == :after_save || type == :after_delete
    routes[type][className] ||= []
    routes[type][className].push block
  else
    routes[type][className] = block
  end
  @routes
end

.routesOpenStruct

A hash-like structure composing of all the registered webhook triggers and functions. These are :before_save, :after_save, :before_delete, :after_delete or :function.

Returns:

  • (OpenStruct)


148
149
150
151
152
# File 'lib/parse/webhooks.rb', line 148

def routes
  return @routes unless @routes.nil?
  r = Parse::API::Hooks::TRIGGER_NAMES_LOCAL + [:function]
  @routes = OpenStruct.new(r.reduce({}) { |h, t| h[t] = {}; h })
end

.run_function(name, params) ⇒ Object

Run a locally registered webhook function. This bypasses calling a function through Parse-Server if the method handler is registered locally.

Returns:

  • (Object)

    the result of the function.



198
199
200
201
202
203
# File 'lib/parse/webhooks.rb', line 198

def run_function(name, params)
  payload = Payload.new
  payload.function_name = name
  payload.params = params
  call_route(:function, name, payload)
end

.success(data = true) ⇒ Hash

Generates a success response for Parse Server.

Parameters:

  • data (Object) (defaults to: true)

    the data to send back with the success.

Returns:

  • (Hash)

    a success data payload



489
490
491
# File 'lib/parse/webhooks.rb', line 489

def success(data = true)
  { success: data }.to_json
end

.trigger_audit(pretty: false, network: true, client: nil, include_framework: false) ⇒ Hash, String

Audit trigger logic across all registered classes, cross-referencing model ActiveModel callbacks, locally registered webhook blocks, and the triggers registered on Parse Server. See TriggerAudit.

The server comparison reads the master-key-only hooks/triggers endpoint, so network: true (the default) requires a master-key client. Pass network: false for a credential-free audit of callbacks vs. local routes.

Parameters:

  • pretty (Boolean) (defaults to: false)

    when true, return the human-readable String summary instead of the Hash report.

  • network (Boolean) (defaults to: true)

    query Parse Server for registered triggers.

  • client (Parse::Client, nil) (defaults to: nil)

    optional client override.

  • include_framework (Boolean) (defaults to: false)

    include gem-internal callbacks.

Returns:

  • (Hash, String)

    the report Hash, or the pretty String when pretty: true.



494
495
496
497
498
499
# File 'lib/parse/webhooks/trigger_audit.rb', line 494

def trigger_audit(pretty: false, network: true, client: nil, include_framework: false)
  audit = TriggerAudit.new(
    network: network, client: client, include_framework: include_framework
  )
  pretty ? audit.pretty : audit.to_h
end

.trigger_class_from_path(path) ⇒ String?

Extract the Parse class name from a webhook request path. Parse Server registers each trigger at <endpoint>/<triggerName>/<className> (functions at <endpoint>/<functionName>), so for a trigger the class is the last segment and the second-to-last is a known trigger name. Returns nil for a function path, a path with no recognizable trigger segment, or a className that fails the conservative charset check (Parse class names are [A-Za-z0-9_], built-ins prefixed with _). The charset gate keeps an attacker-supplied path (reachable when allow_unauthenticated is set) from injecting an arbitrary routing / scrub key.

Parameters:

  • path (String)

    the request PATH_INFO.

Returns:

  • (String, nil)

    the sanitized class name, or nil.



569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
# File 'lib/parse/webhooks.rb', line 569

def trigger_class_from_path(path)
  segments = path.to_s.split("/").reject(&:empty?)
  return nil if segments.size < 2
  trigger, klass = segments[-2], segments[-1]
  # register_triggers! builds the URL with the LOCAL snake_case trigger
  # name (`after_find`), while Parse Server sends the camelCase form in the
  # body — accept both so the path segment is recognized either way.
  known = (Parse::API::Hooks::TRIGGER_NAMES + Parse::API::Hooks::TRIGGER_NAMES_LOCAL).map(&:to_s)
  return nil unless known.include?(trigger)
  # Allow a leading `@` for the Parse pseudo-classes (`@Connect` for the
  # connection-global LiveQuery trigger, `@File` for file triggers): the
  # SDK encodes the className in the per-trigger URL, so beforeConnect
  # would not route without it. Mirrors the trigger-className validator
  # (Parse::API::PathSegment.trigger_class_name!). Still anchored and
  # charset-limited -- this gate keeps an attacker-supplied path (reachable
  # only under allow_unauthenticated) from injecting an arbitrary routing
  # / scrub key.
  return nil unless /\A@?_?[A-Za-z][A-Za-z0-9_]*\z/.match?(klass)
  klass
end

Instance Method Details

#keyString

The Parse Webhook Key to be used for authenticating webhook requests. See key on setting this value.

Returns:



129
130
131
# File 'lib/parse/webhooks.rb', line 129

def key
  self.class.key
end