k9o 1.2.0-aarch64-linux → 1.5.0-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -1
  3. data/README.md +1575 -180
  4. data/k9o.gemspec +2 -2
  5. data/lib/k9o/_ffi/kibo_kms_rest_ffi.rb +928 -25
  6. data/lib/k9o/_ffi/libkibo_kms_rest_ffi.so +0 -0
  7. data/lib/k9o/ai.rb +15 -0
  8. data/lib/k9o/analytics.rb +403 -0
  9. data/lib/k9o/approvals.rb +253 -0
  10. data/lib/k9o/audit.rb +162 -14
  11. data/lib/k9o/client.rb +28 -9
  12. data/lib/k9o/compute.rb +32 -15
  13. data/lib/k9o/crypto.rb +471 -0
  14. data/lib/k9o/custody.rb +28 -0
  15. data/lib/k9o/data.rb +125 -5
  16. data/lib/k9o/ed25519.rb +182 -0
  17. data/lib/k9o/end_users.rb +93 -0
  18. data/lib/k9o/envelope.rb +20 -5
  19. data/lib/k9o/functions.rb +127 -9
  20. data/lib/k9o/gate.rb +52 -22
  21. data/lib/k9o/http_transport.rb +78 -16
  22. data/lib/k9o/jcs.rb +117 -0
  23. data/lib/k9o/kerror.rb +57 -5
  24. data/lib/k9o/keys.rb +131 -0
  25. data/lib/k9o/kibo.rb +3 -3
  26. data/lib/k9o/kibo_client_core.rb +385 -0
  27. data/lib/k9o/kibo_ffi_core.rb +178 -0
  28. data/lib/k9o/locate.rb +118 -0
  29. data/lib/k9o/me.rb +27 -87
  30. data/lib/k9o/metadata.rb +342 -0
  31. data/lib/k9o/moat.rb +443 -0
  32. data/lib/k9o/org.rb +81 -68
  33. data/lib/k9o/orgs.rb +1 -0
  34. data/lib/k9o/policies.rb +25 -6
  35. data/lib/k9o/projects.rb +43 -8
  36. data/lib/k9o/query.rb +174 -0
  37. data/lib/k9o/rampart.rb +273 -0
  38. data/lib/k9o/realtime.rb +261 -32
  39. data/lib/k9o/region.rb +20 -18
  40. data/lib/k9o/schedules.rb +109 -0
  41. data/lib/k9o/schema.rb +35 -0
  42. data/lib/k9o/streams.rb +94 -0
  43. data/lib/k9o/studio.rb +21 -11
  44. data/lib/k9o/url_grammar.rb +46 -20
  45. data/lib/k9o/vault.rb +79 -84
  46. data/lib/k9o/vectors.rb +124 -0
  47. data/lib/k9o/version.rb +1 -1
  48. data/lib/k9o.rb +17 -0
  49. data/sig/k9o/ai.rbs +4 -0
  50. data/sig/k9o/approvals.rbs +71 -0
  51. data/sig/k9o/datasets.rbs +125 -0
  52. data/sig/k9o/gate.rbs +9 -6
  53. data/sig/k9o/kerror.rbs +24 -0
  54. data/sig/k9o/kibo_client_core.rbs +25 -0
  55. data/sig/k9o/metadata.rbs +54 -0
  56. data/sig/k9o/moat.rbs +29 -0
  57. data/sig/k9o/query.rbs +26 -0
  58. data/sig/k9o/rampart.rbs +59 -0
  59. data/sig/k9o/realtime.rbs +34 -0
  60. data/sig/k9o/region.rbs +14 -3
  61. data/sig/k9o/workload.rbs +64 -0
  62. metadata +33 -4
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'kerror'
4
+
5
+ module K9o
6
+ # The bound Kibo core for K9o::KiboClientCore: it hands the SDK's plain Hashes and Strings to the generated uniffi
7
+ # binding (+KiboKmsRestFfi+, zk-bundle-v2.16.3 or later) as the typed records, integer enums and federation variant
8
+ # the binding wants, and turns the binding's errors into the SDK's "CODE: detail" form. There is no cryptography here:
9
+ # sealing, opening, the key pin and the token exchange all run in the native core. Requiring +k9o+ does not load the
10
+ # binding; building this object does, and raises a K9o::KError saying what is missing when it cannot.
11
+ #
12
+ # core = K9o::KiboFfiCore.new # binding and library on the default paths
13
+ # core = K9o::KiboFfiCore.new(library: '/opt/kibo/libkibo_kms_rest_ffi.so') # library at an absolute path
14
+ #
15
+ # The generated binding opens the native library by its bare name, +kibo_kms_rest_ffi+. Ruby's +ffi+ gem looks for it in
16
+ # the system library paths (+/opt/homebrew/lib+, +/opt/local/lib+, +/usr/local/lib+, +/usr/lib+, and on Linux the
17
+ # loader paths such as +LD_LIBRARY_PATH+). macOS does not pass +DYLD_LIBRARY_PATH+ to the system Ruby, so on macOS put
18
+ # the library in one of those directories or pass +library:+ with its absolute path. +library:+ makes this adapter load
19
+ # the binding with that path in place of the bare name; the binding file itself is not changed.
20
+ class KiboFfiCore
21
+ # Raised from the binding's errors. The message is "<CODE>: <detail>", which KiboClientCore turns into the item's
22
+ # code and message.
23
+ class CoreError < StandardError; end
24
+
25
+ PROVIDERS = { 'aws-kms' => :AWS_KMS, 'gcp-kms' => :GCP_KMS, 'azure-kv' => :AZURE_KV,
26
+ 'hashicorp-vault' => :HASHICORP_VAULT }.freeze
27
+ POSTURES = { 'byok' => :BYOK, 'hyok' => :HYOK }.freeze
28
+ BINDING_LIB_LINE = /^(\s*)ffi_lib\s+'kibo_kms_rest_ffi'\s*$/.freeze
29
+ REQUIRED = %i[app_crypto_seal_blocking app_crypto_open_blocking acquire_kms_credential_blocking
30
+ core_capabilities].freeze
31
+
32
+ # +library+: absolute path of the native library, for a library that is not on the default paths.
33
+ # +binding_path+: path of the generated +kibo_kms_rest_ffi.rb+, when it is not on the load path. Needed only if
34
+ # +KiboKmsRestFfi+ is not already loaded and +require 'kibo_kms_rest_ffi'+ would not find it.
35
+ # +ffi+: the binding module itself, when you have already loaded it under another name.
36
+ def initialize(library: nil, binding_path: nil, ffi: nil)
37
+ @ffi = ffi || load_binding(library, binding_path)
38
+ REQUIRED.each do |fn|
39
+ next if @ffi.respond_to?(fn)
40
+
41
+ raise unavailable("the loaded KiboKmsRestFfi does not export #{fn}; it is older than zk-bundle-v2.16.3")
42
+ end
43
+ end
44
+
45
+ def core_capabilities
46
+ guard { @ffi.core_capabilities }
47
+ end
48
+
49
+ def acquire_kms_credential_blocking(input)
50
+ guard do
51
+ @ffi.acquire_kms_credential_blocking(
52
+ @ffi::FfiFederationInput.new(spec: spec(input[:spec]), identity_token: input[:identity_token],
53
+ now_unix_secs: input[:now_unix_secs])
54
+ )
55
+ end
56
+ end
57
+
58
+ def app_crypto_seal_blocking(input)
59
+ guard do
60
+ @ffi.app_crypto_seal_blocking(
61
+ @ffi::AppSealInput.new(
62
+ provider: enum(@ffi::KmsProvider, PROVIDERS, input[:provider], 'provider'),
63
+ posture: enum(@ffi::FfiPosture, POSTURES, input[:posture], 'posture'),
64
+ kms_cred: cred(input[:kms_cred]),
65
+ key_ref: @ffi::FfiKeyRef.new(version: input[:key_ref][:version], external_ref: input[:key_ref][:external_ref]),
66
+ provider_config: provider_config(input[:provider_config]),
67
+ project_id: input[:project_id], crypt: input[:crypt], context: input[:context],
68
+ plaintext: input[:plaintext], expected_key_refs: input[:expected_key_refs]
69
+ )
70
+ )
71
+ end
72
+ end
73
+
74
+ def app_crypto_open_blocking(input)
75
+ guard do
76
+ @ffi.app_crypto_open_blocking(
77
+ @ffi::AppOpenInput.new(
78
+ kms_cred: cred(input[:kms_cred]), provider_config: provider_config(input[:provider_config]),
79
+ project_id: input[:project_id], crypt: input[:crypt], context: input[:context],
80
+ container: input[:container], expected_key_refs: input[:expected_key_refs]
81
+ )
82
+ )
83
+ end
84
+ end
85
+
86
+ def inspect
87
+ "#<#{self.class.name}>"
88
+ end
89
+
90
+ private
91
+
92
+ def unavailable(message)
93
+ KError.new(code: 'KIBO_WASM_ERROR', message: message,
94
+ recommendation: 'Install the generated Kibo binding (kibo_kms_rest_ffi.rb) and the native library ' \
95
+ '(zk-bundle-v2.16.3 or later); see "Encrypt through client.crypto" in the README.')
96
+ end
97
+
98
+ def load_binding(library, binding_path)
99
+ require 'ffi'
100
+ existing = loaded_binding
101
+ return existing if existing
102
+ return require_binding(binding_path) if library.nil?
103
+
104
+ load_with_library(library, binding_path)
105
+ rescue LoadError => e
106
+ raise unavailable("the Kibo binding or its native library could not be loaded: #{e.message}")
107
+ end
108
+
109
+ def loaded_binding
110
+ return nil unless Object.const_defined?(:KiboKmsRestFfi)
111
+
112
+ mod = Object.const_get(:KiboKmsRestFfi)
113
+ mod.respond_to?(:app_crypto_seal_blocking) ? mod : nil
114
+ end
115
+
116
+ def require_binding(binding_path)
117
+ binding_path ? require(File.expand_path(binding_path)) : require('kibo_kms_rest_ffi')
118
+ Object.const_get(:KiboKmsRestFfi)
119
+ end
120
+
121
+ # The binding opens +ffi_lib 'kibo_kms_rest_ffi'+ at load. To use an absolute path, the file is read and loaded with
122
+ # that one line pointing at the path; nothing on disk changes.
123
+ def load_with_library(library, binding_path)
124
+ path = File.expand_path(library)
125
+ raise LoadError, "no native library at #{path}" unless File.file?(path)
126
+
127
+ file = binding_path ? File.expand_path(binding_path) : $LOAD_PATH.map { |d| File.join(d.to_s, 'kibo_kms_rest_ffi.rb') }.find { |f| File.file?(f) }
128
+ raise LoadError, 'cannot find kibo_kms_rest_ffi.rb; pass binding_path: with its path' unless file
129
+
130
+ source = File.read(file)
131
+ raise LoadError, "#{file} does not look like the generated Kibo binding" unless source.match?(BINDING_LIB_LINE)
132
+
133
+ # rubocop:disable Security/Eval
134
+ TOPLEVEL_BINDING.eval(source.sub(BINDING_LIB_LINE) { "#{Regexp.last_match(1)}ffi_lib #{path.inspect}" }, file)
135
+ # rubocop:enable Security/Eval
136
+ Object.const_get(:KiboKmsRestFfi)
137
+ end
138
+
139
+ def enum(mod, table, wire, field)
140
+ name = table[wire.to_s]
141
+ raise KError.new(code: 'K9O_INVALID_FIELD_VALUE', message: "unknown #{field} #{wire.inspect}", context: { 'field' => field }) unless name
142
+
143
+ mod.const_get(name)
144
+ end
145
+
146
+ def cred(h)
147
+ @ffi::FfiCredential.new(secret: h[:secret], expires_at_unix_secs: h[:expires_at_unix_secs])
148
+ end
149
+
150
+ def provider_config(h)
151
+ h ||= {}
152
+ @ffi::FfiProviderConfig.new(aws_region: h[:aws_region], vault_address: h[:vault_address])
153
+ end
154
+
155
+ # The core's federation spec is a variant class per cloud.
156
+ def spec(s)
157
+ v = @ffi::FfiFederationSpec
158
+ case s[:provider].to_s
159
+ when 'aws' then v::AWS.new(s[:role_arn], s[:session_name], s[:region], s[:key_arn], s[:duration_secs])
160
+ when 'gcp' then v::GCP.new(s[:audience])
161
+ when 'azure' then v::AZURE.new(s[:tenant_id], s[:client_id], s[:scope])
162
+ when 'vault' then v::VAULT.new(s[:vault_addr], s[:mount], s[:role])
163
+ else
164
+ raise KError.new(code: 'K9O_INVALID_FIELD_VALUE', message: "unknown federation provider #{s[:provider].inspect}",
165
+ context: { 'field' => 'federation.provider' })
166
+ end
167
+ end
168
+
169
+ # The binding's error classes keep the code and detail as readers; their +message+ is not "CODE: detail".
170
+ def guard
171
+ yield
172
+ rescue @ffi::ZkFfiUniffiError::Platform, @ffi::ZkFfiUniffiError::PostureViolation => e
173
+ raise CoreError, "#{e.code}: #{e.detail}"
174
+ rescue @ffi::ZkFfiUniffiError::UpstreamKms => e
175
+ raise CoreError, "UPSTREAM_KMS_ERROR: #{e.detail}"
176
+ end
177
+ end
178
+ end
data/lib/k9o/locate.rb ADDED
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module K9o
6
+ # Locating the end user's region for password sign-in.
7
+ #
8
+ # K9o::Region finds the org's regional API host. An end user can live in another region of the project, so
9
+ # +gate.sign_in+ first asks the project which region holds the address
10
+ # (+POST /@<project>/gate/locate { email }+ at that regional base, with no credential) and sends the password to the
11
+ # origin it names. The answer is the same for a known and an unknown address.
12
+ #
13
+ # Rules:
14
+ # - The located origin is used for that one attempt. It is never cached. The one thing kept is a hint per refresh
15
+ # token (at most 64, in memory), so that refresh and sign-out reach the region that issued the session; a stale
16
+ # hint gets a 401.
17
+ # - An origin is used only if it is https (http only when the base is itself http on a loopback host and the origin is
18
+ # loopback too), has no userinfo, port, path, query or fragment, and its host is exactly the base host's org,
19
+ # family and apex with the region code ([a-z]{2}\d{2}) added or swapped in the gateway label, and that code equals
20
+ # the answer's +region+. Anything else, or +origin: null+, means the base is used. A password never goes to a host
21
+ # that fails this.
22
+ module Locate
23
+ REGION_CODE = /\A[a-z]{2}\d{2}\z/.freeze
24
+ HINT_LIMIT = 64
25
+ HINT_LOCK = Mutex.new
26
+ HINTS = {}
27
+
28
+ module_function
29
+
30
+ def loopback?(host)
31
+ h = host.to_s.downcase
32
+ h == 'localhost' || h.end_with?('.localhost') || h == '127.0.0.1' || h == '[::1]' || h == '::1'
33
+ end
34
+
35
+ # The origin the credential goes to: the located one if it passes the rule, else +base+. Pure; never raises.
36
+ # +answer+ is the +data+ of the locate envelope.
37
+ def choose_origin(base, answer)
38
+ return base unless answer.is_a?(Hash)
39
+
40
+ region = answer['region'] || answer[:region]
41
+ origin = answer['origin'] || answer[:origin]
42
+ return base unless origin.is_a?(String) && region.is_a?(String) && REGION_CODE.match?(region)
43
+
44
+ b = URI.parse(base)
45
+ u = URI.parse(origin)
46
+ return base if b.host.nil? || u.host.nil? || !u.scheme.is_a?(String)
47
+ return base unless u.userinfo.nil? && u.query.nil? && u.fragment.nil?
48
+ return base unless u.port == u.default_port
49
+ return base unless u.path.empty? || u.path == '/'
50
+
51
+ bs = b.scheme.to_s.downcase
52
+ us = u.scheme.to_s.downcase
53
+ unless us == 'https' || (us == 'http' && bs == 'http' && loopback?(b.host) && loopback?(u.host))
54
+ return base
55
+ end
56
+
57
+ bh = b.host.downcase
58
+ uh = u.host.downcase
59
+ return "#{bs}://#{bh}#{b.port == b.default_port ? '' : ":#{b.port}"}" if us == bs && uh == bh && u.port == b.port
60
+
61
+ from = UrlGrammar.parse_api_host(bh)
62
+ to = UrlGrammar.parse_api_host(uh)
63
+ return base if from.nil? || to.nil? || to.region != region
64
+ return base unless from.org_slug == to.org_slug && from.family == to.family && from.apex == to.apex
65
+
66
+ "#{us}://#{uh}"
67
+ rescue URI::Error, ArgumentError
68
+ base
69
+ end
70
+
71
+ # Ask the project which region holds +email+ and return the origin the credential goes to. A 404 from the door
72
+ # means the locate door is off and the base is used. Any other non-2xx answer (429, 503, anything else) raises as any other gateway error does, and nothing is sent after it. A 2xx
73
+ # answer that cannot be read means the base is used.
74
+ def origin_for(config, email, path)
75
+ base = Region.resolve_origin(config).sub(%r{/+\z}, '')
76
+ begin
77
+ env = HttpTransport.request(config: config, method: 'POST', path: path, body: { 'email' => email },
78
+ authenticated: false)
79
+ rescue KError => e
80
+ # 404: the locate door is off, so the base is used.
81
+ raise unless e.status == 404 || (e.code == 'K9O_INTERNAL_ERROR' && e.status.to_i.between?(200, 299))
82
+
83
+ return base
84
+ end
85
+ choose_origin(base, env.data)
86
+ end
87
+
88
+ # Remember which origin issued the session with this refresh token (a hint, not authority).
89
+ def remember(refresh_token, origin)
90
+ return unless refresh_token.is_a?(String) && !refresh_token.empty?
91
+
92
+ HINT_LOCK.synchronize do
93
+ HINTS.delete(refresh_token)
94
+ HINTS[refresh_token] = origin
95
+ HINTS.delete(HINTS.keys.first) while HINTS.size > HINT_LIMIT
96
+ end
97
+ end
98
+
99
+ def hint(refresh_token)
100
+ HINT_LOCK.synchronize { HINTS[refresh_token] }
101
+ end
102
+
103
+ def forget(refresh_token)
104
+ HINT_LOCK.synchronize { HINTS.delete(refresh_token) }
105
+ end
106
+
107
+ # A refresh answers with a new refresh token and the old one stops working: the hint follows the session. Data
108
+ # without a new token changes nothing.
109
+ def rotate(old_refresh_token, data)
110
+ token = data.is_a?(Hash) ? data['refreshToken'] : nil
111
+ origin = hint(old_refresh_token)
112
+ return unless token.is_a?(String) && !token.empty? && origin
113
+
114
+ forget(old_refresh_token)
115
+ remember(token, origin)
116
+ end
117
+ end
118
+ end
data/lib/k9o/me.rb CHANGED
@@ -1,23 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module K9o
4
- # Operator-self namespace — k9o.me.*. Cross-org;
5
- # org-plane routes under /me. Sub-namespaces: profile / sessions /
6
- # api_tokens / notifications / audit_log / data_export.
4
+ # Operator-self namespace — k9o.me.*. Cross-org; org-plane routes under /me.
5
+ # Sub-namespaces: profile / sessions / api_tokens.
7
6
  class Me
8
- attr_reader :profile, :sessions, :api_tokens, :notifications, :audit_log, :data_export
7
+ attr_reader :profile, :sessions, :api_tokens
9
8
 
10
9
  def initialize(config)
11
10
  @profile = Profile.new(config)
12
11
  @sessions = Sessions.new(config)
13
12
  @api_tokens = ApiTokens.new(config)
14
- @notifications = Notifications.new(config)
15
- @audit_log = AuditLog.new(config)
16
- @data_export = DataExport.new(config)
17
13
  end
18
14
 
19
15
  # k9o.me.profile.*
20
16
  class Profile
17
+ PATCH_FIELDS = { display_name: 'displayName', locale: 'locale', timezone: 'timezone' }.freeze
18
+
21
19
  def initialize(config)
22
20
  @config = config
23
21
  end
@@ -26,9 +24,17 @@ module K9o
26
24
  HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('me'))
27
25
  end
28
26
 
29
- def update(display_name: nil)
30
- HttpTransport.request(config: @config, method: 'PATCH', path: UrlGrammar.org_path('me'),
31
- body: { 'displayName' => display_name })
27
+ # PATCH /me. Pass only the fields to change (display_name, locale, timezone); an explicit +nil+ is sent as
28
+ # null.
29
+ def update(**fields)
30
+ patch = {}
31
+ fields.each do |key, value|
32
+ wire = PATCH_FIELDS[key]
33
+ raise ArgumentError, "unknown profile field: #{key}" if wire.nil?
34
+
35
+ patch[wire] = value
36
+ end
37
+ HttpTransport.request(config: @config, method: 'PATCH', path: UrlGrammar.org_path('me'), body: patch)
32
38
  end
33
39
  end
34
40
 
@@ -42,13 +48,15 @@ module K9o
42
48
  HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('me/sessions'))
43
49
  end
44
50
 
51
+ # POST /auth/sessions/<id>/revoke
45
52
  def revoke(session_id)
46
- HttpTransport.request(config: @config, method: 'DELETE',
47
- path: UrlGrammar.org_path("me/sessions/#{enc(session_id)}"))
53
+ HttpTransport.request(config: @config, method: 'POST',
54
+ path: UrlGrammar.org_path("auth/sessions/#{enc(session_id)}/revoke"))
48
55
  end
49
56
 
57
+ # POST /me/sessions/revoke-all
50
58
  def revoke_all
51
- HttpTransport.request(config: @config, method: 'DELETE', path: UrlGrammar.org_path('me/sessions'))
59
+ HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('me/sessions/revoke-all'))
52
60
  end
53
61
 
54
62
  private
@@ -68,10 +76,12 @@ module K9o
68
76
  HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('me/api-tokens'))
69
77
  end
70
78
 
71
- def create(name:, expires_in_days: nil, scope: nil)
72
- body = { 'name' => name }
73
- body['expiresInDays'] = expires_in_days if expires_in_days
74
- body['scope'] = scope if scope
79
+ # POST /me/api-tokens { label, scopeManifest, ttlSeconds? }. +scope_manifest+ is a Hash with the gateway's keys
80
+ # (each may be nil for "no restriction"): 'tier' (readonly / readwrite / admin, required), 'projects', 'resourceClasses',
81
+ # 'collections', 'cidrs'. The response carries the token's plaintext once.
82
+ def create(label:, scope_manifest:, ttl_seconds: nil)
83
+ body = { 'label' => label, 'scopeManifest' => scope_manifest }
84
+ body['ttlSeconds'] = ttl_seconds unless ttl_seconds.nil?
75
85
  HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('me/api-tokens'), body: body)
76
86
  end
77
87
 
@@ -86,75 +96,5 @@ module K9o
86
96
  ::URI.encode_www_form_component(s)
87
97
  end
88
98
  end
89
-
90
- # k9o.me.notifications.*
91
- class Notifications
92
- def initialize(config)
93
- @config = config
94
- end
95
-
96
- def list(unread_only: nil, limit: nil, cursor: nil)
97
- q = {}
98
- q['unreadOnly'] = unread_only unless unread_only.nil?
99
- q['limit'] = limit if limit
100
- q['cursor'] = cursor unless cursor.nil?
101
- HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('me/notifications'), query: q)
102
- end
103
-
104
- def mark_read(notification_id)
105
- HttpTransport.request(config: @config, method: 'PATCH',
106
- path: UrlGrammar.org_path("me/notifications/#{enc(notification_id)}"), body: { 'read' => true })
107
- end
108
-
109
- def mark_all_read
110
- HttpTransport.request(config: @config, method: 'PATCH', path: UrlGrammar.org_path('me/notifications'),
111
- body: { 'read' => true })
112
- end
113
-
114
- private
115
-
116
- def enc(s)
117
- ::URI.encode_www_form_component(s)
118
- end
119
- end
120
-
121
- # k9o.me.audit_log.*
122
- class AuditLog
123
- def initialize(config)
124
- @config = config
125
- end
126
-
127
- def query(since: nil, until_: nil, type: nil, limit: nil, cursor: nil)
128
- q = {}
129
- q['since'] = since if since
130
- q['until'] = until_ if until_
131
- q['type'] = type if type
132
- q['limit'] = limit if limit
133
- q['cursor'] = cursor if cursor
134
- HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('me/audit-log'), query: q)
135
- end
136
- end
137
-
138
- # k9o.me.data_export.*
139
- class DataExport
140
- def initialize(config)
141
- @config = config
142
- end
143
-
144
- def request(format: 'json')
145
- HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('me/data-export'),
146
- body: { 'format' => format })
147
- end
148
-
149
- def status(job_id)
150
- HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path("me/data-export/#{enc(job_id)}"))
151
- end
152
-
153
- private
154
-
155
- def enc(s)
156
- ::URI.encode_www_form_component(s)
157
- end
158
- end
159
99
  end
160
100
  end