alula-ruby 1.6.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e8587f55af4a330e24dfdb5f5e6df0ca0cb581f16d38a05b132925b03df0152
4
- data.tar.gz: 5af4c678137eb61daf3d7416644a95beaffa85c41870294775b3e4d801a88ccb
3
+ metadata.gz: 4c9520f289b6a7b5175a8a32b3a1934d6f1ecae534f4054cfa043533d7dd6572
4
+ data.tar.gz: 11bf384b95594822f1a8f4c93c71f294409e78e12d63bbad4f700f3060dd222e
5
5
  SHA512:
6
- metadata.gz: 744017971308b19fc198941335cfe71b2cdc4bbf0c223d41fb7dbb9cc325cfe13f667f7f59fd7c9d8dbb6a2fd7c2e38d301566e24e7729239c4fd213083f71f7
7
- data.tar.gz: 2343e372fd84218e713abfaa2574e4ead77a180c27057cee0bdfc5fcd354184ce01b953041ebf87b503e743622dedf51cb9a357d33cd4a7ead1f0eb378d7bab1
6
+ metadata.gz: 68f0c65cda2ed0d8c83e7ac321057f986e39ccadb89df5c7c58a37b1490b46ad3158b776370f87dc8cc6c8f1e68a068f88c8f44ad4d8c5a6581344472ca84021
7
+ data.tar.gz: 9608f45a747a310a1f13ef6341f458ac37b6ed0ba4a5ad30e485532d7424875dd2991562f9b6402152191167fc0a8eccfc0d092b8a84cb3ece489af73d9e1581
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.0.6
1
+ 3.3.0
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM ruby:3.0.6-alpine
1
+ FROM ruby:3.3.0-alpine
2
2
 
3
3
  RUN gem install bundler
4
4
 
data/VERSION.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  | Version | Date | Description |
4
4
  | ------- | --------- | --------------------------------------------------------------------------- |
5
+ | v1.8.0 | 2024-06-19 | Use Video API endpoints instead of core API passthrough |
6
+ | v1.7.0 | 2024-06-13 | Add DELETE, PATCH/POST, unregister capabilities for cameras |
5
7
  | v1.6.0 | 2024-01-26 | Add user language attribute |
6
8
  | v1.5.0 | 2024-01-09 | Add more known errors that use a different format |
7
9
  | v1.4.0 | 2024-01-04 | Add more known errors that use a different format, bugfix for `NotImplementedError` raising |
@@ -73,6 +73,7 @@ services:
73
73
  IPDAPI_WS_WORK_VIGILANCE_CANCEL_BATCH_WINDOW_MSEC: ${IPDAPI_WS_WORK_VIGILANCE_CANCEL_BATCH_WINDOW_MSEC:-10000}
74
74
  IPDAPI_WS_WORK_VIGILANCE_CANCEL_TERMINATE_DELAY_MSEC: ${IPDAPI_WS_WORK_VIGILANCE_CANCEL_TERMINATE_DELAY_MSEC:-500}
75
75
  IPDAPI_WS_WORK_VIGILANCE_TERMINATE_DELAY_MSEC: ${IPDAPI_WS_WORK_VIGILANCE_TERMINATE_DELAY_MSEC:-500}
76
+ LOG_LEVEL: warn
76
77
  MARIADB_ADDRESS: mariadb
77
78
  MARIADB_PASS: ""
78
79
  MARIADB_USER: root
@@ -82,13 +83,14 @@ services:
82
83
  MONGODB_USER: ipd
83
84
  RMQ_EPHEMERAL_ADDRESS: amqp://rmq-ephemeral
84
85
  RMQ_PERSISTENT_ADDRESS: amqp://rmq-persistent
86
+ REDIS_ADDRESS: redis-ipdapi
85
87
  healthcheck:
86
- interval: 15s
87
- retries: 5
88
+ interval: 5s
89
+ retries: 10
88
90
  start_period: 30s
89
- test: curl -s http://localhost/public/v1/healthcheck/default || exit -1
90
- timeout: 15s
91
- image: 6z1wlx5zf1.execute-api.us-east-1.amazonaws.com/alula/ipdapi:latest-master
91
+ test: apk add curl && curl -s http://localhost/public/v1/healthcheck/default || exit -1
92
+ timeout: 10s
93
+ image: 613707345027.dkr.ecr.us-east-1.amazonaws.com/api/ipdapi:staging
92
94
  networks:
93
95
  mesh_net:
94
96
  profiles:
@@ -101,23 +103,24 @@ services:
101
103
  ports:
102
104
  - "8080:80"
103
105
  mariadb:
106
+ hostname: mariadb
107
+ image: mariadb:10.3
104
108
  command:
105
109
  - --wait_timeout=28800
106
110
  - --max_connections=2048
107
111
  environment:
108
- MYSQL_ALLOW_EMPTY_PASSWORD: "true"
109
- healthcheck:
110
- interval: 15s
111
- retries: 5
112
- test: [CMD, mysqladmin, ping, -h, localhost]
113
- timeout: 15s
114
- image: mariadb:10.3
115
- networks:
116
- mesh_net:
112
+ MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
117
113
  volumes:
118
114
  - mariadb_data:/var/lib/mysql
119
115
  - ./utils/mariadb/init:/docker-entrypoint-initdb.d
120
116
  - ./utils/mariadb/conf:/etc/mysql/conf.d
117
+ healthcheck:
118
+ test: mysql -D mysql --silent --execute "SELECT 1;"
119
+ interval: 30s
120
+ timeout: 10s
121
+ retries: 5
122
+ networks:
123
+ mesh_net:
121
124
  mongodb:
122
125
  command: --replSet api0 --bind_ip_all
123
126
  environment:
@@ -154,6 +157,19 @@ services:
154
157
  image: rabbitmq:management
155
158
  networks:
156
159
  mesh_net:
160
+ redis-ipdapi:
161
+ container_name: redis-ipdapi
162
+ image: redis:6.0.9-alpine
163
+ healthcheck:
164
+ test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
165
+ interval: 1s
166
+ timeout: 5s
167
+ retries: 30
168
+ networks:
169
+ mesh_net:
170
+ ports:
171
+ - 6379:6379
172
+ restart: always
157
173
  version: "3.9"
158
174
  volumes:
159
175
  dcp-mongo:
@@ -4,4 +4,4 @@ echo "INIT $(date +%s)" > /var/run/alula/ipdapi-status
4
4
  cat scripts/npm | grep -v npm | bash
5
5
  node app/migrations.js --create --up
6
6
  echo "READY $(date +%s)" >> /var/run/alula/ipdapi-status
7
- npm start
7
+ exec node app/main.js
@@ -13,6 +13,7 @@ module Alula
13
13
  attributes: as_patchable_json
14
14
  }
15
15
  }
16
+ payload[:data].delete(:id) if video_request?(resource_url)
16
17
 
17
18
  response = Alula::Client.request(:patch, resource_url, payload, {})
18
19
 
@@ -79,6 +80,10 @@ module Alula
79
80
  raise Alula::UnknownError, "Unknown HTTP response code, aborting. Code: #{response.http_status}"
80
81
  end
81
82
  end
83
+
84
+ def video_request?(resource_path)
85
+ resource_path.match(%r{^/video})
86
+ end
82
87
  end
83
88
  end
84
89
  end
data/lib/alula/client.rb CHANGED
@@ -7,7 +7,7 @@ module Alula
7
7
  class << self
8
8
  DEFAULT_CUSTOM_OPTIONS = {
9
9
  omitRelationships: true
10
- }
10
+ }.freeze
11
11
 
12
12
  def config
13
13
  @_config ||= Alula::ClientConfiguration.new
@@ -18,13 +18,13 @@ module Alula
18
18
  end
19
19
 
20
20
  def request(http_method, resource_path, filters = {}, opts = {})
21
- unless resource_path.match(%r{^/public/v1})
22
- validate_request!(http_method, resource_path)
23
- end
24
-
21
+ validate_request!(http_method, resource_path) unless resource_path.match(%r{^/public/v1})
25
22
  request_opts = build_options(http_method, resource_path, filters, opts)
23
+ api_url = video_request?(resource_path) ? config.video_api_url : config.api_url
24
+ request_resource_path = video_request?(resource_path) ? resource_path.gsub(%r{^/video}, '') : resource_path
25
+
26
26
  # TODO: Handle network failures
27
- response = make_request(http_method, config.api_url + resource_path, request_opts)
27
+ response = make_request(http_method, api_url + request_resource_path, request_opts)
28
28
 
29
29
  begin
30
30
  resp = AlulaResponse.from_httparty_response(response)
@@ -41,7 +41,11 @@ module Alula
41
41
  def validate_request!(http_method, resource_path)
42
42
  config.ensure_api_key_set
43
43
  config.ensure_api_url_set
44
- config.ensure_customer_id_set if resource_path.match(%r{^/video})
44
+ if video_request?(resource_path)
45
+ config.ensure_video_api_url_set
46
+ config.ensure_video_api_key_set
47
+ config.ensure_customer_id_set
48
+ end
45
49
  ensure_method_allowable(http_method)
46
50
  config.ensure_role_set if %i[patch post put].include? http_method
47
51
  end
@@ -51,9 +55,13 @@ module Alula
51
55
  end
52
56
 
53
57
  def ensure_method_allowable(http_method)
54
- unless %i[get post put patch delete].include?(http_method)
55
- raise "Unable to send a request with #{http_method} http method"
56
- end
58
+ return if %i[get post put patch delete].include?(http_method)
59
+
60
+ raise "Unable to send a request with #{http_method} http method"
61
+ end
62
+
63
+ def video_request?(resource_path)
64
+ resource_path.match(%r{^/video})
57
65
  end
58
66
 
59
67
  def build_options(http_method, resource_path, filters = {}, opts = {})
@@ -63,6 +71,7 @@ module Alula
63
71
  'User-Agent': "#{Alula::Client.config.user_agent || 'No Agent Set'}/alula-ruby v#{Alula::VERSION}"
64
72
  }
65
73
  }.merge(opts)
74
+ handle_video_request(request_opts) if video_request?(resource_path) # Add token for all video requests
66
75
 
67
76
  request_opts[:headers]['Content-Type'] = 'application/json' unless opts[:multipart]
68
77
  case http_method
@@ -70,11 +79,7 @@ module Alula
70
79
  :post
71
80
  request_opts[:body] = opts[:multipart] ? filters : JSON.generate(filters)
72
81
  when :get
73
- if resource_path.match(%r{^/(rest|rpc)})
74
- request_opts = request_opts.merge build_rest_options(filters)
75
- elsif resource_path.match(%r{^/video})
76
- add_customer_id(request_opts)
77
- end
82
+ request_opts = request_opts.merge build_rest_options(filters) if resource_path.match(%r{^/(rest|rpc)})
78
83
  when :delete
79
84
  # Nothing special
80
85
  end
@@ -111,8 +116,17 @@ module Alula
111
116
  filters
112
117
  end
113
118
 
114
- def add_customer_id(request_opts)
115
- request_opts[:query] = { 'customerId' => config.customer_id }
119
+ def handle_video_request(request_opts)
120
+ if Alula::Client.config.internal
121
+ set_token(request_opts, { internal: true })
122
+ else
123
+ set_token(request_opts, { customerId: Alula::Client.config.customer_id })
124
+ end
125
+ end
126
+
127
+ def set_token(request_opts, payload)
128
+ jwt = TokenExchange.fetch_video_token(payload)
129
+ request_opts[:headers]['token'] = jwt
116
130
  end
117
131
  end
118
132
  end
@@ -1,7 +1,7 @@
1
1
  module Alula
2
2
  class ClientConfiguration
3
- attr_accessor :api_url, :debug, :user_agent
4
- REQUEST_ATTRIBUTES = %i[api_key customer_id]
3
+ attr_accessor :api_url, :video_api_url, :debug, :user_agent
4
+ REQUEST_ATTRIBUTES = %i[api_key video_api_key customer_id internal]
5
5
 
6
6
  # Using RequestStore so we're thread safe even with our non-thread-safe architecture :(
7
7
  REQUEST_ATTRIBUTES.each do |prop|
@@ -42,11 +42,25 @@ module Alula
42
42
  end
43
43
  end
44
44
 
45
+ def ensure_video_api_url_set
46
+ return unless video_api_url.nil?
47
+
48
+ raise Alula::NotConfiguredError,
49
+ 'did you forget to set the Alula::Client.config.video_api_url config option?'
50
+ end
51
+
52
+ def ensure_video_api_key_set
53
+ return if video_api_key
54
+
55
+ raise Alula::NotConfiguredError,
56
+ 'Set your video API access token before making requests to the video API'
57
+ end
58
+
45
59
  def ensure_customer_id_set
46
- unless customer_id
47
- raise Alula::NotConfiguredError,
48
- 'Set your customer_id before making requests to the video API'
49
- end
60
+ return if internal || customer_id
61
+
62
+ raise Alula::NotConfiguredError,
63
+ 'Set your customer_id before making requests to the video API'
50
64
  end
51
65
 
52
66
  def ensure_role_set
data/lib/alula/errors.rb CHANGED
@@ -87,6 +87,8 @@ module Alula
87
87
  NotFoundError.new(message)
88
88
  when 'Device Not Found'
89
89
  NotFoundError.new(message)
90
+ when 'Unable to unregister device'
91
+ NotFoundError.new(message)
90
92
  else
91
93
  Alula.logger.error response
92
94
  raise NotImplementedError, "Unable to derive error for #{response.data['error'].to_json}"
@@ -128,6 +130,8 @@ module Alula
128
130
  NotFoundError.new(message)
129
131
  when 'Device Not Found'
130
132
  NotFoundError.new(message)
133
+ when 'Unable to unregister device'
134
+ NotFoundError.new(message)
131
135
  else
132
136
  Alula.logger.error response
133
137
  raise NotImplementedError, "Unable to derive error for #{response.data['errors'].to_json}"
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ require 'base64'
3
+ require 'openssl'
4
+
5
+ module Alula
6
+ # Helper class to build JWT tokens
7
+ # Used for internal video API calls or video API calls with a customer ID
8
+ module JwtHelper
9
+ ONE_HOUR = 60 * 60
10
+ class << self
11
+ def build_jwt_token(jwt_payload)
12
+ jwt_secret = Alula::Client.config.video_api_key
13
+
14
+ header = build_jwt_header
15
+
16
+ token_data = add_timestamp_to_payload(jwt_payload)
17
+
18
+ encoded_header = convert_to_base64(JSON.generate(header))
19
+ encoded_data = convert_to_base64(JSON.generate(token_data))
20
+
21
+ token = "#{encoded_header}.#{encoded_data}"
22
+ signature = sign_token(token, jwt_secret)
23
+
24
+ "#{token}.#{signature}"
25
+ end
26
+
27
+ def build_jwt_header
28
+ {
29
+ 'typ' => 'JWT',
30
+ 'alg' => 'HS256'
31
+ }
32
+ end
33
+
34
+ def add_timestamp_to_payload(payload)
35
+ current_timestamp = Time.now.to_i
36
+ payload.merge(
37
+ {
38
+ 'iat' => current_timestamp,
39
+ 'exp' => current_timestamp + ONE_HOUR # expiry time is 60 minutes from time of creation
40
+ }
41
+ )
42
+ end
43
+
44
+ def sign_token(token, secret)
45
+ signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret, token)
46
+ convert_to_base64(signature)
47
+ end
48
+
49
+ def convert_to_base64(source)
50
+ # Encode in classical base64,
51
+ encoded_source = Base64.strict_encode64(source)
52
+
53
+ # Remove padding equal characters,
54
+ encoded_source = encoded_source.gsub('=', '')
55
+
56
+ # Replace characters according to base64url specifications,
57
+ encoded_source.tr('+/', '-_')
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alula
4
+ class VideoUnregisterProc < Alula::RpcResource
5
+ class Response < Alula::RpcResponse
6
+ end
7
+
8
+ def self.call(device_id:)
9
+ payload = {
10
+ deviceId: device_id
11
+ }
12
+
13
+ request(
14
+ http_method: :post,
15
+ path: '/rpc/v1/video/unregister',
16
+ payload: payload,
17
+ handler: Response
18
+ )
19
+ end
20
+ end
21
+ end
@@ -1,17 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/jwt_helper'
4
+
1
5
  module Alula
6
+ # Helper class to generate OAuth access tokens (core API) and JWT tokens (VSP API)
2
7
  class TokenExchange
3
- def self.token_for_user(user_id)
4
- url = '/rest/v1/oauth/accesstokens'
5
- payload = { data: { attributes: { userId: user_id } } }
6
- opts = {}
7
-
8
- response = Alula::Client.request(:post, url, payload, opts)
9
-
10
- if response.ok?
11
- ImpersonatedToken.new(response.data['data']['attributes'])
12
- else
13
- error_class = AlulaError.for_response(response)
14
- raise error_class
8
+ class << self
9
+ def token_for_user(user_id)
10
+ url = '/rest/v1/oauth/accesstokens'
11
+ payload = { data: { attributes: { userId: user_id } } }
12
+ opts = {}
13
+
14
+ response = Alula::Client.request(:post, url, payload, opts)
15
+
16
+ if response.ok?
17
+ ImpersonatedToken.new(response.data['data']['attributes'])
18
+ else
19
+ error_class = AlulaError.for_response(response)
20
+ raise error_class
21
+ end
22
+ end
23
+
24
+ def fetch_video_token(payload)
25
+ cache_key = generate_cache_key(payload)
26
+ cached_token, expiry = retrieve_cached_token(cache_key)
27
+
28
+ if cached_token && Time.now.to_i < expiry
29
+ jwt_token = cached_token
30
+ else
31
+ jwt_token = build_jwt_token(payload)
32
+ expiry = Time.now.to_i + JwtHelper::ONE_HOUR # 1 hour expiry
33
+ store_token_in_cache(cache_key, jwt_token, expiry)
34
+ end
35
+
36
+ jwt_token
37
+ end
38
+
39
+ private
40
+
41
+ def build_jwt_token(payload)
42
+ JwtHelper.build_jwt_token(payload)
43
+ end
44
+
45
+ def store_token_in_cache(cache_key, jwt_token, expiry)
46
+ @token_cache ||= {}
47
+ @token_cache[cache_key] = [jwt_token, expiry]
48
+ end
49
+
50
+ def retrieve_cached_token(cache_key)
51
+ @token_cache ||= {}
52
+ @token_cache[cache_key]
53
+ end
54
+
55
+ def generate_cache_key(payload)
56
+ if payload[:internal]
57
+ 'jwt_token_internal'
58
+ elsif payload[:customerId]
59
+ "jwt_token_customer_#{payload[:customerId]}"
60
+ else
61
+ raise ArgumentError, 'Invalid payload'
62
+ end
15
63
  end
16
64
  end
17
65
 
@@ -32,6 +32,11 @@ module Alula
32
32
 
33
33
  api_name :video
34
34
 
35
+ # Instance method
36
+ def resource_url(id = self.id)
37
+ "/#{self.class.api_name}/v1/#{self.class.resource_name}/#{id}"
38
+ end
39
+
35
40
  def construct_from(json_object)
36
41
  @raw_data = json_object.dup
37
42
  @values = json_object
@@ -2,15 +2,88 @@ module Alula
2
2
  module Video
3
3
  class Device < Alula::Video::BaseResource
4
4
  extend Alula::ApiOperations::Request
5
+ extend Alula::ApiOperations::Save
6
+ extend Alula::ApiOperations::Delete
5
7
 
6
- field :account_id, type: :string
7
- field :active, type: :boolean
8
- field :brand, type: :string
9
- field :hardware_id, type: :string
10
- field :manufacturer, type: :string
11
- field :name, type: :string
12
- field :serial_number, type: :string
13
- field :verification_code, type: :string
8
+ field :account_id,
9
+ type: :string,
10
+ patchable_by: %i[system station dealer user],
11
+ creatable_by: %i[system station dealer user]
12
+
13
+ field :active,
14
+ type: :boolean,
15
+ patchable_by: %i[system station dealer user],
16
+ creatable_by: %i[system station dealer user]
17
+
18
+ field :brand,
19
+ type: :string,
20
+ patchable_by: %i[system station dealer user],
21
+ creatable_by: %i[system station dealer user]
22
+
23
+ field :hardware_id,
24
+ type: :string,
25
+ patchable_by: %i[system station dealer user],
26
+ creatable_by: %i[system station dealer user]
27
+
28
+ field :manufacturer,
29
+ type: :string,
30
+ patchable_by: %i[system station dealer user],
31
+ creatable_by: %i[system station dealer user]
32
+
33
+ field :name,
34
+ type: :string,
35
+ patchable_by: %i[system station dealer user],
36
+ creatable_by: %i[system station dealer user]
37
+
38
+ field :serial_number,
39
+ type: :string,
40
+ patchable_by: %i[system station dealer user],
41
+ creatable_by: %i[system station dealer user]
42
+
43
+ field :verification_code,
44
+ type: :string,
45
+ patchable_by: %i[system station dealer user],
46
+ creatable_by: %i[system station dealer user]
47
+
48
+ field :timezone,
49
+ type: :string,
50
+ patchable_by: %i[system station dealer user],
51
+ creatable_by: %i[system station dealer user]
52
+
53
+ field :timezone_id,
54
+ type: :string,
55
+ patchable_by: %i[system station dealer user],
56
+ creatable_by: %i[system station dealer user]
57
+
58
+ field :daylight_saving,
59
+ type: :number,
60
+ patchable_by: %i[system station dealer user],
61
+ creatable_by: %i[system station dealer user]
62
+
63
+ field :time_format,
64
+ type: :number,
65
+ patchable_by: %i[system station dealer user],
66
+ creatable_by: %i[system station dealer user]
67
+
68
+ field :customer_id,
69
+ type: :string,
70
+ patchable_by: %i[system station dealer user],
71
+ creatable_by: %i[system station dealer user]
72
+
73
+ field :online,
74
+ type: :object,
75
+ patchable_by: %i[system station dealer user],
76
+ creatable_by: %i[system station dealer user]
77
+
78
+ field :settings,
79
+ type: :object,
80
+ patchable_by: %i[system station dealer user],
81
+ creatable_by: %i[system station dealer user]
82
+
83
+ field :model,
84
+ type: :string,
85
+ patchable_by: %i[system station dealer user],
86
+ creatable_by: %i[system station dealer user]
14
87
 
15
88
  def is_jsw?
16
89
  manufacturer == 'jsw'
data/lib/alula/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alula
4
- VERSION = '1.6.0'
4
+ VERSION = '1.8.0'
5
5
  end
data/lib/alula.rb CHANGED
@@ -94,6 +94,7 @@ require_relative 'alula/procedures/user_plansvideo_price_get'
94
94
  require_relative 'alula/procedures/user_get_locked_proc'
95
95
  require_relative 'alula/procedures/user_unlock_proc'
96
96
  require_relative 'alula/procedures/user_password_reset_proc'
97
+ require_relative 'alula/procedures/video_unregister_proc'
97
98
 
98
99
  module Alula
99
100
  #
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alula-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Titus Johnson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-29 00:00:00.000000000 Z
11
+ date: 2024-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -210,6 +210,7 @@ files:
210
210
  - lib/alula/errors.rb
211
211
  - lib/alula/filter_builder.rb
212
212
  - lib/alula/helpers/device_attribute_translations.rb
213
+ - lib/alula/helpers/jwt_helper.rb
213
214
  - lib/alula/list_object.rb
214
215
  - lib/alula/meta.rb
215
216
  - lib/alula/monkey_patches.rb
@@ -240,6 +241,7 @@ files:
240
241
  - lib/alula/procedures/user_transfer_reject.rb
241
242
  - lib/alula/procedures/user_transfer_request.rb
242
243
  - lib/alula/procedures/user_unlock_proc.rb
244
+ - lib/alula/procedures/video_unregister_proc.rb
243
245
  - lib/alula/query_interface.rb
244
246
  - lib/alula/rate_limit.rb
245
247
  - lib/alula/relationship_attributes.rb