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.
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'kerror'
4
+
5
+ module K9o
6
+ # Barbican — free, embeddable L2 guardrail. Input normalization -> Aho-Corasick
7
+ # deny-list matching over a signed ruleset -> local verdict, plus a streaming
8
+ # output redactor (buffer-and-scan-before-flush).
9
+ #
10
+ # Thin wrapper over the bound core (`k9o-keep-core`, via uniffi) — mirrors the
11
+ # structural pattern of +K9o::Kibo+ (native-load guard evaluated once at
12
+ # require time; every call forwards typed arguments to the generated binding
13
+ # and maps its error -> +K9o::KError+). ZERO guardrail logic lives here: every
14
+ # normalization pass, deny match, verdict, and redaction decision is the
15
+ # audited native core's.
16
+ #
17
+ # CLAIM BOUNDARY: Barbican is advisory / defense-in-depth (edge filtration).
18
+ # It is never the enforced sandbox boundary.
19
+ #
20
+ # The generated uniffi binding + native lib (+lib/k9o/_ffi/+) are build
21
+ # artifacts (CI-injected per platform; gitignored) — on a platform/Ruby
22
+ # without them, +require+ still succeeds and +Barbican.new+ raises a clear
23
+ # +K9o::KError+, so loading +k9o+ never breaks for the rest of the SDK.
24
+ class Barbican
25
+ # Attempt to load the native binding; never raises at require time.
26
+ NATIVE_AVAILABLE =
27
+ begin
28
+ require_relative '_ffi/k9o_keep_core_ffi_uniffi'
29
+ true
30
+ rescue LoadError
31
+ false
32
+ end
33
+
34
+ # +decode_depth+ of 0 uses the core default (3) — a bounded base64/leetspeak
35
+ # de-obfuscation depth (decode-bomb guard).
36
+ def initialize(fail_open: false, leet: true, decode_depth: 0)
37
+ self.class.ensure_native!
38
+ config = K9oKeepCoreFfiUniffi::KeepConfig.new(
39
+ fail_open: fail_open, leet: leet, decode_depth: decode_depth
40
+ )
41
+ @core = K9oKeepCoreFfiUniffi::KeepCore.new(config)
42
+ end
43
+
44
+ # Load a static ruleset bundled with your gem (Mode-A path: provenance is
45
+ # vouched for by your package, so signature verification is skipped — the
46
+ # core still enforces anti-rollback, never loading a version <= the current
47
+ # one). +rules+ = an array of {id:, pattern:, severity:} (severity one of
48
+ # low|medium|high|critical; string or symbol keys both accepted). Returns
49
+ # the accepted version.
50
+ def load_ruleset(ruleset_version, rules)
51
+ blob = K9oKeepCoreFfiUniffi::KeepRulesetBlob.new(
52
+ ruleset_version: ruleset_version,
53
+ rules: rules.map { |r| build_rule(r) }
54
+ )
55
+ begin
56
+ @core.load_ruleset_unsigned(blob)
57
+ rescue StandardError => e
58
+ raise self.class.to_kerror(e)
59
+ end
60
+ end
61
+
62
+ # Load a Barbican key manifest (compact JWS, +typ k9o.barbican.keys.v1+). The
63
+ # core checks it against the root key compiled into it and, only if it
64
+ # verifies, REPLACES the trusted ruleset-signing keys with the ones it lists.
65
+ # A refusal (wrong signer, rollback, a key changing its public key, a
66
+ # malformed entry) raises +K9O_BARBICAN_RULESET_INVALID+ with
67
+ # +context[:stage] == 'key_manifest'+ and leaves the trusted keys unchanged.
68
+ # Returns the manifest +sequence+.
69
+ def load_key_manifest(jws, now_secs = nil)
70
+ @core.load_key_manifest(jws, now_secs || Time.now.to_i)
71
+ rescue StandardError => e
72
+ raise self.class.to_kerror(e, 'key_manifest')
73
+ end
74
+
75
+ # Verify a ruleset manifest (compact JWS) under a trusted key and return its
76
+ # authenticated fields (+ruleset_version+ / +signing_key_kid+ /
77
+ # +artifact_hash+ / +artifact_url+ / +valid_until+).
78
+ def verify_manifest(jws)
79
+ @core.verify_manifest(jws)
80
+ rescue StandardError => e
81
+ raise self.class.to_kerror(e)
82
+ end
83
+
84
+ # Load a signed ruleset. The core verifies the Ed25519 signature ITSELF, over
85
+ # its own canonical bytes, under a key from the loaded key manifest: there is
86
+ # no key or verifier to pass. Any failure (no key manifest loaded, unknown
87
+ # key, bad or non-canonical signature, stale version, malformed rules) raises
88
+ # +K9O_BARBICAN_RULESET_INVALID+ with +context[:reason]+ and leaves the
89
+ # current ruleset running. +signature+ is the raw 64-byte binary String.
90
+ # Returns the accepted version.
91
+ def load_ruleset_signed(ruleset_version, rules, signing_key_kid, signature, created_at = 0)
92
+ @core.load_ruleset_signed(signed_blob(ruleset_version, rules, signing_key_kid, signature, created_at))
93
+ rescue StandardError => e
94
+ raise self.class.to_kerror(e)
95
+ end
96
+
97
+ # Lowercase hex SHA-256 of a ruleset's canonical signing bytes — the value a
98
+ # manifest's +artifact_hash+ names. A digest only; it decides no trust.
99
+ def ruleset_digest(ruleset_version, rules, signing_key_kid, signature, created_at = 0)
100
+ @core.ruleset_digest(signed_blob(ruleset_version, rules, signing_key_kid, signature, created_at))
101
+ rescue StandardError => e
102
+ raise self.class.to_kerror(e)
103
+ end
104
+
105
+ # The +sequence+ of the key manifest in force (0 before one is loaded).
106
+ def key_manifest_sequence
107
+ @core.key_manifest_sequence
108
+ end
109
+
110
+ # The highest VERIFIED ruleset version loaded (0 = none): the anti-rollback floor for signed updates. A refresh
111
+ # compares a published version against this, not the version in force.
112
+ def signed_floor
113
+ @core.signed_floor
114
+ end
115
+
116
+ # Whether the ruleset in force came from a verified load. +false+ when none is loaded, or when +load_ruleset+
117
+ # (unsigned, host-vouched) replaced a verified one.
118
+ def active_ruleset_is_signed?
119
+ @core.active_ruleset_is_signed
120
+ end
121
+
122
+ # The signing kids the loaded key manifest currently trusts, sorted.
123
+ def trusted_kids
124
+ @core.trusted_kids
125
+ end
126
+
127
+ # The trusted kids the key manifest flags deprecated, sorted.
128
+ def deprecated_kids
129
+ @core.deprecated_kids
130
+ end
131
+
132
+ # +false+ on every shipped build. +true+ means this binding trusts the
133
+ # PUBLISHED TEST root and must never be used outside tests.
134
+ def self.uses_test_trust_root?
135
+ ensure_native!
136
+ K9oKeepCoreFfiUniffi.uses_test_trust_root
137
+ end
138
+
139
+ # Inspect an input (normalize -> deny -> local verdict). Obfuscation is
140
+ # peeled BEFORE matching, so static deny-lists aren't defeated by trivial
141
+ # tricks. +waypoint_class+ is the manifest's meter hint — it NEVER gates the
142
+ # decision. Returns the verdict object (+decision+ / +reason+ / +matched+ /
143
+ # +schema_valid+ / +waypoint_class_hint+ / +fail_mode+ / +ruleset_version+ /
144
+ # +core_version+).
145
+ def inspect(text, waypoint_class: nil)
146
+ @core.inspect_input(text, waypoint_class)
147
+ end
148
+
149
+ # Local L3 stand-in classification ("clear"|"suspicious"|"malicious"). No
150
+ # receipt — a classification is not an enforcement decision.
151
+ def classify(text)
152
+ @core.classify_local(text)
153
+ end
154
+
155
+ # Open an output stream-filter bound to the active ruleset's exfil markers.
156
+ def create_output_filter
157
+ OutputFilter.new(@core.output_filter)
158
+ end
159
+
160
+ # The bound core's semver (audit anchor; rides every verdict too).
161
+ def core_version
162
+ @core.core_version
163
+ end
164
+
165
+ # Guard: raise a clear KError if the native lib is absent (evaluated BEFORE
166
+ # any reference to the +K9oKeepCoreFfiUniffi+ namespace, which may be
167
+ # undefined).
168
+ def self.ensure_native!
169
+ return if NATIVE_AVAILABLE
170
+
171
+ raise K9o::KError.new(
172
+ code: 'K9O_BARBICAN_WASM_ERROR',
173
+ message: 'barbican native library not available for this platform/Ruby; install the platform-specific k9o gem.'
174
+ )
175
+ end
176
+
177
+ # Map the bound core's error -> +K9o::KError+. +KeepError::RulesetInvalid+
178
+ # is the only typed variant (bad/stale/malformed ruleset) — its reason token
179
+ # (+version_stale+ / +signature_invalid+ / +malformed+ / an unknown-severity
180
+ # message) rides in +context[:reason]+. Anything else from the native layer
181
+ # (e.g. a Rust panic surfaced as +InternalError+) maps to the generic code.
182
+ def self.to_kerror(e, stage = 'ruleset')
183
+ if e.respond_to?(:reason)
184
+ K9o::KError.new(
185
+ code: 'K9O_BARBICAN_RULESET_INVALID',
186
+ message: e.to_s,
187
+ context: { reason: e.reason, stage: stage }
188
+ )
189
+ else
190
+ K9o::KError.new(code: 'K9O_BARBICAN_WASM_ERROR', message: e.to_s)
191
+ end
192
+ end
193
+
194
+ # Accept string or symbol keys ({"id" => ..} or {id: ..}) for ergonomics.
195
+ def build_rule(r)
196
+ K9oKeepCoreFfiUniffi::KeepRule.new(
197
+ id: r[:id] || r['id'],
198
+ pattern: r[:pattern] || r['pattern'],
199
+ severity: r[:severity] || r['severity']
200
+ )
201
+ end
202
+ private :build_rule
203
+
204
+ def signed_blob(ruleset_version, rules, signing_key_kid, signature, created_at)
205
+ K9oKeepCoreFfiUniffi::KeepSignedRulesetBlob.new(
206
+ ruleset_version: ruleset_version,
207
+ rules: rules.map { |r| build_rule(r) },
208
+ signing_key_kid: signing_key_kid,
209
+ signature: signature.b,
210
+ created_at: created_at
211
+ )
212
+ end
213
+ private :signed_blob
214
+ end
215
+
216
+ # A streaming output redactor. Buffer-and-scan-before-flush: never emits a
217
+ # byte the core has not cleared (no RST-after-leak).
218
+ class OutputFilter
219
+ def initialize(handle)
220
+ @handle = handle
221
+ end
222
+
223
+ # Feed a chunk; returns the cleared (redacted) prefix safe to emit now.
224
+ def push(chunk)
225
+ @handle.push(chunk)
226
+ end
227
+
228
+ # Flush the residual buffer (also scanned) at end of stream.
229
+ def finish
230
+ @handle.finish
231
+ end
232
+
233
+ # Leak report (counts only — never the leaked values): an object with
234
+ # +emails+ / +secrets+ / +markers+ / +total+.
235
+ def report
236
+ @handle.report
237
+ end
238
+ end
239
+ end
data/lib/k9o/client.rb ADDED
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # The customer-facing client returned by K9o.create_client.
5
+ class K9oClient
6
+ attr_reader :config, :projects, :gate, :studio, :compute, :realtime, :org,
7
+ :orgs, :migrations, :functions, :policies, :audit, :me, :kibo, :ai, :storage
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ @projects = Projects.new(config)
12
+ @gate = Gate.new(config)
13
+ @studio = Studio.new(config)
14
+ @compute = Compute.new(config)
15
+ @realtime = Realtime.new(config)
16
+ @org = Org.new(config)
17
+ @orgs = Orgs.new(config)
18
+ @migrations = Migrations.new(config)
19
+ @functions = Functions.new(config)
20
+ @policies = Policies.new(config)
21
+ @audit = Audit.new(config)
22
+ @me = Me.new(config)
23
+ @kibo = KiboNamespace.new(config)
24
+ @ai = Ai.new(config)
25
+ @storage = Storage.new(config)
26
+ end
27
+
28
+ def from(collection)
29
+ FromBuilder.new(@config, collection)
30
+ end
31
+
32
+ def base_url
33
+ @config.base_url
34
+ end
35
+ end
36
+
37
+ # Factory — construct a K9oClient bound to the supplied options.
38
+ #
39
+ # Per spec sample:
40
+ # k9o = K9o.create_client(
41
+ # api_key: ENV["K9O_API_KEY"],
42
+ # org_slug: "acme",
43
+ # project_slug: "member-portal",
44
+ # )
45
+ #
46
+ # base_url is OPTIONAL. When omitted it
47
+ # defaults to the platform apex https://api.k9o.io; when the host is the
48
+ # bare apex and org_slug is set, the canonical org-subdomain wire root
49
+ # https://<org_slug>.api.k9o.io is composed. Any other host passes through.
50
+ def self.create_client(base_url: nil, api_key: nil, org_slug: nil, project_slug: nil, default_headers: {}, http_client: nil)
51
+ normalised = UrlGrammar.resolve_base_url(base_url, org_slug)
52
+ config = HttpClientConfig.new(
53
+ base_url: normalised,
54
+ api_key: api_key, org_slug: org_slug, project_slug: project_slug,
55
+ default_headers: default_headers, http_client: http_client
56
+ )
57
+ K9oClient.new(config)
58
+ end
59
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Compute namespace (Compute LLMs registry).
5
+ # k9o.compute.llms.*
6
+ class Compute
7
+ # Curated 9-model closed enum (extensible v2.x).
8
+ LLM_MODEL_IDS = %w[
9
+ mistral-7b mistral-8x7b llama-3-8b llama-3-70b
10
+ claude-3-5-sonnet gpt-4o gpt-4o-mini
11
+ text-embedding-3-small text-embedding-3-large
12
+ ].freeze
13
+
14
+ attr_reader :llms
15
+
16
+ def initialize(config)
17
+ @llms = Llms.new(config)
18
+ end
19
+
20
+ # k9o.compute.llms.*.
21
+ class Llms
22
+ def initialize(config)
23
+ @config = config
24
+ end
25
+
26
+ def list(modality: nil)
27
+ slug = Studio.require_project_slug(@config, 'compute.llms')
28
+ q = modality ? { 'modality' => modality } : nil
29
+ HttpTransport.request(config: @config, method: 'GET',
30
+ path: UrlGrammar.project_path(slug, 'compute/llms'), query: q)
31
+ end
32
+
33
+ def read(model_id)
34
+ slug = Studio.require_project_slug(@config, 'compute.llms')
35
+ HttpTransport.request(config: @config, method: 'GET',
36
+ path: UrlGrammar.project_path(slug, "compute/llms/#{enc(model_id)}"))
37
+ end
38
+
39
+ # Synchronous inference (PROJECT_AUTHOR/ADMIN).
40
+ def invoke(model_id, prompt:, params: nil)
41
+ slug = Studio.require_project_slug(@config, 'compute.llms')
42
+ body = { 'prompt' => prompt }
43
+ body['params'] = params if params
44
+ HttpTransport.request(config: @config, method: 'POST',
45
+ path: UrlGrammar.project_path(slug, "compute/llms/#{enc(model_id)}/invoke"), body: body)
46
+ end
47
+
48
+ # Streaming inference (SSE per-token). Returns an
49
+ # Enumerator of chunk hashes (Ruby idiom). Consume with
50
+ # `.each { |chunk| ... }`.
51
+ def invoke_stream(model_id, prompt:, params: nil)
52
+ slug = Studio.require_project_slug(@config, 'compute.llms')
53
+ body = { 'prompt' => prompt }
54
+ body['params'] = params if params
55
+ HttpTransport.stream_sse(config: @config, method: 'POST',
56
+ path: UrlGrammar.project_path(slug, "compute/llms/#{enc(model_id)}/invoke-stream"),
57
+ body: body)
58
+ end
59
+
60
+ private
61
+
62
+ def enc(s)
63
+ ::URI.encode_www_form_component(s)
64
+ end
65
+ end
66
+ end
67
+ end
data/lib/k9o/data.rb ADDED
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Data-plane DSL.
5
+ #
6
+ # Usage:
7
+ #
8
+ # response = k9o.from('posts').select.eq('status', 'published').limit(10).execute
9
+ # response.data.each { |row| puts row }
10
+ class FromBuilder
11
+ def initialize(config, collection)
12
+ @config = config
13
+ @collection = collection
14
+ end
15
+
16
+ def select
17
+ SelectBuilder.new(@config, @collection)
18
+ end
19
+
20
+ def insert(record)
21
+ HttpTransport.request(
22
+ config: @config, method: 'POST', path: FromBuilder.collection_path(@config, @collection), body: record
23
+ )
24
+ end
25
+
26
+ def update(patch)
27
+ UpdateBuilder.new(@config, @collection, patch)
28
+ end
29
+
30
+ def delete
31
+ DeleteBuilder.new(@config, @collection)
32
+ end
33
+
34
+ def self.collection_path(config, collection)
35
+ raise KError.new(
36
+ code: 'K9O_MISSING_PROJECT_CONTEXT',
37
+ message: 'data-plane operations require project_slug to be bound on the client',
38
+ context: { collection: collection },
39
+ recommendation: 'Pass project_slug: in K9o.create_client(...).'
40
+ ) if config.project_slug.nil? || config.project_slug.empty?
41
+
42
+ unless collection.is_a?(String) && collection.match?(/\A[a-z][a-z0-9_-]{0,62}\z/)
43
+ raise KError.new(
44
+ code: 'K9O_INVALID_FIELD_VALUE',
45
+ message: "collection name #{collection.inspect} does not match required format",
46
+ context: { collection: collection }
47
+ )
48
+ end
49
+ UrlGrammar.project_path(config.project_slug, "data/#{collection}")
50
+ end
51
+ end
52
+
53
+ # Shared filter/pagination state.
54
+ class BaseBuilder
55
+ def initialize(config, collection)
56
+ @config = config
57
+ @collection = collection
58
+ @filters = []
59
+ @limit_val = nil
60
+ @cursor_val = nil
61
+ @count_mode = nil
62
+ end
63
+
64
+ %i[eq neq gt gte lt lte like ilike is].each do |op|
65
+ define_method(op) do |column, value|
66
+ @filters << [column, op.to_s, value.to_s]
67
+ self
68
+ end
69
+ end
70
+
71
+ def in_(column, values)
72
+ @filters << [column, 'in', "(#{values.map(&:to_s).join(',')})"]
73
+ self
74
+ end
75
+
76
+ def not_in(column, values)
77
+ @filters << [column, 'notIn', "(#{values.map(&:to_s).join(',')})"]
78
+ self
79
+ end
80
+
81
+ def filter(column, op, value)
82
+ @filters << [column, op.to_s, value.to_s]
83
+ self
84
+ end
85
+
86
+ def limit(n)
87
+ @limit_val = n
88
+ self
89
+ end
90
+
91
+ def cursor(opaque)
92
+ @cursor_val = opaque
93
+ self
94
+ end
95
+
96
+ def count(mode)
97
+ @count_mode = mode
98
+ self
99
+ end
100
+
101
+ protected
102
+
103
+ def to_query
104
+ q = {}
105
+ @filters.each do |col, op, val|
106
+ q[col] = val.start_with?('(') ? "#{op}.#{val}" : "#{op}.#{val}"
107
+ end
108
+ q['limit'] = @limit_val.to_s if @limit_val
109
+ q['cursor'] = @cursor_val if @cursor_val
110
+ q['count'] = @count_mode if @count_mode
111
+ q
112
+ end
113
+ end
114
+
115
+ class SelectBuilder < BaseBuilder
116
+ def execute
117
+ HttpTransport.request(
118
+ config: @config, method: 'GET',
119
+ path: FromBuilder.collection_path(@config, @collection),
120
+ query: to_query
121
+ )
122
+ end
123
+ end
124
+
125
+ class UpdateBuilder < BaseBuilder
126
+ def initialize(config, collection, patch)
127
+ super(config, collection)
128
+ @patch = patch
129
+ end
130
+
131
+ def execute
132
+ id_filter = @filters.find { |col, op, _| col == 'id' && op == 'eq' }
133
+ raise KError.new(
134
+ code: 'K9O_INVALID_FIELD_VALUE',
135
+ message: 'update() requires .eq("id", ...) filter; changing many rows in one call is not supported',
136
+ context: { collection: @collection }
137
+ ) unless id_filter
138
+
139
+ path = "#{FromBuilder.collection_path(@config, @collection)}/#{id_filter[2]}"
140
+ HttpTransport.request(config: @config, method: 'PATCH', path: path, body: @patch)
141
+ end
142
+ end
143
+
144
+ class DeleteBuilder < BaseBuilder
145
+ def execute
146
+ id_filter = @filters.find { |col, op, _| col == 'id' && op == 'eq' }
147
+ raise KError.new(
148
+ code: 'K9O_INVALID_FIELD_VALUE',
149
+ message: 'delete() requires .eq("id", ...) filter; deleting many rows in one call is not supported',
150
+ context: { collection: @collection }
151
+ ) unless id_filter
152
+
153
+ path = "#{FromBuilder.collection_path(@config, @collection)}/#{id_filter[2]}"
154
+ HttpTransport.request(config: @config, method: 'DELETE', path: path)
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Standard envelope returned by every k9o surface on success.
5
+ class Envelope
6
+ attr_reader :data, :meta
7
+
8
+ def initialize(data:, meta:)
9
+ @data = data
10
+ @meta = meta
11
+ end
12
+
13
+ def self.from_wire(wire)
14
+ Envelope.new(
15
+ data: wire['data'],
16
+ meta: Meta.from_wire(wire['meta'] || {})
17
+ )
18
+ end
19
+ end
20
+
21
+ # Meta accessor sugar.
22
+ class Meta
23
+ attr_reader :cursor, :subscription, :idempotency, :warnings, :cost, :timing, :context
24
+
25
+ def initialize(cursor: nil, subscription: nil, idempotency: nil, warnings: [], cost: {}, timing: nil, context: nil)
26
+ @cursor = cursor
27
+ @subscription = subscription
28
+ @idempotency = idempotency
29
+ @warnings = warnings
30
+ @cost = cost
31
+ @timing = timing
32
+ @context = context
33
+ end
34
+
35
+ def self.from_wire(wire)
36
+ cursor = wire['cursor'] ? MetaCursor.new(next_: wire['cursor']['next'], total_count: wire['cursor']['totalCount']) : nil
37
+ sub = wire['subscription'] ? MetaSubscription.new(is_initial: !!wire['subscription']['isInitial']) : nil
38
+ idem = wire['idempotency'] ? MetaIdempotency.new(replayed: !!wire['idempotency']['replayed']) : nil
39
+ warnings = (wire['warnings'] || []).map { |w| MetaWarning.new(code: w['code'] || '', message: w['message'] || '') }
40
+ timing = wire['timing'] ? MetaTiming.new(total_ms: wire['timing']['totalMs'].to_i) : nil
41
+ context = wire['context'] && wire['context']['requestId'] ? MetaContext.new(request_id: wire['context']['requestId']) : nil
42
+ Meta.new(
43
+ cursor: cursor, subscription: sub, idempotency: idem,
44
+ warnings: warnings, cost: wire['cost'] || {},
45
+ timing: timing, context: context
46
+ )
47
+ end
48
+ end
49
+
50
+ class MetaCursor
51
+ attr_reader :next, :total_count
52
+
53
+ def initialize(next_:, total_count:)
54
+ @next = next_
55
+ @total_count = total_count
56
+ end
57
+ end
58
+
59
+ MetaSubscription = Struct.new(:is_initial, keyword_init: true)
60
+ MetaIdempotency = Struct.new(:replayed, keyword_init: true)
61
+ MetaWarning = Struct.new(:code, :message, keyword_init: true)
62
+ MetaTiming = Struct.new(:total_ms, keyword_init: true)
63
+ MetaContext = Struct.new(:request_id, keyword_init: true)
64
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Function lifecycle namespace — k9o.functions.*.
5
+ class Functions
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def deploy(name:, runtime:, code:, tier: nil, config: nil)
11
+ slug = require_project_slug
12
+ body = { 'name' => name, 'runtime' => runtime, 'code' => code }
13
+ body['tier'] = tier if tier
14
+ body['config'] = config if config
15
+ HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.project_path(slug, 'functions'), body: body)
16
+ end
17
+
18
+ def list(status: nil)
19
+ slug = require_project_slug
20
+ q = status ? { 'status' => status } : nil
21
+ HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, 'functions'), query: q)
22
+ end
23
+
24
+ def get(name)
25
+ slug = require_project_slug
26
+ HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, "functions/#{enc(name)}"))
27
+ end
28
+
29
+ def delete(name)
30
+ slug = require_project_slug
31
+ HttpTransport.request(config: @config, method: 'DELETE', path: UrlGrammar.project_path(slug, "functions/#{enc(name)}"))
32
+ end
33
+
34
+ # Invoke a function; payload passed to the runtime verbatim.
35
+ def invoke(name, payload)
36
+ slug = require_project_slug
37
+ HttpTransport.request(config: @config, method: 'POST',
38
+ path: UrlGrammar.project_path(slug, "functions/#{enc(name)}/invoke"), body: payload)
39
+ end
40
+
41
+ private
42
+
43
+ def require_project_slug
44
+ Studio.require_project_slug(@config, 'functions')
45
+ end
46
+
47
+ def enc(s)
48
+ ::URI.encode_www_form_component(s)
49
+ end
50
+ end
51
+ end