SDK Migration Guide¶
Migrating your extension to use the official Kiket SDKs provides better authentication handling, automatic telemetry, secret management, and future-proof compatibility with platform updates.
Why Migrate?¶
| Before (Manual) | After (SDK) |
|---|---|
| Manual HMAC signature verification | Automatic verification built-in |
| Hardcoded secrets in ENV | Multi-tenant secret resolution |
| Manual API client setup | Pre-configured client with runtime tokens |
| No telemetry | Automatic telemetry and error tracking |
| Custom error handling | Standardized responses and retries |
Migration by Language¶
Ruby: From Sinatra to Ruby SDK¶
Before (Manual Sinatra):
require 'sinatra'
require 'json'
require 'net/http'
post '/events/issue-created' do
# Manual signature verification
signature = request.env['HTTP_X_KIKET_SIGNATURE']
body = request.body.read
expected = OpenSSL::HMAC.hexdigest('sha256', ENV['WEBHOOK_SECRET'], body)
halt 401 unless Rack::Utils.secure_compare(signature, expected)
payload = JSON.parse(body)
# Manual API calls with hardcoded token
uri = URI("https://kiket.dev/api/v1/issues/#{payload['issue']['id']}/comments")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV['API_TOKEN']}"
request['Content-Type'] = 'application/json'
request.body = { body: 'Processed!' }.to_json
http.request(request)
status 204
end
After (Ruby SDK):
require 'kiket_sdk'
sdk = KiketSDK.new
sdk.webhook('issue.created', version: 'v1') do |payload, context|
# Signature verification is automatic
# Runtime token is automatically used for API calls
context[:endpoints].comment(
payload['issue']['id'],
body: 'Processed!'
)
{ ok: true }
end
sdk.run!(port: 9292)
Python: From Flask to Python SDK¶
Before (Manual Flask):
from flask import Flask, request
import hmac
import hashlib
import os
import requests
app = Flask(__name__)
@app.route('/events/issue-created', methods=['POST'])
def handle_issue():
# Manual signature verification
signature = request.headers.get('X-Kiket-Signature')
body = request.get_data()
expected = hmac.new(
os.environ['WEBHOOK_SECRET'].encode(),
body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return '', 401
payload = request.get_json()
# Manual API calls
requests.post(
f"https://kiket.dev/api/v1/issues/{payload['issue']['id']}/comments",
headers={'Authorization': f"Bearer {os.environ['API_TOKEN']}"},
json={'body': 'Processed!'}
)
return '', 204
After (Python SDK):
from kiket_sdk import KiketSDK
sdk = KiketSDK()
@sdk.webhook('issue.created', version='v1')
def handle_issue(payload, context):
# Signature verification is automatic
# Runtime token is automatically used for API calls
context.endpoints.comment(
payload['issue']['id'],
body='Processed!'
)
return {'ok': True}
sdk.run(port=9292)
Node.js: From Express to Node SDK¶
Before (Manual Express):
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/events/issue-created', async (req, res) => {
// Manual signature verification
const signature = req.headers['x-kiket-signature'];
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(req.body)
.digest('hex');
if (signature !== expected) {
return res.status(401).send();
}
const payload = JSON.parse(req.body.toString());
// Manual API calls
await fetch(
`https://kiket.dev/api/v1/issues/${payload.issue.id}/comments`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ body: 'Processed!' }),
}
);
res.status(204).send();
});
After (Node SDK):
import { KiketSDK } from '@kiket/sdk';
const sdk = new KiketSDK();
sdk.webhook('issue.created', { version: 'v1' }, async (payload, context) => {
// Signature verification is automatic
// Runtime token is automatically used for API calls
await context.endpoints.comment(payload.issue.id, {
body: 'Processed!',
});
return { ok: true };
});
sdk.run({ port: 9292 });
Scope Checks Migration¶
The SDK provides built-in scope verification for OAuth 2.0 permission enforcement.
Declarative Scopes (Recommended)¶
Specify required scopes when registering handlers:
sdk.register('issue.created', version: 'v1', required_scopes: %w[issues:read]) do |payload, context|
# Handler only executes if installation has issues:read scope
# Returns 403 automatically if scope is missing
end
Runtime Scope Checks¶
For conditional scope requirements, use context[:require_scopes]:
sdk.register('workflow.action', version: 'v1') do |payload, context|
action = payload['action']
if action == 'create_issue'
context[:require_scopes].call('issues:write')
# Creates issue...
elsif action == 'add_comment'
context[:require_scopes].call('comments:write')
# Adds comment...
end
end
Available Scopes¶
| Scope | Description |
|---|---|
issues:read |
Read issue data |
issues:write |
Create and update issues |
comments:write |
Add comments to issues |
projects:read |
Read project metadata |
users:read |
Read user profiles |
secrets:read |
Access configured secrets |
custom_data:read |
Read custom data modules |
custom_data:write |
Write to custom data modules |
JWT Runtime Token Authentication¶
The SDK automatically verifies JWT runtime tokens included in webhook payloads. This provides:
- Organization context: Tokens include
org_id,proj_id,ext_id - Scope enforcement: Token scopes are validated against handler requirements
- Automatic refresh: Kiket manages token lifecycle
How It Works¶
- Kiket generates a short-lived JWT when dispatching webhooks
- Token is included in
payload.authentication.runtime_token - SDK verifies token signature against Kiket's public key
- Verified claims (org_id, scopes) are available in
context[:auth]
sdk.register('notify.send', version: 'v1') do |payload, context|
# Access verified JWT claims
org_id = context[:auth][:org_id]
project_id = context[:auth][:proj_id]
scopes = context[:auth][:scopes]
# Runtime token is automatically used for API calls
context[:endpoints].log_event('notification.sent', { org_id: org_id })
end
Migrating from API Keys¶
Before (Static API Key):
After (Runtime Token):
sdk.register('event.handle', version: 'v1') do |payload, context|
# Runtime token scoped to specific org/project
# Automatically used by context[:endpoints]
context[:endpoints].create_issue(project_id: context[:auth][:proj_id], title: 'New Issue')
end
Secret Access Migration¶
The SDK provides a unified way to access secrets that supports multi-tenant deployments.
Old Pattern (Single-Tenant)¶
New Pattern (Multi-Tenant with Fallback)¶
sdk.webhook('notify.send', version: 'v1') do |payload, context|
# Checks payload secrets first (per-org), falls back to ENV (extension default)
slack_token = context[:secret].call('SLACK_BOT_TOKEN')
# Use the token...
end
Resolution order:
1. payload['secrets']['SLACK_BOT_TOKEN'] - Per-organization/project secret from setup wizard
2. ENV['SLACK_BOT_TOKEN'] - Extension default (Cloud Run environment variable)
This pattern enables: - Multi-tenant: Each customer can configure their own Slack workspace - Fallback: Extensions work with a default configuration if not overridden - Zero code changes: Same code works for both scenarios
Manifest Updates¶
Ensure your manifest uses the SDK-compatible format:
model_version: "1.0"
extension:
id: com.yourcompany.extension
name: My Extension
version: 2.0.0 # Bump version when migrating
delivery: http
callback:
url: ${EXTENSION_BASE_URL}
secret: env.KIKET_WEBHOOK_SECRET
# Define secrets that can be configured per-organization
configuration:
SLACK_BOT_TOKEN:
type: string
secret: true
required: true
label: Slack Bot Token
description: Your Slack app's bot token (xoxb-...)
Testing After Migration¶
-
Run the extension locally:
-
Use the CLI replay tool:
-
Verify in sandbox project:
- Install extension in a sandbox project
- Trigger events and check Activity logs
- Confirm API calls are using runtime tokens
Checklist¶
- Install the SDK for your language
- Replace manual signature verification with SDK handlers
- Add
required_scopesto all handler registrations - Use
context[:require_scopes]for conditional scope checks - Replace manual API client with
context.endpoints - Update secret access to use
context[:secret].call()pattern - Remove hardcoded API keys - use JWT runtime tokens instead
- Update manifest to SDK-compatible format
- Bump extension version in manifest
- Test locally with replay fixtures
- Test in sandbox project
- Update README with SDK usage examples