k9o 1.2.0-aarch64-linux → 1.5.2-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 +123 -1
  3. data/README.md +1602 -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 +80 -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,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Schedules: k9o.schedules.* run a function on a timetable (cron). They need the project admin role to change.
5
+ #
6
+ # list(status:, function_name:, limit:, cursor:) GET /@<slug>/forge/schedules
7
+ # get(id) GET /@<slug>/forge/schedules/<id>
8
+ # create(function_name:, schedule_expression:, ...) POST /@<slug>/forge/schedules
9
+ # update(id, **fields) PATCH /@<slug>/forge/schedules/<id>
10
+ # delete(id) DELETE /@<slug>/forge/schedules/<id> (204, no body)
11
+ # runs(id, limit:, cursor:) GET /@<slug>/forge/schedules/<id>/runs
12
+ # run_now(id) POST /@<slug>/forge/schedules/<id>/run-now (202)
13
+ #
14
+ # Lists are cursor-paginated, newest first: pass +meta.cursor.next+ of one page as +cursor:+ of the next, until it
15
+ # is nil. A schedule interval under 5 minutes is rejected with 400 K9O_INVALID_FIELD_VALUE (field schedule_expression), on every plan.
16
+ class Schedules
17
+ CATCHUP_POLICIES = %w[skip run_once run_all].freeze
18
+ STATUSES = %w[active paused].freeze
19
+ UPDATE_FIELDS = %i[schedule_expression schedule_timezone catchup_policy status enabled log_ok_invocations].freeze
20
+
21
+ def initialize(config)
22
+ @config = config
23
+ end
24
+
25
+ # Schedules, newest first. +status+ is active or paused; +function_name+ keeps one function's schedules;
26
+ # +limit+ is 1 to 200 (the gateway defaults to 50).
27
+ def list(status: nil, function_name: nil, limit: nil, cursor: nil)
28
+ ApprovalChecks.check_limit(limit)
29
+ query = {}
30
+ query['status'] = ApprovalChecks.enum!(status, STATUSES, 'status') unless status.nil?
31
+ query['function_name'] = function_name unless function_name.nil?
32
+ query['limit'] = limit unless limit.nil?
33
+ query['cursor'] = cursor unless cursor.nil?
34
+ call('GET', path, query: query)
35
+ end
36
+
37
+ def get(id)
38
+ call('GET', path(one(id)))
39
+ end
40
+
41
+ # Create a schedule; answers 201 with the schedule. +schedule_expression+ is a five-field cron or one of @yearly,
42
+ # @annually, @monthly, @weekly, @daily, @midnight, @hourly (stored and returned expanded). +schedule_timezone+
43
+ # defaults to UTC; +catchup_policy+ to skip.
44
+ def create(function_name:, schedule_expression:, schedule_timezone: nil, catchup_policy: nil, collection: nil,
45
+ enabled: nil, log_ok_invocations: nil)
46
+ body = { 'functionName' => function_name, 'scheduleExpression' => schedule_expression }
47
+ body['scheduleTimezone'] = schedule_timezone unless schedule_timezone.nil?
48
+ body['catchupPolicy'] = ApprovalChecks.enum!(catchup_policy, CATCHUP_POLICIES, 'catchup_policy') unless catchup_policy.nil?
49
+ body['collection'] = collection unless collection.nil?
50
+ body['enabled'] = enabled unless enabled.nil?
51
+ body['logOkInvocations'] = log_ok_invocations unless log_ok_invocations.nil?
52
+ call('POST', path, body: body)
53
+ end
54
+
55
+ # Change a schedule; send at least one of +schedule_expression+, +schedule_timezone+, +catchup_policy+,
56
+ # +status+ ('paused' stops it, 'active' resumes it), +enabled+, +log_ok_invocations+. The function cannot change.
57
+ def update(id, **fields)
58
+ unknown = fields.keys - UPDATE_FIELDS
59
+ raise ApprovalChecks.invalid("unknown field: #{unknown.join(', ')}") unless unknown.empty?
60
+ raise ApprovalChecks.invalid("patch needs at least one of: #{UPDATE_FIELDS.join(', ')}") if fields.empty?
61
+
62
+ seg = one(id)
63
+ body = {}
64
+ body['scheduleExpression'] = fields[:schedule_expression] if fields.key?(:schedule_expression)
65
+ body['scheduleTimezone'] = fields[:schedule_timezone] if fields.key?(:schedule_timezone)
66
+ if fields.key?(:catchup_policy)
67
+ body['catchupPolicy'] = ApprovalChecks.enum!(fields[:catchup_policy], CATCHUP_POLICIES, 'catchup_policy')
68
+ end
69
+ body['status'] = ApprovalChecks.enum!(fields[:status], STATUSES, 'status') if fields.key?(:status)
70
+ body['enabled'] = fields[:enabled] if fields.key?(:enabled)
71
+ body['logOkInvocations'] = fields[:log_ok_invocations] if fields.key?(:log_ok_invocations)
72
+ call('PATCH', path(seg), body: body)
73
+ end
74
+
75
+ # Delete a schedule. Answers 204, so +data+ is nil.
76
+ def delete(id)
77
+ call('DELETE', path(one(id)))
78
+ end
79
+
80
+ # The schedule's runs, newest first, one page at a time.
81
+ def runs(id, limit: nil, cursor: nil)
82
+ ApprovalChecks.check_limit(limit)
83
+ seg = one(id)
84
+ query = {}
85
+ query['limit'] = limit unless limit.nil?
86
+ query['cursor'] = cursor unless cursor.nil?
87
+ call('GET', path("#{seg}/runs"), query: query)
88
+ end
89
+
90
+ # Start one run now, outside the timetable. Answers 202; +data+ is the body exactly as the gateway sent it.
91
+ def run_now(id)
92
+ call('POST', path("#{one(id)}/run-now"))
93
+ end
94
+
95
+ private
96
+
97
+ def one(id)
98
+ "/#{ApprovalChecks.id_segment(id, 'schedule')}"
99
+ end
100
+
101
+ def call(method, route, body: nil, query: nil)
102
+ HttpTransport.request(config: @config, method: method, path: route, body: body, query: query)
103
+ end
104
+
105
+ def path(tail = '')
106
+ UrlGrammar.project_path(Studio.require_project_slug(@config, 'schedules'), "forge/schedules#{tail}")
107
+ end
108
+ end
109
+ end
data/lib/k9o/schema.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Schema: k9o.schema.*
5
+ #
6
+ # collections.list(limit:, cursor:) GET /@<slug>/schema/collections
7
+ #
8
+ # The list is cursor-paginated and ordered by collection name: pass +meta.cursor.next+ of one page as +cursor+ of
9
+ # the next, until it is nil.
10
+ class Schema
11
+ attr_reader :collections
12
+
13
+ def initialize(config)
14
+ @collections = SchemaCollections.new(config)
15
+ end
16
+ end
17
+
18
+ class SchemaCollections
19
+ def initialize(config)
20
+ @config = config
21
+ end
22
+
23
+ # The project's collections by name, one page at a time. +limit+ is 1 to 200 (default 50).
24
+ def list(limit: nil, cursor: nil)
25
+ route = UrlGrammar.project_path(Studio.require_project_slug(@config, 'schema'), 'schema/collections')
26
+ ApprovalChecks.check_limit(limit)
27
+ raise ApprovalChecks.invalid('cursor must be a string', cursor: cursor) unless cursor.nil? || cursor.is_a?(String)
28
+
29
+ query = {}
30
+ query['limit'] = limit unless limit.nil?
31
+ query['cursor'] = cursor unless cursor.nil?
32
+ HttpTransport.request(config: @config, method: 'GET', path: route, query: query)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # User data streams namespace: k9o.streams.*.
5
+ #
6
+ # Routes under /@<slug>/data-streams/user. Publish needs project admin; subscribe and replay need admin or
7
+ # auditor. While the stream data plane is unavailable the gateway answers K9O_STREAM_UNAVAILABLE (503); a bad
8
+ # subject answers K9O_STREAM_INVALID. Both raise K9o::KError.
9
+ class Streams
10
+ @reconnect_delay = 0.25
11
+
12
+ class << self
13
+ # Seconds to wait before reconnecting after the server closes a subscribe connection.
14
+ attr_accessor :reconnect_delay
15
+ end
16
+
17
+ def initialize(config)
18
+ @config = config
19
+ end
20
+
21
+ # POST /@<slug>/data-streams/user — +max_bytes+ is the storage ceiling; the slug is derived from +display_name+.
22
+ def create(display_name, max_bytes, retention_seconds: nil)
23
+ body = { 'displayName' => display_name, 'maxBytes' => max_bytes }
24
+ body['retentionSeconds'] = retention_seconds unless retention_seconds.nil?
25
+ call('POST', path, body)
26
+ end
27
+
28
+ # GET /@<slug>/data-streams/user — the project's active user streams.
29
+ def list
30
+ call('GET', path)
31
+ end
32
+
33
+ # GET /@<slug>/data-streams/user/<slug>
34
+ def get(slug)
35
+ call('GET', path(slug))
36
+ end
37
+
38
+ # DELETE /@<slug>/data-streams/user/<slug> — retire the stream.
39
+ def delete(slug)
40
+ call('DELETE', path(slug))
41
+ end
42
+
43
+ # POST .../<slug>/publish — +data+ is any JSON value; +subject+ is an optional dot-separated routing suffix.
44
+ def publish(slug, data, subject: nil)
45
+ body = { 'data' => data }
46
+ body['subject'] = subject unless subject.nil?
47
+ call('POST', path(slug, 'publish'), body)
48
+ end
49
+
50
+ # POST .../<slug>/replay — a bounded range of past events: data['events'], data['nextCursor'], data['hasMore'].
51
+ def replay(slug, from_sequence: nil, until_sequence: nil, limit: nil)
52
+ body = {}
53
+ body['fromSequence'] = from_sequence unless from_sequence.nil?
54
+ body['untilSequence'] = until_sequence unless until_sequence.nil?
55
+ body['limit'] = limit unless limit.nil?
56
+ call('POST', path(slug, 'replay'), body)
57
+ end
58
+
59
+ # GET .../<slug>/subscribe — an Enumerator tailing live events { 'sequence', 'subject', 'data', 'publishedAt' }.
60
+ # The gateway bounds each connection and closes it; the enumerator reconnects from the last sequence it yielded.
61
+ # Stop by leaving the +each+ block (break) or by taking a finite number (+first(n)+).
62
+ def subscribe(slug, from_sequence: nil)
63
+ route = path(slug, 'subscribe')
64
+ Enumerator.new do |yielder|
65
+ cursor = from_sequence
66
+ loop do
67
+ query = cursor.nil? ? nil : { 'fromSequence' => cursor.to_s }
68
+ HttpTransport.stream_sse(config: @config, method: 'GET', path: route, query: query).each do |event|
69
+ next unless event.is_a?(Hash) && event.key?('sequence')
70
+
71
+ cursor = event['sequence']
72
+ yielder << event
73
+ end
74
+ delay = self.class.reconnect_delay
75
+ sleep(delay) if delay.positive?
76
+ end
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def call(method, route, body = nil)
83
+ HttpTransport.request(config: @config, method: method, path: route, body: body)
84
+ end
85
+
86
+ def path(stream_slug = nil, suffix = nil)
87
+ project = Studio.require_project_slug(@config, 'streams')
88
+ sub = +'data-streams/user'
89
+ sub << "/#{::URI.encode_www_form_component(stream_slug)}" if stream_slug
90
+ sub << "/#{suffix}" if suffix
91
+ UrlGrammar.project_path(project, sub)
92
+ end
93
+ end
94
+ end
data/lib/k9o/studio.rb CHANGED
@@ -25,7 +25,7 @@ module K9o
25
25
  slug
26
26
  end
27
27
 
28
- # k9o.studio.workflows.*.
28
+ # k9o.studio.workflows.*
29
29
  class Workflows
30
30
  attr_reader :runs
31
31
 
@@ -34,17 +34,19 @@ module K9o
34
34
  @runs = Runs.new(config)
35
35
  end
36
36
 
37
- def list(status: nil, trigger_type: nil, limit: nil, cursor: nil)
37
+ # GET /@<slug>/workflows — the route filters by +status+ and pages with +limit+ / +cursor+.
38
+ def list(status: nil, limit: nil, cursor: nil)
38
39
  slug = Studio.require_project_slug(@config, 'studio.workflows')
39
40
  q = {}
40
41
  q['status'] = status if status
41
- q['triggerType'] = trigger_type if trigger_type
42
42
  q['limit'] = limit if limit
43
43
  q['cursor'] = cursor if cursor
44
44
  HttpTransport.request(config: @config, method: 'GET',
45
45
  path: UrlGrammar.project_path(slug, 'workflows'), query: q)
46
46
  end
47
47
 
48
+ # POST /@<slug>/workflows. +workflow+ is the gateway's body: 'name', 'triggerType', 'triggerConfig',
49
+ # 'actions' (each { 'index', 'action_type', 'config', ... }), optional 'description', 'status', 'rbacBinding'.
48
50
  def create(workflow)
49
51
  slug = Studio.require_project_slug(@config, 'studio.workflows')
50
52
  HttpTransport.request(config: @config, method: 'POST',
@@ -69,12 +71,12 @@ module K9o
69
71
  path: UrlGrammar.project_path(slug, "workflows/#{encode(workflow_id)}"))
70
72
  end
71
73
 
72
- # Manual-trigger a run (trigger_type=manual only).
74
+ # Manual-trigger a run (manual workflows only): POST /@<slug>/workflows/<id>/runs { triggerContext }.
73
75
  def trigger(workflow_id, trigger_context = {})
74
76
  slug = Studio.require_project_slug(@config, 'studio.workflows')
75
77
  HttpTransport.request(config: @config, method: 'POST',
76
78
  path: UrlGrammar.project_path(slug, "workflows/#{encode(workflow_id)}/runs"),
77
- body: trigger_context)
79
+ body: { 'triggerContext' => trigger_context })
78
80
  end
79
81
 
80
82
  private
@@ -83,15 +85,18 @@ module K9o
83
85
  ::URI.encode_www_form_component(s)
84
86
  end
85
87
 
86
- # k9o.studio.workflows.runs.*.
88
+ # k9o.studio.workflows.runs.*
87
89
  class Runs
88
90
  def initialize(config)
89
91
  @config = config
90
92
  end
91
93
 
92
- def list(workflow_id, status: nil)
94
+ # GET /@<slug>/workflows/<id>/runs — pages with +limit+ / +cursor+ (the route has no status filter).
95
+ def list(workflow_id, limit: nil, cursor: nil)
93
96
  slug = Studio.require_project_slug(@config, 'studio.workflows')
94
- q = status ? { 'status' => status } : nil
97
+ q = {}
98
+ q['limit'] = limit if limit
99
+ q['cursor'] = cursor if cursor
95
100
  HttpTransport.request(config: @config, method: 'GET',
96
101
  path: UrlGrammar.project_path(slug, "workflows/#{enc(workflow_id)}/runs"), query: q)
97
102
  end
@@ -116,15 +121,20 @@ module K9o
116
121
  end
117
122
  end
118
123
 
119
- # k9o.studio.dashboards.*.
124
+ # k9o.studio.dashboards.* — widget fields on the wire are snake_case ('widget_type', 'data_source', 'config').
120
125
  class Dashboards
121
126
  def initialize(config)
122
127
  @config = config
123
128
  end
124
129
 
125
- def list(status: nil)
130
+ # GET /@<slug>/dashboards — filters: +status+, +pinned_only+; pages with +limit+ / +cursor+.
131
+ def list(status: nil, pinned_only: nil, limit: nil, cursor: nil)
126
132
  slug = Studio.require_project_slug(@config, 'studio.dashboards')
127
- q = status ? { 'status' => status } : nil
133
+ q = {}
134
+ q['status'] = status if status
135
+ q['pinnedOnly'] = pinned_only.to_s unless pinned_only.nil?
136
+ q['limit'] = limit if limit
137
+ q['cursor'] = cursor if cursor
128
138
  HttpTransport.request(config: @config, method: 'GET',
129
139
  path: UrlGrammar.project_path(slug, 'dashboards'), query: q)
130
140
  end
@@ -7,11 +7,18 @@ module K9o
7
7
  module UrlGrammar
8
8
  SLUG_RE = /\A[a-z][a-z0-9-]{1,62}\z/
9
9
 
10
- # Bare platform apex hosts org-subdomain derivation applies to —
11
- # production and staging share the <org_slug>.<apex> wire-routing
12
- # scheme (confirmed live). A third apex needs the same confirmation
13
- # before joining this set.
14
- ORG_DERIVABLE_APEXES = %w[api.k9o.io api-staging.k9o.io].freeze
10
+ # The convenience default when the caller passes no base_url. A default value only: never used to parse a host.
11
+ DEFAULT_BASE_URL = 'https://api.k9o.io'
12
+
13
+ # `api` or `api-staging`, optionally followed by `-<rgn>`: the label that marks a gateway host.
14
+ API_LABEL = /\A(api|api-staging)(?:-([a-z]{2}\d{2}))?\z/.freeze
15
+
16
+ # The structure of a gateway API host, read from the host itself.
17
+ # org_slug: first label of an org host (+acme+ in +acme.api.corp.internal+); nil for a platform host.
18
+ # family: +api+ or +api-staging+.
19
+ # region: 4-character region code when the host is regional (+api-de01+), else nil.
20
+ # apex: everything after the gateway label; never matched against a list.
21
+ ApiHost = Struct.new(:org_slug, :family, :region, :apex)
15
22
 
16
23
  module_function
17
24
 
@@ -65,24 +72,43 @@ module K9o
65
72
  "#{uri.scheme}://#{host_port}"
66
73
  end
67
74
 
68
- # Resolve the effective base_url. base_url is
69
- # OPTIONAL — it defaults to the platform apex https://api.k9o.io. When the
70
- # (validated) host is a bare apex in ORG_DERIVABLE_APEXES and org_slug is
71
- # supplied, the SDK composes the canonical org-subdomain wire root
72
- # https://<org_slug>.<apex>. An explicit <org_slug> subdomain,
73
- # a self-hosted / Sovereign host, or a dev host (anything not in that
74
- # set) passes through unchanged — org_slug is intentionally NOT composed
75
- # onto a non-apex host.
76
- #
77
- # Fixed 2026-08-30 (SDK-2): this used to silently ignore org_slug for
78
- # api-staging.k9o.io too, not just genuine self-hosted/custom hosts,
79
- # since only the literal production apex string was checked.
75
+ # Read the org slug and region out of a gateway host without knowing its apex. The gateway label is +api+ or
76
+ # +api-staging+, optionally followed by +-<rgn>+; the label before it is the org slug and whatever follows it is
77
+ # the apex. Returns nil for any other shape (localhost, an IP, a single-label host, a host with no gateway
78
+ # label), which callers use exactly as given.
79
+ def parse_api_host(host)
80
+ labels = host.to_s.downcase.split('.', -1)
81
+ return nil if labels.empty? || labels.any?(&:empty?)
82
+
83
+ build = lambda do |gateway_label, org_slug, rest|
84
+ m = API_LABEL.match(gateway_label)
85
+ next nil if m.nil? || rest.empty?
86
+
87
+ ApiHost.new(org_slug, m[1], m[2], rest.join('.'))
88
+ end
89
+ # Org host first: <org>.<api[-rgn]>.<apex>.
90
+ if labels.length >= 3 && labels[0].match?(SLUG_RE) && !labels[0].end_with?('-') && API_LABEL.match?(labels[1])
91
+ return build.call(labels[1], labels[0], labels[2..-1])
92
+ end
93
+ # Platform host: api.<apex>.
94
+ return build.call(labels[0], nil, labels[1..-1]) if labels.length >= 2 && API_LABEL.match?(labels[0])
95
+
96
+ nil
97
+ end
98
+
99
+ # Resolve the effective base_url. base_url is OPTIONAL: omitted, it defaults to DEFAULT_BASE_URL (the public
100
+ # cloud, a convenience only). When the (validated) host is a platform host (+api.<apex>+ or +api-staging.<apex>+,
101
+ # any apex) and org_slug is supplied, the canonical org host +<org_slug>.<host>+ is composed. Any other host (an
102
+ # org host already, an internal or air-gapped host, localhost) passes through unchanged; org_slug is then only
103
+ # sent as a header.
80
104
  def resolve_base_url(base_url, org_slug = nil)
81
- validated = validate_base_url(base_url.nil? ? 'https://api.k9o.io' : base_url)
105
+ validated = validate_base_url(base_url.nil? ? DEFAULT_BASE_URL : base_url)
82
106
  uri = URI.parse(validated)
83
- if ORG_DERIVABLE_APEXES.include?(uri.host) && org_slug
107
+ host = parse_api_host(uri.host)
108
+ if !host.nil? && host.org_slug.nil? && host.region.nil? && org_slug
84
109
  validate_slug(org_slug, 'org')
85
- return "#{uri.scheme}://#{org_slug}.#{uri.host}"
110
+ host_port = uri.port && uri.port != uri.default_port ? "#{uri.host}:#{uri.port}" : uri.host
111
+ return "#{uri.scheme}://#{org_slug}.#{host_port}"
86
112
  end
87
113
  validated
88
114
  end