k9o 1.1.0
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 +7 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE +201 -0
- data/NOTICE +4 -0
- data/README.md +546 -0
- data/k9o.gemspec +41 -0
- data/lib/k9o/ai.rb +28 -0
- data/lib/k9o/audit.rb +29 -0
- data/lib/k9o/barbican.rb +239 -0
- data/lib/k9o/client.rb +59 -0
- data/lib/k9o/compute.rb +67 -0
- data/lib/k9o/data.rb +157 -0
- data/lib/k9o/envelope.rb +64 -0
- data/lib/k9o/functions.rb +51 -0
- data/lib/k9o/gate.rb +64 -0
- data/lib/k9o/http_transport.rb +318 -0
- data/lib/k9o/kerror.rb +52 -0
- data/lib/k9o/kibo.rb +114 -0
- data/lib/k9o/me.rb +160 -0
- data/lib/k9o/migrations.rb +244 -0
- data/lib/k9o/multipart.rb +42 -0
- data/lib/k9o/org.rb +213 -0
- data/lib/k9o/orgs.rb +33 -0
- data/lib/k9o/policies.rb +65 -0
- data/lib/k9o/projects.rb +33 -0
- data/lib/k9o/realtime.rb +80 -0
- data/lib/k9o/region.rb +95 -0
- data/lib/k9o/storage.rb +176 -0
- data/lib/k9o/studio.rb +175 -0
- data/lib/k9o/url_grammar.rb +113 -0
- data/lib/k9o/vault.rb +235 -0
- data/lib/k9o/version.rb +5 -0
- data/lib/k9o.rb +54 -0
- data/sig/k9o/ai.rbs +16 -0
- data/sig/k9o/gate.rbs +11 -0
- data/sig/k9o/migrations.rbs +138 -0
- data/sig/k9o/region.rbs +19 -0
- data/sig/k9o/storage.rbs +41 -0
- metadata +98 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module K9o
|
|
6
|
+
# URL grammar helpers.
|
|
7
|
+
module UrlGrammar
|
|
8
|
+
SLUG_RE = /\A[a-z][a-z0-9-]{1,62}\z/
|
|
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
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def validate_base_url(base_url)
|
|
19
|
+
raise KError.new(
|
|
20
|
+
code: 'K9O_INVALID_BASE_URL',
|
|
21
|
+
message: "base_url #{base_url.inspect} is not a valid URL",
|
|
22
|
+
context: { base_url: base_url }
|
|
23
|
+
) unless base_url.is_a?(String) && !base_url.empty?
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
uri = URI.parse(base_url)
|
|
27
|
+
rescue URI::InvalidURIError => e
|
|
28
|
+
raise KError.new(
|
|
29
|
+
code: 'K9O_INVALID_BASE_URL',
|
|
30
|
+
message: "base_url #{base_url.inspect} is not a valid URL: #{e.message}",
|
|
31
|
+
context: { base_url: base_url }
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless %w[http https].include?(uri.scheme)
|
|
36
|
+
raise KError.new(
|
|
37
|
+
code: 'K9O_INVALID_BASE_URL',
|
|
38
|
+
message: "base_url #{base_url.inspect} uses unsupported scheme #{uri.scheme.inspect}; expected http or https",
|
|
39
|
+
context: { base_url: base_url, scheme: uri.scheme }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
if uri.host.nil? || uri.host.empty?
|
|
43
|
+
raise KError.new(
|
|
44
|
+
code: 'K9O_INVALID_BASE_URL',
|
|
45
|
+
message: "base_url #{base_url.inspect} missing host",
|
|
46
|
+
context: { base_url: base_url }
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
if uri.path && uri.path != '' && uri.path != '/'
|
|
50
|
+
raise KError.new(
|
|
51
|
+
code: 'K9O_INVALID_BASE_URL',
|
|
52
|
+
message: "base_url #{base_url.inspect} must not contain a path segment; got #{uri.path.inspect}",
|
|
53
|
+
context: { base_url: base_url, path: uri.path }
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
if uri.query || uri.fragment
|
|
57
|
+
raise KError.new(
|
|
58
|
+
code: 'K9O_INVALID_BASE_URL',
|
|
59
|
+
message: "base_url #{base_url.inspect} must not contain query or fragment",
|
|
60
|
+
context: { base_url: base_url }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
host_port = uri.port && uri.port != uri.default_port ? "#{uri.host}:#{uri.port}" : uri.host
|
|
65
|
+
"#{uri.scheme}://#{host_port}"
|
|
66
|
+
end
|
|
67
|
+
|
|
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.
|
|
80
|
+
def resolve_base_url(base_url, org_slug = nil)
|
|
81
|
+
validated = validate_base_url(base_url.nil? ? 'https://api.k9o.io' : base_url)
|
|
82
|
+
uri = URI.parse(validated)
|
|
83
|
+
if ORG_DERIVABLE_APEXES.include?(uri.host) && org_slug
|
|
84
|
+
validate_slug(org_slug, 'org')
|
|
85
|
+
return "#{uri.scheme}://#{org_slug}.#{uri.host}"
|
|
86
|
+
end
|
|
87
|
+
validated
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate_slug(slug, kind)
|
|
91
|
+
unless slug.is_a?(String) && slug.match?(SLUG_RE) && !slug.end_with?('-')
|
|
92
|
+
raise KError.new(
|
|
93
|
+
code: 'K9O_INVALID_FIELD_VALUE',
|
|
94
|
+
message: "#{kind}_slug #{slug.inspect} does not match required format ^[a-z][a-z0-9-]{1,62}$ (must not end with hyphen)",
|
|
95
|
+
context: { slug: slug, kind: kind }
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def project_path(project_slug, sub_resource)
|
|
101
|
+
cleaned = sub_resource.sub(%r{\A/}, '')
|
|
102
|
+
"/@#{project_slug}/#{cleaned}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def org_path(resource)
|
|
106
|
+
"/#{resource.sub(%r{\A/}, '')}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def platform_path(resource)
|
|
110
|
+
"/#{resource.sub(%r{\A/}, '')}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/k9o/vault.rb
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module K9o
|
|
7
|
+
# `k9o.kibo.vault.secrets.*` — the typed secrets surface
|
|
8
|
+
# (`services/gateway/src/routes/vault-secrets.ts`). Full family: list / get /
|
|
9
|
+
# create / reveal_challenge / reveal / update / rotate / move / delete.
|
|
10
|
+
#
|
|
11
|
+
# Contract live-verified against the real staging gateway (not just the
|
|
12
|
+
# route's OpenAPI schema, which is wrong/incomplete in several places noted
|
|
13
|
+
# below) by the `@k9o/kibo` TS reference build — this wrapper mirrors that
|
|
14
|
+
# verified behavior exactly:
|
|
15
|
+
#
|
|
16
|
+
# - Every operation on a NAMED secret (everything except `list`) REQUIRES
|
|
17
|
+
# `region` on the query string. Omitting it fails with
|
|
18
|
+
# K9O_INVALID_FIELD_VALUE ("this request requires an authoritative
|
|
19
|
+
# region"). NOT documented in the route's OpenAPI schema. `list` is the
|
|
20
|
+
# one exception (verified it does NOT need `region`).
|
|
21
|
+
# - The wire `value` field is base64 in BOTH directions, contradicting the
|
|
22
|
+
# OpenAPI's plain-language "the plaintext to seal" description (reads
|
|
23
|
+
# like it should be raw). A raw non-base64 `value` on create/update is
|
|
24
|
+
# rejected as unparseable. `create`/`update` take plaintext as a normal
|
|
25
|
+
# Ruby String and base64-encode it here; `reveal` base64-decodes the
|
|
26
|
+
# wire `value` back to plaintext before returning it — callers never
|
|
27
|
+
# see base64 on either side.
|
|
28
|
+
# - `update`/`rotate`/`move` responses are LIVE-VERIFIED NARROWER than (or
|
|
29
|
+
# entirely undocumented vs.) the OpenAPI's claimed `SecretSummary` — see
|
|
30
|
+
# the comment on each method below.
|
|
31
|
+
#
|
|
32
|
+
# `data` is returned as the raw wire Hash (camelCase string keys), matching
|
|
33
|
+
# this package's existing convention (Projects/Policies/Studio/etc. all
|
|
34
|
+
# return `envelope.data` untransformed — there is no generic
|
|
35
|
+
# camelCase<->snake_case response transform anywhere in this gem); `reveal`
|
|
36
|
+
# is the one exception, where the `value` key is base64-decoded in place
|
|
37
|
+
# (mirroring the TS reference's own `value`-only transform).
|
|
38
|
+
class VaultSecrets
|
|
39
|
+
def initialize(config)
|
|
40
|
+
@config = config
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# List secrets (metadata only — never lists ciphertext).
|
|
44
|
+
# `GET /@<project_slug>/vault/secrets`.
|
|
45
|
+
# The one secrets operation that does NOT need `region` (live-verified).
|
|
46
|
+
def list(prefix: nil, cursor: nil, limit: nil)
|
|
47
|
+
slug = require_project_slug
|
|
48
|
+
q = {}
|
|
49
|
+
q['prefix'] = prefix if prefix
|
|
50
|
+
q['cursor'] = cursor if cursor
|
|
51
|
+
q['limit'] = limit if limit
|
|
52
|
+
HttpTransport.request(
|
|
53
|
+
config: @config, method: 'GET',
|
|
54
|
+
path: UrlGrammar.project_path(slug, 'vault/secrets'),
|
|
55
|
+
query: q
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Read a secret's metadata (never its value — use `reveal_challenge` +
|
|
60
|
+
# `reveal` for that).
|
|
61
|
+
# `GET /@<project_slug>/vault/secrets/<name>?region=<region>`.
|
|
62
|
+
#
|
|
63
|
+
# KNOWN LIVE BUG (staging, 2026-09-05): this call currently fails with
|
|
64
|
+
# K9O_REVEAL_CHALLENGE_REQUIRED on every attempt — the gateway does not
|
|
65
|
+
# honor its own documented "no X-K9O-Reveal-Challenge header => return
|
|
66
|
+
# metadata" behavior; it demands a challenge unconditionally. Implemented
|
|
67
|
+
# per the documented contract anyway (this is a backend defect to flag,
|
|
68
|
+
# not something to design around) — `list(prefix: exact_name)` is the
|
|
69
|
+
# current working alternative for a single secret's metadata until the
|
|
70
|
+
# gateway side is fixed.
|
|
71
|
+
def get(name, region:)
|
|
72
|
+
slug = require_project_slug
|
|
73
|
+
HttpTransport.request(
|
|
74
|
+
config: @config, method: 'GET',
|
|
75
|
+
path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
|
|
76
|
+
query: { 'region' => region }
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Create a secret under the caller's own auth.
|
|
81
|
+
# `POST /@<project_slug>/vault/secrets/<name>?region=<region>` → the full
|
|
82
|
+
# `sec_<base62>` summary.
|
|
83
|
+
#
|
|
84
|
+
# `region` scopes the vault WRITE itself (residency guard) and is
|
|
85
|
+
# REQUIRED — live-verified against staging, not documented in the route's
|
|
86
|
+
# OpenAPI schema.
|
|
87
|
+
#
|
|
88
|
+
# `visibility` (`{'kind' => 'private'}` or
|
|
89
|
+
# `{'kind' => 'shared', 'principals' => [...], 'roles' => [...]}`) and
|
|
90
|
+
# `metadata` (`Hash[String, String]`) are passed through verbatim — their
|
|
91
|
+
# own field names are already single words, so no wire-casing applies.
|
|
92
|
+
def create(name:, value:, region:, visibility: nil, metadata: nil)
|
|
93
|
+
slug = require_project_slug
|
|
94
|
+
body = { 'value' => Base64.strict_encode64(value) }
|
|
95
|
+
body['visibility'] = visibility if visibility
|
|
96
|
+
body['metadata'] = metadata if metadata
|
|
97
|
+
HttpTransport.request(
|
|
98
|
+
config: @config, method: 'POST',
|
|
99
|
+
path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
|
|
100
|
+
query: { 'region' => region },
|
|
101
|
+
body: body
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Mint a single-use reveal challenge — present it as
|
|
106
|
+
# `X-K9O-Reveal-Challenge` on the immediately-following `reveal` call.
|
|
107
|
+
# `POST .../<name>?op=reveal-challenge®ion=<region>`.
|
|
108
|
+
def reveal_challenge(name, region:)
|
|
109
|
+
slug = require_project_slug
|
|
110
|
+
HttpTransport.request(
|
|
111
|
+
config: @config, method: 'POST',
|
|
112
|
+
path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
|
|
113
|
+
query: { 'op' => 'reveal-challenge', 'region' => region }
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Reveal a secret's plaintext value using a single-use challenge from
|
|
118
|
+
# `reveal_challenge`. `GET .../<name>?region=<region>` with
|
|
119
|
+
# `X-K9O-Reveal-Challenge`. Base64-decodes the wire's `value` back to
|
|
120
|
+
# plaintext (live-verified: the reveal response's `value` is base64,
|
|
121
|
+
# symmetric with `create`'s encode) before returning it to the caller.
|
|
122
|
+
def reveal(name, region:, challenge:)
|
|
123
|
+
slug = require_project_slug
|
|
124
|
+
envelope = HttpTransport.request(
|
|
125
|
+
config: @config, method: 'GET',
|
|
126
|
+
path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
|
|
127
|
+
query: { 'region' => region },
|
|
128
|
+
headers: { 'X-K9O-Reveal-Challenge' => challenge }
|
|
129
|
+
)
|
|
130
|
+
data = envelope.data
|
|
131
|
+
if data.is_a?(Hash) && data.key?('value')
|
|
132
|
+
data = data.merge('value' => Base64.decode64(data['value']))
|
|
133
|
+
end
|
|
134
|
+
Envelope.new(data: data, meta: envelope.meta)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Update a secret's value (bumps version) — owner-only.
|
|
138
|
+
# `PUT .../<name>?region=<region>`.
|
|
139
|
+
#
|
|
140
|
+
# LIVE-VERIFIED NARROWER than documented: the gateway returns only
|
|
141
|
+
# `{id, name, version}` on a real PUT, not the full summary the OpenAPI
|
|
142
|
+
# schema claims — call `get` afterward if the full summary is needed.
|
|
143
|
+
def update(name, region:, value:, metadata: nil)
|
|
144
|
+
slug = require_project_slug
|
|
145
|
+
body = { 'value' => Base64.strict_encode64(value) }
|
|
146
|
+
body['metadata'] = metadata if metadata
|
|
147
|
+
HttpTransport.request(
|
|
148
|
+
config: @config, method: 'PUT',
|
|
149
|
+
path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
|
|
150
|
+
query: { 'region' => region },
|
|
151
|
+
body: body
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Rotate a secret's wrapping key (re-wraps under a fresh `kid`; content
|
|
156
|
+
# unchanged) — owner-only. `POST .../<name>?op=rotate®ion=<region>`.
|
|
157
|
+
#
|
|
158
|
+
# LIVE-VERIFIED undocumented shape: `{id, name, version, rotatedAt,
|
|
159
|
+
# ledgerSequenceNumber}` — the route's OpenAPI schema doesn't document
|
|
160
|
+
# this response at all.
|
|
161
|
+
def rotate(name, region:)
|
|
162
|
+
slug = require_project_slug
|
|
163
|
+
HttpTransport.request(
|
|
164
|
+
config: @config, method: 'POST',
|
|
165
|
+
path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
|
|
166
|
+
query: { 'op' => 'rotate', 'region' => region }
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Move (rename) a secret's hierarchical path — owner-only.
|
|
171
|
+
# `POST .../<name>?op=move®ion=<region>` with `{newPath}`.
|
|
172
|
+
#
|
|
173
|
+
# LIVE-VERIFIED undocumented shape: `{id, name, event}` (observed
|
|
174
|
+
# `event: "SECRET_PATH_CHANGED"`; treat as a plain String, not a closed
|
|
175
|
+
# enum — only one sample was observed).
|
|
176
|
+
def move(name, region:, new_path:)
|
|
177
|
+
slug = require_project_slug
|
|
178
|
+
HttpTransport.request(
|
|
179
|
+
config: @config, method: 'POST',
|
|
180
|
+
path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
|
|
181
|
+
query: { 'op' => 'move', 'region' => region },
|
|
182
|
+
body: { 'newPath' => new_path }
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Retire a secret — owner-only. `DELETE .../<name>?region=<region>` →
|
|
187
|
+
# `204 No Content` (HttpTransport already maps that to `data: nil`).
|
|
188
|
+
def delete(name, region:)
|
|
189
|
+
slug = require_project_slug
|
|
190
|
+
HttpTransport.request(
|
|
191
|
+
config: @config, method: 'DELETE',
|
|
192
|
+
path: UrlGrammar.project_path(slug, "vault/secrets/#{enc(name)}"),
|
|
193
|
+
query: { 'region' => region }
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
def require_project_slug
|
|
200
|
+
Studio.require_project_slug(@config, 'kibo.vault.secrets')
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def enc(s)
|
|
204
|
+
::URI.encode_www_form_component(s)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# `k9o.kibo.vault.*` — the Kibo vault surface (member-plane, project-scoped).
|
|
209
|
+
# Currently just `secrets`; a `zk_deks` accessor
|
|
210
|
+
# (F.5, `POST vault/zk-deks` / `GET vault/zk-deks/<id>`) would
|
|
211
|
+
# nest here alongside it if/when that surface is built for Ruby — out of
|
|
212
|
+
# scope for this wrapper, which only covers the vault-secrets audit gap.
|
|
213
|
+
class KiboVault
|
|
214
|
+
attr_reader :secrets
|
|
215
|
+
|
|
216
|
+
def initialize(config)
|
|
217
|
+
@secrets = VaultSecrets.new(config)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# `k9o.kibo.*` — per-client namespace root for the Kibo vault REST surface.
|
|
222
|
+
# Distinct from the module-level `K9o::Kibo` (native ZK `encrypt`/`decrypt`
|
|
223
|
+
# FFI bindings, /, called directly as
|
|
224
|
+
# `K9o::Kibo.encrypt`/`.decrypt` since that surface needs no per-client HTTP
|
|
225
|
+
# config): this class exists so `client.kibo.vault.secrets.*` is reachable
|
|
226
|
+
# per client instance, mirroring the TS reference's
|
|
227
|
+
# `client.kibo.vault.{zk_deks, secrets}` shape.
|
|
228
|
+
class KiboNamespace
|
|
229
|
+
attr_reader :vault
|
|
230
|
+
|
|
231
|
+
def initialize(config)
|
|
232
|
+
@vault = KiboVault.new(config)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
data/lib/k9o/version.rb
ADDED
data/lib/k9o.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# k9o — Kilimanjaro platform SDK for Ruby.
|
|
4
|
+
# Idiomatic: snake_case methods + raise K9o::KError +
|
|
5
|
+
# Net::HTTP transport.
|
|
6
|
+
#
|
|
7
|
+
# k9o = K9o.create_client(
|
|
8
|
+
# base_url: "https://acme.api.k9o.io",
|
|
9
|
+
# api_key: ENV["K9O_API_KEY"],
|
|
10
|
+
# org_slug: "acme",
|
|
11
|
+
# project_slug: "member-portal",
|
|
12
|
+
# )
|
|
13
|
+
#
|
|
14
|
+
# begin
|
|
15
|
+
# response = k9o.from("posts").select.limit(10).execute
|
|
16
|
+
# response.data.each { |row| puts row }
|
|
17
|
+
#
|
|
18
|
+
# session = k9o.gate.sign_in(email: "u@x.com", password: "...")
|
|
19
|
+
# puts "signed in: #{session.data['user']['id']}"
|
|
20
|
+
# rescue K9o::KError => e
|
|
21
|
+
# warn "[#{e.code}] #{e.message}: #{e.recommendation}"
|
|
22
|
+
# raise
|
|
23
|
+
# end
|
|
24
|
+
|
|
25
|
+
require_relative 'k9o/version'
|
|
26
|
+
require_relative 'k9o/kerror'
|
|
27
|
+
require_relative 'k9o/envelope'
|
|
28
|
+
require_relative 'k9o/url_grammar'
|
|
29
|
+
require_relative 'k9o/region'
|
|
30
|
+
require_relative 'k9o/http_transport'
|
|
31
|
+
require_relative 'k9o/data'
|
|
32
|
+
require_relative 'k9o/projects'
|
|
33
|
+
require_relative 'k9o/gate'
|
|
34
|
+
require_relative 'k9o/studio'
|
|
35
|
+
require_relative 'k9o/compute'
|
|
36
|
+
require_relative 'k9o/ai'
|
|
37
|
+
require_relative 'k9o/storage'
|
|
38
|
+
require_relative 'k9o/realtime'
|
|
39
|
+
require_relative 'k9o/migrations'
|
|
40
|
+
require_relative 'k9o/functions'
|
|
41
|
+
require_relative 'k9o/policies'
|
|
42
|
+
require_relative 'k9o/audit'
|
|
43
|
+
require_relative 'k9o/orgs'
|
|
44
|
+
require_relative 'k9o/me'
|
|
45
|
+
require_relative 'k9o/org'
|
|
46
|
+
# kibo ZK BYOK/HYOK crypto — self-guards its native binding (LoadError-safe), so
|
|
47
|
+
# requiring k9o never breaks on a platform without the kibo native lib.
|
|
48
|
+
require_relative 'k9o/kibo'
|
|
49
|
+
# kibo vault REST surface (client.kibo.vault.secrets) —.
|
|
50
|
+
require_relative 'k9o/vault'
|
|
51
|
+
# Barbican guardrail SDK — self-guards its native binding (LoadError-safe), so
|
|
52
|
+
# requiring k9o never breaks on a platform without the barbican native lib.
|
|
53
|
+
require_relative 'k9o/barbican'
|
|
54
|
+
require_relative 'k9o/client'
|
data/sig/k9o/ai.rbs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# RBS signatures for the `client.ai` namespace (platform AI primitives).
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
class Ai
|
|
5
|
+
# `input` is one string or an array of strings, sent exactly as given.
|
|
6
|
+
# `model` is omitted from the request body when nil (server default).
|
|
7
|
+
# Returns an Envelope whose `data` is
|
|
8
|
+
# { "embeddings" => Array[Array[Float]], "model" => String, "dimensions" => Integer }.
|
|
9
|
+
def initialize: (untyped config) -> void
|
|
10
|
+
def embed: (input: (String | Array[String]), ?model: String?) -> Envelope
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class K9oClient
|
|
14
|
+
attr_reader ai: Ai
|
|
15
|
+
end
|
|
16
|
+
end
|
data/sig/k9o/gate.rbs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# RBS signatures for the `client.gate` namespace (additions).
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
class Gate
|
|
5
|
+
# GET /@<slug>/gate/hosted. Unauthenticated (no credential sent).
|
|
6
|
+
# Returns an Envelope whose `data` is { "signInUrl" => String, "rpId" => String }.
|
|
7
|
+
# Raises KError (K9O_MISSING_PROJECT_CONTEXT) without a project slug; the
|
|
8
|
+
# uniform 404 K9O_PROJECT_NOT_FOUND for unknown org / deleted / non-Gate project.
|
|
9
|
+
def hosted: () -> Envelope
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# RBS signatures for the migration control-plane type model (ch13 §2.2, Ruby
|
|
2
|
+
# TIER-3: documented + statically-checkable hash pass-through). These describe
|
|
3
|
+
# the FIXED control-plane Operation union + its sub-shapes so steep/typeprof
|
|
4
|
+
# can check migration authoring. The runtime carries plain Hashes; the wire
|
|
5
|
+
# JSON matches packages/core/src/migrations.ts (snake_case keys, `searchable`
|
|
6
|
+
# NESTED inside the `encrypted` object form).
|
|
7
|
+
#
|
|
8
|
+
# Closed-union note: RBS cannot express "exactly one key set", so the Operation
|
|
9
|
+
# variants are typed as distinct single-key hash shapes and the closed-union
|
|
10
|
+
# property is enforced at wire + server validation (true for every tier).
|
|
11
|
+
|
|
12
|
+
module K9o
|
|
13
|
+
# The `encrypted` union: the bare `true` shorthand, or the object form with a
|
|
14
|
+
# NESTED `searchable` flag.
|
|
15
|
+
type column_encryption = { store: ("inline" | "vault"), ?searchable: bool }
|
|
16
|
+
type encrypted = (true | column_encryption)
|
|
17
|
+
|
|
18
|
+
type fk_action = ("CASCADE" | "RESTRICT" | "SET NULL" | "NO ACTION")
|
|
19
|
+
|
|
20
|
+
type column_references = {
|
|
21
|
+
collection: String,
|
|
22
|
+
on: String,
|
|
23
|
+
?on_delete: fk_action,
|
|
24
|
+
?on_update: fk_action,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# `type` stays a String (parser-owned vocabulary; not an enum).
|
|
28
|
+
type column_spec = {
|
|
29
|
+
name: String,
|
|
30
|
+
type: String,
|
|
31
|
+
?primary_key: bool,
|
|
32
|
+
?not_null: bool,
|
|
33
|
+
?unique: bool,
|
|
34
|
+
?default: String,
|
|
35
|
+
?references: column_references,
|
|
36
|
+
?encrypted: encrypted,
|
|
37
|
+
?blind_indexed: bool,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type index_spec = {
|
|
41
|
+
?name: String,
|
|
42
|
+
columns: Array[String],
|
|
43
|
+
?unique: bool,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# KEY-TAGGED Operation union — EXACTLY these 6 (FROZEN at v2; rename_column is
|
|
47
|
+
# the pending 7th, intentionally absent). Each variant is a single-key hash.
|
|
48
|
+
type create_table_op = { create_table: { name: String, columns: Array[column_spec], ?indexes: Array[index_spec] } }
|
|
49
|
+
type drop_table_op = { drop_table: String }
|
|
50
|
+
type add_column_op = { add_column: { table: String, column: column_spec } }
|
|
51
|
+
type drop_column_op = { drop_column: { table: String, column: String } }
|
|
52
|
+
type alter_table_op = { alter_table: { name: String, ?add_columns: Array[column_spec], ?drop_columns: Array[String] } }
|
|
53
|
+
type create_index_op = { create_index: { table: String, index: index_spec } }
|
|
54
|
+
type drop_index_op = { drop_index: { name: String } }
|
|
55
|
+
|
|
56
|
+
type operation = (create_table_op | drop_table_op | add_column_op | drop_column_op | alter_table_op | create_index_op | drop_index_op)
|
|
57
|
+
|
|
58
|
+
type action_rule = { ?rule: untyped }
|
|
59
|
+
|
|
60
|
+
type relationship_spec = {
|
|
61
|
+
collection: String,
|
|
62
|
+
on: String,
|
|
63
|
+
as: String,
|
|
64
|
+
?type: ("inner" | "left"),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type collection_policy = {
|
|
68
|
+
?references: Array[relationship_spec],
|
|
69
|
+
?actions: { ?select: action_rule, ?insert: action_rule, ?update: action_rule, ?delete: action_rule },
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type storage_bucket_policy = {
|
|
73
|
+
?public: bool,
|
|
74
|
+
?actions: { ?read: action_rule, ?upload: action_rule, ?delete: action_rule, ?list: action_rule },
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type migration_policy = {
|
|
78
|
+
?collections: Hash[String, collection_policy],
|
|
79
|
+
?storage: { ?buckets: Hash[String, storage_bucket_policy] },
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type migration = {
|
|
83
|
+
version: Integer,
|
|
84
|
+
slug: String,
|
|
85
|
+
up: Array[operation],
|
|
86
|
+
down: Array[operation],
|
|
87
|
+
?policy: migration_policy,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type applied_migration = {
|
|
91
|
+
version: Integer,
|
|
92
|
+
slug: String,
|
|
93
|
+
checksum: String,
|
|
94
|
+
appliedAt: String,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type pending_migration = {
|
|
98
|
+
pendingId: String,
|
|
99
|
+
version: Integer,
|
|
100
|
+
slug: String,
|
|
101
|
+
checksum: String,
|
|
102
|
+
submittedAt: String,
|
|
103
|
+
submittedBy: String,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class Migrations
|
|
107
|
+
@config: untyped
|
|
108
|
+
def initialize: (untyped config) -> void
|
|
109
|
+
def apply: (Array[migration] migrations) -> untyped
|
|
110
|
+
def list: () -> untyped
|
|
111
|
+
def get: (untyped version) -> untyped
|
|
112
|
+
def queue: (Array[migration] migrations) -> untyped
|
|
113
|
+
def list_pending: () -> untyped
|
|
114
|
+
def apply_pending: (String pending_id) -> untyped
|
|
115
|
+
def reject_pending: (String pending_id, ?reason: String?) -> untyped
|
|
116
|
+
def delete_pending: (String pending_id) -> untyped
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
def require_project_slug: () -> String
|
|
120
|
+
def enc: (untyped s) -> String
|
|
121
|
+
|
|
122
|
+
# Typed keyword-arg constructors for the control-plane Operation union.
|
|
123
|
+
module Build
|
|
124
|
+
def self?.column: (name: String, type: String, ?primary_key: bool?, ?not_null: bool?, ?unique: bool?, ?default: String?, ?references: column_references?, ?encrypted: encrypted?, ?blind_indexed: bool?) -> column_spec
|
|
125
|
+
def self?.column_encryption: (store: ("inline" | "vault"), ?searchable: bool?) -> column_encryption
|
|
126
|
+
def self?.references: (collection: String, on: String, ?on_delete: fk_action?, ?on_update: fk_action?) -> column_references
|
|
127
|
+
def self?.index: (columns: Array[String], ?name: String?, ?unique: bool?) -> index_spec
|
|
128
|
+
def self?.create_table: (name: String, columns: Array[column_spec], ?indexes: Array[index_spec]?) -> create_table_op
|
|
129
|
+
def self?.drop_table: (String name) -> drop_table_op
|
|
130
|
+
def self?.add_column: (table: String, column: column_spec) -> add_column_op
|
|
131
|
+
def self?.drop_column: (table: String, column: String) -> drop_column_op
|
|
132
|
+
def self?.alter_table: (name: String, ?add_columns: Array[column_spec]?, ?drop_columns: Array[String]?) -> alter_table_op
|
|
133
|
+
def self?.create_index: (table: String, index: index_spec) -> create_index_op
|
|
134
|
+
def self?.drop_index: (name: String) -> drop_index_op
|
|
135
|
+
def self?.migration: (version: Integer, slug: String, up: Array[operation], down: Array[operation], ?policy: migration_policy?) -> migration
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/sig/k9o/region.rbs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# RBS signatures for regional host resolution (K9o::Region).
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
module Region
|
|
5
|
+
REGION_FREE_ORG_HOST: Regexp
|
|
6
|
+
REGION_CODE: Regexp
|
|
7
|
+
|
|
8
|
+
# The regional origin `location` may name for `host`, or nil.
|
|
9
|
+
def self.origin_from_location: (String host, String location, String base) -> String?
|
|
10
|
+
# The origin requests go to: cached regional origin, else the configured base URL. Never raises.
|
|
11
|
+
def self.resolve_origin: (HttpClientConfig config) -> String
|
|
12
|
+
def self.probe: (HttpClientConfig config) -> [String, bool]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class HttpClientConfig
|
|
16
|
+
attr_reader region_mutex: Thread::Mutex
|
|
17
|
+
attr_accessor regional_origin: String?
|
|
18
|
+
end
|
|
19
|
+
end
|
data/sig/k9o/storage.rbs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# RBS signatures for the `client.storage` namespace (buckets and objects).
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
module Multipart
|
|
5
|
+
type part = { name: String, value: untyped, ?filename: String, ?content_type: String }
|
|
6
|
+
def self.encode: (Array[part] parts) -> [String, String]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class Storage
|
|
10
|
+
MAX_EXPIRES_IN: Integer
|
|
11
|
+
|
|
12
|
+
def initialize: (untyped config) -> void
|
|
13
|
+
def list_buckets: () -> Envelope
|
|
14
|
+
def get_bucket: (String name) -> Envelope
|
|
15
|
+
def create_bucket: (String name, ?public: bool?, ?file_size_limit: Integer?, ?allowed_mime_types: Array[String]?,
|
|
16
|
+
?versioning: bool?, ?delete_after_days: Integer?, ?byos_binding_id: String?) -> Envelope
|
|
17
|
+
# `patch` keys: snake_case or wire camelCase; an explicit nil clears a limit.
|
|
18
|
+
def update_bucket: (String name, Hash[(String | Symbol), untyped] patch) -> Envelope
|
|
19
|
+
def delete_bucket: (String name, ?empty: bool) -> Envelope
|
|
20
|
+
def from: (String bucket) -> BucketHandle
|
|
21
|
+
|
|
22
|
+
class BucketHandle
|
|
23
|
+
def initialize: (untyped config, String bucket) -> void
|
|
24
|
+
# `data` is a String (sent as its bytes) or an IO-like responding to #read.
|
|
25
|
+
def upload: (String path, (String | _Reader) data, ?content_type: String?, ?upsert: bool,
|
|
26
|
+
?metadata: Hash[String, String]?) -> Envelope
|
|
27
|
+
# `expires_in` is required: Integer 1..604800 (K9O_INVALID_FIELD_VALUE otherwise).
|
|
28
|
+
def create_signed_url: (String path, expires_in: Integer, ?download: bool?) -> Envelope
|
|
29
|
+
def list: (?prefix: String?) -> Envelope
|
|
30
|
+
def remove: (String path) -> Envelope
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
interface _Reader
|
|
34
|
+
def read: () -> String
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class K9oClient
|
|
39
|
+
attr_reader storage: Storage
|
|
40
|
+
end
|
|
41
|
+
end
|