@permission-protocol/sdk 0.1.0-alpha.1
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/dist/index.d.mts +297 -0
- package/dist/index.d.ts +297 -0
- package/dist/index.js +356 -0
- package/dist/index.mjs +325 -0
- package/package.json +51 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HashablePayload v1 Implementation
|
|
3
|
+
* Conforms to spec/hashable-payload-v1.md
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* HashablePayload v1 - the canonical envelope for computing inputHash.
|
|
7
|
+
* Matches spec/hashable-payload-v1.md exactly.
|
|
8
|
+
*/
|
|
9
|
+
interface HashablePayloadV1 {
|
|
10
|
+
tool: string;
|
|
11
|
+
operation: string;
|
|
12
|
+
input: unknown;
|
|
13
|
+
metadata: Record<string, unknown> | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Canonicalize any JSON value.
|
|
17
|
+
*
|
|
18
|
+
* Rules (from spec/hashable-payload-v1.md):
|
|
19
|
+
* - null → "null"
|
|
20
|
+
* - primitives → JSON.stringify(value)
|
|
21
|
+
* - arrays → "[" + items.map(canonicalize).join(",") + "]"
|
|
22
|
+
* - objects: sort keys lexicographically, omit undefined values
|
|
23
|
+
*/
|
|
24
|
+
declare function canonicalize(obj: unknown): string;
|
|
25
|
+
/**
|
|
26
|
+
* Compute inputHash from HashablePayload v1.
|
|
27
|
+
*
|
|
28
|
+
* Algorithm (from spec/hashable-payload-v1.md):
|
|
29
|
+
* 1. Canonicalize the payload
|
|
30
|
+
* 2. SHA-256 hash (UTF-8 encoded)
|
|
31
|
+
* 3. Lowercase hex encoding
|
|
32
|
+
* 4. Prefix with "sha256:"
|
|
33
|
+
*/
|
|
34
|
+
declare function computeHash(payload: HashablePayloadV1): string;
|
|
35
|
+
/**
|
|
36
|
+
* Build HashablePayload v1 from SDK execute options.
|
|
37
|
+
*/
|
|
38
|
+
declare function buildHashablePayload(opts: {
|
|
39
|
+
tool: string;
|
|
40
|
+
operation: string;
|
|
41
|
+
input: unknown;
|
|
42
|
+
metadata?: Record<string, unknown>;
|
|
43
|
+
}): HashablePayloadV1;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Permission Protocol SDK Types
|
|
47
|
+
*
|
|
48
|
+
* Public types for the SDK. Internal types are not exported.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
interface ConfigureOptions {
|
|
52
|
+
/**
|
|
53
|
+
* The Permission Protocol API endpoint.
|
|
54
|
+
* Example: 'https://api.permissionprotocol.com'
|
|
55
|
+
*/
|
|
56
|
+
endpoint: string;
|
|
57
|
+
/**
|
|
58
|
+
* API key for authentication.
|
|
59
|
+
*/
|
|
60
|
+
apiKey: string;
|
|
61
|
+
/**
|
|
62
|
+
* Request timeout in milliseconds.
|
|
63
|
+
* Default: 10000 (10 seconds)
|
|
64
|
+
*/
|
|
65
|
+
timeoutMs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Protocol version to use.
|
|
68
|
+
* Default: 1
|
|
69
|
+
*/
|
|
70
|
+
protocolVersion?: number;
|
|
71
|
+
/**
|
|
72
|
+
* SDK version string (sent in X-PP-SDK-Version header).
|
|
73
|
+
* Default: package.json version
|
|
74
|
+
*/
|
|
75
|
+
sdkVersion?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Optional receipt verification hook.
|
|
78
|
+
* Called after receiving a receipt from hosted PP.
|
|
79
|
+
* Return false to reject the receipt (throws INVALID_RECEIPT).
|
|
80
|
+
* Default: no-op (always returns true)
|
|
81
|
+
*
|
|
82
|
+
* Use this hook for future signature verification.
|
|
83
|
+
*/
|
|
84
|
+
verifyReceipt?: (receipt: PermissionReceipt) => boolean | Promise<boolean>;
|
|
85
|
+
}
|
|
86
|
+
interface ExecuteOptions<TInput> {
|
|
87
|
+
/**
|
|
88
|
+
* Tenant/organization identifier.
|
|
89
|
+
*/
|
|
90
|
+
tenantId: string;
|
|
91
|
+
/**
|
|
92
|
+
* Agent identifier making the request.
|
|
93
|
+
*/
|
|
94
|
+
agentId: string;
|
|
95
|
+
/**
|
|
96
|
+
* Tool being invoked (e.g., 'wordpress', 'github', 'slack').
|
|
97
|
+
*/
|
|
98
|
+
tool: string;
|
|
99
|
+
/**
|
|
100
|
+
* Operation on the tool (e.g., 'publish_post', 'create_pr').
|
|
101
|
+
*/
|
|
102
|
+
operation: string;
|
|
103
|
+
/**
|
|
104
|
+
* Input payload for the operation.
|
|
105
|
+
* Can be any JSON-serializable value.
|
|
106
|
+
*/
|
|
107
|
+
input: TInput;
|
|
108
|
+
/**
|
|
109
|
+
* Optional metadata for risk assessment.
|
|
110
|
+
* Included in the input hash.
|
|
111
|
+
*/
|
|
112
|
+
metadata?: Record<string, unknown>;
|
|
113
|
+
/**
|
|
114
|
+
* Optional reversibility hint for risk assessment.
|
|
115
|
+
* Default: 'UNKNOWN' (no policy signal encoded by SDK)
|
|
116
|
+
*/
|
|
117
|
+
reversibility?: 'REVERSIBLE' | 'PARTIALLY_REVERSIBLE' | 'IRREVERSIBLE' | 'UNKNOWN';
|
|
118
|
+
/**
|
|
119
|
+
* Execution mode.
|
|
120
|
+
* - 'hosted': Connect to hosted Permission Protocol (default)
|
|
121
|
+
* - 'dev': Local development mode with auto-approval
|
|
122
|
+
*/
|
|
123
|
+
mode?: 'hosted' | 'dev';
|
|
124
|
+
}
|
|
125
|
+
type ExecuteResult<TResult> = {
|
|
126
|
+
status: 'APPROVED';
|
|
127
|
+
receipt: PermissionReceipt;
|
|
128
|
+
result?: TResult;
|
|
129
|
+
} | {
|
|
130
|
+
status: 'REQUIRES_APPROVAL';
|
|
131
|
+
receipt: PermissionReceipt;
|
|
132
|
+
} | {
|
|
133
|
+
status: 'DENIED';
|
|
134
|
+
receipt: PermissionReceipt;
|
|
135
|
+
};
|
|
136
|
+
interface PermissionReceipt {
|
|
137
|
+
/**
|
|
138
|
+
* Unique receipt identifier.
|
|
139
|
+
*/
|
|
140
|
+
receiptId: string;
|
|
141
|
+
/**
|
|
142
|
+
* Tenant/organization identifier.
|
|
143
|
+
*/
|
|
144
|
+
tenantId: string;
|
|
145
|
+
/**
|
|
146
|
+
* Agent that requested authorization.
|
|
147
|
+
*/
|
|
148
|
+
agentId: string;
|
|
149
|
+
/**
|
|
150
|
+
* Tool that was requested.
|
|
151
|
+
*/
|
|
152
|
+
tool: string;
|
|
153
|
+
/**
|
|
154
|
+
* Operation that was requested.
|
|
155
|
+
*/
|
|
156
|
+
operation: string;
|
|
157
|
+
/**
|
|
158
|
+
* SHA-256 hash of the canonical input payload.
|
|
159
|
+
* Format: 'sha256:<hex>'
|
|
160
|
+
*/
|
|
161
|
+
inputHash: string;
|
|
162
|
+
/**
|
|
163
|
+
* The decision from Permission Protocol.
|
|
164
|
+
*/
|
|
165
|
+
decision: 'APPROVED' | 'DENIED' | 'REQUIRES_APPROVAL';
|
|
166
|
+
/**
|
|
167
|
+
* Reason codes explaining the decision.
|
|
168
|
+
* May include: REQUIRES_FOUNDER_VETO, APPROVAL_EXPIRED, EXECUTION_ERROR, etc.
|
|
169
|
+
*/
|
|
170
|
+
reasonCodes: string[];
|
|
171
|
+
/**
|
|
172
|
+
* Who approved the action.
|
|
173
|
+
* - 'policy': Auto-approved by policy
|
|
174
|
+
* - 'human': Approved by a human
|
|
175
|
+
* - 'founder': Approved by founder
|
|
176
|
+
* - 'dev_mock': Dev mode auto-approval (NOT valid for production)
|
|
177
|
+
*/
|
|
178
|
+
approver: 'policy' | 'human' | 'founder' | 'dev_mock';
|
|
179
|
+
/**
|
|
180
|
+
* Policy version that made the decision (if available).
|
|
181
|
+
*/
|
|
182
|
+
policyVersion?: string;
|
|
183
|
+
/**
|
|
184
|
+
* When the receipt was created (ISO 8601).
|
|
185
|
+
*/
|
|
186
|
+
createdAt: string;
|
|
187
|
+
/**
|
|
188
|
+
* Receipt signature (future-proof placeholder).
|
|
189
|
+
* Used for verifying receipt authenticity.
|
|
190
|
+
*/
|
|
191
|
+
receiptSig?: string;
|
|
192
|
+
/**
|
|
193
|
+
* Signature algorithm used (future-proof placeholder).
|
|
194
|
+
*/
|
|
195
|
+
sigAlg?: 'ed25519' | 'hmac-sha256';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* PermissionRouter - Main SDK Entry Point
|
|
200
|
+
*
|
|
201
|
+
* The only two methods developers need:
|
|
202
|
+
* - PermissionRouter.configure(opts) - Configure the client
|
|
203
|
+
* - PermissionRouter.execute(opts) - Request authorization
|
|
204
|
+
*
|
|
205
|
+
* This SDK does NOT:
|
|
206
|
+
* - Make decisions
|
|
207
|
+
* - Execute tools
|
|
208
|
+
* - Store receipts
|
|
209
|
+
* - Implement policy logic
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Permission Protocol SDK
|
|
214
|
+
*
|
|
215
|
+
* Standardized authorization for AI agents.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* import { PermissionRouter } from '@permission-protocol/sdk';
|
|
220
|
+
*
|
|
221
|
+
* // Configure once
|
|
222
|
+
* PermissionRouter.configure({
|
|
223
|
+
* endpoint: 'https://api.permissionprotocol.com',
|
|
224
|
+
* apiKey: process.env.PP_API_KEY!,
|
|
225
|
+
* });
|
|
226
|
+
*
|
|
227
|
+
* // Request authorization
|
|
228
|
+
* const decision = await PermissionRouter.execute({
|
|
229
|
+
* tenantId: 'acme',
|
|
230
|
+
* agentId: 'seo-agent',
|
|
231
|
+
* tool: 'wordpress',
|
|
232
|
+
* operation: 'publish_post',
|
|
233
|
+
* input: postPayload,
|
|
234
|
+
* });
|
|
235
|
+
*
|
|
236
|
+
* if (decision.status !== 'APPROVED') {
|
|
237
|
+
* // Handle denial or pending approval
|
|
238
|
+
* process.exit(2);
|
|
239
|
+
* }
|
|
240
|
+
*
|
|
241
|
+
* // Tool execution happens elsewhere - SDK does NOT execute tools
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
declare const PermissionRouter: {
|
|
245
|
+
/**
|
|
246
|
+
* Configure the Permission Protocol client.
|
|
247
|
+
*
|
|
248
|
+
* Must be called before execute() in hosted mode.
|
|
249
|
+
*
|
|
250
|
+
* @param opts - Configuration options
|
|
251
|
+
*/
|
|
252
|
+
configure(opts: ConfigureOptions): void;
|
|
253
|
+
/**
|
|
254
|
+
* Request authorization for an action.
|
|
255
|
+
*
|
|
256
|
+
* Returns a decision + receipt. Does NOT execute the action.
|
|
257
|
+
*
|
|
258
|
+
* @param opts - Execute options
|
|
259
|
+
* @returns Promise resolving to decision (APPROVED, REQUIRES_APPROVAL, or DENIED) with receipt
|
|
260
|
+
* @throws PermissionProtocolError on network failure, timeout, or invalid response
|
|
261
|
+
*/
|
|
262
|
+
execute<TInput, TResult = unknown>(opts: ExecuteOptions<TInput>): Promise<ExecuteResult<TResult>>;
|
|
263
|
+
/**
|
|
264
|
+
* Check if the client is configured.
|
|
265
|
+
* Useful for testing and debugging.
|
|
266
|
+
*/
|
|
267
|
+
isConfigured(): boolean;
|
|
268
|
+
/**
|
|
269
|
+
* Reset configuration (for testing).
|
|
270
|
+
* @internal
|
|
271
|
+
*/
|
|
272
|
+
_reset(): void;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Permission Protocol SDK Errors
|
|
277
|
+
*
|
|
278
|
+
* Fail-closed error handling. No silent success.
|
|
279
|
+
*/
|
|
280
|
+
/**
|
|
281
|
+
* Error codes for Permission Protocol errors.
|
|
282
|
+
*/
|
|
283
|
+
type ErrorCode = 'EXECUTION_UNAUTHORIZED' | 'INVALID_RECEIPT' | 'MISCONFIGURED';
|
|
284
|
+
/**
|
|
285
|
+
* Error thrown by Permission Protocol SDK.
|
|
286
|
+
*
|
|
287
|
+
* The SDK fails closed on all errors - there is no silent success path.
|
|
288
|
+
*/
|
|
289
|
+
declare class PermissionProtocolError extends Error {
|
|
290
|
+
/**
|
|
291
|
+
* Error code for programmatic handling.
|
|
292
|
+
*/
|
|
293
|
+
readonly code: ErrorCode;
|
|
294
|
+
constructor(code: ErrorCode, message: string);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export { type ConfigureOptions, type ErrorCode, type ExecuteOptions, type ExecuteResult, type HashablePayloadV1, PermissionProtocolError, type PermissionReceipt, PermissionRouter, buildHashablePayload, canonicalize, computeHash };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HashablePayload v1 Implementation
|
|
3
|
+
* Conforms to spec/hashable-payload-v1.md
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* HashablePayload v1 - the canonical envelope for computing inputHash.
|
|
7
|
+
* Matches spec/hashable-payload-v1.md exactly.
|
|
8
|
+
*/
|
|
9
|
+
interface HashablePayloadV1 {
|
|
10
|
+
tool: string;
|
|
11
|
+
operation: string;
|
|
12
|
+
input: unknown;
|
|
13
|
+
metadata: Record<string, unknown> | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Canonicalize any JSON value.
|
|
17
|
+
*
|
|
18
|
+
* Rules (from spec/hashable-payload-v1.md):
|
|
19
|
+
* - null → "null"
|
|
20
|
+
* - primitives → JSON.stringify(value)
|
|
21
|
+
* - arrays → "[" + items.map(canonicalize).join(",") + "]"
|
|
22
|
+
* - objects: sort keys lexicographically, omit undefined values
|
|
23
|
+
*/
|
|
24
|
+
declare function canonicalize(obj: unknown): string;
|
|
25
|
+
/**
|
|
26
|
+
* Compute inputHash from HashablePayload v1.
|
|
27
|
+
*
|
|
28
|
+
* Algorithm (from spec/hashable-payload-v1.md):
|
|
29
|
+
* 1. Canonicalize the payload
|
|
30
|
+
* 2. SHA-256 hash (UTF-8 encoded)
|
|
31
|
+
* 3. Lowercase hex encoding
|
|
32
|
+
* 4. Prefix with "sha256:"
|
|
33
|
+
*/
|
|
34
|
+
declare function computeHash(payload: HashablePayloadV1): string;
|
|
35
|
+
/**
|
|
36
|
+
* Build HashablePayload v1 from SDK execute options.
|
|
37
|
+
*/
|
|
38
|
+
declare function buildHashablePayload(opts: {
|
|
39
|
+
tool: string;
|
|
40
|
+
operation: string;
|
|
41
|
+
input: unknown;
|
|
42
|
+
metadata?: Record<string, unknown>;
|
|
43
|
+
}): HashablePayloadV1;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Permission Protocol SDK Types
|
|
47
|
+
*
|
|
48
|
+
* Public types for the SDK. Internal types are not exported.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
interface ConfigureOptions {
|
|
52
|
+
/**
|
|
53
|
+
* The Permission Protocol API endpoint.
|
|
54
|
+
* Example: 'https://api.permissionprotocol.com'
|
|
55
|
+
*/
|
|
56
|
+
endpoint: string;
|
|
57
|
+
/**
|
|
58
|
+
* API key for authentication.
|
|
59
|
+
*/
|
|
60
|
+
apiKey: string;
|
|
61
|
+
/**
|
|
62
|
+
* Request timeout in milliseconds.
|
|
63
|
+
* Default: 10000 (10 seconds)
|
|
64
|
+
*/
|
|
65
|
+
timeoutMs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Protocol version to use.
|
|
68
|
+
* Default: 1
|
|
69
|
+
*/
|
|
70
|
+
protocolVersion?: number;
|
|
71
|
+
/**
|
|
72
|
+
* SDK version string (sent in X-PP-SDK-Version header).
|
|
73
|
+
* Default: package.json version
|
|
74
|
+
*/
|
|
75
|
+
sdkVersion?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Optional receipt verification hook.
|
|
78
|
+
* Called after receiving a receipt from hosted PP.
|
|
79
|
+
* Return false to reject the receipt (throws INVALID_RECEIPT).
|
|
80
|
+
* Default: no-op (always returns true)
|
|
81
|
+
*
|
|
82
|
+
* Use this hook for future signature verification.
|
|
83
|
+
*/
|
|
84
|
+
verifyReceipt?: (receipt: PermissionReceipt) => boolean | Promise<boolean>;
|
|
85
|
+
}
|
|
86
|
+
interface ExecuteOptions<TInput> {
|
|
87
|
+
/**
|
|
88
|
+
* Tenant/organization identifier.
|
|
89
|
+
*/
|
|
90
|
+
tenantId: string;
|
|
91
|
+
/**
|
|
92
|
+
* Agent identifier making the request.
|
|
93
|
+
*/
|
|
94
|
+
agentId: string;
|
|
95
|
+
/**
|
|
96
|
+
* Tool being invoked (e.g., 'wordpress', 'github', 'slack').
|
|
97
|
+
*/
|
|
98
|
+
tool: string;
|
|
99
|
+
/**
|
|
100
|
+
* Operation on the tool (e.g., 'publish_post', 'create_pr').
|
|
101
|
+
*/
|
|
102
|
+
operation: string;
|
|
103
|
+
/**
|
|
104
|
+
* Input payload for the operation.
|
|
105
|
+
* Can be any JSON-serializable value.
|
|
106
|
+
*/
|
|
107
|
+
input: TInput;
|
|
108
|
+
/**
|
|
109
|
+
* Optional metadata for risk assessment.
|
|
110
|
+
* Included in the input hash.
|
|
111
|
+
*/
|
|
112
|
+
metadata?: Record<string, unknown>;
|
|
113
|
+
/**
|
|
114
|
+
* Optional reversibility hint for risk assessment.
|
|
115
|
+
* Default: 'UNKNOWN' (no policy signal encoded by SDK)
|
|
116
|
+
*/
|
|
117
|
+
reversibility?: 'REVERSIBLE' | 'PARTIALLY_REVERSIBLE' | 'IRREVERSIBLE' | 'UNKNOWN';
|
|
118
|
+
/**
|
|
119
|
+
* Execution mode.
|
|
120
|
+
* - 'hosted': Connect to hosted Permission Protocol (default)
|
|
121
|
+
* - 'dev': Local development mode with auto-approval
|
|
122
|
+
*/
|
|
123
|
+
mode?: 'hosted' | 'dev';
|
|
124
|
+
}
|
|
125
|
+
type ExecuteResult<TResult> = {
|
|
126
|
+
status: 'APPROVED';
|
|
127
|
+
receipt: PermissionReceipt;
|
|
128
|
+
result?: TResult;
|
|
129
|
+
} | {
|
|
130
|
+
status: 'REQUIRES_APPROVAL';
|
|
131
|
+
receipt: PermissionReceipt;
|
|
132
|
+
} | {
|
|
133
|
+
status: 'DENIED';
|
|
134
|
+
receipt: PermissionReceipt;
|
|
135
|
+
};
|
|
136
|
+
interface PermissionReceipt {
|
|
137
|
+
/**
|
|
138
|
+
* Unique receipt identifier.
|
|
139
|
+
*/
|
|
140
|
+
receiptId: string;
|
|
141
|
+
/**
|
|
142
|
+
* Tenant/organization identifier.
|
|
143
|
+
*/
|
|
144
|
+
tenantId: string;
|
|
145
|
+
/**
|
|
146
|
+
* Agent that requested authorization.
|
|
147
|
+
*/
|
|
148
|
+
agentId: string;
|
|
149
|
+
/**
|
|
150
|
+
* Tool that was requested.
|
|
151
|
+
*/
|
|
152
|
+
tool: string;
|
|
153
|
+
/**
|
|
154
|
+
* Operation that was requested.
|
|
155
|
+
*/
|
|
156
|
+
operation: string;
|
|
157
|
+
/**
|
|
158
|
+
* SHA-256 hash of the canonical input payload.
|
|
159
|
+
* Format: 'sha256:<hex>'
|
|
160
|
+
*/
|
|
161
|
+
inputHash: string;
|
|
162
|
+
/**
|
|
163
|
+
* The decision from Permission Protocol.
|
|
164
|
+
*/
|
|
165
|
+
decision: 'APPROVED' | 'DENIED' | 'REQUIRES_APPROVAL';
|
|
166
|
+
/**
|
|
167
|
+
* Reason codes explaining the decision.
|
|
168
|
+
* May include: REQUIRES_FOUNDER_VETO, APPROVAL_EXPIRED, EXECUTION_ERROR, etc.
|
|
169
|
+
*/
|
|
170
|
+
reasonCodes: string[];
|
|
171
|
+
/**
|
|
172
|
+
* Who approved the action.
|
|
173
|
+
* - 'policy': Auto-approved by policy
|
|
174
|
+
* - 'human': Approved by a human
|
|
175
|
+
* - 'founder': Approved by founder
|
|
176
|
+
* - 'dev_mock': Dev mode auto-approval (NOT valid for production)
|
|
177
|
+
*/
|
|
178
|
+
approver: 'policy' | 'human' | 'founder' | 'dev_mock';
|
|
179
|
+
/**
|
|
180
|
+
* Policy version that made the decision (if available).
|
|
181
|
+
*/
|
|
182
|
+
policyVersion?: string;
|
|
183
|
+
/**
|
|
184
|
+
* When the receipt was created (ISO 8601).
|
|
185
|
+
*/
|
|
186
|
+
createdAt: string;
|
|
187
|
+
/**
|
|
188
|
+
* Receipt signature (future-proof placeholder).
|
|
189
|
+
* Used for verifying receipt authenticity.
|
|
190
|
+
*/
|
|
191
|
+
receiptSig?: string;
|
|
192
|
+
/**
|
|
193
|
+
* Signature algorithm used (future-proof placeholder).
|
|
194
|
+
*/
|
|
195
|
+
sigAlg?: 'ed25519' | 'hmac-sha256';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* PermissionRouter - Main SDK Entry Point
|
|
200
|
+
*
|
|
201
|
+
* The only two methods developers need:
|
|
202
|
+
* - PermissionRouter.configure(opts) - Configure the client
|
|
203
|
+
* - PermissionRouter.execute(opts) - Request authorization
|
|
204
|
+
*
|
|
205
|
+
* This SDK does NOT:
|
|
206
|
+
* - Make decisions
|
|
207
|
+
* - Execute tools
|
|
208
|
+
* - Store receipts
|
|
209
|
+
* - Implement policy logic
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Permission Protocol SDK
|
|
214
|
+
*
|
|
215
|
+
* Standardized authorization for AI agents.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* import { PermissionRouter } from '@permission-protocol/sdk';
|
|
220
|
+
*
|
|
221
|
+
* // Configure once
|
|
222
|
+
* PermissionRouter.configure({
|
|
223
|
+
* endpoint: 'https://api.permissionprotocol.com',
|
|
224
|
+
* apiKey: process.env.PP_API_KEY!,
|
|
225
|
+
* });
|
|
226
|
+
*
|
|
227
|
+
* // Request authorization
|
|
228
|
+
* const decision = await PermissionRouter.execute({
|
|
229
|
+
* tenantId: 'acme',
|
|
230
|
+
* agentId: 'seo-agent',
|
|
231
|
+
* tool: 'wordpress',
|
|
232
|
+
* operation: 'publish_post',
|
|
233
|
+
* input: postPayload,
|
|
234
|
+
* });
|
|
235
|
+
*
|
|
236
|
+
* if (decision.status !== 'APPROVED') {
|
|
237
|
+
* // Handle denial or pending approval
|
|
238
|
+
* process.exit(2);
|
|
239
|
+
* }
|
|
240
|
+
*
|
|
241
|
+
* // Tool execution happens elsewhere - SDK does NOT execute tools
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
declare const PermissionRouter: {
|
|
245
|
+
/**
|
|
246
|
+
* Configure the Permission Protocol client.
|
|
247
|
+
*
|
|
248
|
+
* Must be called before execute() in hosted mode.
|
|
249
|
+
*
|
|
250
|
+
* @param opts - Configuration options
|
|
251
|
+
*/
|
|
252
|
+
configure(opts: ConfigureOptions): void;
|
|
253
|
+
/**
|
|
254
|
+
* Request authorization for an action.
|
|
255
|
+
*
|
|
256
|
+
* Returns a decision + receipt. Does NOT execute the action.
|
|
257
|
+
*
|
|
258
|
+
* @param opts - Execute options
|
|
259
|
+
* @returns Promise resolving to decision (APPROVED, REQUIRES_APPROVAL, or DENIED) with receipt
|
|
260
|
+
* @throws PermissionProtocolError on network failure, timeout, or invalid response
|
|
261
|
+
*/
|
|
262
|
+
execute<TInput, TResult = unknown>(opts: ExecuteOptions<TInput>): Promise<ExecuteResult<TResult>>;
|
|
263
|
+
/**
|
|
264
|
+
* Check if the client is configured.
|
|
265
|
+
* Useful for testing and debugging.
|
|
266
|
+
*/
|
|
267
|
+
isConfigured(): boolean;
|
|
268
|
+
/**
|
|
269
|
+
* Reset configuration (for testing).
|
|
270
|
+
* @internal
|
|
271
|
+
*/
|
|
272
|
+
_reset(): void;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Permission Protocol SDK Errors
|
|
277
|
+
*
|
|
278
|
+
* Fail-closed error handling. No silent success.
|
|
279
|
+
*/
|
|
280
|
+
/**
|
|
281
|
+
* Error codes for Permission Protocol errors.
|
|
282
|
+
*/
|
|
283
|
+
type ErrorCode = 'EXECUTION_UNAUTHORIZED' | 'INVALID_RECEIPT' | 'MISCONFIGURED';
|
|
284
|
+
/**
|
|
285
|
+
* Error thrown by Permission Protocol SDK.
|
|
286
|
+
*
|
|
287
|
+
* The SDK fails closed on all errors - there is no silent success path.
|
|
288
|
+
*/
|
|
289
|
+
declare class PermissionProtocolError extends Error {
|
|
290
|
+
/**
|
|
291
|
+
* Error code for programmatic handling.
|
|
292
|
+
*/
|
|
293
|
+
readonly code: ErrorCode;
|
|
294
|
+
constructor(code: ErrorCode, message: string);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export { type ConfigureOptions, type ErrorCode, type ExecuteOptions, type ExecuteResult, type HashablePayloadV1, PermissionProtocolError, type PermissionReceipt, PermissionRouter, buildHashablePayload, canonicalize, computeHash };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
PermissionProtocolError: () => PermissionProtocolError,
|
|
24
|
+
PermissionRouter: () => PermissionRouter,
|
|
25
|
+
buildHashablePayload: () => buildHashablePayload,
|
|
26
|
+
canonicalize: () => canonicalize,
|
|
27
|
+
computeHash: () => computeHash
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/hash.ts
|
|
32
|
+
var import_crypto = require("crypto");
|
|
33
|
+
function canonicalize(obj) {
|
|
34
|
+
if (obj === null) {
|
|
35
|
+
return "null";
|
|
36
|
+
}
|
|
37
|
+
if (typeof obj !== "object") {
|
|
38
|
+
return JSON.stringify(obj);
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(obj)) {
|
|
41
|
+
return "[" + obj.map(canonicalize).join(",") + "]";
|
|
42
|
+
}
|
|
43
|
+
const record = obj;
|
|
44
|
+
const sorted = Object.keys(record).filter((k) => record[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(record[k])}`);
|
|
45
|
+
return "{" + sorted.join(",") + "}";
|
|
46
|
+
}
|
|
47
|
+
function computeHash(payload) {
|
|
48
|
+
const canonical = canonicalize(payload);
|
|
49
|
+
const hash = (0, import_crypto.createHash)("sha256").update(canonical, "utf8").digest("hex");
|
|
50
|
+
return `sha256:${hash}`;
|
|
51
|
+
}
|
|
52
|
+
function buildHashablePayload(opts) {
|
|
53
|
+
return {
|
|
54
|
+
tool: opts.tool,
|
|
55
|
+
operation: opts.operation,
|
|
56
|
+
input: opts.input,
|
|
57
|
+
metadata: opts.metadata ?? null
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/devApprovalMock.ts
|
|
62
|
+
function devApprovalMock(opts) {
|
|
63
|
+
console.warn(
|
|
64
|
+
'\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n\u2551 PERMISSION PROTOCOL - DEV MODE \u2551\n\u2551 \u2551\n\u2551 This approval is NOT valid for production. \u2551\n\u2551 Real enforcement requires hosted Permission Protocol. \u2551\n\u2551 \u2551\n\u2551 Set mode: "hosted" for production use. \u2551\n\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n'
|
|
65
|
+
);
|
|
66
|
+
const hashablePayload = buildHashablePayload(opts);
|
|
67
|
+
const inputHash = computeHash(hashablePayload);
|
|
68
|
+
const receipt = {
|
|
69
|
+
receiptId: `dev_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
|
|
70
|
+
tenantId: opts.tenantId,
|
|
71
|
+
agentId: opts.agentId,
|
|
72
|
+
tool: opts.tool,
|
|
73
|
+
operation: opts.operation,
|
|
74
|
+
inputHash,
|
|
75
|
+
decision: "APPROVED",
|
|
76
|
+
reasonCodes: ["DEV_MODE_APPROVAL"],
|
|
77
|
+
approver: "dev_mock",
|
|
78
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
status: "APPROVED",
|
|
82
|
+
receipt
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/errors.ts
|
|
87
|
+
var PermissionProtocolError = class _PermissionProtocolError extends Error {
|
|
88
|
+
/**
|
|
89
|
+
* Error code for programmatic handling.
|
|
90
|
+
*/
|
|
91
|
+
code;
|
|
92
|
+
constructor(code, message) {
|
|
93
|
+
super(`[PermissionProtocol:${code}] ${message}`);
|
|
94
|
+
this.name = "PermissionProtocolError";
|
|
95
|
+
this.code = code;
|
|
96
|
+
Object.setPrototypeOf(this, _PermissionProtocolError.prototype);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/mapping.ts
|
|
101
|
+
function mapHostedResponse(hosted, _opts) {
|
|
102
|
+
if (!hosted.receipt || !hosted.hostedStatus) {
|
|
103
|
+
throw new PermissionProtocolError(
|
|
104
|
+
"INVALID_RECEIPT",
|
|
105
|
+
"Hosted response missing required fields (receipt or hostedStatus)"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (!hosted.receipt.receiptId) {
|
|
109
|
+
throw new PermissionProtocolError(
|
|
110
|
+
"INVALID_RECEIPT",
|
|
111
|
+
"Hosted response receipt missing receiptId"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return mapStatus(hosted.hostedStatus, hosted.receipt, hosted.result);
|
|
115
|
+
}
|
|
116
|
+
function mapStatus(status, receipt, result) {
|
|
117
|
+
switch (status) {
|
|
118
|
+
// === APPROVED ===
|
|
119
|
+
case "APPROVED":
|
|
120
|
+
return {
|
|
121
|
+
status: "APPROVED",
|
|
122
|
+
receipt: { ...receipt, decision: "APPROVED" },
|
|
123
|
+
result
|
|
124
|
+
};
|
|
125
|
+
// === REQUIRES_APPROVAL (direct) ===
|
|
126
|
+
case "REQUIRES_APPROVAL":
|
|
127
|
+
return {
|
|
128
|
+
status: "REQUIRES_APPROVAL",
|
|
129
|
+
receipt: { ...receipt, decision: "REQUIRES_APPROVAL" }
|
|
130
|
+
};
|
|
131
|
+
// === REQUIRES_FOUNDER_VETO -> REQUIRES_APPROVAL + reason code ===
|
|
132
|
+
case "REQUIRES_FOUNDER_VETO":
|
|
133
|
+
return {
|
|
134
|
+
status: "REQUIRES_APPROVAL",
|
|
135
|
+
receipt: {
|
|
136
|
+
...receipt,
|
|
137
|
+
decision: "REQUIRES_APPROVAL",
|
|
138
|
+
reasonCodes: ensureReasonCode(receipt.reasonCodes, "REQUIRES_FOUNDER_VETO")
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
// === EXECUTING -> REQUIRES_APPROVAL + reason code ===
|
|
142
|
+
case "EXECUTING":
|
|
143
|
+
return {
|
|
144
|
+
status: "REQUIRES_APPROVAL",
|
|
145
|
+
receipt: {
|
|
146
|
+
...receipt,
|
|
147
|
+
decision: "REQUIRES_APPROVAL",
|
|
148
|
+
reasonCodes: ensureReasonCode(receipt.reasonCodes, "EXECUTING_PENDING_RESULT")
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
// === PENDING_DECISION -> REQUIRES_APPROVAL + reason code ===
|
|
152
|
+
case "PENDING_DECISION":
|
|
153
|
+
return {
|
|
154
|
+
status: "REQUIRES_APPROVAL",
|
|
155
|
+
receipt: {
|
|
156
|
+
...receipt,
|
|
157
|
+
decision: "REQUIRES_APPROVAL",
|
|
158
|
+
reasonCodes: ensureReasonCode(receipt.reasonCodes, "PENDING_DECISION")
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
// === DENIED (direct) ===
|
|
162
|
+
case "DENIED":
|
|
163
|
+
return {
|
|
164
|
+
status: "DENIED",
|
|
165
|
+
receipt: { ...receipt, decision: "DENIED" }
|
|
166
|
+
};
|
|
167
|
+
// === EXPIRED -> DENIED + reason code ===
|
|
168
|
+
case "EXPIRED":
|
|
169
|
+
return {
|
|
170
|
+
status: "DENIED",
|
|
171
|
+
receipt: {
|
|
172
|
+
...receipt,
|
|
173
|
+
decision: "DENIED",
|
|
174
|
+
reasonCodes: ensureReasonCode(receipt.reasonCodes, "APPROVAL_EXPIRED")
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
// === ERROR -> DENIED + reason code ===
|
|
178
|
+
case "ERROR":
|
|
179
|
+
return {
|
|
180
|
+
status: "DENIED",
|
|
181
|
+
receipt: {
|
|
182
|
+
...receipt,
|
|
183
|
+
decision: "DENIED",
|
|
184
|
+
reasonCodes: ensureReasonCode(receipt.reasonCodes, "EXECUTION_ERROR")
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
// === Unknown status -> fail closed ===
|
|
188
|
+
default:
|
|
189
|
+
throw new PermissionProtocolError(
|
|
190
|
+
"EXECUTION_UNAUTHORIZED",
|
|
191
|
+
`Unknown status from Permission Protocol: ${status}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function ensureReasonCode(codes, code) {
|
|
196
|
+
if (codes.includes(code)) {
|
|
197
|
+
return codes;
|
|
198
|
+
}
|
|
199
|
+
return [...codes, code];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/client.ts
|
|
203
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
204
|
+
var DEFAULT_SDK_VERSION = "0.1.0-alpha.1";
|
|
205
|
+
var DEFAULT_PROTOCOL_VERSION = 1;
|
|
206
|
+
async function fetchHostedPP(opts, body, headers) {
|
|
207
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
208
|
+
const sdkVersion = opts.sdkVersion ?? DEFAULT_SDK_VERSION;
|
|
209
|
+
const protocolVersion = opts.protocolVersion ?? DEFAULT_PROTOCOL_VERSION;
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
212
|
+
try {
|
|
213
|
+
const response = await fetch(`${opts.endpoint}/api/permission/v1/execute`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: {
|
|
216
|
+
"Content-Type": "application/json",
|
|
217
|
+
"Authorization": `Bearer ${opts.apiKey}`,
|
|
218
|
+
"X-PP-SDK-Version": sdkVersion,
|
|
219
|
+
"X-PP-Protocol-Version": String(protocolVersion),
|
|
220
|
+
"X-PP-Tenant-Id": headers.tenantId,
|
|
221
|
+
"X-PP-Agent-Id": headers.agentId
|
|
222
|
+
},
|
|
223
|
+
body: JSON.stringify(body),
|
|
224
|
+
signal: controller.signal
|
|
225
|
+
});
|
|
226
|
+
clearTimeout(timeoutId);
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
throw new PermissionProtocolError(
|
|
229
|
+
"EXECUTION_UNAUTHORIZED",
|
|
230
|
+
`Permission Protocol returned ${response.status}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
let hosted;
|
|
234
|
+
try {
|
|
235
|
+
hosted = await response.json();
|
|
236
|
+
} catch {
|
|
237
|
+
throw new PermissionProtocolError(
|
|
238
|
+
"EXECUTION_UNAUTHORIZED",
|
|
239
|
+
"Permission Protocol returned malformed response"
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return hosted;
|
|
243
|
+
} catch (err) {
|
|
244
|
+
clearTimeout(timeoutId);
|
|
245
|
+
if (err instanceof PermissionProtocolError) {
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
const error = err;
|
|
249
|
+
const message = error.name === "AbortError" ? "Permission Protocol request timed out" : "Permission Protocol unreachable";
|
|
250
|
+
throw new PermissionProtocolError("EXECUTION_UNAUTHORIZED", message);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/router.ts
|
|
255
|
+
var config = null;
|
|
256
|
+
var PermissionRouter = {
|
|
257
|
+
/**
|
|
258
|
+
* Configure the Permission Protocol client.
|
|
259
|
+
*
|
|
260
|
+
* Must be called before execute() in hosted mode.
|
|
261
|
+
*
|
|
262
|
+
* @param opts - Configuration options
|
|
263
|
+
*/
|
|
264
|
+
configure(opts) {
|
|
265
|
+
config = opts;
|
|
266
|
+
},
|
|
267
|
+
/**
|
|
268
|
+
* Request authorization for an action.
|
|
269
|
+
*
|
|
270
|
+
* Returns a decision + receipt. Does NOT execute the action.
|
|
271
|
+
*
|
|
272
|
+
* @param opts - Execute options
|
|
273
|
+
* @returns Promise resolving to decision (APPROVED, REQUIRES_APPROVAL, or DENIED) with receipt
|
|
274
|
+
* @throws PermissionProtocolError on network failure, timeout, or invalid response
|
|
275
|
+
*/
|
|
276
|
+
async execute(opts) {
|
|
277
|
+
if (opts.mode === "dev") {
|
|
278
|
+
return devApprovalMock(opts);
|
|
279
|
+
}
|
|
280
|
+
if (!config) {
|
|
281
|
+
throw new PermissionProtocolError(
|
|
282
|
+
"MISCONFIGURED",
|
|
283
|
+
"Permission Protocol client not configured. Call PermissionRouter.configure() first."
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const hashablePayload = buildHashablePayload(opts);
|
|
287
|
+
const inputHash = computeHash(hashablePayload);
|
|
288
|
+
const body = {
|
|
289
|
+
tenantId: opts.tenantId,
|
|
290
|
+
actor: { agentId: opts.agentId },
|
|
291
|
+
intent: {
|
|
292
|
+
name: `${opts.tool}:${opts.operation}`,
|
|
293
|
+
summary: `${opts.tool} ${opts.operation}`,
|
|
294
|
+
category: opts.tool
|
|
295
|
+
},
|
|
296
|
+
action: {
|
|
297
|
+
tool: opts.tool,
|
|
298
|
+
operation: opts.operation,
|
|
299
|
+
parameters: opts.input
|
|
300
|
+
},
|
|
301
|
+
context: {
|
|
302
|
+
environment: "production",
|
|
303
|
+
reversibility: opts.reversibility ?? "UNKNOWN",
|
|
304
|
+
metadata: opts.metadata
|
|
305
|
+
},
|
|
306
|
+
hashes: { inputHash }
|
|
307
|
+
};
|
|
308
|
+
const hosted = await fetchHostedPP(
|
|
309
|
+
{
|
|
310
|
+
endpoint: config.endpoint,
|
|
311
|
+
apiKey: config.apiKey,
|
|
312
|
+
timeoutMs: config.timeoutMs,
|
|
313
|
+
protocolVersion: config.protocolVersion,
|
|
314
|
+
sdkVersion: config.sdkVersion
|
|
315
|
+
},
|
|
316
|
+
body,
|
|
317
|
+
{
|
|
318
|
+
tenantId: opts.tenantId,
|
|
319
|
+
agentId: opts.agentId
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
const result = mapHostedResponse(hosted, opts);
|
|
323
|
+
if (config.verifyReceipt) {
|
|
324
|
+
const valid = await config.verifyReceipt(result.receipt);
|
|
325
|
+
if (!valid) {
|
|
326
|
+
throw new PermissionProtocolError(
|
|
327
|
+
"INVALID_RECEIPT",
|
|
328
|
+
"Receipt verification failed"
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
},
|
|
334
|
+
/**
|
|
335
|
+
* Check if the client is configured.
|
|
336
|
+
* Useful for testing and debugging.
|
|
337
|
+
*/
|
|
338
|
+
isConfigured() {
|
|
339
|
+
return config !== null;
|
|
340
|
+
},
|
|
341
|
+
/**
|
|
342
|
+
* Reset configuration (for testing).
|
|
343
|
+
* @internal
|
|
344
|
+
*/
|
|
345
|
+
_reset() {
|
|
346
|
+
config = null;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
350
|
+
0 && (module.exports = {
|
|
351
|
+
PermissionProtocolError,
|
|
352
|
+
PermissionRouter,
|
|
353
|
+
buildHashablePayload,
|
|
354
|
+
canonicalize,
|
|
355
|
+
computeHash
|
|
356
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// src/hash.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
function canonicalize(obj) {
|
|
4
|
+
if (obj === null) {
|
|
5
|
+
return "null";
|
|
6
|
+
}
|
|
7
|
+
if (typeof obj !== "object") {
|
|
8
|
+
return JSON.stringify(obj);
|
|
9
|
+
}
|
|
10
|
+
if (Array.isArray(obj)) {
|
|
11
|
+
return "[" + obj.map(canonicalize).join(",") + "]";
|
|
12
|
+
}
|
|
13
|
+
const record = obj;
|
|
14
|
+
const sorted = Object.keys(record).filter((k) => record[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(record[k])}`);
|
|
15
|
+
return "{" + sorted.join(",") + "}";
|
|
16
|
+
}
|
|
17
|
+
function computeHash(payload) {
|
|
18
|
+
const canonical = canonicalize(payload);
|
|
19
|
+
const hash = createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
20
|
+
return `sha256:${hash}`;
|
|
21
|
+
}
|
|
22
|
+
function buildHashablePayload(opts) {
|
|
23
|
+
return {
|
|
24
|
+
tool: opts.tool,
|
|
25
|
+
operation: opts.operation,
|
|
26
|
+
input: opts.input,
|
|
27
|
+
metadata: opts.metadata ?? null
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/devApprovalMock.ts
|
|
32
|
+
function devApprovalMock(opts) {
|
|
33
|
+
console.warn(
|
|
34
|
+
'\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n\u2551 PERMISSION PROTOCOL - DEV MODE \u2551\n\u2551 \u2551\n\u2551 This approval is NOT valid for production. \u2551\n\u2551 Real enforcement requires hosted Permission Protocol. \u2551\n\u2551 \u2551\n\u2551 Set mode: "hosted" for production use. \u2551\n\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n'
|
|
35
|
+
);
|
|
36
|
+
const hashablePayload = buildHashablePayload(opts);
|
|
37
|
+
const inputHash = computeHash(hashablePayload);
|
|
38
|
+
const receipt = {
|
|
39
|
+
receiptId: `dev_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
|
|
40
|
+
tenantId: opts.tenantId,
|
|
41
|
+
agentId: opts.agentId,
|
|
42
|
+
tool: opts.tool,
|
|
43
|
+
operation: opts.operation,
|
|
44
|
+
inputHash,
|
|
45
|
+
decision: "APPROVED",
|
|
46
|
+
reasonCodes: ["DEV_MODE_APPROVAL"],
|
|
47
|
+
approver: "dev_mock",
|
|
48
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
status: "APPROVED",
|
|
52
|
+
receipt
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/errors.ts
|
|
57
|
+
var PermissionProtocolError = class _PermissionProtocolError extends Error {
|
|
58
|
+
/**
|
|
59
|
+
* Error code for programmatic handling.
|
|
60
|
+
*/
|
|
61
|
+
code;
|
|
62
|
+
constructor(code, message) {
|
|
63
|
+
super(`[PermissionProtocol:${code}] ${message}`);
|
|
64
|
+
this.name = "PermissionProtocolError";
|
|
65
|
+
this.code = code;
|
|
66
|
+
Object.setPrototypeOf(this, _PermissionProtocolError.prototype);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/mapping.ts
|
|
71
|
+
function mapHostedResponse(hosted, _opts) {
|
|
72
|
+
if (!hosted.receipt || !hosted.hostedStatus) {
|
|
73
|
+
throw new PermissionProtocolError(
|
|
74
|
+
"INVALID_RECEIPT",
|
|
75
|
+
"Hosted response missing required fields (receipt or hostedStatus)"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (!hosted.receipt.receiptId) {
|
|
79
|
+
throw new PermissionProtocolError(
|
|
80
|
+
"INVALID_RECEIPT",
|
|
81
|
+
"Hosted response receipt missing receiptId"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return mapStatus(hosted.hostedStatus, hosted.receipt, hosted.result);
|
|
85
|
+
}
|
|
86
|
+
function mapStatus(status, receipt, result) {
|
|
87
|
+
switch (status) {
|
|
88
|
+
// === APPROVED ===
|
|
89
|
+
case "APPROVED":
|
|
90
|
+
return {
|
|
91
|
+
status: "APPROVED",
|
|
92
|
+
receipt: { ...receipt, decision: "APPROVED" },
|
|
93
|
+
result
|
|
94
|
+
};
|
|
95
|
+
// === REQUIRES_APPROVAL (direct) ===
|
|
96
|
+
case "REQUIRES_APPROVAL":
|
|
97
|
+
return {
|
|
98
|
+
status: "REQUIRES_APPROVAL",
|
|
99
|
+
receipt: { ...receipt, decision: "REQUIRES_APPROVAL" }
|
|
100
|
+
};
|
|
101
|
+
// === REQUIRES_FOUNDER_VETO -> REQUIRES_APPROVAL + reason code ===
|
|
102
|
+
case "REQUIRES_FOUNDER_VETO":
|
|
103
|
+
return {
|
|
104
|
+
status: "REQUIRES_APPROVAL",
|
|
105
|
+
receipt: {
|
|
106
|
+
...receipt,
|
|
107
|
+
decision: "REQUIRES_APPROVAL",
|
|
108
|
+
reasonCodes: ensureReasonCode(receipt.reasonCodes, "REQUIRES_FOUNDER_VETO")
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
// === EXECUTING -> REQUIRES_APPROVAL + reason code ===
|
|
112
|
+
case "EXECUTING":
|
|
113
|
+
return {
|
|
114
|
+
status: "REQUIRES_APPROVAL",
|
|
115
|
+
receipt: {
|
|
116
|
+
...receipt,
|
|
117
|
+
decision: "REQUIRES_APPROVAL",
|
|
118
|
+
reasonCodes: ensureReasonCode(receipt.reasonCodes, "EXECUTING_PENDING_RESULT")
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
// === PENDING_DECISION -> REQUIRES_APPROVAL + reason code ===
|
|
122
|
+
case "PENDING_DECISION":
|
|
123
|
+
return {
|
|
124
|
+
status: "REQUIRES_APPROVAL",
|
|
125
|
+
receipt: {
|
|
126
|
+
...receipt,
|
|
127
|
+
decision: "REQUIRES_APPROVAL",
|
|
128
|
+
reasonCodes: ensureReasonCode(receipt.reasonCodes, "PENDING_DECISION")
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
// === DENIED (direct) ===
|
|
132
|
+
case "DENIED":
|
|
133
|
+
return {
|
|
134
|
+
status: "DENIED",
|
|
135
|
+
receipt: { ...receipt, decision: "DENIED" }
|
|
136
|
+
};
|
|
137
|
+
// === EXPIRED -> DENIED + reason code ===
|
|
138
|
+
case "EXPIRED":
|
|
139
|
+
return {
|
|
140
|
+
status: "DENIED",
|
|
141
|
+
receipt: {
|
|
142
|
+
...receipt,
|
|
143
|
+
decision: "DENIED",
|
|
144
|
+
reasonCodes: ensureReasonCode(receipt.reasonCodes, "APPROVAL_EXPIRED")
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
// === ERROR -> DENIED + reason code ===
|
|
148
|
+
case "ERROR":
|
|
149
|
+
return {
|
|
150
|
+
status: "DENIED",
|
|
151
|
+
receipt: {
|
|
152
|
+
...receipt,
|
|
153
|
+
decision: "DENIED",
|
|
154
|
+
reasonCodes: ensureReasonCode(receipt.reasonCodes, "EXECUTION_ERROR")
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
// === Unknown status -> fail closed ===
|
|
158
|
+
default:
|
|
159
|
+
throw new PermissionProtocolError(
|
|
160
|
+
"EXECUTION_UNAUTHORIZED",
|
|
161
|
+
`Unknown status from Permission Protocol: ${status}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function ensureReasonCode(codes, code) {
|
|
166
|
+
if (codes.includes(code)) {
|
|
167
|
+
return codes;
|
|
168
|
+
}
|
|
169
|
+
return [...codes, code];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/client.ts
|
|
173
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
174
|
+
var DEFAULT_SDK_VERSION = "0.1.0-alpha.1";
|
|
175
|
+
var DEFAULT_PROTOCOL_VERSION = 1;
|
|
176
|
+
async function fetchHostedPP(opts, body, headers) {
|
|
177
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
178
|
+
const sdkVersion = opts.sdkVersion ?? DEFAULT_SDK_VERSION;
|
|
179
|
+
const protocolVersion = opts.protocolVersion ?? DEFAULT_PROTOCOL_VERSION;
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(`${opts.endpoint}/api/permission/v1/execute`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": "application/json",
|
|
187
|
+
"Authorization": `Bearer ${opts.apiKey}`,
|
|
188
|
+
"X-PP-SDK-Version": sdkVersion,
|
|
189
|
+
"X-PP-Protocol-Version": String(protocolVersion),
|
|
190
|
+
"X-PP-Tenant-Id": headers.tenantId,
|
|
191
|
+
"X-PP-Agent-Id": headers.agentId
|
|
192
|
+
},
|
|
193
|
+
body: JSON.stringify(body),
|
|
194
|
+
signal: controller.signal
|
|
195
|
+
});
|
|
196
|
+
clearTimeout(timeoutId);
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
throw new PermissionProtocolError(
|
|
199
|
+
"EXECUTION_UNAUTHORIZED",
|
|
200
|
+
`Permission Protocol returned ${response.status}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
let hosted;
|
|
204
|
+
try {
|
|
205
|
+
hosted = await response.json();
|
|
206
|
+
} catch {
|
|
207
|
+
throw new PermissionProtocolError(
|
|
208
|
+
"EXECUTION_UNAUTHORIZED",
|
|
209
|
+
"Permission Protocol returned malformed response"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return hosted;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
clearTimeout(timeoutId);
|
|
215
|
+
if (err instanceof PermissionProtocolError) {
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
const error = err;
|
|
219
|
+
const message = error.name === "AbortError" ? "Permission Protocol request timed out" : "Permission Protocol unreachable";
|
|
220
|
+
throw new PermissionProtocolError("EXECUTION_UNAUTHORIZED", message);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/router.ts
|
|
225
|
+
var config = null;
|
|
226
|
+
var PermissionRouter = {
|
|
227
|
+
/**
|
|
228
|
+
* Configure the Permission Protocol client.
|
|
229
|
+
*
|
|
230
|
+
* Must be called before execute() in hosted mode.
|
|
231
|
+
*
|
|
232
|
+
* @param opts - Configuration options
|
|
233
|
+
*/
|
|
234
|
+
configure(opts) {
|
|
235
|
+
config = opts;
|
|
236
|
+
},
|
|
237
|
+
/**
|
|
238
|
+
* Request authorization for an action.
|
|
239
|
+
*
|
|
240
|
+
* Returns a decision + receipt. Does NOT execute the action.
|
|
241
|
+
*
|
|
242
|
+
* @param opts - Execute options
|
|
243
|
+
* @returns Promise resolving to decision (APPROVED, REQUIRES_APPROVAL, or DENIED) with receipt
|
|
244
|
+
* @throws PermissionProtocolError on network failure, timeout, or invalid response
|
|
245
|
+
*/
|
|
246
|
+
async execute(opts) {
|
|
247
|
+
if (opts.mode === "dev") {
|
|
248
|
+
return devApprovalMock(opts);
|
|
249
|
+
}
|
|
250
|
+
if (!config) {
|
|
251
|
+
throw new PermissionProtocolError(
|
|
252
|
+
"MISCONFIGURED",
|
|
253
|
+
"Permission Protocol client not configured. Call PermissionRouter.configure() first."
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const hashablePayload = buildHashablePayload(opts);
|
|
257
|
+
const inputHash = computeHash(hashablePayload);
|
|
258
|
+
const body = {
|
|
259
|
+
tenantId: opts.tenantId,
|
|
260
|
+
actor: { agentId: opts.agentId },
|
|
261
|
+
intent: {
|
|
262
|
+
name: `${opts.tool}:${opts.operation}`,
|
|
263
|
+
summary: `${opts.tool} ${opts.operation}`,
|
|
264
|
+
category: opts.tool
|
|
265
|
+
},
|
|
266
|
+
action: {
|
|
267
|
+
tool: opts.tool,
|
|
268
|
+
operation: opts.operation,
|
|
269
|
+
parameters: opts.input
|
|
270
|
+
},
|
|
271
|
+
context: {
|
|
272
|
+
environment: "production",
|
|
273
|
+
reversibility: opts.reversibility ?? "UNKNOWN",
|
|
274
|
+
metadata: opts.metadata
|
|
275
|
+
},
|
|
276
|
+
hashes: { inputHash }
|
|
277
|
+
};
|
|
278
|
+
const hosted = await fetchHostedPP(
|
|
279
|
+
{
|
|
280
|
+
endpoint: config.endpoint,
|
|
281
|
+
apiKey: config.apiKey,
|
|
282
|
+
timeoutMs: config.timeoutMs,
|
|
283
|
+
protocolVersion: config.protocolVersion,
|
|
284
|
+
sdkVersion: config.sdkVersion
|
|
285
|
+
},
|
|
286
|
+
body,
|
|
287
|
+
{
|
|
288
|
+
tenantId: opts.tenantId,
|
|
289
|
+
agentId: opts.agentId
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
const result = mapHostedResponse(hosted, opts);
|
|
293
|
+
if (config.verifyReceipt) {
|
|
294
|
+
const valid = await config.verifyReceipt(result.receipt);
|
|
295
|
+
if (!valid) {
|
|
296
|
+
throw new PermissionProtocolError(
|
|
297
|
+
"INVALID_RECEIPT",
|
|
298
|
+
"Receipt verification failed"
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
},
|
|
304
|
+
/**
|
|
305
|
+
* Check if the client is configured.
|
|
306
|
+
* Useful for testing and debugging.
|
|
307
|
+
*/
|
|
308
|
+
isConfigured() {
|
|
309
|
+
return config !== null;
|
|
310
|
+
},
|
|
311
|
+
/**
|
|
312
|
+
* Reset configuration (for testing).
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
315
|
+
_reset() {
|
|
316
|
+
config = null;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
export {
|
|
320
|
+
PermissionProtocolError,
|
|
321
|
+
PermissionRouter,
|
|
322
|
+
buildHashablePayload,
|
|
323
|
+
canonicalize,
|
|
324
|
+
computeHash
|
|
325
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@permission-protocol/sdk",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Standardized authorization and receipts for autonomous AI actions",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"homepage": "https://github.com/permission-protocol/permission-protocol",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/permission-protocol/permission-protocol"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"ai",
|
|
26
|
+
"agents",
|
|
27
|
+
"authorization",
|
|
28
|
+
"receipts",
|
|
29
|
+
"audit",
|
|
30
|
+
"human-in-the-loop",
|
|
31
|
+
"ci",
|
|
32
|
+
"governance"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"test:golden": "vitest run -t 'HashablePayload v1 conformance'",
|
|
42
|
+
"lint": "eslint src --ext .ts",
|
|
43
|
+
"prepublishOnly": "npm run build"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.0.0",
|
|
47
|
+
"tsup": "^8.0.0",
|
|
48
|
+
"typescript": "^5.0.0",
|
|
49
|
+
"vitest": "^1.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|