heedkit 0.1.0 → 0.2.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 +4 -4
- data/README.md +24 -4
- data/lib/heedkit/client.rb +49 -23
- data/lib/heedkit/version.rb +1 -1
- data/lib/heedkit.rb +6 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4698932cd6e8ecddfd61917c6003c55149e36cb04727182776a692714cab1e1a
|
|
4
|
+
data.tar.gz: 3cfe868e7d6073346735bc69977bb73dce742a66015d3d818d9a91fbd177c79e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f60562b31b82e6b6e572b1515ce0a26c6b7f2c6a000b889ec7d1a3785b581f8908d7cb028d766959fef0fd339a297c156d3f6578cec802c927a792a30b3ce1d
|
|
7
|
+
data.tar.gz: 3f1a9e51e15843bde1c925077e0293a78919c81e4766a598bd11bb4dc939fb362afc5a56a2fa7a3c65d9d4418814d92ad35f87c9d94b0ef2224a4b57effde41a
|
data/README.md
CHANGED
|
@@ -14,6 +14,7 @@ gem "heedkit"
|
|
|
14
14
|
HeedKit.configure do |c|
|
|
15
15
|
c.project_key = ENV["HEEDKIT_PROJECT_KEY"] # "fk_..."
|
|
16
16
|
c.endpoint = "https://acme.heedkit.com" # your HeedKit base URL
|
|
17
|
+
c.secret_key = ENV["HEEDKIT_SERVER_SECRET"] # "fk_secret_..." — lets identify() sign users
|
|
17
18
|
end
|
|
18
19
|
```
|
|
19
20
|
|
|
@@ -41,11 +42,30 @@ and views:
|
|
|
41
42
|
|
|
42
43
|
```ruby
|
|
43
44
|
fk = HeedKit.client
|
|
45
|
+
# With secret_key configured, identify() signs the external_id automatically
|
|
46
|
+
# (the API rejects unsigned ids with 401 invalid_user_signature).
|
|
44
47
|
user = fk.identify(external_id: "user-123", email: "ada@example.com")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
fk.
|
|
48
|
-
fk.
|
|
48
|
+
identity = user["identity"] # signed replay token — pass it to the calls below
|
|
49
|
+
|
|
50
|
+
fk.submit(identity:, title: "Dark mode", kind: "feature_request")
|
|
51
|
+
fk.features(identity:, sort: "top") # identity optional here (anonymous list works)
|
|
52
|
+
fk.vote(feature_id, identity:)
|
|
53
|
+
fk.comment(feature_id, identity:, body: "Yes please")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The client is stateless — one instance can serve many end-users; each call carries the
|
|
57
|
+
`identity` token of the user it acts for (`X-HeedKit-Identity` header).
|
|
58
|
+
|
|
59
|
+
Serving a browser widget? Expose the signed payload from an authenticated route and pass
|
|
60
|
+
it to the JS SDK:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# GET /heedkit/identity (authenticated)
|
|
64
|
+
render json: {
|
|
65
|
+
externalId: current_user.id.to_s,
|
|
66
|
+
userHash: HeedKit.client.user_hash_for(current_user.id),
|
|
67
|
+
name: current_user.name, email: current_user.email
|
|
68
|
+
}
|
|
49
69
|
```
|
|
50
70
|
|
|
51
71
|
`identify` / `submit` / `features` / `vote` / `comment` authenticate with the project key
|
data/lib/heedkit/client.rb
CHANGED
|
@@ -3,24 +3,42 @@
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "json"
|
|
5
5
|
require "uri"
|
|
6
|
+
require "openssl"
|
|
6
7
|
|
|
7
8
|
module HeedKit
|
|
8
9
|
# Server-side client for the HeedKit API. Talks to the public roadmap endpoint
|
|
9
10
|
# and the end-user SDK endpoints (X-Project-Key auth).
|
|
11
|
+
#
|
|
12
|
+
# Identity: this SDK runs where the project SECRET may live, so it can sign
|
|
13
|
+
# identities itself — configure `secret_key` and #identify computes the required
|
|
14
|
+
# user_hash automatically (or expose #user_hash_for to your frontend widget).
|
|
15
|
+
# Authenticated calls (submit/vote/comment) take the `identity` token #identify
|
|
16
|
+
# returned; the client is stateless so one instance can serve many end-users.
|
|
10
17
|
class Client
|
|
11
18
|
DEFAULT_TIMEOUT = 5
|
|
12
19
|
|
|
13
20
|
attr_reader :project_key, :endpoint
|
|
14
21
|
|
|
15
|
-
def initialize(project_key:, endpoint:, timeout: DEFAULT_TIMEOUT)
|
|
22
|
+
def initialize(project_key:, endpoint:, secret_key: nil, timeout: DEFAULT_TIMEOUT)
|
|
16
23
|
raise ArgumentError, "project_key is required" if project_key.to_s.empty?
|
|
17
24
|
raise ArgumentError, "endpoint is required" if endpoint.to_s.empty?
|
|
18
25
|
|
|
19
26
|
@project_key = project_key
|
|
20
27
|
@endpoint = endpoint.to_s.chomp("/")
|
|
28
|
+
@secret_key = secret_key
|
|
21
29
|
@timeout = timeout
|
|
22
30
|
end
|
|
23
31
|
|
|
32
|
+
# HMAC_SHA256(secret_key, external_id) as lowercase hex — the signature /sdk/init
|
|
33
|
+
# requires alongside any external_id (unsigned ids are rejected with
|
|
34
|
+
# 401 invalid_user_signature). Also handy for building the identity payload a
|
|
35
|
+
# frontend widget fetches ({ externalId, userHash, name, email }).
|
|
36
|
+
def user_hash_for(external_id)
|
|
37
|
+
raise Error, "secret_key not configured — pass secret_key: to HeedKit::Client.new" if @secret_key.to_s.empty?
|
|
38
|
+
|
|
39
|
+
OpenSSL::HMAC.hexdigest("SHA256", @secret_key, external_id.to_s)
|
|
40
|
+
end
|
|
41
|
+
|
|
24
42
|
# GET the public roadmap. Returns a HeedKit::Roadmap.
|
|
25
43
|
def roadmap
|
|
26
44
|
Roadmap.from_payload(get("/public/projects/#{project_key}/roadmap"))
|
|
@@ -32,44 +50,52 @@ module HeedKit
|
|
|
32
50
|
end
|
|
33
51
|
|
|
34
52
|
# POST /sdk/init — identify (find-or-create) an end-user. Returns the parsed body
|
|
35
|
-
# ({ "end_user_id" => ..., "project" => {...} })
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
# ({ "end_user_id" => ..., "identity" => "<replay token>", "project" => {...} });
|
|
54
|
+
# pass that "identity" to submit/vote/comment. With an external_id, user_hash is
|
|
55
|
+
# computed from the configured secret_key when not given explicitly.
|
|
56
|
+
def identify(external_id: nil, user_hash: nil, email: nil, name: nil, avatar_url: nil, platform: nil)
|
|
57
|
+
user_hash ||= user_hash_for(external_id) if external_id && !@secret_key.to_s.empty?
|
|
58
|
+
sdk_post("/sdk/init", external_id:, user_hash:, email:, name:, avatar_url:, platform:)
|
|
38
59
|
end
|
|
39
60
|
|
|
40
|
-
# GET /sdk/features — public features plus the caller's own private submissions
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
# GET /sdk/features — public features, plus the caller's own private submissions
|
|
62
|
+
# when an identity token is given.
|
|
63
|
+
def features(identity: nil, status: nil, kind: nil, sort: "top", cursor: nil)
|
|
64
|
+
query = { status:, kind:, sort:, cursor: }.compact
|
|
65
|
+
sdk_get("/sdk/features", query, identity:)
|
|
44
66
|
end
|
|
45
67
|
|
|
46
|
-
# POST /sdk/features — submit a feature
|
|
47
|
-
def submit(
|
|
48
|
-
sdk_post("/sdk/features",
|
|
68
|
+
# POST /sdk/features — submit a feature as the end-user the token names.
|
|
69
|
+
def submit(identity:, title:, description: nil, kind: "feature_request", tag: nil)
|
|
70
|
+
sdk_post("/sdk/features", identity:, title:, description:, kind:, tag:)
|
|
49
71
|
end
|
|
50
72
|
|
|
51
|
-
# POST /sdk/features/:id/vote — toggle
|
|
52
|
-
def vote(feature_id,
|
|
53
|
-
sdk_post("/sdk/features/#{feature_id}/vote",
|
|
73
|
+
# POST /sdk/features/:id/vote — toggle the end-user's vote.
|
|
74
|
+
def vote(feature_id, identity:)
|
|
75
|
+
sdk_post("/sdk/features/#{feature_id}/vote", identity:)
|
|
54
76
|
end
|
|
55
77
|
|
|
56
|
-
# POST /sdk/features/:id/comments — comment as
|
|
57
|
-
def comment(feature_id,
|
|
58
|
-
sdk_post("/sdk/features/#{feature_id}/comments",
|
|
78
|
+
# POST /sdk/features/:id/comments — comment as the end-user the token names.
|
|
79
|
+
def comment(feature_id, identity:, body:)
|
|
80
|
+
sdk_post("/sdk/features/#{feature_id}/comments", identity:, body:)
|
|
59
81
|
end
|
|
60
82
|
|
|
61
83
|
private
|
|
62
84
|
|
|
63
|
-
def sdk_get(path, query)
|
|
64
|
-
get(path, query:, headers: key_header)
|
|
85
|
+
def sdk_get(path, query, identity: nil)
|
|
86
|
+
get(path, query:, headers: key_header(identity))
|
|
65
87
|
end
|
|
66
88
|
|
|
67
|
-
def sdk_post(path, **body)
|
|
68
|
-
post(path, body: body.compact, headers: key_header)
|
|
89
|
+
def sdk_post(path, identity: nil, **body)
|
|
90
|
+
post(path, body: body.compact, headers: key_header(identity))
|
|
69
91
|
end
|
|
70
92
|
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
# Project key + (when present) the signed identity replay token. The caller is
|
|
94
|
+
# identified by this header, never by body params.
|
|
95
|
+
def key_header(identity = nil)
|
|
96
|
+
h = { "X-Project-Key" => project_key }
|
|
97
|
+
h["X-HeedKit-Identity"] = identity if identity
|
|
98
|
+
h
|
|
73
99
|
end
|
|
74
100
|
|
|
75
101
|
def get(path, query: {}, headers: {})
|
data/lib/heedkit/version.rb
CHANGED
data/lib/heedkit.rb
CHANGED
|
@@ -19,7 +19,10 @@ module HeedKit
|
|
|
19
19
|
class Error < StandardError; end
|
|
20
20
|
|
|
21
21
|
class Configuration
|
|
22
|
-
|
|
22
|
+
# secret_key is the project's SERVER secret (fk_secret_…) — set it so
|
|
23
|
+
# Client#identify can sign external ids (user_hash). Keep it out of anything
|
|
24
|
+
# that reaches a browser or app binary.
|
|
25
|
+
attr_accessor :project_key, :endpoint, :secret_key, :timeout
|
|
23
26
|
|
|
24
27
|
def initialize
|
|
25
28
|
@endpoint = "https://api.heedkit.com"
|
|
@@ -38,7 +41,8 @@ module HeedKit
|
|
|
38
41
|
|
|
39
42
|
# A client built from the global configuration.
|
|
40
43
|
def client
|
|
41
|
-
Client.new(project_key: configuration.project_key, endpoint: configuration.endpoint,
|
|
44
|
+
Client.new(project_key: configuration.project_key, endpoint: configuration.endpoint,
|
|
45
|
+
secret_key: configuration.secret_key, timeout: configuration.timeout)
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
def roadmap
|