k9o 1.0.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.
Potentially problematic release.
This version of k9o might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE +201 -0
- data/NOTICE +4 -0
- data/README.md +67 -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,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# Migration lifecycle namespace — k9o.migrations.*.
|
|
5
|
+
#
|
|
6
|
+
# TYPE MODEL (ch13 §2.2, cemented f21b3c15 — Ruby is TIER-3):
|
|
7
|
+
#
|
|
8
|
+
# Ruby keeps a RUNTIME HASH PASS-THROUGH for migration payloads (the
|
|
9
|
+
# gateway is the fail-closed source of truth; SDK types are ergonomics +
|
|
10
|
+
# serialization, never validation). The structured shape is documented
|
|
11
|
+
# below AND statically checkable via the shipped RBS signatures in
|
|
12
|
+
# sig/k9o/migrations.rbs (steep/typeprof), per the tier-3 contract:
|
|
13
|
+
# "documented + statically-checkable pass-through".
|
|
14
|
+
#
|
|
15
|
+
# The control-plane request/submit shape (the +migrations+ array) is the
|
|
16
|
+
# FIXED migration Operation union — control-plane, so it is TYPED. Build
|
|
17
|
+
# well-formed operation hashes with the keyword-arg helpers on
|
|
18
|
+
# K9o::Migrations::Build (idiomatic Ruby "typed" constructors) which emit
|
|
19
|
+
# exactly the wire JSON; or hand-author the hash against the documented
|
|
20
|
+
# shape. The closed-union property (exactly these 7 ops) is enforced at
|
|
21
|
+
# wire + server validation for all tiers.
|
|
22
|
+
#
|
|
23
|
+
# WIRE SHAPE (snake_case keys, matches packages/core/src/migrations.ts):
|
|
24
|
+
#
|
|
25
|
+
# migration = {
|
|
26
|
+
# 'version' => Integer, # NOTE: integer, not a "001" string
|
|
27
|
+
# 'slug' => String,
|
|
28
|
+
# 'up' => [Operation, ...],
|
|
29
|
+
# 'down' => [Operation, ...],
|
|
30
|
+
# 'policy' => MigrationPolicy, # optional
|
|
31
|
+
# }
|
|
32
|
+
#
|
|
33
|
+
# Operation — KEY-TAGGED union, EXACTLY ONE key, one of these 7 (alter_table
|
|
34
|
+
# is the batched column mutation, live in the gateway runner; rename_column
|
|
35
|
+
# remains a pending future op, intentionally absent):
|
|
36
|
+
# { 'create_table' => { 'name' => String,
|
|
37
|
+
# 'columns' => [ColumnSpec, ...],
|
|
38
|
+
# 'indexes' => [IndexSpec, ...] } } # indexes optional
|
|
39
|
+
# { 'drop_table' => String }
|
|
40
|
+
# { 'add_column' => { 'table' => String, 'column' => ColumnSpec } }
|
|
41
|
+
# { 'drop_column' => { 'table' => String, 'column' => String } }
|
|
42
|
+
# { 'create_index' => { 'table' => String, 'index' => IndexSpec } }
|
|
43
|
+
# { 'drop_index' => { 'name' => String } }
|
|
44
|
+
#
|
|
45
|
+
# ColumnSpec = {
|
|
46
|
+
# 'name' => String,
|
|
47
|
+
# 'type' => String, # parser-owned vocabulary; NOT an enum
|
|
48
|
+
# 'primary_key' => true|false, # optional
|
|
49
|
+
# 'not_null' => true|false, # optional
|
|
50
|
+
# 'unique' => true|false, # optional
|
|
51
|
+
# 'default' => String, # optional
|
|
52
|
+
# 'references' => ColumnReferences, # optional
|
|
53
|
+
# 'encrypted' => true | { 'store' => 'inline'|'vault',
|
|
54
|
+
# 'searchable' => true|false }, # optional
|
|
55
|
+
# # NOTE: 'searchable' is NESTED inside the object form,
|
|
56
|
+
# # NOT a sibling of the column.
|
|
57
|
+
# 'blind_indexed' => true|false, # optional (C-mode equality lookup)
|
|
58
|
+
# }
|
|
59
|
+
#
|
|
60
|
+
# IndexSpec = { 'name' => String (optional), 'columns' => [String, ...],
|
|
61
|
+
# 'unique' => true|false (optional) }
|
|
62
|
+
# ColumnReferences = { 'collection' => String, 'on' => String,
|
|
63
|
+
# 'on_delete' => 'CASCADE'|'RESTRICT'|'SET NULL'|'NO ACTION' (opt),
|
|
64
|
+
# 'on_update' => same (opt) }
|
|
65
|
+
#
|
|
66
|
+
# MigrationPolicy = { 'collections' => { String => CollectionPolicy },
|
|
67
|
+
# 'storage' => { 'buckets' => { String => StorageBucketPolicy } } }
|
|
68
|
+
# CollectionPolicy = { 'references' => [RelationshipSpec, ...],
|
|
69
|
+
# 'actions' => { 'select'|'insert'|'update'|'delete' => ActionRule } }
|
|
70
|
+
# RelationshipSpec = { 'collection' => String, 'on' => String, 'as' => String,
|
|
71
|
+
# 'type' => 'inner'|'left' (opt) }
|
|
72
|
+
# ActionRule = { 'rule' => any (opt) }
|
|
73
|
+
# StorageBucketPolicy = { 'public' => true|false (opt),
|
|
74
|
+
# 'actions' => { 'read'|'upload'|'delete'|'list' => ActionRule } }
|
|
75
|
+
#
|
|
76
|
+
# RESPONSE records (returned in the envelope data, pass-through hashes):
|
|
77
|
+
# AppliedMigration = { 'version' => Integer, 'slug' => String,
|
|
78
|
+
# 'checksum' => String, 'appliedAt' => String }
|
|
79
|
+
# PendingMigration = { 'pendingId' => String, 'version' => Integer,
|
|
80
|
+
# 'slug' => String, 'checksum' => String,
|
|
81
|
+
# 'submittedAt' => String, 'submittedBy' => String }
|
|
82
|
+
class Migrations
|
|
83
|
+
def initialize(config)
|
|
84
|
+
@config = config
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Apply migrations immediately (PROJECT_ADMIN minimum).
|
|
88
|
+
# +migrations+ is an array of migration hashes (see the shape above; build
|
|
89
|
+
# operation hashes with K9o::Migrations::Build).
|
|
90
|
+
def apply(migrations)
|
|
91
|
+
slug = require_project_slug
|
|
92
|
+
HttpTransport.request(config: @config, method: 'POST',
|
|
93
|
+
path: UrlGrammar.project_path(slug, 'migrations/apply'), body: { 'migrations' => migrations })
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def list
|
|
97
|
+
slug = require_project_slug
|
|
98
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, 'migrations'))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def get(version)
|
|
102
|
+
slug = require_project_slug
|
|
103
|
+
HttpTransport.request(config: @config, method: 'GET',
|
|
104
|
+
path: UrlGrammar.project_path(slug, "migrations/#{enc(version)}"))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Queue migrations for maker-checker approval per ch06 §8.
|
|
108
|
+
def queue(migrations)
|
|
109
|
+
slug = require_project_slug
|
|
110
|
+
HttpTransport.request(config: @config, method: 'POST',
|
|
111
|
+
path: UrlGrammar.project_path(slug, 'migrations/queue'), body: { 'migrations' => migrations })
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def list_pending
|
|
115
|
+
slug = require_project_slug
|
|
116
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, 'migrations/pending'))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def apply_pending(pending_id)
|
|
120
|
+
slug = require_project_slug
|
|
121
|
+
HttpTransport.request(config: @config, method: 'POST',
|
|
122
|
+
path: UrlGrammar.project_path(slug, "migrations/pending/#{enc(pending_id)}/apply"))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def reject_pending(pending_id, reason: nil)
|
|
126
|
+
slug = require_project_slug
|
|
127
|
+
body = reason ? { 'reason' => reason } : nil
|
|
128
|
+
HttpTransport.request(config: @config, method: 'POST',
|
|
129
|
+
path: UrlGrammar.project_path(slug, "migrations/pending/#{enc(pending_id)}/reject"), body: body)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def delete_pending(pending_id)
|
|
133
|
+
slug = require_project_slug
|
|
134
|
+
HttpTransport.request(config: @config, method: 'DELETE',
|
|
135
|
+
path: UrlGrammar.project_path(slug, "migrations/pending/#{enc(pending_id)}"))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Typed keyword-arg constructors for the FIXED control-plane migration
|
|
139
|
+
# Operation union + its sub-shapes. Each returns the exact wire hash; nil
|
|
140
|
+
# keyword args are omitted so the emitted JSON matches the canonical
|
|
141
|
+
# `omitempty`/optional shape (packages/core/src/migrations.ts,
|
|
142
|
+
# packages/go/migrations.go). These are the tier-3 "typed request object"
|
|
143
|
+
# surface — the RBS in sig/k9o/migrations.rbs gives them static checking.
|
|
144
|
+
module Build
|
|
145
|
+
module_function
|
|
146
|
+
|
|
147
|
+
# --- ColumnSpec --------------------------------------------------------
|
|
148
|
+
# +encrypted+: pass `true` for the inline shorthand, or the object hash
|
|
149
|
+
# `{ 'store' => 'inline'|'vault', 'searchable' => bool }`. Prefer the
|
|
150
|
+
# column_encryption helper to build the object form correctly (searchable
|
|
151
|
+
# NESTED, never a sibling).
|
|
152
|
+
def column(name:, type:, primary_key: nil, not_null: nil, unique: nil,
|
|
153
|
+
default: nil, references: nil, encrypted: nil, blind_indexed: nil)
|
|
154
|
+
h = { 'name' => name, 'type' => type }
|
|
155
|
+
h['primary_key'] = primary_key unless primary_key.nil?
|
|
156
|
+
h['not_null'] = not_null unless not_null.nil?
|
|
157
|
+
h['unique'] = unique unless unique.nil?
|
|
158
|
+
h['default'] = default unless default.nil?
|
|
159
|
+
h['references'] = references unless references.nil?
|
|
160
|
+
h['encrypted'] = encrypted unless encrypted.nil?
|
|
161
|
+
h['blind_indexed'] = blind_indexed unless blind_indexed.nil?
|
|
162
|
+
h
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Object form of the `encrypted` union. The bare `true` shorthand needs no
|
|
166
|
+
# helper — just pass `encrypted: true`. Here +searchable+ is NESTED inside
|
|
167
|
+
# the returned object, per the cemented shape.
|
|
168
|
+
def column_encryption(store:, searchable: nil)
|
|
169
|
+
h = { 'store' => store }
|
|
170
|
+
h['searchable'] = searchable unless searchable.nil?
|
|
171
|
+
h
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def references(collection:, on:, on_delete: nil, on_update: nil)
|
|
175
|
+
h = { 'collection' => collection, 'on' => on }
|
|
176
|
+
h['on_delete'] = on_delete unless on_delete.nil?
|
|
177
|
+
h['on_update'] = on_update unless on_update.nil?
|
|
178
|
+
h
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def index(columns:, name: nil, unique: nil)
|
|
182
|
+
h = { 'columns' => columns }
|
|
183
|
+
h['name'] = name unless name.nil?
|
|
184
|
+
h['unique'] = unique unless unique.nil?
|
|
185
|
+
h
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# --- Operation union (exactly one key per op) --------------------------
|
|
189
|
+
def create_table(name:, columns:, indexes: nil)
|
|
190
|
+
op = { 'name' => name, 'columns' => columns }
|
|
191
|
+
op['indexes'] = indexes unless indexes.nil?
|
|
192
|
+
{ 'create_table' => op }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def drop_table(name)
|
|
196
|
+
{ 'drop_table' => name }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def add_column(table:, column:)
|
|
200
|
+
{ 'add_column' => { 'table' => table, 'column' => column } }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def drop_column(table:, column:)
|
|
204
|
+
{ 'drop_column' => { 'table' => table, 'column' => column } }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# alter_table — BATCHED column mutation: one op carries optional
|
|
208
|
+
# add_columns + drop_columns (not separate ops). add_columns entries are
|
|
209
|
+
# full column_spec; drop_columns is column names. Live in the gateway
|
|
210
|
+
# runner.
|
|
211
|
+
def alter_table(name:, add_columns: nil, drop_columns: nil)
|
|
212
|
+
op = { 'name' => name }
|
|
213
|
+
op['add_columns'] = add_columns unless add_columns.nil?
|
|
214
|
+
op['drop_columns'] = drop_columns unless drop_columns.nil?
|
|
215
|
+
{ 'alter_table' => op }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def create_index(table:, index:)
|
|
219
|
+
{ 'create_index' => { 'table' => table, 'index' => index } }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def drop_index(name:)
|
|
223
|
+
{ 'drop_index' => { 'name' => name } }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# --- Migration ---------------------------------------------------------
|
|
227
|
+
def migration(version:, slug:, up:, down:, policy: nil)
|
|
228
|
+
h = { 'version' => version, 'slug' => slug, 'up' => up, 'down' => down }
|
|
229
|
+
h['policy'] = policy unless policy.nil?
|
|
230
|
+
h
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
private
|
|
235
|
+
|
|
236
|
+
def require_project_slug
|
|
237
|
+
Studio.require_project_slug(@config, 'migrations')
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def enc(s)
|
|
241
|
+
::URI.encode_www_form_component(s)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module K9o
|
|
6
|
+
# Minimal multipart/form-data encoder (stdlib only). Parts are hashes:
|
|
7
|
+
# { name: 'path', value: 'a.txt' } # plain field
|
|
8
|
+
# { name: 'file', value: bytes, filename: 'a.txt', # file part
|
|
9
|
+
# content_type: 'text/plain' }
|
|
10
|
+
# Values are sent as given, byte for byte (a String is its own bytes).
|
|
11
|
+
module Multipart
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Returns [body (binary String), content_type header value].
|
|
15
|
+
def encode(parts)
|
|
16
|
+
payload = parts.map { |p| p[:value].to_s.b }
|
|
17
|
+
boundary = nil
|
|
18
|
+
loop do
|
|
19
|
+
boundary = "----k9o-#{SecureRandom.hex(16)}"
|
|
20
|
+
break unless payload.any? { |v| v.include?(boundary) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
body = String.new(encoding: Encoding::BINARY)
|
|
24
|
+
parts.each_with_index do |p, i|
|
|
25
|
+
disposition = %(form-data; name="#{escape(p[:name])}")
|
|
26
|
+
disposition += %(; filename="#{escape(p[:filename])}") if p[:filename]
|
|
27
|
+
body << "--#{boundary}\r\n".b
|
|
28
|
+
body << "Content-Disposition: #{disposition}\r\n".b
|
|
29
|
+
body << "Content-Type: #{p[:content_type]}\r\n".b if p[:content_type]
|
|
30
|
+
body << "\r\n".b
|
|
31
|
+
body << payload[i]
|
|
32
|
+
body << "\r\n".b
|
|
33
|
+
end
|
|
34
|
+
body << "--#{boundary}--\r\n".b
|
|
35
|
+
[body, "multipart/form-data; boundary=#{boundary}"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def escape(value)
|
|
39
|
+
value.to_s.gsub('"', '%22').gsub("\r", '%0D').gsub("\n", '%0A')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/k9o/org.rb
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# Org-plane bound namespace — k9o.org.*. Singular
|
|
5
|
+
# bound-org namespace (distinct from k9o.orgs collection): members /
|
|
6
|
+
# groups / smtp / audit_log / gate (roles + idp_providers) + default_llm.
|
|
7
|
+
# Org-plane URL grammar per v2.5.68 §5.4 routes against the org subdomain
|
|
8
|
+
# without the @<projectSlug>/ prefix.
|
|
9
|
+
class Org
|
|
10
|
+
attr_reader :members, :groups, :smtp, :audit_log, :gate, :default_llm
|
|
11
|
+
|
|
12
|
+
def initialize(config)
|
|
13
|
+
@config = config
|
|
14
|
+
@members = Members.new(config)
|
|
15
|
+
@groups = Groups.new(config)
|
|
16
|
+
@smtp = Smtp.new(config)
|
|
17
|
+
@audit_log = AuditLog.new(config)
|
|
18
|
+
@gate = Gate.new(config)
|
|
19
|
+
@default_llm = DefaultLlm.new(config)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Read the current org (slug + name + status + settings).
|
|
23
|
+
def get
|
|
24
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('org'))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.enc(str)
|
|
28
|
+
::URI.encode_www_form_component(str)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# k9o.org.members.*
|
|
32
|
+
class Members
|
|
33
|
+
def initialize(config)
|
|
34
|
+
@config = config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def list(status: nil)
|
|
38
|
+
q = status ? { 'status' => status } : nil
|
|
39
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('members'), query: q)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def invite(email:, role:)
|
|
43
|
+
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('members'),
|
|
44
|
+
body: { 'email' => email, 'role' => role })
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get(user_id)
|
|
48
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path("members/#{Org.enc(user_id)}"))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def remove(user_id)
|
|
52
|
+
HttpTransport.request(config: @config, method: 'DELETE', path: UrlGrammar.org_path("members/#{Org.enc(user_id)}"))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# k9o.org.groups.*
|
|
57
|
+
class Groups
|
|
58
|
+
def initialize(config)
|
|
59
|
+
@config = config
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def list
|
|
63
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('groups'))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def create(name:)
|
|
67
|
+
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('groups'), body: { 'name' => name })
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def get(group_id)
|
|
71
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path("groups/#{Org.enc(group_id)}"))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delete(group_id)
|
|
75
|
+
HttpTransport.request(config: @config, method: 'DELETE', path: UrlGrammar.org_path("groups/#{Org.enc(group_id)}"))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# k9o.org.audit_log.* (org-plane; distinct from project Audit).
|
|
80
|
+
class AuditLog
|
|
81
|
+
def initialize(config)
|
|
82
|
+
@config = config
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def query(since: nil, until_: nil, type: nil, actor_id: nil, target_kind: nil, target_id: nil, limit: nil, cursor: nil)
|
|
86
|
+
q = {}
|
|
87
|
+
q['since'] = since if since
|
|
88
|
+
q['until'] = until_ if until_
|
|
89
|
+
q['type'] = type if type
|
|
90
|
+
q['actorId'] = actor_id if actor_id
|
|
91
|
+
q['targetKind'] = target_kind if target_kind
|
|
92
|
+
q['targetId'] = target_id if target_id
|
|
93
|
+
q['limit'] = limit if limit
|
|
94
|
+
q['cursor'] = cursor if cursor
|
|
95
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('audit-log'), query: q)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# k9o.org.gate.*
|
|
100
|
+
class Gate
|
|
101
|
+
attr_reader :roles, :idp_providers
|
|
102
|
+
|
|
103
|
+
def initialize(config)
|
|
104
|
+
@roles = Roles.new(config)
|
|
105
|
+
@idp_providers = IdpProviders.new(config)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# k9o.org.gate.roles.*
|
|
109
|
+
class Roles
|
|
110
|
+
def initialize(config)
|
|
111
|
+
@config = config
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def list(user_id: nil, role: nil)
|
|
115
|
+
q = {}
|
|
116
|
+
q['userId'] = user_id if user_id
|
|
117
|
+
q['role'] = role if role
|
|
118
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('org-role-grants'), query: q)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def grant(user_id:, role:)
|
|
122
|
+
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('org-role-grants'),
|
|
123
|
+
body: { 'userId' => user_id, 'role' => role })
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def get(grant_id)
|
|
127
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path("org-role-grants/#{Org.enc(grant_id)}"))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def revoke(grant_id)
|
|
131
|
+
HttpTransport.request(config: @config, method: 'DELETE', path: UrlGrammar.org_path("org-role-grants/#{Org.enc(grant_id)}"))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# k9o.org.gate.idp_providers.*
|
|
136
|
+
class IdpProviders
|
|
137
|
+
def initialize(config)
|
|
138
|
+
@config = config
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def list
|
|
142
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('idp-providers'))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def create(name:, kind:, config:)
|
|
146
|
+
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('idp-providers'),
|
|
147
|
+
body: { 'name' => name, 'kind' => kind, 'config' => config })
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def get(idp_id)
|
|
151
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path("idp-providers/#{Org.enc(idp_id)}"))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def configure(idp_id, patch)
|
|
155
|
+
HttpTransport.request(config: @config, method: 'PATCH',
|
|
156
|
+
path: UrlGrammar.org_path("idp-providers/#{Org.enc(idp_id)}"), body: patch)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def delete(idp_id)
|
|
160
|
+
HttpTransport.request(config: @config, method: 'DELETE', path: UrlGrammar.org_path("idp-providers/#{Org.enc(idp_id)}"))
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# k9o.org.smtp.*
|
|
166
|
+
class Smtp
|
|
167
|
+
def initialize(config)
|
|
168
|
+
@config = config
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def get
|
|
172
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('org/smtp'))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def set(host:, port:, username:, password:, from_email:, from_name:, encryption:, reply_to_email: nil)
|
|
176
|
+
smtp = {
|
|
177
|
+
'host' => host, 'port' => port, 'username' => username, 'password' => password,
|
|
178
|
+
'fromEmail' => from_email, 'fromName' => from_name, 'encryption' => encryption
|
|
179
|
+
}
|
|
180
|
+
smtp['replyToEmail'] = reply_to_email if reply_to_email
|
|
181
|
+
HttpTransport.request(config: @config, method: 'PATCH', path: UrlGrammar.org_path('org'), body: { 'smtpConfig' => smtp })
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def test(to:, from_override: nil)
|
|
185
|
+
body = { 'to' => to }
|
|
186
|
+
body['fromOverride'] = from_override if from_override
|
|
187
|
+
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('org/smtp/test-email'), body: body)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def unset
|
|
191
|
+
HttpTransport.request(config: @config, method: 'PATCH', path: UrlGrammar.org_path('org'), body: { 'smtpConfig' => nil })
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# k9o.org.default_llm.* per ch07 §10.3 (ORG_ADMIN; tier-gated).
|
|
196
|
+
class DefaultLlm
|
|
197
|
+
def initialize(config)
|
|
198
|
+
@config = config
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Read the org-default LLM model id (nil when unset → mistral-7b).
|
|
202
|
+
def get
|
|
203
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('org/default-llm'))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Set the org-default LLM (ORG_ADMIN; tier-gated per §10.1).
|
|
207
|
+
def set(model_id)
|
|
208
|
+
HttpTransport.request(config: @config, method: 'PATCH',
|
|
209
|
+
path: UrlGrammar.org_path('org/default-llm'), body: { 'modelId' => model_id })
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
data/lib/k9o/orgs.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# Org lifecycle namespace (collection-style) — k9o.orgs.*.
|
|
5
|
+
# Distinct from k9o.org (singular bound-org namespace).
|
|
6
|
+
class Orgs
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def list
|
|
12
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('orgs'))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(org_slug)
|
|
16
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path("orgs/#{enc(org_slug)}"))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def restore(org_id)
|
|
20
|
+
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path("orgs/#{enc(org_id)}/restore"))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def current
|
|
24
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('org'))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def enc(s)
|
|
30
|
+
::URI.encode_www_form_component(s)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/k9o/policies.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# Policy bundle lifecycle namespace — k9o.policies.*.
|
|
5
|
+
class Policies
|
|
6
|
+
def initialize(config)
|
|
7
|
+
@config = config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def current
|
|
11
|
+
slug = require_project_slug
|
|
12
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, 'policy'))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def list(status: nil)
|
|
16
|
+
slug = require_project_slug
|
|
17
|
+
q = status ? { 'status' => status } : nil
|
|
18
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, 'policies'), query: q)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create(name:, rules:)
|
|
22
|
+
slug = require_project_slug
|
|
23
|
+
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.project_path(slug, 'policies'),
|
|
24
|
+
body: { 'name' => name, 'rules' => rules })
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate(rules)
|
|
28
|
+
slug = require_project_slug
|
|
29
|
+
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.project_path(slug, 'policies/validate'),
|
|
30
|
+
body: { 'rules' => rules })
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get(bundle_id)
|
|
34
|
+
slug = require_project_slug
|
|
35
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, "policies/#{enc(bundle_id)}"))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def update(bundle_id, patch)
|
|
39
|
+
slug = require_project_slug
|
|
40
|
+
HttpTransport.request(config: @config, method: 'PATCH',
|
|
41
|
+
path: UrlGrammar.project_path(slug, "policies/#{enc(bundle_id)}"), body: patch)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def delete(bundle_id)
|
|
45
|
+
slug = require_project_slug
|
|
46
|
+
HttpTransport.request(config: @config, method: 'DELETE', path: UrlGrammar.project_path(slug, "policies/#{enc(bundle_id)}"))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def publish(bundle_id)
|
|
50
|
+
slug = require_project_slug
|
|
51
|
+
HttpTransport.request(config: @config, method: 'POST',
|
|
52
|
+
path: UrlGrammar.project_path(slug, "policies/#{enc(bundle_id)}/publish"))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def require_project_slug
|
|
58
|
+
Studio.require_project_slug(@config, 'policies')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def enc(s)
|
|
62
|
+
::URI.encode_www_form_component(s)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/k9o/projects.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# Project lifecycle namespace — k9o.projects.* (Ruby snake_case).
|
|
5
|
+
class Projects
|
|
6
|
+
def initialize(config)
|
|
7
|
+
@config = config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def list
|
|
11
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.org_path('projects'))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create(slug:, name:, custody_mode: nil)
|
|
15
|
+
UrlGrammar.validate_slug(slug, 'project')
|
|
16
|
+
body = { 'slug' => slug, 'name' => name }
|
|
17
|
+
body['custodyMode'] = custody_mode if custody_mode
|
|
18
|
+
HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.org_path('projects'), body: body)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def get(slug)
|
|
22
|
+
UrlGrammar.validate_slug(slug, 'project')
|
|
23
|
+
HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, 'project'))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update(slug, name: nil)
|
|
27
|
+
UrlGrammar.validate_slug(slug, 'project')
|
|
28
|
+
patch = {}
|
|
29
|
+
patch['name'] = name if name
|
|
30
|
+
HttpTransport.request(config: @config, method: 'PATCH', path: UrlGrammar.project_path(slug, 'project'), body: patch)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|