@creedspace/sdk 1.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.
package/src/index.ts ADDED
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Creed Space SDK for TypeScript
3
+ *
4
+ * Provides governance infrastructure for AI agents with:
5
+ * - Tool call authorization with cryptographic proof
6
+ * - Callback-based flow control (onAllow, onDeny, onRequireHuman)
7
+ * - Hash-chain audit trails
8
+ *
9
+ * Design inspired by Superagent's clean SDK patterns.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { createClient } from '@creed-space/sdk';
14
+ *
15
+ * const client = createClient({ apiKey: 'crd_live_...' });
16
+ *
17
+ * const result = await client.decide({
18
+ * toolName: 'send_email',
19
+ * arguments: { to: 'user@example.com', subject: 'Hello' },
20
+ * onAllow: (decision) => {
21
+ * console.log('Authorized:', decision.token);
22
+ * // Execute the tool
23
+ * },
24
+ * onDeny: (decision) => {
25
+ * console.log('Denied:', decision.reasons);
26
+ * },
27
+ * onRequireHuman: (decision) => {
28
+ * // Feature planned - will trigger human review workflow
29
+ * console.log('Human review required (planned feature)');
30
+ * },
31
+ * });
32
+ * ```
33
+ */
34
+
35
+ // ============================================================================
36
+ // Types
37
+ // ============================================================================
38
+
39
+ export type DecisionType = 'ALLOW' | 'DENY' | 'REQUIRE_HUMAN';
40
+
41
+ export interface Risk {
42
+ score: number;
43
+ labels: string[];
44
+ }
45
+
46
+ export interface DecideRequest {
47
+ /** Name of the tool to execute */
48
+ toolName: string;
49
+ /** Tool arguments */
50
+ arguments: Record<string, unknown>;
51
+ /** Constitution ID (default: 'default') */
52
+ constitutionId?: string;
53
+ /** Optional context */
54
+ context?: {
55
+ tenantId?: string;
56
+ projectId?: string;
57
+ userId?: string;
58
+ sessionId?: string;
59
+ };
60
+ /** Callback when tool is allowed */
61
+ onAllow?: (decision: AllowDecision) => void | Promise<void>;
62
+ /** Callback when tool is denied */
63
+ onDeny?: (decision: DenyDecision) => void | Promise<void>;
64
+ /** Callback when human review is required (planned feature) */
65
+ onRequireHuman?: (decision: RequireHumanDecision) => void | Promise<void>;
66
+ }
67
+
68
+ export interface AllowDecision {
69
+ decision: 'ALLOW';
70
+ runId: string;
71
+ actionId: string;
72
+ toolCallId: string;
73
+ argsHash: string;
74
+ risk: Risk;
75
+ /** Signed JWT decision token */
76
+ decisionToken: string;
77
+ /** Token expiry */
78
+ expiresAt: string;
79
+ }
80
+
81
+ export interface DenyDecision {
82
+ decision: 'DENY';
83
+ runId: string;
84
+ actionId: string;
85
+ toolCallId: string;
86
+ argsHash: string;
87
+ risk: Risk;
88
+ reasons: string[];
89
+ guidance: {
90
+ message: string;
91
+ suggestion?: string;
92
+ };
93
+ }
94
+
95
+ export interface RequireHumanDecision {
96
+ decision: 'REQUIRE_HUMAN';
97
+ runId: string;
98
+ actionId: string;
99
+ toolCallId: string;
100
+ argsHash: string;
101
+ risk: Risk;
102
+ /** Review ID for tracking (planned) */
103
+ reviewId?: string;
104
+ /** Status - always 'planned' until feature is released */
105
+ featureStatus: 'planned';
106
+ }
107
+
108
+ export type DecideResult = AllowDecision | DenyDecision | RequireHumanDecision;
109
+
110
+ export interface AuthorizeRequest {
111
+ /** The decision token to verify */
112
+ decisionToken: string;
113
+ /** Tool name (must match token) */
114
+ toolName?: string;
115
+ /** Args hash (must match token) */
116
+ argsHash?: string;
117
+ }
118
+
119
+ export interface AuthorizeResult {
120
+ authorized: boolean;
121
+ claims?: {
122
+ actionId: string;
123
+ toolName: string;
124
+ toolCallId: string;
125
+ argsHash: string;
126
+ runId: string;
127
+ decision: string;
128
+ issuedAt: string;
129
+ expiresAt: string;
130
+ };
131
+ error?: string;
132
+ message: string;
133
+ }
134
+
135
+ export interface AuditRequest {
136
+ /** Run ID to query */
137
+ runId: string;
138
+ /** Specific action ID (optional) */
139
+ actionId?: string;
140
+ /** Max events to return */
141
+ limit?: number;
142
+ }
143
+
144
+ export interface AuditEvent {
145
+ seq: number;
146
+ type: string;
147
+ timestamp: string;
148
+ data: Record<string, unknown>;
149
+ hash: string;
150
+ }
151
+
152
+ export interface AuditResult {
153
+ runId: string;
154
+ actionId?: string;
155
+ eventCount: number;
156
+ events: AuditEvent[];
157
+ integrity: {
158
+ chain: string;
159
+ verified: boolean;
160
+ };
161
+ }
162
+
163
+ export interface CreedClientOptions {
164
+ /** API key (crd_live_... or crd_test_...) */
165
+ apiKey: string;
166
+ /** Base URL (default: https://api.creed.space) */
167
+ baseUrl?: string;
168
+ /** Custom fetch implementation */
169
+ fetch?: typeof fetch;
170
+ /** Request timeout in ms (default: 30000) */
171
+ timeoutMs?: number;
172
+ }
173
+
174
+ export interface CreedClient {
175
+ /** Get a governance decision for a tool call */
176
+ decide(request: DecideRequest): Promise<DecideResult>;
177
+ /** Verify a decision token before execution */
178
+ authorize(request: AuthorizeRequest): Promise<AuthorizeResult>;
179
+ /** Query the audit trail */
180
+ audit(request: AuditRequest): Promise<AuditResult>;
181
+ /** Get service status */
182
+ status(): Promise<StatusResult>;
183
+ }
184
+
185
+ export interface StatusResult {
186
+ service: string;
187
+ version: string;
188
+ features: Record<string, { status: string; description: string }>;
189
+ decisionTypes: Record<string, string>;
190
+ }
191
+
192
+ // ============================================================================
193
+ // Errors
194
+ // ============================================================================
195
+
196
+ export class CreedError extends Error {
197
+ constructor(
198
+ message: string,
199
+ public code: string,
200
+ public statusCode?: number
201
+ ) {
202
+ super(message);
203
+ this.name = 'CreedError';
204
+ }
205
+ }
206
+
207
+ // ============================================================================
208
+ // Client Implementation
209
+ // ============================================================================
210
+
211
+ /**
212
+ * Create a Creed Space client.
213
+ *
214
+ * @param options - Client configuration
215
+ * @returns CreedClient instance
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * const client = createClient({
220
+ * apiKey: process.env.CREED_API_KEY!,
221
+ * baseUrl: 'https://api.creed.space',
222
+ * });
223
+ * ```
224
+ */
225
+ export function createClient(options: CreedClientOptions): CreedClient {
226
+ const {
227
+ apiKey,
228
+ baseUrl = 'https://api.creed.space',
229
+ fetch: customFetch = fetch,
230
+ timeoutMs = 30000,
231
+ } = options;
232
+
233
+ if (!apiKey) {
234
+ throw new CreedError('API key is required', 'MISSING_API_KEY');
235
+ }
236
+
237
+ async function request<T>(
238
+ endpoint: string,
239
+ method: string,
240
+ body?: Record<string, unknown>
241
+ ): Promise<T> {
242
+ const controller = new AbortController();
243
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
244
+
245
+ try {
246
+ const response = await customFetch(`${baseUrl}${endpoint}`, {
247
+ method,
248
+ headers: {
249
+ 'Content-Type': 'application/json',
250
+ Authorization: `Bearer ${apiKey}`,
251
+ 'X-Creed-SDK': 'typescript/1.0.0',
252
+ },
253
+ body: body ? JSON.stringify(body) : undefined,
254
+ signal: controller.signal,
255
+ });
256
+
257
+ if (!response.ok) {
258
+ const errorBody = await response.text();
259
+ throw new CreedError(
260
+ errorBody || `Request failed with status ${response.status}`,
261
+ 'REQUEST_FAILED',
262
+ response.status
263
+ );
264
+ }
265
+
266
+ return response.json();
267
+ } catch (error) {
268
+ if (error instanceof CreedError) throw error;
269
+ if ((error as Error).name === 'AbortError') {
270
+ throw new CreedError('Request timed out', 'TIMEOUT');
271
+ }
272
+ throw new CreedError((error as Error).message, 'NETWORK_ERROR');
273
+ } finally {
274
+ clearTimeout(timeoutId);
275
+ }
276
+ }
277
+
278
+ return {
279
+ async decide(req: DecideRequest): Promise<DecideResult> {
280
+ const response = await request<DecideResult>('/v1/decide', 'POST', {
281
+ tool_name: req.toolName,
282
+ arguments: req.arguments,
283
+ constitution_id: req.constitutionId || 'default',
284
+ context: req.context || {},
285
+ });
286
+
287
+ // Invoke appropriate callback
288
+ if (response.decision === 'ALLOW' && req.onAllow) {
289
+ await req.onAllow(response as AllowDecision);
290
+ } else if (response.decision === 'DENY' && req.onDeny) {
291
+ await req.onDeny(response as DenyDecision);
292
+ } else if (response.decision === 'REQUIRE_HUMAN' && req.onRequireHuman) {
293
+ // Note: REQUIRE_HUMAN is a planned feature
294
+ const humanDecision: RequireHumanDecision = {
295
+ ...(response as RequireHumanDecision),
296
+ featureStatus: 'planned',
297
+ };
298
+ await req.onRequireHuman(humanDecision);
299
+ }
300
+
301
+ return response;
302
+ },
303
+
304
+ async authorize(req: AuthorizeRequest): Promise<AuthorizeResult> {
305
+ return request<AuthorizeResult>('/v1/authorize', 'POST', {
306
+ decision_token: req.decisionToken,
307
+ tool_name: req.toolName,
308
+ args_hash: req.argsHash,
309
+ });
310
+ },
311
+
312
+ async audit(req: AuditRequest): Promise<AuditResult> {
313
+ return request<AuditResult>('/v1/audit', 'POST', {
314
+ run_id: req.runId,
315
+ action_id: req.actionId,
316
+ limit: req.limit || 50,
317
+ });
318
+ },
319
+
320
+ async status(): Promise<StatusResult> {
321
+ return request<StatusResult>('/v1/status', 'GET');
322
+ },
323
+ };
324
+ }
325
+
326
+ // ============================================================================
327
+ // Utility Functions
328
+ // ============================================================================
329
+
330
+ /**
331
+ * Compute SHA-256 hash of arguments (for verification).
332
+ * Uses the same canonical JSON format as the server.
333
+ */
334
+ export async function computeArgsHash(
335
+ args: Record<string, unknown>
336
+ ): Promise<string> {
337
+ // Canonical JSON: sorted keys, no whitespace
338
+ const canonical = JSON.stringify(args, Object.keys(args).sort());
339
+ const encoder = new TextEncoder();
340
+ const data = encoder.encode(canonical);
341
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
342
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
343
+ return 'sha256:' + hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
344
+ }
345
+
346
+ /**
347
+ * Check if a decision token is expired (without verification).
348
+ */
349
+ export function isTokenExpired(token: string): boolean {
350
+ try {
351
+ const parts = token.split('.');
352
+ if (parts.length !== 3) return true;
353
+ const payload = JSON.parse(atob(parts[1]));
354
+ return payload.exp * 1000 < Date.now();
355
+ } catch {
356
+ return true;
357
+ }
358
+ }
359
+
360
+ // ============================================================================
361
+ // Re-exports for convenience
362
+ // ============================================================================
363
+
364
+ export default createClient;