taqlyn 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6d583ab227c70a787a0c7df284bb5af6ee135640c1a19661e609621a7c2bb7aa
4
+ data.tar.gz: 8e46da06d29fd8c6e76593be32376d9261ecd555d85083fadcbc2c51a996277a
5
+ SHA512:
6
+ metadata.gz: c6ba2debea8fc0f2e2ed9047eb217738a4fb9e683a10e5c679d7c532c0672d6dcf8d9609cb21a313ce8f745eb94f92edd8fdea0bd683da9f7578b67d2ef90cb8
7
+ data.tar.gz: f12edc7c7289f49b28a83825a29e2e9c148be74092ff19323d5beb72edd7121f6c442bd003bb2e9b94c84991f3f91a534cb427d7281ff5aee7d9c5543bcc5f5b
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+ require_relative "signer"
7
+
8
+ module Taqlyn
9
+ class ApiError < StandardError
10
+ attr_reader :status, :body
11
+
12
+ def initialize(status, body)
13
+ @status = status
14
+ @body = body
15
+ super("Taqlyn API #{status}")
16
+ end
17
+ end
18
+
19
+ class Client
20
+ DEFAULT_API_BASE_URL = "https://api.taqlyn.com"
21
+ SHORT_LINKS_PATH = "/v1/short-links"
22
+
23
+ def initialize(client_id:, private_key:, base_url: nil, now: nil)
24
+ raw_url = (base_url && !base_url.to_s.strip.empty?) ? base_url : (ENV["TAQLYN_BASE_URL"] || ENV["TAQLYN_API_URL"] || DEFAULT_API_BASE_URL)
25
+ raise ArgumentError, "base_url is required" if raw_url.to_s.strip.empty?
26
+ raise ArgumentError, "client_id is required" if client_id.to_s.strip.empty?
27
+ unless client_id.start_with?("app_test_", "app_live_")
28
+ raise ArgumentError, "client_id must start with app_test_ or app_live_"
29
+ end
30
+
31
+ @base_url = raw_url.to_s.sub(%r{/+\z}, "")
32
+ @client_id = client_id.strip
33
+ @key = Signer.load_private_key(private_key)
34
+ @now = now || -> { Time.now.to_i }
35
+ end
36
+
37
+ def create_short_link(destination_web:, mode: nil, destination_path: nil)
38
+ raise ArgumentError, "destination_web is required" if destination_web.to_s.strip.empty?
39
+
40
+ body = { "destinationWeb" => destination_web.strip }
41
+ body["mode"] = mode if mode
42
+ body["destinationPath"] = destination_path if destination_path
43
+ request("POST", SHORT_LINKS_PATH, body)
44
+ end
45
+
46
+ private
47
+
48
+ def request(method, path, payload)
49
+ json = JSON.generate(payload)
50
+ uri = URI.parse(@base_url + path)
51
+ headers = Signer.headers(@key, @client_id, method, path, json, @now.call)
52
+ http = Net::HTTP.new(uri.host, uri.port)
53
+ http.use_ssl = uri.scheme == "https"
54
+ req = Net::HTTP::Post.new(uri.request_uri)
55
+ req.body = json
56
+ req["Accept"] = "application/json"
57
+ req["Content-Type"] = "application/json"
58
+ headers.each { |k, v| req[k] = v }
59
+ res = http.request(req)
60
+ unless res.is_a?(Net::HTTPSuccess)
61
+ raise ApiError.new(res.code.to_i, res.body)
62
+ end
63
+ JSON.parse(res.body)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "digest"
5
+ require "openssl"
6
+
7
+ module Taqlyn
8
+ module Signer
9
+ module_function
10
+
11
+ def normalize_pem(pem)
12
+ value = pem.to_s.strip
13
+ value = value.gsub('\n', "\n") if value.include?('\n') && !value.include?("\n")
14
+ value
15
+ end
16
+
17
+ def canonical_message(method, path, unix_timestamp, client_id, body)
18
+ body_bytes = body.is_a?(String) ? body.b : body.to_s.b
19
+ [
20
+ "taqlyn-v1",
21
+ method.to_s.upcase,
22
+ path,
23
+ unix_timestamp.to_s,
24
+ client_id,
25
+ Digest::SHA256.hexdigest(body_bytes)
26
+ ].join("\n")
27
+ end
28
+
29
+ def load_private_key(pem)
30
+ normalized = normalize_pem(pem)
31
+ raise ArgumentError, "sk_* is a UX handle only; pass the PKCS#8 PEM" if normalized.start_with?("sk_")
32
+ raise ArgumentError, "Private key must be a PKCS#8 PEM" unless normalized.include?("BEGIN")
33
+
34
+ OpenSSL::PKey.read(normalized)
35
+ end
36
+
37
+ def headers(pkey, client_id, method, path, body, unix_timestamp)
38
+ message = canonical_message(method, path, unix_timestamp, client_id, body)
39
+ signature = pkey.sign(nil, message)
40
+ {
41
+ "X-Taqlyn-Client-Id" => client_id,
42
+ "X-Taqlyn-Timestamp" => unix_timestamp.to_s,
43
+ "X-Taqlyn-Signature" => Base64.strict_encode64(signature)
44
+ }
45
+ end
46
+ end
47
+ end
data/lib/taqlyn.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "taqlyn/client"
4
+
5
+ module Taqlyn
6
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: taqlyn
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Taqlyn
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/taqlyn.rb
20
+ - lib/taqlyn/client.rb
21
+ - lib/taqlyn/signer.rb
22
+ homepage:
23
+ licenses:
24
+ - MIT
25
+ metadata: {}
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '3.1'
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubygems_version: 3.5.22
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: Taqlyn server SDK — Ed25519-signed short-link API
45
+ test_files: []