commit-guard-protocol 0.1.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.
@@ -0,0 +1,301 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/next/index.ts
8
+ var next_exports = {};
9
+ __export(next_exports, {
10
+ createCommitGuardToolkit: () => createCommitGuardToolkit,
11
+ defineCommitGuardRoute: () => defineCommitGuardRoute,
12
+ zodSchema: () => zodSchema
13
+ });
14
+
15
+ // src/commit-context.ts
16
+ import { createHmac, timingSafeEqual } from "crypto";
17
+ function canonicalize(value) {
18
+ if (value === null) return null;
19
+ const t = typeof value;
20
+ if (t !== "object") return value;
21
+ if (Array.isArray(value)) return value.map(canonicalize);
22
+ const obj = value;
23
+ const keys = Object.keys(obj).sort();
24
+ const out = {};
25
+ for (const k of keys) {
26
+ const v = obj[k];
27
+ if (v !== void 0) out[k] = canonicalize(v);
28
+ }
29
+ return out;
30
+ }
31
+ function canonicalJson(value) {
32
+ return JSON.stringify(canonicalize(value));
33
+ }
34
+ function base64urlEncode(buf) {
35
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
36
+ }
37
+ function base64urlDecodeToBuffer(s) {
38
+ const padLen = (4 - s.length % 4) % 4;
39
+ const padded = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(padLen);
40
+ return Buffer.from(padded, "base64");
41
+ }
42
+ function hmacSha256(secret, data) {
43
+ return createHmac("sha256", secret).update(data).digest();
44
+ }
45
+ function signCommitContext(secret, ctx) {
46
+ const payload = canonicalJson(ctx);
47
+ const sig = base64urlEncode(hmacSha256(secret, payload));
48
+ return { ctx, sig };
49
+ }
50
+ function validateCommitContextShape(ctx) {
51
+ return ctx && typeof ctx === "object" && ctx.v === 1 && typeof ctx.action === "string" && "args" in ctx && typeof ctx.idempotency_key === "string" && typeof ctx.expires_at === "number";
52
+ }
53
+ function verifySignedCommitContext(params) {
54
+ const { secret, signed, expectedAction } = params;
55
+ const nowMs = params.nowMs ?? Date.now();
56
+ if (!signed || typeof signed !== "object") {
57
+ return {
58
+ ok: false,
59
+ error: { code: "MALFORMED_COMMIT", message: "Missing commit payload." }
60
+ };
61
+ }
62
+ const { ctx, sig } = signed;
63
+ if (!validateCommitContextShape(ctx) || typeof sig !== "string") {
64
+ return {
65
+ ok: false,
66
+ error: { code: "MALFORMED_COMMIT", message: "Invalid commit context shape." }
67
+ };
68
+ }
69
+ if (ctx.action !== expectedAction) {
70
+ return {
71
+ ok: false,
72
+ error: {
73
+ code: "ACTION_MISMATCH",
74
+ message: "Commit action mismatch.",
75
+ details: { expected: expectedAction, got: ctx.action }
76
+ }
77
+ };
78
+ }
79
+ if (nowMs > ctx.expires_at) {
80
+ return {
81
+ ok: false,
82
+ error: { code: "EXPIRED_COMMIT", message: "Commit context expired." }
83
+ };
84
+ }
85
+ const payload = canonicalJson(ctx);
86
+ const expectedSig = base64urlEncode(hmacSha256(secret, payload));
87
+ let a;
88
+ let b;
89
+ try {
90
+ a = base64urlDecodeToBuffer(sig);
91
+ b = base64urlDecodeToBuffer(expectedSig);
92
+ } catch {
93
+ return {
94
+ ok: false,
95
+ error: { code: "INVALID_SIGNATURE", message: "Invalid commit signature." }
96
+ };
97
+ }
98
+ if (a.length !== b.length) {
99
+ return {
100
+ ok: false,
101
+ error: { code: "INVALID_SIGNATURE", message: "Invalid commit signature." }
102
+ };
103
+ }
104
+ if (!timingSafeEqual(a, b)) {
105
+ return {
106
+ ok: false,
107
+ error: { code: "INVALID_SIGNATURE", message: "Invalid commit signature." }
108
+ };
109
+ }
110
+ return { ok: true, ctx };
111
+ }
112
+
113
+ // src/cgp.ts
114
+ function fail(error, debug) {
115
+ return { phase: "FAILED", error, debug };
116
+ }
117
+ async function commit(req, opts) {
118
+ const now = opts.nowMs ? opts.nowMs() : Date.now();
119
+ const verified = verifySignedCommitContext({
120
+ secret: opts.secret,
121
+ signed: req.commit,
122
+ expectedAction: opts.action,
123
+ nowMs: now
124
+ });
125
+ if (!verified.ok) return fail(verified.error);
126
+ let args;
127
+ try {
128
+ args = opts.schema.parse(verified.ctx.args);
129
+ } catch (e) {
130
+ return fail({
131
+ code: "SCHEMA_INVALID",
132
+ message: "Invalid args schema.",
133
+ details: { cause: String(e?.message ?? e) }
134
+ });
135
+ }
136
+ if (opts.authorize) {
137
+ const allowed = await opts.authorize({
138
+ action: opts.action,
139
+ args,
140
+ commit: {
141
+ idempotency_key: verified.ctx.idempotency_key,
142
+ expires_at: verified.ctx.expires_at
143
+ }
144
+ });
145
+ if (!allowed) {
146
+ return fail({
147
+ code: "FORBIDDEN",
148
+ message: "Commit not allowed."
149
+ });
150
+ }
151
+ }
152
+ const key = verified.ctx.idempotency_key;
153
+ let begin;
154
+ try {
155
+ begin = await opts.store.begin(key);
156
+ } catch (e) {
157
+ return fail({
158
+ code: "IDEMPOTENCY_STORE_ERROR",
159
+ message: "Idempotency store begin failed.",
160
+ details: { cause: String(e?.message ?? e) }
161
+ });
162
+ }
163
+ if (begin.status === "COMPLETED") {
164
+ return { phase: "COMPLETED", result: begin.value };
165
+ }
166
+ if (begin.status === "IN_PROGRESS") {
167
+ return fail({
168
+ code: "IDEMPOTENCY_CONFLICT",
169
+ message: "Commit already in progress."
170
+ });
171
+ }
172
+ try {
173
+ const result = await opts.execute(args, {
174
+ action: opts.action,
175
+ idempotencyKey: key,
176
+ commitContext: verified.ctx
177
+ });
178
+ try {
179
+ await opts.store.complete(key, result);
180
+ } catch (e) {
181
+ return fail({
182
+ code: "IDEMPOTENCY_STORE_ERROR",
183
+ message: "Idempotency store complete failed.",
184
+ details: { cause: String(e?.message ?? e) }
185
+ });
186
+ }
187
+ return { phase: "COMPLETED", result };
188
+ } catch (e) {
189
+ const err = {
190
+ code: "EXECUTION_FAILED",
191
+ message: "Commit execution failed.",
192
+ details: { cause: String(e?.message ?? e) }
193
+ };
194
+ try {
195
+ await opts.store.fail?.(key, err);
196
+ } catch {
197
+ }
198
+ return fail(err);
199
+ }
200
+ }
201
+
202
+ // src/idempotency/memory-store.ts
203
+ var MemoryIdempotencyStore = class {
204
+ map = /* @__PURE__ */ new Map();
205
+ async begin(key) {
206
+ const existing = this.map.get(key);
207
+ if (!existing) {
208
+ this.map.set(key, { status: "IN_PROGRESS" });
209
+ return { status: "NEW" };
210
+ }
211
+ if (existing.status === "COMPLETED") {
212
+ return { status: "COMPLETED", value: existing.value };
213
+ }
214
+ return { status: "IN_PROGRESS" };
215
+ }
216
+ async complete(key, value) {
217
+ this.map.set(key, { status: "COMPLETED", value });
218
+ }
219
+ async fail(key, error) {
220
+ this.map.set(key, { status: "FAILED", error });
221
+ }
222
+ };
223
+
224
+ // src/next/toolkit.ts
225
+ function statusFromResponse(res) {
226
+ if (res.phase === "COMPLETED") return 200;
227
+ const code = res.error?.code;
228
+ if (!code) return 400;
229
+ switch (code) {
230
+ case "FORBIDDEN":
231
+ return 403;
232
+ case "SCHEMA_INVALID":
233
+ case "MALFORMED_COMMIT":
234
+ case "ACTION_MISMATCH":
235
+ case "EXPIRED_COMMIT":
236
+ case "INVALID_SIGNATURE":
237
+ case "IDEMPOTENCY_CONFLICT":
238
+ return 400;
239
+ case "IDEMPOTENCY_STORE_ERROR":
240
+ case "EXECUTION_FAILED":
241
+ return 500;
242
+ default:
243
+ return 400;
244
+ }
245
+ }
246
+ function createCommitGuardToolkit(config) {
247
+ const store = config.store ?? new MemoryIdempotencyStore();
248
+ return {
249
+ define: (def) => {
250
+ return async function POST(request) {
251
+ let body;
252
+ try {
253
+ body = await request.json();
254
+ } catch {
255
+ const res2 = {
256
+ phase: "FAILED",
257
+ error: { code: "MALFORMED_COMMIT", message: "Invalid JSON body." }
258
+ };
259
+ return globalThis.Response.json(res2, { status: 400 });
260
+ }
261
+ const signed = body?.commit;
262
+ const action = def.tool;
263
+ const res = await commit(
264
+ { action, commit: signed },
265
+ {
266
+ action,
267
+ secret: config.secret,
268
+ schema: def.schema,
269
+ store,
270
+ execute: def.execute,
271
+ nowMs: config.nowMs,
272
+ authorize: def.authorize ? async ({ action: action2, args }) => def.authorize({ tool: action2, args, request }) : void 0
273
+ }
274
+ );
275
+ return globalThis.Response.json(res, { status: statusFromResponse(res) });
276
+ };
277
+ }
278
+ };
279
+ }
280
+ function zodSchema(zodLike) {
281
+ return { parse: zodLike.parse };
282
+ }
283
+
284
+ // src/next/defineCommitGuardRoute.ts
285
+ function defineCommitGuardRoute(handler) {
286
+ return handler;
287
+ }
288
+
289
+ export {
290
+ canonicalize,
291
+ canonicalJson,
292
+ signCommitContext,
293
+ validateCommitContextShape,
294
+ verifySignedCommitContext,
295
+ commit,
296
+ MemoryIdempotencyStore,
297
+ createCommitGuardToolkit,
298
+ zodSchema,
299
+ defineCommitGuardRoute,
300
+ next_exports
301
+ };
@@ -0,0 +1,230 @@
1
+ type CommitGuardPhase = "COLLECTING" | "CONFIRMING" | "COMMIT_READY" | "COMPLETED" | "FAILED";
2
+ /**
3
+ * Canonical action identifier.
4
+ * Examples:
5
+ * - "checkout.commit"
6
+ * - "wallet.withdraw.commit"
7
+ */
8
+ type CommitGuardAction = string;
9
+ /**
10
+ * A stable identifier for idempotent commit execution.
11
+ * Must be unique per logical side-effect.
12
+ */
13
+ type IdempotencyKey = string;
14
+ /** Unix epoch milliseconds */
15
+ type EpochMs = number;
16
+ /**
17
+ * Commit context envelope that crosses the commit boundary.
18
+ * This MUST remain minimal and stable.
19
+ */
20
+ interface CommitContext<A extends CommitGuardAction = CommitGuardAction, Args = unknown> {
21
+ /** Protocol version for forward compatibility. */
22
+ v: 1;
23
+ /** Action being committed (tool name / command). */
24
+ action: A;
25
+ /** Arguments to execute. Must match schema at commit time. */
26
+ args: Args;
27
+ /** Idempotency key to guarantee execute-exactly-once semantics. */
28
+ idempotency_key: IdempotencyKey;
29
+ /** Expiration timestamp (epoch ms). Reject if now > expires_at. */
30
+ expires_at: EpochMs;
31
+ /**
32
+ * Optional nonce to add uniqueness to the signed payload if desired.
33
+ * Not required for correctness (idempotency_key covers execution semantics).
34
+ */
35
+ nonce?: string;
36
+ /**
37
+ * Optional metadata for audit/tracing.
38
+ * Must not affect deterministic execution.
39
+ */
40
+ meta?: {
41
+ trace_id?: string;
42
+ actor_id?: string;
43
+ tags?: Record<string, string>;
44
+ };
45
+ }
46
+ /**
47
+ * Signed commit context. Signature is computed over a canonical representation of `ctx`.
48
+ */
49
+ interface SignedCommitContext<A extends CommitGuardAction = CommitGuardAction, Args = unknown> {
50
+ ctx: CommitContext<A, Args>;
51
+ sig: string;
52
+ }
53
+ /**
54
+ * Agent directive returned by Tx phase to instruct what to do next.
55
+ */
56
+ type AgentDirective = ExecuteDirective | ConfirmDirective;
57
+ /**
58
+ * Directive to execute a commit (Phase: COMMIT_READY).
59
+ */
60
+ interface ExecuteDirective<A extends CommitGuardAction = CommitGuardAction, Args = unknown> {
61
+ type: "EXECUTE";
62
+ tool: A;
63
+ idempotency_key: IdempotencyKey;
64
+ args: Args;
65
+ /** The signed commit context the server expects for the commit call. */
66
+ commit: SignedCommitContext<A, Args>;
67
+ }
68
+ /**
69
+ * Directive to request explicit confirmation from the user/system.
70
+ * Useful if the host wants a confirm step in Tx (still no side effects).
71
+ */
72
+ interface ConfirmDirective {
73
+ type: "CONFIRM";
74
+ message: string;
75
+ data?: Record<string, unknown>;
76
+ }
77
+ /**
78
+ * Structured Commit Guard response returned to the protocol caller.
79
+ */
80
+ interface CommitGuardResponse<T = unknown> {
81
+ phase: CommitGuardPhase;
82
+ /** Present on COMMIT_READY to instruct the agent what to do next. */
83
+ agent_directive?: AgentDirective;
84
+ /** Present on COMPLETED. */
85
+ result?: T;
86
+ /** Present on FAILED. */
87
+ error?: CommitGuardError;
88
+ /** Optional non-contractual debug payload (avoid leaking secrets). */
89
+ debug?: Record<string, unknown>;
90
+ }
91
+ /**
92
+ * Stable error model for commit boundary failures.
93
+ * Keep codes stable. Messages may evolve.
94
+ */
95
+ interface CommitGuardError {
96
+ code: CommitGuardErrorCode;
97
+ message: string;
98
+ details?: Record<string, unknown>;
99
+ }
100
+ type CommitGuardErrorCode = "INVALID_SIGNATURE" | "EXPIRED_COMMIT" | "MALFORMED_COMMIT" | "ACTION_MISMATCH" | "SCHEMA_INVALID" | "IDEMPOTENCY_CONFLICT" | "IDEMPOTENCY_STORE_ERROR" | "EXECUTION_FAILED" | "FORBIDDEN";
101
+ /**
102
+ * Input to the Commit endpoint/handler.
103
+ */
104
+ interface CommitRequest<A extends CommitGuardAction = CommitGuardAction, Args = unknown> {
105
+ action: A;
106
+ commit: SignedCommitContext<A, Args>;
107
+ }
108
+ /**
109
+ * Minimal schema abstraction to avoid framework coupling.
110
+ * Adapters may wrap Zod/Ajv/etc.
111
+ */
112
+ interface Schema<Args = unknown> {
113
+ parse(input: unknown): Args;
114
+ }
115
+ /**
116
+ * Idempotency store contract.
117
+ * Production requires an implementation with atomic begin semantics (e.g., Redis).
118
+ */
119
+ interface IdempotencyStore {
120
+ /**
121
+ * If key is new: atomically mark as IN_PROGRESS and return NEW.
122
+ * If already completed: return COMPLETED with stored value.
123
+ * If in progress: return IN_PROGRESS.
124
+ */
125
+ begin(key: IdempotencyKey): Promise<IdempotencyBegin>;
126
+ /** Persist final result for the given key. */
127
+ complete(key: IdempotencyKey, value: unknown): Promise<void>;
128
+ /** Optional: persist failure outcome for consistent retries. */
129
+ fail?(key: IdempotencyKey, error: CommitGuardError): Promise<void>;
130
+ }
131
+ type IdempotencyBegin = {
132
+ status: "NEW";
133
+ } | {
134
+ status: "COMPLETED";
135
+ value: unknown;
136
+ } | {
137
+ status: "IN_PROGRESS";
138
+ };
139
+
140
+ type CommitMeta<A extends CommitGuardAction> = {
141
+ action: A;
142
+ idempotencyKey: string;
143
+ commitContext: CommitContext<A, unknown>;
144
+ };
145
+ interface CommitGuardOptions<A extends CommitGuardAction, Args, Result> {
146
+ /** Expected action/tool name for this commit handler. */
147
+ action: A;
148
+ /** HMAC secret used to verify commit signature. */
149
+ secret: string;
150
+ /** Args schema validator/parser (throws on invalid). */
151
+ schema: Schema<Args>;
152
+ /** Idempotency store (Redis recommended for production). */
153
+ store: IdempotencyStore;
154
+ /**
155
+ * Deterministic side-effect execution.
156
+ * Must be idempotent with the provided idempotency_key.
157
+ */
158
+ execute: (args: Args, meta: CommitMeta<A>) => Promise<Result>;
159
+ /**
160
+ * Optional host policy hook (authz, allowlists, feature flags).
161
+ * Return false to reject with FORBIDDEN.
162
+ */
163
+ authorize?: (ctx: {
164
+ action: A;
165
+ args: Args;
166
+ commit: {
167
+ idempotency_key: string;
168
+ expires_at: number;
169
+ };
170
+ }) => Promise<boolean> | boolean;
171
+ /**
172
+ * Optional now provider for tests.
173
+ * Defaults to Date.now().
174
+ */
175
+ nowMs?: () => number;
176
+ }
177
+ declare function commit<A extends CommitGuardAction, Args, Result>(req: CommitRequest<A, unknown>, opts: CommitGuardOptions<A, Args, Result>): Promise<CommitGuardResponse<Result>>;
178
+
179
+ type CommitGuardNextToolkitConfig = {
180
+ /**
181
+ * HMAC signing secret used to verify signed commit contexts.
182
+ * In Next: process.env.COMMIT_GUARD_SECRET
183
+ */
184
+ secret: string;
185
+ /**
186
+ * Idempotency store.
187
+ * Default: Memory store (dev/test only).
188
+ * Production: pass a Redis-backed IdempotencyStore (recommended).
189
+ */
190
+ store?: IdempotencyStore;
191
+ /**
192
+ * Optional now provider (tests).
193
+ */
194
+ nowMs?: () => number;
195
+ };
196
+ type DefineCommitGuardHandler<A extends CommitGuardAction, Args, Result> = {
197
+ tool: A;
198
+ schema: Schema<Args>;
199
+ execute: (args: Args, meta: CommitMeta<A>) => Promise<Result>;
200
+ authorize?: (ctx: {
201
+ tool: A;
202
+ args: Args;
203
+ request: globalThis.Request;
204
+ }) => Promise<boolean> | boolean;
205
+ };
206
+ declare function createCommitGuardToolkit(config: CommitGuardNextToolkitConfig): {
207
+ define: <A extends CommitGuardAction, Args, Result>(def: DefineCommitGuardHandler<A, Args, Result>) => (request: globalThis.Request) => Promise<globalThis.Response>;
208
+ };
209
+ declare function zodSchema<Args>(zodLike: {
210
+ parse: (input: unknown) => Args;
211
+ }): Schema<Args>;
212
+
213
+ /**
214
+ * Minimal helper for Next.js App Router exports.
215
+ *
216
+ * Keeps the integration surface extremely small
217
+ * and avoids turning the protocol into a framework.
218
+ */
219
+ declare function defineCommitGuardRoute(handler: (request: Request) => Promise<Response>): (request: Request) => Promise<Response>;
220
+
221
+ type index_CommitGuardNextToolkitConfig = CommitGuardNextToolkitConfig;
222
+ type index_DefineCommitGuardHandler<A extends CommitGuardAction, Args, Result> = DefineCommitGuardHandler<A, Args, Result>;
223
+ declare const index_createCommitGuardToolkit: typeof createCommitGuardToolkit;
224
+ declare const index_defineCommitGuardRoute: typeof defineCommitGuardRoute;
225
+ declare const index_zodSchema: typeof zodSchema;
226
+ declare namespace index {
227
+ export { type index_CommitGuardNextToolkitConfig as CommitGuardNextToolkitConfig, type index_DefineCommitGuardHandler as DefineCommitGuardHandler, index_createCommitGuardToolkit as createCommitGuardToolkit, index_defineCommitGuardRoute as defineCommitGuardRoute, index_zodSchema as zodSchema };
228
+ }
229
+
230
+ export { type AgentDirective as A, type CommitGuardAction as C, type DefineCommitGuardHandler as D, type EpochMs as E, type IdempotencyStore as I, type SignedCommitContext as S, type CommitContext as a, type CommitGuardError as b, type IdempotencyKey as c, type IdempotencyBegin as d, type CommitMeta as e, type Schema as f, type CommitGuardResponse as g, type CommitGuardErrorCode as h, type CommitGuardOptions as i, type CommitGuardPhase as j, type CommitRequest as k, type ConfirmDirective as l, type ExecuteDirective as m, commit as n, index as o, type CommitGuardNextToolkitConfig as p, createCommitGuardToolkit as q, defineCommitGuardRoute as r, zodSchema as z };
@@ -0,0 +1,146 @@
1
+ import { C as CommitGuardAction, a as CommitContext, S as SignedCommitContext, b as CommitGuardError, I as IdempotencyStore, c as IdempotencyKey, d as IdempotencyBegin, e as CommitMeta, f as Schema, g as CommitGuardResponse } from './index-BjnJWUxR.js';
2
+ export { A as AgentDirective, h as CommitGuardErrorCode, i as CommitGuardOptions, j as CommitGuardPhase, k as CommitRequest, l as ConfirmDirective, E as EpochMs, m as ExecuteDirective, n as commit, o as next } from './index-BjnJWUxR.js';
3
+
4
+ /**
5
+ * Canonical JSON: stable key ordering + recursive canonicalization.
6
+ * - Removes undefined fields
7
+ * - Sorts object keys
8
+ * - Keeps array order
9
+ *
10
+ * NOTE: This is the main CPU cost in sign/verify.
11
+ * Keep commit_context reasonably small (args + meta).
12
+ */
13
+ declare function canonicalize(value: unknown): unknown;
14
+ declare function canonicalJson(value: unknown): string;
15
+ declare function signCommitContext<A extends CommitGuardAction, Args = unknown>(secret: string, ctx: CommitContext<A, Args>): SignedCommitContext<A, Args>;
16
+ declare function validateCommitContextShape(ctx: any): ctx is CommitContext<CommitGuardAction, unknown>;
17
+ /**
18
+ * Verify signature + expiration + action match.
19
+ *
20
+ * IMPORTANT:
21
+ * - Returns args as unknown (typed parsing happens later via schema.parse()).
22
+ * - Node-first implementation (uses node:crypto).
23
+ */
24
+ declare function verifySignedCommitContext<A extends CommitGuardAction>(params: {
25
+ secret: string;
26
+ signed: SignedCommitContext<A, unknown>;
27
+ expectedAction: A;
28
+ nowMs?: number;
29
+ }): {
30
+ ok: true;
31
+ ctx: CommitContext<A, unknown>;
32
+ } | {
33
+ ok: false;
34
+ error: CommitGuardError;
35
+ };
36
+
37
+ declare class MemoryIdempotencyStore implements IdempotencyStore {
38
+ private readonly map;
39
+ begin(key: IdempotencyKey): Promise<IdempotencyBegin>;
40
+ complete(key: IdempotencyKey, value: unknown): Promise<void>;
41
+ fail(key: IdempotencyKey, error: CommitGuardError): Promise<void>;
42
+ }
43
+
44
+ /**
45
+ * Minimal Redis client shape needed by this store.
46
+ * This avoids a hard dependency on the "redis" package (even for .d.ts build).
47
+ *
48
+ * Compatible with node-redis v4+ client:
49
+ * - client.get(key)
50
+ * - client.set(key, value, { NX/XX, EX })
51
+ */
52
+ type RedisLikeClient = {
53
+ get(key: string): Promise<string | null>;
54
+ set(key: string, value: string, opts?: {
55
+ NX?: boolean;
56
+ XX?: boolean;
57
+ EX?: number;
58
+ }): Promise<"OK" | null>;
59
+ };
60
+ type RedisIdempotencyStoreOptions = {
61
+ /**
62
+ * Prefix for keys stored in Redis.
63
+ * Default: "cgp:idemp:"
64
+ */
65
+ prefix?: string;
66
+ /**
67
+ * TTL in seconds for completed results.
68
+ * Keep it reasonably long to serve duplicate commits safely.
69
+ * Default: 24h
70
+ */
71
+ ttlSeconds?: number;
72
+ /**
73
+ * TTL in seconds for in-progress locks.
74
+ * Should be short to avoid dead locks.
75
+ * Default: 60s
76
+ */
77
+ inProgressTtlSeconds?: number;
78
+ };
79
+ declare class RedisIdempotencyStore implements IdempotencyStore {
80
+ private readonly client;
81
+ private readonly prefix;
82
+ private readonly ttlSeconds;
83
+ private readonly inProgressTtlSeconds;
84
+ constructor(client: RedisLikeClient, opts?: RedisIdempotencyStoreOptions);
85
+ begin(key: IdempotencyKey): Promise<IdempotencyBegin>;
86
+ complete(key: IdempotencyKey, value: unknown): Promise<void>;
87
+ fail(key: IdempotencyKey, error: CommitGuardError): Promise<void>;
88
+ }
89
+
90
+ type TxOptions = {
91
+ secret: string;
92
+ expiresInMs?: number;
93
+ nowMs?: () => number;
94
+ };
95
+ /**
96
+ * Tx service contract:
97
+ * - returns COMMIT_READY with signed commit payload
98
+ * - MUST NOT execute side effects
99
+ */
100
+ interface TxService<TxInput> {
101
+ tx(input: TxInput, opts: TxOptions): Promise<CommitGuardResponse>;
102
+ }
103
+ /**
104
+ * Commit executor contract:
105
+ * - executes irreversible side effects deterministically
106
+ * - must be idempotent w.r.t meta.idempotencyKey
107
+ */
108
+ interface CommitExecutor<A extends CommitGuardAction, Args, Result> {
109
+ execute(args: Args, meta: CommitMeta<A>): Promise<Result>;
110
+ }
111
+ /**
112
+ * Optional convenience type for adapters.
113
+ */
114
+ type CommitHandlerDefinition<A extends CommitGuardAction, Args, Result> = {
115
+ tool: A;
116
+ schema: Schema<Args>;
117
+ executor: CommitExecutor<A, Args, Result>;
118
+ };
119
+
120
+ type TxReadyInput<A extends CommitGuardAction, Args> = {
121
+ action: A;
122
+ args: Args;
123
+ secret: string;
124
+ idempotencyKey?: IdempotencyKey;
125
+ expiresInMs?: number;
126
+ nowMs?: number;
127
+ meta?: CommitContext["meta"];
128
+ nonce?: string;
129
+ };
130
+ /**
131
+ * Builds a COMMIT_READY response containing a signed commit payload.
132
+ * No side effects are performed here.
133
+ */
134
+ declare function txReady<A extends CommitGuardAction, Args>(input: TxReadyInput<A, Args>): CommitGuardResponse;
135
+
136
+ declare function defineCommitExecutor<A extends CommitGuardAction, Args, Result>(def: {
137
+ tool: A;
138
+ schema: Schema<Args>;
139
+ executor: CommitExecutor<A, Args, Result>;
140
+ }): {
141
+ tool: A;
142
+ schema: Schema<Args>;
143
+ execute: (args: Args, meta: CommitMeta<A>) => Promise<Result>;
144
+ };
145
+
146
+ export { CommitContext, type CommitExecutor, CommitGuardAction, CommitGuardError, CommitGuardResponse, type CommitHandlerDefinition, CommitMeta, IdempotencyBegin, IdempotencyKey, IdempotencyStore, MemoryIdempotencyStore, RedisIdempotencyStore, type RedisIdempotencyStoreOptions, type RedisLikeClient, Schema, SignedCommitContext, type TxOptions, type TxReadyInput, type TxService, canonicalJson, canonicalize, defineCommitExecutor, signCommitContext, txReady, validateCommitContextShape, verifySignedCommitContext };
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ import {
2
+ MemoryIdempotencyStore,
3
+ canonicalJson,
4
+ canonicalize,
5
+ commit,
6
+ next_exports,
7
+ signCommitContext,
8
+ validateCommitContextShape,
9
+ verifySignedCommitContext
10
+ } from "./chunk-WLDMTPVD.js";
11
+
12
+ // src/idempotency/redis-store.ts
13
+ function keyFor(prefix, key) {
14
+ return `${prefix}${key}`;
15
+ }
16
+ var RedisIdempotencyStore = class {
17
+ constructor(client, opts = {}) {
18
+ this.client = client;
19
+ this.prefix = opts.prefix ?? "cgp:idemp:";
20
+ this.ttlSeconds = opts.ttlSeconds ?? 60 * 60 * 24;
21
+ this.inProgressTtlSeconds = opts.inProgressTtlSeconds ?? 60;
22
+ }
23
+ prefix;
24
+ ttlSeconds;
25
+ inProgressTtlSeconds;
26
+ async begin(key) {
27
+ const redisKey = keyFor(this.prefix, key);
28
+ const existing = await this.client.get(redisKey);
29
+ if (existing) {
30
+ const parsed2 = JSON.parse(existing);
31
+ if (parsed2.status === "COMPLETED") {
32
+ return { status: "COMPLETED", value: parsed2.value };
33
+ }
34
+ return { status: "IN_PROGRESS" };
35
+ }
36
+ const lockValue = { status: "IN_PROGRESS" };
37
+ const setOk = await this.client.set(redisKey, JSON.stringify(lockValue), {
38
+ NX: true,
39
+ EX: this.inProgressTtlSeconds
40
+ });
41
+ if (setOk === "OK") return { status: "NEW" };
42
+ const after = await this.client.get(redisKey);
43
+ if (!after) return { status: "IN_PROGRESS" };
44
+ const parsed = JSON.parse(after);
45
+ if (parsed.status === "COMPLETED") {
46
+ return { status: "COMPLETED", value: parsed.value };
47
+ }
48
+ return { status: "IN_PROGRESS" };
49
+ }
50
+ async complete(key, value) {
51
+ const redisKey = keyFor(this.prefix, key);
52
+ const payload = { status: "COMPLETED", value };
53
+ await this.client.set(redisKey, JSON.stringify(payload), {
54
+ XX: true,
55
+ // only if key exists (must have been begin()'d)
56
+ EX: this.ttlSeconds
57
+ });
58
+ }
59
+ async fail(key, error) {
60
+ const redisKey = keyFor(this.prefix, key);
61
+ const payload = { status: "FAILED", error };
62
+ await this.client.set(redisKey, JSON.stringify(payload), {
63
+ XX: true,
64
+ EX: this.ttlSeconds
65
+ });
66
+ }
67
+ };
68
+
69
+ // src/helpers/txReady.ts
70
+ import { randomUUID } from "crypto";
71
+ function txReady(input) {
72
+ const now = input.nowMs ?? Date.now();
73
+ const expiresInMs = input.expiresInMs ?? 6e4;
74
+ const idempotencyKey = input.idempotencyKey ?? randomUUID();
75
+ const ctx = {
76
+ v: 1,
77
+ action: input.action,
78
+ args: input.args,
79
+ idempotency_key: idempotencyKey,
80
+ expires_at: now + expiresInMs,
81
+ meta: input.meta,
82
+ nonce: input.nonce
83
+ };
84
+ const signed = signCommitContext(input.secret, ctx);
85
+ const directive = {
86
+ type: "EXECUTE",
87
+ tool: input.action,
88
+ idempotency_key: idempotencyKey,
89
+ args: input.args,
90
+ commit: signed
91
+ };
92
+ return {
93
+ phase: "COMMIT_READY",
94
+ agent_directive: directive
95
+ };
96
+ }
97
+
98
+ // src/helpers/defineCommitExecutor.ts
99
+ function defineCommitExecutor(def) {
100
+ return {
101
+ tool: def.tool,
102
+ schema: def.schema,
103
+ execute: async (args, meta) => {
104
+ return def.executor.execute(args, meta);
105
+ }
106
+ };
107
+ }
108
+ export {
109
+ MemoryIdempotencyStore,
110
+ RedisIdempotencyStore,
111
+ canonicalJson,
112
+ canonicalize,
113
+ commit,
114
+ defineCommitExecutor,
115
+ next_exports as next,
116
+ signCommitContext,
117
+ txReady,
118
+ validateCommitContextShape,
119
+ verifySignedCommitContext
120
+ };
@@ -0,0 +1 @@
1
+ export { p as CommitGuardNextToolkitConfig, D as DefineCommitGuardHandler, q as createCommitGuardToolkit, r as defineCommitGuardRoute, z as zodSchema } from '../index-BjnJWUxR.js';
@@ -0,0 +1,10 @@
1
+ import {
2
+ createCommitGuardToolkit,
3
+ defineCommitGuardRoute,
4
+ zodSchema
5
+ } from "../chunk-WLDMTPVD.js";
6
+ export {
7
+ createCommitGuardToolkit,
8
+ defineCommitGuardRoute,
9
+ zodSchema
10
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "commit-guard-protocol",
3
+ "version": "0.1.0",
4
+ "description": "Commit Guard Protocol - a minimal deterministic commit boundary for irreversible actions.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./next": {
14
+ "import": "./dist/next/index.js",
15
+ "types": "./dist/next/index.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "sideEffects": false,
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "scripts": {
26
+ "build": "tsup src/index.ts src/next/index.ts --format esm --dts --clean",
27
+ "typecheck": "tsc -p tsconfig.json --noEmit",
28
+ "clean": "rm -rf dist"
29
+ },
30
+ "keywords": [
31
+ "agents",
32
+ "llm",
33
+ "commit",
34
+ "idempotency",
35
+ "protocol",
36
+ "hmac",
37
+ "deterministic",
38
+ "nextjs",
39
+ "production-safety"
40
+ ],
41
+ "license": "MIT",
42
+ "devDependencies": {
43
+ "tsup": "^8.0.0"
44
+ },
45
+ "peerDependencies": {
46
+ "redis": ">=5"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "redis": {
50
+ "optional": true
51
+ }
52
+ }
53
+ }