edgebase_core 0.1.4

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,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Auto-generated client wrapper methods — DO NOT EDIT.
4
+ #
5
+ # Regenerate: npx tsx tools/sdk-codegen/generate.ts
6
+ # Source: wrapper-config.json + openapi.json (0.1.0)
7
+
8
+ module EdgebaseCore
9
+ class GeneratedAuthMethods
10
+ # Authentication wrapper methods
11
+
12
+ def initialize(core)
13
+ @core = core
14
+ end
15
+
16
+ # Sign up with email and password
17
+ def sign_up(body = nil)
18
+ @core.auth_signup(body)
19
+ end
20
+
21
+ # Sign in with email and password
22
+ def sign_in(body = nil)
23
+ @core.auth_signin(body)
24
+ end
25
+
26
+ # Sign out and revoke refresh token
27
+ def sign_out(body = nil)
28
+ @core.auth_signout(body)
29
+ end
30
+
31
+ # Sign in anonymously
32
+ def sign_in_anonymously(body = nil)
33
+ @core.auth_signin_anonymous(body)
34
+ end
35
+
36
+ # Send magic link to email
37
+ def sign_in_with_magic_link(body = nil)
38
+ @core.auth_signin_magic_link(body)
39
+ end
40
+
41
+ # Verify magic link token
42
+ def verify_magic_link(body = nil)
43
+ @core.auth_verify_magic_link(body)
44
+ end
45
+
46
+ # Send OTP SMS to phone number
47
+ def sign_in_with_phone(body = nil)
48
+ @core.auth_signin_phone(body)
49
+ end
50
+
51
+ # Verify phone OTP and create session
52
+ def verify_phone(body = nil)
53
+ @core.auth_verify_phone(body)
54
+ end
55
+
56
+ # Send OTP code to email
57
+ def sign_in_with_email_otp(body = nil)
58
+ @core.auth_signin_email_otp(body)
59
+ end
60
+
61
+ # Verify email OTP and create session
62
+ def verify_email_otp(body = nil)
63
+ @core.auth_verify_email_otp(body)
64
+ end
65
+
66
+ # Link phone number to existing account
67
+ def link_with_phone(body = nil)
68
+ @core.auth_link_phone(body)
69
+ end
70
+
71
+ # Verify OTP and link phone to account
72
+ def verify_link_phone(body = nil)
73
+ @core.auth_verify_link_phone(body)
74
+ end
75
+
76
+ # Link email and password to existing account
77
+ def link_with_email(body = nil)
78
+ @core.auth_link_email(body)
79
+ end
80
+
81
+ # Request email change with password confirmation
82
+ def change_email(body = nil)
83
+ @core.auth_change_email(body)
84
+ end
85
+
86
+ # Verify email change token
87
+ def verify_email_change(body = nil)
88
+ @core.auth_verify_email_change(body)
89
+ end
90
+
91
+ # Verify email address with token
92
+ def verify_email(body = nil)
93
+ @core.auth_verify_email(body)
94
+ end
95
+
96
+ # Request password reset email
97
+ def request_password_reset(body = nil)
98
+ @core.auth_request_password_reset(body)
99
+ end
100
+
101
+ # Reset password with token
102
+ def reset_password(body = nil)
103
+ @core.auth_reset_password(body)
104
+ end
105
+
106
+ # Change password for authenticated user
107
+ def change_password(body = nil)
108
+ @core.auth_change_password(body)
109
+ end
110
+
111
+ # Get current authenticated user info
112
+ def get_me()
113
+ @core.auth_get_me()
114
+ end
115
+
116
+ # Update user profile
117
+ def update_profile(body = nil)
118
+ @core.auth_update_profile(body)
119
+ end
120
+
121
+ # List active sessions
122
+ def list_sessions()
123
+ @core.auth_get_sessions()
124
+ end
125
+
126
+ # Delete a session
127
+ def revoke_session(id)
128
+ @core.auth_delete_session(id)
129
+ end
130
+
131
+ # Enroll new TOTP factor
132
+ def enroll_totp()
133
+ @core.auth_mfa_totp_enroll()
134
+ end
135
+
136
+ # Confirm TOTP enrollment with code
137
+ def verify_totp_enrollment(body = nil)
138
+ @core.auth_mfa_totp_verify(body)
139
+ end
140
+
141
+ # Verify MFA code during signin
142
+ def verify_totp(body = nil)
143
+ @core.auth_mfa_verify(body)
144
+ end
145
+
146
+ # Use recovery code during MFA signin
147
+ def use_recovery_code(body = nil)
148
+ @core.auth_mfa_recovery(body)
149
+ end
150
+
151
+ # Disable TOTP factor
152
+ def disable_totp(body = nil)
153
+ @core.auth_mfa_totp_delete(body)
154
+ end
155
+
156
+ # List MFA factors for authenticated user
157
+ def list_factors()
158
+ @core.auth_mfa_factors()
159
+ end
160
+
161
+ # Generate passkey registration options
162
+ def passkeys_register_options()
163
+ @core.auth_passkeys_register_options()
164
+ end
165
+
166
+ # Verify and store passkey registration
167
+ def passkeys_register(body = nil)
168
+ @core.auth_passkeys_register(body)
169
+ end
170
+
171
+ # Generate passkey authentication options
172
+ def passkeys_auth_options(body = nil)
173
+ @core.auth_passkeys_auth_options(body)
174
+ end
175
+
176
+ # Authenticate with passkey
177
+ def passkeys_authenticate(body = nil)
178
+ @core.auth_passkeys_authenticate(body)
179
+ end
180
+
181
+ # List passkeys for authenticated user
182
+ def passkeys_list()
183
+ @core.auth_passkeys_list()
184
+ end
185
+
186
+ # Delete a passkey
187
+ def passkeys_delete(credential_id)
188
+ @core.auth_passkeys_delete(credential_id)
189
+ end
190
+ end
191
+
192
+ class GeneratedStorageMethods
193
+ # Storage wrapper methods (bucket-scoped)
194
+
195
+ def initialize(core)
196
+ @core = core
197
+ end
198
+
199
+ # Delete file
200
+ def delete(bucket, key)
201
+ @core.delete_file(bucket, key)
202
+ end
203
+
204
+ # Batch delete files
205
+ def delete_many(bucket, body = nil)
206
+ @core.delete_batch(bucket, body)
207
+ end
208
+
209
+ # Check if file exists
210
+ def exists(bucket, key)
211
+ @core.check_file_exists(bucket, key)
212
+ end
213
+
214
+ # Get file metadata
215
+ def get_metadata(bucket, key)
216
+ @core.get_file_metadata(bucket, key)
217
+ end
218
+
219
+ # Update file metadata
220
+ def update_metadata(bucket, key, body = nil)
221
+ @core.update_file_metadata(bucket, key, body)
222
+ end
223
+
224
+ # Create signed download URL
225
+ def create_signed_url(bucket, body = nil)
226
+ @core.create_signed_download_url(bucket, body)
227
+ end
228
+
229
+ # Batch create signed download URLs
230
+ def create_signed_urls(bucket, body = nil)
231
+ @core.create_signed_download_urls(bucket, body)
232
+ end
233
+
234
+ # Create signed upload URL
235
+ def create_signed_upload_url(bucket, body = nil)
236
+ @core.create_signed_upload_url(bucket, body)
237
+ end
238
+
239
+ # Start multipart upload
240
+ def create_multipart_upload(bucket, body = nil)
241
+ @core.create_multipart_upload(bucket, body)
242
+ end
243
+
244
+ # Complete multipart upload
245
+ def complete_multipart_upload(bucket, body = nil)
246
+ @core.complete_multipart_upload(bucket, body)
247
+ end
248
+
249
+ # Abort multipart upload
250
+ def abort_multipart_upload(bucket, body = nil)
251
+ @core.abort_multipart_upload(bucket, body)
252
+ end
253
+ end
254
+
255
+ class GeneratedAnalyticsMethods
256
+ # Analytics wrapper methods
257
+
258
+ def initialize(core)
259
+ @core = core
260
+ end
261
+
262
+ # Track custom events
263
+ def track(body = nil)
264
+ @core.track_events(body)
265
+ end
266
+ end
267
+
268
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ require_relative "context_manager"
8
+ require_relative "errors"
9
+
10
+ module EdgebaseCore
11
+ DEFAULT_OPEN_TIMEOUT = 30
12
+ DEFAULT_READ_TIMEOUT = 120
13
+
14
+ # Synchronous HTTP client for server-side use.
15
+ #
16
+ # Features:
17
+ # - Service Key header injection (X-EdgeBase-Service-Key)
18
+ # - Optional Bearer token injection (for impersonation)
19
+ # - Legacy context state for compatibility (not serialized into HTTP headers)
20
+ class HttpClient
21
+ attr_reader :base_url
22
+
23
+ def initialize(base_url, context_manager: nil, service_key: nil, bearer_token: nil)
24
+ @base_url = base_url.chomp("/")
25
+ @context_manager = context_manager || ContextManager.new
26
+ @service_key = service_key
27
+ @bearer_token = bearer_token
28
+ end
29
+
30
+ def get(path, params: nil)
31
+ request("GET", path, params: params)
32
+ end
33
+
34
+ def post(path, body = nil, params: nil)
35
+ request("POST", path, params: params, json_body: body)
36
+ end
37
+
38
+ def patch(path, body = nil, params: nil)
39
+ request("PATCH", path, params: params, json_body: body)
40
+ end
41
+
42
+ def put(path, body = nil, params: nil)
43
+ request("PUT", path, params: params, json_body: body)
44
+ end
45
+
46
+ def delete(path, params: nil)
47
+ request("DELETE", path, params: params)
48
+ end
49
+
50
+ # HEAD request — returns true if resource exists (2xx).
51
+ def head(path)
52
+ uri = URI(build_url(path))
53
+ http = build_http(uri)
54
+ req = Net::HTTP::Head.new(uri)
55
+ auth_headers.each { |k, v| req[k] = v }
56
+ response = http.request(req)
57
+ response.code.to_i < 400
58
+ end
59
+
60
+ # POST multipart form data (for file uploads).
61
+ def post_multipart(path, files:, data: nil)
62
+ uri = URI(build_url(path))
63
+ http = build_http(uri)
64
+
65
+ boundary = "EdgeBase#{rand(10**16)}"
66
+ body = build_multipart_body(files, data, boundary)
67
+
68
+ req = Net::HTTP::Post.new(uri)
69
+ headers = auth_headers
70
+ headers.delete("Content-Type")
71
+ headers["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
72
+ headers.each { |k, v| req[k] = v }
73
+ req.body = body
74
+
75
+ parse_response(http.request(req))
76
+ end
77
+
78
+ # POST raw binary data (for multipart upload-part).
79
+ def post_raw(path, data:, content_type: "application/octet-stream")
80
+ uri = URI(build_url(path))
81
+ http = build_http(uri)
82
+
83
+ req = Net::HTTP::Post.new(uri)
84
+ headers = auth_headers
85
+ headers["Content-Type"] = content_type
86
+ headers.each { |k, v| req[k] = v }
87
+ req.body = data
88
+
89
+ parse_response(http.request(req))
90
+ end
91
+
92
+ # GET raw bytes (for file downloads).
93
+ def get_raw(path)
94
+ uri = URI(build_url(path))
95
+ http = build_http(uri)
96
+ req = Net::HTTP::Get.new(uri)
97
+ auth_headers.each { |k, v| req[k] = v }
98
+ response = http.request(req)
99
+ if response.code.to_i >= 400
100
+ raise EdgeBaseError.new(response.code.to_i, response.body)
101
+ end
102
+ response.body
103
+ end
104
+
105
+ private
106
+
107
+ def request(method, path, params: nil, json_body: nil)
108
+ url = build_url(path)
109
+ if params && !params.empty?
110
+ query = URI.encode_www_form(params)
111
+ url = "#{url}?#{query}"
112
+ end
113
+
114
+ uri = URI(url)
115
+ http = build_http(uri)
116
+
117
+ req = case method
118
+ when "GET" then Net::HTTP::Get.new(uri)
119
+ when "POST" then Net::HTTP::Post.new(uri)
120
+ when "PATCH" then Net::HTTP::Patch.new(uri)
121
+ when "PUT" then Net::HTTP::Put.new(uri)
122
+ when "DELETE" then Net::HTTP::Delete.new(uri)
123
+ else Net::HTTP::Get.new(uri)
124
+ end
125
+
126
+ auth_headers.each { |k, v| req[k] = v }
127
+ req.body = JSON.generate(json_body) if json_body
128
+
129
+ parse_response(http.request(req))
130
+ end
131
+
132
+ def build_url(path)
133
+ if path.start_with?("/api/")
134
+ "#{@base_url}#{path}"
135
+ else
136
+ "#{@base_url}/api#{path}"
137
+ end
138
+ end
139
+
140
+ def auth_headers
141
+ headers = { "Content-Type" => "application/json", "Connection" => "close" }
142
+ begin
143
+ if @bearer_token
144
+ headers["Authorization"] = "Bearer #{@bearer_token}"
145
+ end
146
+ if @service_key
147
+ headers["X-EdgeBase-Service-Key"] = @service_key
148
+ headers["Authorization"] = "Bearer #{@service_key}"
149
+ end
150
+ rescue StandardError
151
+ # Token refresh failed — proceed as unauthenticated
152
+ end
153
+ headers
154
+ end
155
+
156
+ def build_http(uri)
157
+ http = Net::HTTP.new(uri.host, uri.port)
158
+ http.use_ssl = (uri.scheme == "https")
159
+ http.open_timeout = request_timeout_seconds(DEFAULT_OPEN_TIMEOUT)
160
+ http.read_timeout = request_timeout_seconds(DEFAULT_READ_TIMEOUT)
161
+ http.keep_alive_timeout = 0 if http.respond_to?(:keep_alive_timeout=)
162
+ http
163
+ end
164
+
165
+ def request_timeout_seconds(default_seconds)
166
+ raw = ENV.fetch("EDGEBASE_HTTP_TIMEOUT_MS", "").strip
167
+ return default_seconds if raw.empty?
168
+
169
+ timeout_ms = Integer(raw, exception: false)
170
+ return default_seconds if timeout_ms.nil? || timeout_ms <= 0
171
+
172
+ timeout_ms / 1000.0
173
+ end
174
+
175
+ def parse_response(response)
176
+ code = response.code.to_i
177
+ if code >= 400
178
+ begin
179
+ data = JSON.parse(response.body)
180
+ raise EdgeBaseError.from_json(data, code)
181
+ rescue EdgeBaseError
182
+ raise
183
+ rescue StandardError
184
+ raise EdgeBaseError.new(code, response.body || "Unknown error")
185
+ end
186
+ end
187
+
188
+ return nil if response.body.nil? || response.body.empty?
189
+
190
+ begin
191
+ JSON.parse(response.body)
192
+ rescue StandardError
193
+ raise EdgeBaseError.new(code, "Expected a JSON response but received malformed JSON.")
194
+ end
195
+ end
196
+
197
+ def build_multipart_body(files, data, boundary)
198
+ body = +""
199
+ # Data fields
200
+ if data
201
+ data.each do |key, value|
202
+ body << "--#{boundary}\r\n"
203
+ body << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
204
+ body << "#{value}\r\n"
205
+ end
206
+ end
207
+ # File fields
208
+ files.each do |field_name, (filename, file_data, content_type)|
209
+ body << "--#{boundary}\r\n"
210
+ body << "Content-Disposition: form-data; name=\"#{field_name}\"; filename=\"#{filename}\"\r\n"
211
+ body << "Content-Type: #{content_type}\r\n\r\n"
212
+ body << file_data
213
+ body << "\r\n"
214
+ end
215
+ body << "--#{boundary}--\r\n"
216
+ body
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "base64"
5
+
6
+ module EdgebaseCore
7
+ # Signed URL result.
8
+ SignedUrlResult = Struct.new(:url, :expires_in, keyword_init: true)
9
+
10
+ # File metadata.
11
+ FileInfo = Struct.new(:key, :size, :content_type, :etag, :custom_metadata, keyword_init: true) do
12
+ def self.from_json(data)
13
+ new(
14
+ key: data["key"] || "",
15
+ size: data["size"] || 0,
16
+ content_type: data["contentType"],
17
+ etag: data["etag"],
18
+ custom_metadata: data["customMetadata"] || data["custom_metadata"] || {}
19
+ )
20
+ end
21
+ end
22
+
23
+ # Storage subsystem — bucket factory.
24
+ #
25
+ # bucket = client.storage.bucket("avatars")
26
+ # url = bucket.get_url("profile.png")
27
+ class StorageClient
28
+ def initialize(client)
29
+ @client = client
30
+ end
31
+
32
+ def bucket(name)
33
+ StorageBucket.new(@client, name)
34
+ end
35
+ end
36
+
37
+ # Bucket-level storage operations.
38
+ class StorageBucket
39
+ attr_reader :name
40
+
41
+ def initialize(client, name)
42
+ @client = client
43
+ @core = EdgebaseCore::GeneratedDbApi.new(client)
44
+ @name = name
45
+ end
46
+
47
+ # Get the public URL of a file.
48
+ def get_url(path)
49
+ "#{@client.base_url}/api/storage/#{@name}/#{URI.encode_www_form_component(path)}"
50
+ end
51
+
52
+ # ── Upload ─────────────────────────────────────────────────────────────
53
+
54
+ def upload(path, data, content_type: "application/octet-stream")
55
+ @client.post_multipart(
56
+ "/storage/#{@name}/upload",
57
+ files: { "file" => [path, data, content_type] },
58
+ data: { "key" => path }
59
+ )
60
+ end
61
+
62
+ def upload_string(path, data, encoding: "raw", content_type: "text/plain")
63
+ raw_bytes = case encoding
64
+ when "raw"
65
+ data.encode("UTF-8")
66
+ when "base64"
67
+ Base64.decode64(data)
68
+ when "base64url"
69
+ Base64.urlsafe_decode64(data)
70
+ when "data_url"
71
+ _, encoded = data.split(",", 2)
72
+ Base64.decode64(encoded)
73
+ else
74
+ data.encode("UTF-8")
75
+ end
76
+ upload(path, raw_bytes, content_type: content_type)
77
+ end
78
+
79
+ # ── Download ───────────────────────────────────────────────────────────
80
+
81
+ def download(path)
82
+ @client.get_raw("/storage/#{@name}/#{URI.encode_www_form_component(path)}")
83
+ end
84
+
85
+ # ── Metadata ───────────────────────────────────────────────────────────
86
+
87
+ def get_metadata(path)
88
+ data = @core.get_file_metadata(@name, path)
89
+ FileInfo.from_json(data)
90
+ end
91
+
92
+ def update_metadata(path, metadata)
93
+ @core.update_file_metadata(@name, path, metadata)
94
+ end
95
+
96
+ # ── Signed URLs ────────────────────────────────────────────────────────
97
+
98
+ def create_signed_url(path, expires_in: "1h")
99
+ data = @core.create_signed_download_url(
100
+ @name, { "key" => path, "expiresIn" => expires_in }
101
+ )
102
+ SignedUrlResult.new(url: data["url"] || "", expires_in: data["expiresIn"] || expires_in)
103
+ end
104
+
105
+ def create_signed_upload_url(path, expires_in: 3600)
106
+ data = @core.create_signed_upload_url(
107
+ @name, { "key" => path, "expiresIn" => "#{expires_in}s" }
108
+ )
109
+ SignedUrlResult.new(url: data["url"] || "", expires_in: data["expiresIn"] || expires_in)
110
+ end
111
+
112
+ # ── Management ─────────────────────────────────────────────────────────
113
+
114
+ def delete_file(path)
115
+ @core.delete_file(@name, path)
116
+ end
117
+
118
+ alias delete delete_file
119
+
120
+ def list_files(prefix: "", limit: 100, offset: 0)
121
+ params = { "limit" => limit.to_s, "offset" => offset.to_s }
122
+ params["prefix"] = prefix unless prefix.empty?
123
+ data = @client.get("/storage/#{@name}", params: params)
124
+ items = data.is_a?(Hash) ? (data["files"] || data["items"] || []) : []
125
+ items.map { |item| FileInfo.from_json(item) }
126
+ end
127
+
128
+ alias list list_files
129
+
130
+ # ── Resumable / Multipart Upload ───────────────────────────────────────
131
+
132
+ def initiate_resumable_upload(path, content_type: "application/octet-stream", total_size: nil)
133
+ body = { "key" => path, "contentType" => content_type }
134
+ body["totalSize"] = total_size if total_size
135
+ data = @core.create_multipart_upload(@name, body)
136
+ data["uploadId"] || ""
137
+ end
138
+
139
+ def resume_upload(path, upload_id, chunk, part_number: 1, is_last_chunk: false)
140
+ encoded_path = URI.encode_www_form_component(path)
141
+ params = "uploadId=#{upload_id}&partNumber=#{part_number}&key=#{encoded_path}"
142
+ @client.post_raw(
143
+ "/storage/#{@name}/multipart/upload-part?#{params}",
144
+ data: chunk,
145
+ content_type: "application/octet-stream"
146
+ )
147
+ end
148
+
149
+ def complete_resumable_upload(path, upload_id, parts)
150
+ @core.complete_multipart_upload(
151
+ @name, { "uploadId" => upload_id, "key" => path, "parts" => parts }
152
+ )
153
+ end
154
+
155
+ def abort_resumable_upload(path, upload_id)
156
+ @core.abort_multipart_upload(
157
+ @name, { "uploadId" => upload_id, "key" => path }
158
+ )
159
+ end
160
+ end
161
+ end