signalwire-sdk 2.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +259 -0
- data/bin/swaig-test +872 -0
- data/lib/signalwire/agent/agent_base.rb +2134 -0
- data/lib/signalwire/contexts/context_builder.rb +861 -0
- data/lib/signalwire/core/logging_config.rb +54 -0
- data/lib/signalwire/datamap/data_map.rb +315 -0
- data/lib/signalwire/logging.rb +92 -0
- data/lib/signalwire/pom/prompt_object_model.rb +269 -0
- data/lib/signalwire/pom/section.rb +202 -0
- data/lib/signalwire/prefabs/concierge.rb +92 -0
- data/lib/signalwire/prefabs/faq_bot.rb +67 -0
- data/lib/signalwire/prefabs/info_gatherer.rb +79 -0
- data/lib/signalwire/prefabs/receptionist.rb +74 -0
- data/lib/signalwire/prefabs/survey.rb +75 -0
- data/lib/signalwire/relay/action.rb +291 -0
- data/lib/signalwire/relay/call.rb +523 -0
- data/lib/signalwire/relay/client.rb +789 -0
- data/lib/signalwire/relay/constants.rb +124 -0
- data/lib/signalwire/relay/message.rb +137 -0
- data/lib/signalwire/relay/relay_event.rb +670 -0
- data/lib/signalwire/rest/http_client.rb +159 -0
- data/lib/signalwire/rest/namespaces/addresses.rb +19 -0
- data/lib/signalwire/rest/namespaces/calling.rb +179 -0
- data/lib/signalwire/rest/namespaces/chat.rb +18 -0
- data/lib/signalwire/rest/namespaces/compat.rb +229 -0
- data/lib/signalwire/rest/namespaces/datasphere.rb +39 -0
- data/lib/signalwire/rest/namespaces/fabric.rb +235 -0
- data/lib/signalwire/rest/namespaces/imported_numbers.rb +18 -0
- data/lib/signalwire/rest/namespaces/logs.rb +46 -0
- data/lib/signalwire/rest/namespaces/lookup.rb +18 -0
- data/lib/signalwire/rest/namespaces/mfa.rb +26 -0
- data/lib/signalwire/rest/namespaces/number_groups.rb +32 -0
- data/lib/signalwire/rest/namespaces/phone_numbers.rb +124 -0
- data/lib/signalwire/rest/namespaces/project.rb +33 -0
- data/lib/signalwire/rest/namespaces/pubsub.rb +18 -0
- data/lib/signalwire/rest/namespaces/queues.rb +28 -0
- data/lib/signalwire/rest/namespaces/recordings.rb +18 -0
- data/lib/signalwire/rest/namespaces/registry.rb +67 -0
- data/lib/signalwire/rest/namespaces/short_codes.rb +26 -0
- data/lib/signalwire/rest/namespaces/sip_profile.rb +22 -0
- data/lib/signalwire/rest/namespaces/verified_callers.rb +24 -0
- data/lib/signalwire/rest/namespaces/video.rb +129 -0
- data/lib/signalwire/rest/pagination.rb +89 -0
- data/lib/signalwire/rest/phone_call_handler.rb +56 -0
- data/lib/signalwire/rest/rest_client.rb +114 -0
- data/lib/signalwire/runtime.rb +98 -0
- data/lib/signalwire/security/session_manager.rb +124 -0
- data/lib/signalwire/security/webhook_middleware.rb +191 -0
- data/lib/signalwire/security/webhook_validator.rb +327 -0
- data/lib/signalwire/server/agent_server.rb +413 -0
- data/lib/signalwire/serverless/lambda_handler.rb +251 -0
- data/lib/signalwire/skills/builtin/api_ninjas_trivia.rb +99 -0
- data/lib/signalwire/skills/builtin/claude_skills.rb +92 -0
- data/lib/signalwire/skills/builtin/custom_skills.rb +54 -0
- data/lib/signalwire/skills/builtin/datasphere.rb +153 -0
- data/lib/signalwire/skills/builtin/datasphere_serverless.rb +107 -0
- data/lib/signalwire/skills/builtin/datetime.rb +97 -0
- data/lib/signalwire/skills/builtin/google_maps.rb +168 -0
- data/lib/signalwire/skills/builtin/info_gatherer.rb +189 -0
- data/lib/signalwire/skills/builtin/joke.rb +65 -0
- data/lib/signalwire/skills/builtin/math.rb +176 -0
- data/lib/signalwire/skills/builtin/mcp_gateway.rb +121 -0
- data/lib/signalwire/skills/builtin/native_vector_search.rb +116 -0
- data/lib/signalwire/skills/builtin/play_background_file.rb +86 -0
- data/lib/signalwire/skills/builtin/spider.rb +169 -0
- data/lib/signalwire/skills/builtin/swml_transfer.rb +118 -0
- data/lib/signalwire/skills/builtin/weather_api.rb +92 -0
- data/lib/signalwire/skills/builtin/web_search.rb +141 -0
- data/lib/signalwire/skills/builtin/wikipedia_search.rb +125 -0
- data/lib/signalwire/skills/skill_base.rb +82 -0
- data/lib/signalwire/skills/skill_manager.rb +97 -0
- data/lib/signalwire/skills/skill_registry.rb +258 -0
- data/lib/signalwire/swaig/function_result.rb +777 -0
- data/lib/signalwire/swml/document.rb +84 -0
- data/lib/signalwire/swml/schema.json +12250 -0
- data/lib/signalwire/swml/schema.rb +81 -0
- data/lib/signalwire/swml/service.rb +650 -0
- data/lib/signalwire/utils/schema_utils.rb +298 -0
- data/lib/signalwire/utils/serverless.rb +19 -0
- data/lib/signalwire/utils/url_validator.rb +138 -0
- data/lib/signalwire/version.rb +5 -0
- data/lib/signalwire.rb +114 -0
- metadata +225 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SignalWire
|
|
4
|
+
module REST
|
|
5
|
+
module Namespaces
|
|
6
|
+
# Short code management (read + update only).
|
|
7
|
+
class ShortCodesResource < BaseResource
|
|
8
|
+
def initialize(http)
|
|
9
|
+
super(http, '/api/relay/rest/short_codes')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def list(**params)
|
|
13
|
+
@http.get(@base_path, params.empty? ? nil : params)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(short_code_id)
|
|
17
|
+
@http.get(_path(short_code_id))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def update(short_code_id, **kwargs)
|
|
21
|
+
@http.put(_path(short_code_id), kwargs)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SignalWire
|
|
4
|
+
module REST
|
|
5
|
+
module Namespaces
|
|
6
|
+
# Project SIP profile (singleton resource).
|
|
7
|
+
class SipProfileResource < BaseResource
|
|
8
|
+
def initialize(http)
|
|
9
|
+
super(http, '/api/relay/rest/sip_profile')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def get
|
|
13
|
+
@http.get(@base_path)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update(**kwargs)
|
|
17
|
+
@http.put(@base_path, kwargs)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SignalWire
|
|
4
|
+
module REST
|
|
5
|
+
module Namespaces
|
|
6
|
+
# Verified caller ID management with verification flow.
|
|
7
|
+
class VerifiedCallersResource < CrudResource
|
|
8
|
+
self.update_method = 'PUT'
|
|
9
|
+
|
|
10
|
+
def initialize(http)
|
|
11
|
+
super(http, '/api/relay/rest/verified_caller_ids')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def redial_verification(caller_id)
|
|
15
|
+
@http.post(_path(caller_id, 'verification'))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def submit_verification(caller_id, **kwargs)
|
|
19
|
+
@http.put(_path(caller_id, 'verification'), kwargs)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SignalWire
|
|
4
|
+
module REST
|
|
5
|
+
module Namespaces
|
|
6
|
+
# Video room management with streams.
|
|
7
|
+
class VideoRooms < CrudResource
|
|
8
|
+
self.update_method = 'PUT'
|
|
9
|
+
|
|
10
|
+
def list_streams(room_id, **params)
|
|
11
|
+
@http.get(_path(room_id, 'streams'), params.empty? ? nil : params)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_stream(room_id, **kwargs)
|
|
15
|
+
@http.post(_path(room_id, 'streams'), kwargs)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Video room token generation.
|
|
20
|
+
class VideoRoomTokens < BaseResource
|
|
21
|
+
def create(**kwargs)
|
|
22
|
+
@http.post(@base_path, kwargs)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Video room session management.
|
|
27
|
+
class VideoRoomSessions < BaseResource
|
|
28
|
+
def list(**params)
|
|
29
|
+
@http.get(@base_path, params.empty? ? nil : params)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get(session_id)
|
|
33
|
+
@http.get(_path(session_id))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def list_events(session_id, **params)
|
|
37
|
+
@http.get(_path(session_id, 'events'), params.empty? ? nil : params)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def list_members(session_id, **params)
|
|
41
|
+
@http.get(_path(session_id, 'members'), params.empty? ? nil : params)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def list_recordings(session_id, **params)
|
|
45
|
+
@http.get(_path(session_id, 'recordings'), params.empty? ? nil : params)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Video room recording management.
|
|
50
|
+
class VideoRoomRecordings < BaseResource
|
|
51
|
+
def list(**params)
|
|
52
|
+
@http.get(@base_path, params.empty? ? nil : params)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def get(recording_id)
|
|
56
|
+
@http.get(_path(recording_id))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def delete(recording_id)
|
|
60
|
+
@http.delete(_path(recording_id))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def list_events(recording_id, **params)
|
|
64
|
+
@http.get(_path(recording_id, 'events'), params.empty? ? nil : params)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Video conference management with tokens and streams.
|
|
69
|
+
class VideoConferences < CrudResource
|
|
70
|
+
self.update_method = 'PUT'
|
|
71
|
+
|
|
72
|
+
def list_conference_tokens(conference_id, **params)
|
|
73
|
+
@http.get(_path(conference_id, 'conference_tokens'), params.empty? ? nil : params)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def list_streams(conference_id, **params)
|
|
77
|
+
@http.get(_path(conference_id, 'streams'), params.empty? ? nil : params)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_stream(conference_id, **kwargs)
|
|
81
|
+
@http.post(_path(conference_id, 'streams'), kwargs)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Video conference token management.
|
|
86
|
+
class VideoConferenceTokens < BaseResource
|
|
87
|
+
def get(token_id)
|
|
88
|
+
@http.get(_path(token_id))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def reset(token_id)
|
|
92
|
+
@http.post(_path(token_id, 'reset'))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Video stream management.
|
|
97
|
+
class VideoStreams < BaseResource
|
|
98
|
+
def get(stream_id)
|
|
99
|
+
@http.get(_path(stream_id))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def update(stream_id, **kwargs)
|
|
103
|
+
@http.put(_path(stream_id), kwargs)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def delete(stream_id)
|
|
107
|
+
@http.delete(_path(stream_id))
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Video API namespace.
|
|
112
|
+
class VideoNamespace
|
|
113
|
+
attr_reader :rooms, :room_tokens, :room_sessions, :room_recordings,
|
|
114
|
+
:conferences, :conference_tokens, :streams
|
|
115
|
+
|
|
116
|
+
def initialize(http)
|
|
117
|
+
base = '/api/video'
|
|
118
|
+
@rooms = VideoRooms.new(http, "#{base}/rooms")
|
|
119
|
+
@room_tokens = VideoRoomTokens.new(http, "#{base}/room_tokens")
|
|
120
|
+
@room_sessions = VideoRoomSessions.new(http, "#{base}/room_sessions")
|
|
121
|
+
@room_recordings = VideoRoomRecordings.new(http, "#{base}/room_recordings")
|
|
122
|
+
@conferences = VideoConferences.new(http, "#{base}/conferences")
|
|
123
|
+
@conference_tokens = VideoConferenceTokens.new(http, "#{base}/conference_tokens")
|
|
124
|
+
@streams = VideoStreams.new(http, "#{base}/streams")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SignalWire
|
|
4
|
+
module REST
|
|
5
|
+
# Iterates items across paginated API responses.
|
|
6
|
+
#
|
|
7
|
+
# Mirrors the Python PaginatedIterator (signalwire.rest._pagination):
|
|
8
|
+
# the constructor records http/path/params/data_key without fetching;
|
|
9
|
+
# iteration walks pages by following the +links.next+ cursor.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# iter = SignalWire::REST::PaginatedIterator.new(http, '/api/path',
|
|
13
|
+
# params: {}, data_key: 'data')
|
|
14
|
+
# iter.each { |item| ... }
|
|
15
|
+
#
|
|
16
|
+
# The iterator is single-pass (matching Python's __next__ semantics);
|
|
17
|
+
# use #to_a to collect every item across all pages.
|
|
18
|
+
class PaginatedIterator
|
|
19
|
+
include Enumerable
|
|
20
|
+
|
|
21
|
+
attr_reader :http, :path, :params, :data_key, :index, :items, :done
|
|
22
|
+
|
|
23
|
+
def initialize(http, path, params = nil, data_key = 'data')
|
|
24
|
+
@http = http
|
|
25
|
+
@path = path
|
|
26
|
+
# Dup so callers can't mutate iterator state via the original Hash.
|
|
27
|
+
@params = params ? params.dup : {}
|
|
28
|
+
@data_key = data_key
|
|
29
|
+
@items = []
|
|
30
|
+
@index = 0
|
|
31
|
+
@done = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def each
|
|
35
|
+
return enum_for(:each) unless block_given?
|
|
36
|
+
|
|
37
|
+
loop do
|
|
38
|
+
item = next_item
|
|
39
|
+
break if item == :__stop__
|
|
40
|
+
|
|
41
|
+
yield item
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Equivalent of Python's __next__. Returns the sentinel +:__stop__+
|
|
46
|
+
# when exhausted (Ruby has no StopIteration error idiom for plain
|
|
47
|
+
# Enumerable), but the public surface is +#each+.
|
|
48
|
+
def next_item
|
|
49
|
+
while @index >= @items.length
|
|
50
|
+
return :__stop__ if @done
|
|
51
|
+
|
|
52
|
+
fetch_next
|
|
53
|
+
end
|
|
54
|
+
item = @items[@index]
|
|
55
|
+
@index += 1
|
|
56
|
+
item
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def fetch_next
|
|
62
|
+
params_for_request = @params.empty? ? nil : @params
|
|
63
|
+
resp = @http.get(@path, params_for_request)
|
|
64
|
+
data = resp[@data_key] || []
|
|
65
|
+
@items.concat(data)
|
|
66
|
+
|
|
67
|
+
links = resp['links'] || {}
|
|
68
|
+
next_url = links['next']
|
|
69
|
+
if next_url && !data.empty?
|
|
70
|
+
uri = URI.parse(next_url)
|
|
71
|
+
query = URI.decode_www_form(uri.query || '')
|
|
72
|
+
# Flatten single-value lists, preserving multi-value entries.
|
|
73
|
+
flat = {}
|
|
74
|
+
query.each do |k, v|
|
|
75
|
+
if flat.key?(k)
|
|
76
|
+
existing = flat[k]
|
|
77
|
+
flat[k] = existing.is_a?(Array) ? existing + [v] : [existing, v]
|
|
78
|
+
else
|
|
79
|
+
flat[k] = v
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
@params = flat
|
|
83
|
+
else
|
|
84
|
+
@done = true
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SignalWire
|
|
4
|
+
module REST
|
|
5
|
+
# PhoneCallHandler -- enum-like constants of the +call_handler+ values
|
|
6
|
+
# accepted by +phone_numbers.update+.
|
|
7
|
+
#
|
|
8
|
+
# Named +PhoneCallHandler+ (not +CallHandler+) to avoid colliding with
|
|
9
|
+
# the RELAY client's inbound-call-handler callback (+Relay::Client#on_call+).
|
|
10
|
+
#
|
|
11
|
+
# Setting a phone number's +call_handler+ together with the handler-specific
|
|
12
|
+
# companion field routes inbound calls and auto-materializes the matching
|
|
13
|
+
# Fabric resource on the server. See the high-level helpers on
|
|
14
|
+
# +SignalWire::REST::Namespaces::PhoneNumbersResource+.
|
|
15
|
+
#
|
|
16
|
+
# === Mapping
|
|
17
|
+
#
|
|
18
|
+
# Constant | Companion field (required) | Auto-creates resource
|
|
19
|
+
# ------------------+--------------------------------+----------------------
|
|
20
|
+
# RELAY_SCRIPT | call_relay_script_url | swml_webhook
|
|
21
|
+
# LAML_WEBHOOKS | call_request_url | cxml_webhook
|
|
22
|
+
# LAML_APPLICATION | call_laml_application_id | cxml_application
|
|
23
|
+
# AI_AGENT | call_ai_agent_id | ai_agent
|
|
24
|
+
# CALL_FLOW | call_flow_id | call_flow
|
|
25
|
+
# RELAY_APPLICATION | call_relay_application | relay_application
|
|
26
|
+
# RELAY_TOPIC | call_relay_topic | (routes via RELAY)
|
|
27
|
+
# RELAY_CONTEXT | call_relay_context | (legacy, prefer topic)
|
|
28
|
+
# RELAY_CONNECTOR | (connector config) | (internal)
|
|
29
|
+
# VIDEO_ROOM | call_video_room_id | (routes to Video API)
|
|
30
|
+
# DIALOGFLOW | call_dialogflow_agent_id | (none)
|
|
31
|
+
#
|
|
32
|
+
# Note: +LAML_WEBHOOKS+ (wire value +laml_webhooks+) produces a *cXML*
|
|
33
|
+
# handler despite the plural name, not a generic webhook. For SWML, use
|
|
34
|
+
# +RELAY_SCRIPT+.
|
|
35
|
+
module PhoneCallHandler
|
|
36
|
+
RELAY_SCRIPT = 'relay_script'
|
|
37
|
+
LAML_WEBHOOKS = 'laml_webhooks'
|
|
38
|
+
LAML_APPLICATION = 'laml_application'
|
|
39
|
+
AI_AGENT = 'ai_agent'
|
|
40
|
+
CALL_FLOW = 'call_flow'
|
|
41
|
+
RELAY_APPLICATION = 'relay_application'
|
|
42
|
+
RELAY_TOPIC = 'relay_topic'
|
|
43
|
+
RELAY_CONTEXT = 'relay_context'
|
|
44
|
+
RELAY_CONNECTOR = 'relay_connector'
|
|
45
|
+
VIDEO_ROOM = 'video_room'
|
|
46
|
+
DIALOGFLOW = 'dialogflow'
|
|
47
|
+
|
|
48
|
+
# All supported wire values, in the same order as the constants.
|
|
49
|
+
ALL = [
|
|
50
|
+
RELAY_SCRIPT, LAML_WEBHOOKS, LAML_APPLICATION, AI_AGENT, CALL_FLOW,
|
|
51
|
+
RELAY_APPLICATION, RELAY_TOPIC, RELAY_CONTEXT, RELAY_CONNECTOR,
|
|
52
|
+
VIDEO_ROOM, DIALOGFLOW
|
|
53
|
+
].freeze
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'http_client'
|
|
4
|
+
require_relative 'pagination'
|
|
5
|
+
require_relative 'phone_call_handler'
|
|
6
|
+
require_relative 'namespaces/fabric'
|
|
7
|
+
require_relative 'namespaces/calling'
|
|
8
|
+
require_relative 'namespaces/phone_numbers'
|
|
9
|
+
require_relative 'namespaces/addresses'
|
|
10
|
+
require_relative 'namespaces/queues'
|
|
11
|
+
require_relative 'namespaces/recordings'
|
|
12
|
+
require_relative 'namespaces/number_groups'
|
|
13
|
+
require_relative 'namespaces/verified_callers'
|
|
14
|
+
require_relative 'namespaces/sip_profile'
|
|
15
|
+
require_relative 'namespaces/lookup'
|
|
16
|
+
require_relative 'namespaces/short_codes'
|
|
17
|
+
require_relative 'namespaces/imported_numbers'
|
|
18
|
+
require_relative 'namespaces/mfa'
|
|
19
|
+
require_relative 'namespaces/registry'
|
|
20
|
+
require_relative 'namespaces/datasphere'
|
|
21
|
+
require_relative 'namespaces/video'
|
|
22
|
+
require_relative 'namespaces/logs'
|
|
23
|
+
require_relative 'namespaces/project'
|
|
24
|
+
require_relative 'namespaces/pubsub'
|
|
25
|
+
require_relative 'namespaces/chat'
|
|
26
|
+
require_relative 'namespaces/compat'
|
|
27
|
+
|
|
28
|
+
module SignalWire
|
|
29
|
+
module REST
|
|
30
|
+
# REST client for the SignalWire platform APIs.
|
|
31
|
+
#
|
|
32
|
+
# Usage:
|
|
33
|
+
# client = SignalWire::REST::RestClient.new(
|
|
34
|
+
# project: 'your-project-id',
|
|
35
|
+
# token: 'your-api-token',
|
|
36
|
+
# host: 'your-space.signalwire.com'
|
|
37
|
+
# )
|
|
38
|
+
#
|
|
39
|
+
# # Or use environment variables:
|
|
40
|
+
# # SIGNALWIRE_PROJECT_ID, SIGNALWIRE_API_TOKEN, SIGNALWIRE_SPACE
|
|
41
|
+
# client = SignalWire::REST::RestClient.new
|
|
42
|
+
#
|
|
43
|
+
# # Use namespaced resources
|
|
44
|
+
# client.fabric.ai_agents.list
|
|
45
|
+
# client.calling.play(call_id, play: [...])
|
|
46
|
+
# client.phone_numbers.search(area_code: '512')
|
|
47
|
+
# client.video.rooms.create(name: 'standup')
|
|
48
|
+
# client.compat.calls.list
|
|
49
|
+
class RestClient
|
|
50
|
+
attr_reader :fabric, :calling, :phone_numbers, :datasphere, :video,
|
|
51
|
+
:compat, :addresses, :queues, :recordings, :number_groups,
|
|
52
|
+
:verified_callers, :sip_profile, :lookup, :short_codes,
|
|
53
|
+
:imported_numbers, :mfa, :registry, :logs, :project,
|
|
54
|
+
:pubsub, :chat, :project_id, :http
|
|
55
|
+
|
|
56
|
+
# +base_url+ overrides the derived +https://{space}+ default. The
|
|
57
|
+
# audit harness uses this to point at the local fixture server.
|
|
58
|
+
def initialize(project: nil, token: nil, host: nil, base_url: nil)
|
|
59
|
+
project_id = project || ENV['SIGNALWIRE_PROJECT_ID'] || ''
|
|
60
|
+
api_token = token || ENV['SIGNALWIRE_API_TOKEN'] || ''
|
|
61
|
+
space = host || ENV['SIGNALWIRE_SPACE'] || ''
|
|
62
|
+
|
|
63
|
+
if project_id.empty? || api_token.empty? || (space.empty? && (base_url.nil? || base_url.empty?))
|
|
64
|
+
raise ArgumentError,
|
|
65
|
+
'project, token, and host are required. ' \
|
|
66
|
+
'Provide them as arguments or set SIGNALWIRE_PROJECT_ID, ' \
|
|
67
|
+
'SIGNALWIRE_API_TOKEN, and SIGNALWIRE_SPACE environment variables.'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@project_id = project_id
|
|
71
|
+
@http = HttpClient.new(project_id, api_token, space, base_url: base_url)
|
|
72
|
+
|
|
73
|
+
# Fabric API
|
|
74
|
+
@fabric = Namespaces::FabricNamespace.new(@http)
|
|
75
|
+
|
|
76
|
+
# Calling API
|
|
77
|
+
@calling = Namespaces::CallingNamespace.new(@http)
|
|
78
|
+
|
|
79
|
+
# Relay REST resources
|
|
80
|
+
@phone_numbers = Namespaces::PhoneNumbersResource.new(@http)
|
|
81
|
+
@addresses = Namespaces::AddressesResource.new(@http)
|
|
82
|
+
@queues = Namespaces::QueuesResource.new(@http)
|
|
83
|
+
@recordings = Namespaces::RecordingsResource.new(@http)
|
|
84
|
+
@number_groups = Namespaces::NumberGroupsResource.new(@http)
|
|
85
|
+
@verified_callers = Namespaces::VerifiedCallersResource.new(@http)
|
|
86
|
+
@sip_profile = Namespaces::SipProfileResource.new(@http)
|
|
87
|
+
@lookup = Namespaces::LookupResource.new(@http)
|
|
88
|
+
@short_codes = Namespaces::ShortCodesResource.new(@http)
|
|
89
|
+
@imported_numbers = Namespaces::ImportedNumbersResource.new(@http)
|
|
90
|
+
@mfa = Namespaces::MfaResource.new(@http)
|
|
91
|
+
@registry = Namespaces::RegistryNamespace.new(@http)
|
|
92
|
+
|
|
93
|
+
# Datasphere API
|
|
94
|
+
@datasphere = Namespaces::DatasphereNamespace.new(@http)
|
|
95
|
+
|
|
96
|
+
# Video API
|
|
97
|
+
@video = Namespaces::VideoNamespace.new(@http)
|
|
98
|
+
|
|
99
|
+
# Logs
|
|
100
|
+
@logs = Namespaces::LogsNamespace.new(@http)
|
|
101
|
+
|
|
102
|
+
# Project management
|
|
103
|
+
@project = Namespaces::ProjectNamespace.new(@http)
|
|
104
|
+
|
|
105
|
+
# PubSub & Chat
|
|
106
|
+
@pubsub = Namespaces::PubSubResource.new(@http)
|
|
107
|
+
@chat = Namespaces::ChatResource.new(@http)
|
|
108
|
+
|
|
109
|
+
# Compatibility (Twilio-compatible) API
|
|
110
|
+
@compat = Namespaces::CompatNamespace.new(@http, project_id)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2025 SignalWire
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the MIT License.
|
|
6
|
+
# See LICENSE file in the project root for full license information.
|
|
7
|
+
|
|
8
|
+
module SignalWire
|
|
9
|
+
# Runtime environment detection.
|
|
10
|
+
#
|
|
11
|
+
# Detects the execution environment (plain server, AWS Lambda, CGI,
|
|
12
|
+
# Google Cloud Functions, Azure Functions) by inspecting well-known
|
|
13
|
+
# environment variables set by each platform.
|
|
14
|
+
#
|
|
15
|
+
# This is the Ruby counterpart to the Python SDK's
|
|
16
|
+
# +signalwire.core.logging_config.get_execution_mode+.
|
|
17
|
+
module Runtime
|
|
18
|
+
MODES = %i[server lambda cgi google_cloud_function azure_function unknown].freeze
|
|
19
|
+
|
|
20
|
+
# Determine the current execution mode.
|
|
21
|
+
#
|
|
22
|
+
# Returns one of:
|
|
23
|
+
# - +:cgi+ - running under a CGI gateway (GATEWAY_INTERFACE is set)
|
|
24
|
+
# - +:lambda+ - running under AWS Lambda
|
|
25
|
+
# - +:google_cloud_function+ - Google Cloud Functions / Cloud Run
|
|
26
|
+
# - +:azure_function+ - Azure Functions
|
|
27
|
+
# - +:server+ - long-running HTTP server (the default)
|
|
28
|
+
#
|
|
29
|
+
# Detection order matters: CGI is checked before Lambda because a
|
|
30
|
+
# Lambda function invoked through an emulator that also sets
|
|
31
|
+
# GATEWAY_INTERFACE should still be treated as CGI.
|
|
32
|
+
#
|
|
33
|
+
# @return [Symbol] one of the values in {MODES}
|
|
34
|
+
def self.execution_mode
|
|
35
|
+
# CGI environment (e.g. Apache mod_cgi)
|
|
36
|
+
return :cgi if ENV['GATEWAY_INTERFACE'] && !ENV['GATEWAY_INTERFACE'].empty?
|
|
37
|
+
|
|
38
|
+
# AWS Lambda
|
|
39
|
+
if (ENV['AWS_LAMBDA_FUNCTION_NAME'] && !ENV['AWS_LAMBDA_FUNCTION_NAME'].empty?) ||
|
|
40
|
+
(ENV['LAMBDA_TASK_ROOT'] && !ENV['LAMBDA_TASK_ROOT'].empty?)
|
|
41
|
+
return :lambda
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Google Cloud Functions / Cloud Run
|
|
45
|
+
if (ENV['FUNCTION_TARGET'] && !ENV['FUNCTION_TARGET'].empty?) ||
|
|
46
|
+
(ENV['K_SERVICE'] && !ENV['K_SERVICE'].empty?) ||
|
|
47
|
+
(ENV['GOOGLE_CLOUD_PROJECT'] && !ENV['GOOGLE_CLOUD_PROJECT'].empty?)
|
|
48
|
+
return :google_cloud_function
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Azure Functions
|
|
52
|
+
if (ENV['AZURE_FUNCTIONS_ENVIRONMENT'] && !ENV['AZURE_FUNCTIONS_ENVIRONMENT'].empty?) ||
|
|
53
|
+
(ENV['FUNCTIONS_WORKER_RUNTIME'] && !ENV['FUNCTIONS_WORKER_RUNTIME'].empty?) ||
|
|
54
|
+
(ENV['AzureWebJobsStorage'] && !ENV['AzureWebJobsStorage'].empty?)
|
|
55
|
+
return :azure_function
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
:server
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# True when the SDK is running inside AWS Lambda.
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def self.lambda?
|
|
64
|
+
execution_mode == :lambda
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# True when the SDK is running inside any serverless platform.
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def self.serverless?
|
|
70
|
+
mode = execution_mode
|
|
71
|
+
mode == :lambda || mode == :cgi ||
|
|
72
|
+
mode == :google_cloud_function || mode == :azure_function
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Construct the base URL for the current Lambda function.
|
|
76
|
+
#
|
|
77
|
+
# Prefers +AWS_LAMBDA_FUNCTION_URL+ when set; otherwise falls back to
|
|
78
|
+
# the standard Function URL shape built from +AWS_LAMBDA_FUNCTION_NAME+
|
|
79
|
+
# and +AWS_REGION+. Returns +nil+ when neither signal is present.
|
|
80
|
+
#
|
|
81
|
+
# The returned URL never has a trailing slash and never contains a
|
|
82
|
+
# path component, so callers must append the agent's route themselves.
|
|
83
|
+
#
|
|
84
|
+
# @return [String, nil]
|
|
85
|
+
def self.lambda_base_url
|
|
86
|
+
explicit = ENV['AWS_LAMBDA_FUNCTION_URL']
|
|
87
|
+
return explicit.chomp('/') if explicit && !explicit.empty?
|
|
88
|
+
|
|
89
|
+
function_name = ENV['AWS_LAMBDA_FUNCTION_NAME']
|
|
90
|
+
return nil if function_name.nil? || function_name.empty?
|
|
91
|
+
|
|
92
|
+
region = ENV['AWS_REGION']
|
|
93
|
+
region = 'us-east-1' if region.nil? || region.empty?
|
|
94
|
+
|
|
95
|
+
"https://#{function_name}.lambda-url.#{region}.on.aws"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2025 SignalWire
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the MIT License.
|
|
6
|
+
# See LICENSE file in the project root for full license information.
|
|
7
|
+
|
|
8
|
+
require 'openssl'
|
|
9
|
+
require 'securerandom'
|
|
10
|
+
require 'base64'
|
|
11
|
+
|
|
12
|
+
module SignalWire
|
|
13
|
+
module Security
|
|
14
|
+
# Stateless HMAC-SHA256 session manager for secure SWAIG tool tokens.
|
|
15
|
+
#
|
|
16
|
+
# Tokens are self-contained: all information needed for validation is
|
|
17
|
+
# encoded inside the token itself. No server-side session state is stored.
|
|
18
|
+
#
|
|
19
|
+
# mgr = SessionManager.new(token_expiry_secs: 900)
|
|
20
|
+
# token = mgr.create_token("lookup_order", "call-abc-123")
|
|
21
|
+
# mgr.validate_token("lookup_order", token, "call-abc-123") # => true
|
|
22
|
+
#
|
|
23
|
+
class SessionManager
|
|
24
|
+
# @param token_expiry_secs [Integer] seconds until tokens expire (minimum 1)
|
|
25
|
+
# @param secret_key [String, nil] hex-encoded secret; generated if omitted
|
|
26
|
+
def initialize(token_expiry_secs: 3600, secret_key: nil)
|
|
27
|
+
@token_expiry_secs = [token_expiry_secs, 1].max
|
|
28
|
+
@secret_key = secret_key || SecureRandom.hex(32)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Create a secure, self-contained token for a function call.
|
|
32
|
+
#
|
|
33
|
+
# Token format (before Base64):
|
|
34
|
+
# call_id.function_name.expiry_timestamp.nonce.hmac_hex
|
|
35
|
+
#
|
|
36
|
+
# @param function_name [String]
|
|
37
|
+
# @param call_id [String]
|
|
38
|
+
# @return [String] URL-safe Base64-encoded token
|
|
39
|
+
def create_token(function_name, call_id)
|
|
40
|
+
expiry = (Time.now.to_i + @token_expiry_secs).to_s
|
|
41
|
+
nonce = SecureRandom.hex(8)
|
|
42
|
+
|
|
43
|
+
message = "#{call_id}:#{function_name}:#{expiry}:#{nonce}"
|
|
44
|
+
signature = compute_hmac(message)
|
|
45
|
+
|
|
46
|
+
token_raw = "#{call_id}.#{function_name}.#{expiry}.#{nonce}.#{signature}"
|
|
47
|
+
Base64.urlsafe_encode64(token_raw, padding: false)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Validate a function-call token.
|
|
51
|
+
#
|
|
52
|
+
# Checks:
|
|
53
|
+
# 1. Correct Base64 / structure (5 dot-separated parts)
|
|
54
|
+
# 2. HMAC signature (timing-safe comparison)
|
|
55
|
+
# 3. Function name matches
|
|
56
|
+
# 4. Call ID matches
|
|
57
|
+
# 5. Token not expired
|
|
58
|
+
#
|
|
59
|
+
# @param function_name [String]
|
|
60
|
+
# @param token [String] the token to validate
|
|
61
|
+
# @param call_id [String]
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def validate_token(function_name, token, call_id)
|
|
64
|
+
return false if token.nil? || token.empty?
|
|
65
|
+
return false if call_id.nil? || call_id.empty?
|
|
66
|
+
|
|
67
|
+
decoded = Base64.urlsafe_decode64(token)
|
|
68
|
+
parts = decoded.split(".")
|
|
69
|
+
return false unless parts.length == 5
|
|
70
|
+
|
|
71
|
+
token_call_id, token_function, token_expiry, token_nonce, token_signature = parts
|
|
72
|
+
|
|
73
|
+
# Verify function name
|
|
74
|
+
return false unless token_function == function_name
|
|
75
|
+
|
|
76
|
+
# Verify call ID
|
|
77
|
+
return false unless token_call_id == call_id
|
|
78
|
+
|
|
79
|
+
# Check expiry
|
|
80
|
+
expiry = Integer(token_expiry)
|
|
81
|
+
return false if expiry < Time.now.to_i
|
|
82
|
+
|
|
83
|
+
# Recompute HMAC and compare with timing-safe comparison
|
|
84
|
+
message = "#{token_call_id}:#{token_function}:#{token_expiry}:#{token_nonce}"
|
|
85
|
+
expected_signature = compute_hmac(message)
|
|
86
|
+
|
|
87
|
+
secure_compare(token_signature, expected_signature)
|
|
88
|
+
rescue ArgumentError, TypeError
|
|
89
|
+
# Bad Base64, bad integer, etc.
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Compute HMAC-SHA256 of +message+ using the instance secret key.
|
|
96
|
+
# @return [String] hex digest
|
|
97
|
+
def compute_hmac(message)
|
|
98
|
+
OpenSSL::HMAC.hexdigest("SHA256", @secret_key, message)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Timing-safe string comparison.
|
|
102
|
+
#
|
|
103
|
+
# Uses OpenSSL.fixed_length_secure_compare when the strings are the same
|
|
104
|
+
# length (which they should be for hex HMAC digests). Falls back to a
|
|
105
|
+
# double-HMAC comparison for differing lengths.
|
|
106
|
+
def secure_compare(a, b)
|
|
107
|
+
return false if a.nil? || b.nil?
|
|
108
|
+
|
|
109
|
+
if a.bytesize == b.bytesize
|
|
110
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
111
|
+
else
|
|
112
|
+
# Different length => definitely not equal, but still constant-time
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
rescue NoMethodError
|
|
116
|
+
# Fallback for older Ruby without fixed_length_secure_compare:
|
|
117
|
+
# compare HMAC of both values so timing doesn't leak content.
|
|
118
|
+
ha = OpenSSL::HMAC.digest("SHA256", @secret_key, a.to_s)
|
|
119
|
+
hb = OpenSSL::HMAC.digest("SHA256", @secret_key, b.to_s)
|
|
120
|
+
ha == hb
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|