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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d90db8da72e11a9058298022e35575c16bb5ddc99ed82e99f311cd337b017f36
4
- data.tar.gz: c6bb8675fad6cba884df875a86617ad9fb2df24a822662fbf58ce20cc94632b4
3
+ metadata.gz: 4698932cd6e8ecddfd61917c6003c55149e36cb04727182776a692714cab1e1a
4
+ data.tar.gz: 3cfe868e7d6073346735bc69977bb73dce742a66015d3d818d9a91fbd177c79e
5
5
  SHA512:
6
- metadata.gz: 548f044f2276926ebe16960f0da8969dab328e9546edb392e6344dc208fbd56458034f0284585349b78a9b09a3d56ea2169a166bb35bdadd46081d808258ad8c
7
- data.tar.gz: 4429b95f990e9065fc41569cc8b2fd59dcaf5d0bedb2d4533ddf401c2086d1103741b63b7f0c163c48f8a1577431229b12d03d6e3298ba8146bd14cb69e9f45f
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
- fk.submit(end_user_id: user["end_user_id"], title: "Dark mode", kind: "feature_request")
46
- fk.features(end_user_id: user["end_user_id"], sort: "top")
47
- fk.vote(feature_id, end_user_id: user["end_user_id"])
48
- fk.comment(feature_id, end_user_id: user["end_user_id"], body: "Yes please")
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
@@ -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
- def identify(external_id: nil, email: nil, name: nil, avatar_url: nil, platform: nil)
37
- sdk_post("/sdk/init", external_id:, email:, name:, avatar_url:, platform:)
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
- def features(end_user_id: nil, status: nil, kind: nil, sort: "top", cursor: nil)
42
- query = { end_user_id:, status:, kind:, sort:, cursor: }.compact
43
- sdk_get("/sdk/features", query)
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 on behalf of an end-user.
47
- def submit(end_user_id:, title:, description: nil, kind: "feature_request", tag: nil)
48
- sdk_post("/sdk/features", end_user_id:, title:, description:, kind:, tag:)
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 a vote.
52
- def vote(feature_id, end_user_id:)
53
- sdk_post("/sdk/features/#{feature_id}/vote", end_user_id:)
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 an end-user.
57
- def comment(feature_id, end_user_id:, body:)
58
- sdk_post("/sdk/features/#{feature_id}/comments", end_user_id:, body:)
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
- def key_header
72
- { "X-Project-Key" => project_key }
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: {})
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HeedKit
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/heedkit.rb CHANGED
@@ -19,7 +19,10 @@ module HeedKit
19
19
  class Error < StandardError; end
20
20
 
21
21
  class Configuration
22
- attr_accessor :project_key, :endpoint, :timeout
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, timeout: configuration.timeout)
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heedkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - HeedKit