@authorbot/domain 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.
- package/LICENSE +21 -0
- package/dist/annotation-policy.d.ts +92 -0
- package/dist/annotation-policy.js +98 -0
- package/dist/annotation-state.d.ts +32 -0
- package/dist/annotation-state.js +58 -0
- package/dist/commands.d.ts +129 -0
- package/dist/commands.js +104 -0
- package/dist/decision-support.d.ts +33 -0
- package/dist/decision-support.js +29 -0
- package/dist/decision.d.ts +18 -0
- package/dist/decision.js +10 -0
- package/dist/git-operation-state.d.ts +44 -0
- package/dist/git-operation-state.js +65 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +16 -0
- package/dist/lease-token.d.ts +35 -0
- package/dist/lease-token.js +44 -0
- package/dist/lease.d.ts +127 -0
- package/dist/lease.js +183 -0
- package/dist/overrides.d.ts +77 -0
- package/dist/overrides.js +111 -0
- package/dist/scopes.d.ts +42 -0
- package/dist/scopes.js +71 -0
- package/dist/submission.d.ts +87 -0
- package/dist/submission.js +132 -0
- package/dist/token.d.ts +73 -0
- package/dist/token.js +102 -0
- package/dist/vote.d.ts +44 -0
- package/dist/vote.js +35 -0
- package/dist/work-item-lifecycle.d.ts +41 -0
- package/dist/work-item-lifecycle.js +75 -0
- package/dist/work-item-state.d.ts +28 -0
- package/dist/work-item-state.js +47 -0
- package/package.json +54 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Denied } from "./decision.js";
|
|
2
|
+
/**
|
|
3
|
+
* Git operation state machine (design section 20.2, Phase 2 contract
|
|
4
|
+
* section 5): `queued -> preparing -> committing -> committed -> verified`,
|
|
5
|
+
* failures -> `conflict | failed`, with bounded retries (3).
|
|
6
|
+
*/
|
|
7
|
+
export declare const GIT_OPERATION_STATES: readonly ["queued", "preparing", "committing", "committed", "verified", "conflict", "failed"];
|
|
8
|
+
export type GitOperationState = (typeof GIT_OPERATION_STATES)[number];
|
|
9
|
+
/** Maximum commit attempts per operation (contract section 5: bounded retries, 3). */
|
|
10
|
+
export declare const MAX_GIT_ATTEMPTS = 3;
|
|
11
|
+
/**
|
|
12
|
+
* Legal transitions. `conflict -> queued` is the (bounded) retry edge;
|
|
13
|
+
* `preparing`/`committing` may fail into `conflict` (stale expected head,
|
|
14
|
+
* non-fast-forward) or `failed` (non-retryable error, retries exhausted).
|
|
15
|
+
*/
|
|
16
|
+
export declare const GIT_OPERATION_TRANSITIONS: Readonly<Record<GitOperationState, readonly GitOperationState[]>>;
|
|
17
|
+
export declare function canTransitionGitOperation(from: GitOperationState, to: GitOperationState): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Attempt-accounted snapshot of an operation. `attempts` counts how many
|
|
20
|
+
* times the operation has entered `preparing` (i.e. commit attempts begun).
|
|
21
|
+
* A fresh operation starts `{ state: "queued", attempts: 0 }`.
|
|
22
|
+
*/
|
|
23
|
+
export interface GitOperationProgress {
|
|
24
|
+
readonly state: GitOperationState;
|
|
25
|
+
readonly attempts: number;
|
|
26
|
+
}
|
|
27
|
+
export declare const INITIAL_GIT_OPERATION: GitOperationProgress;
|
|
28
|
+
export type GitOperationDenialReason = "illegal-transition" | "retries-exhausted";
|
|
29
|
+
export type GitOperationTransitionResult = {
|
|
30
|
+
readonly allowed: true;
|
|
31
|
+
readonly next: GitOperationProgress;
|
|
32
|
+
} | Denied<GitOperationDenialReason>;
|
|
33
|
+
/**
|
|
34
|
+
* Apply a state change with bounded-retry accounting:
|
|
35
|
+
* - `queued -> preparing` increments `attempts` (an attempt begins).
|
|
36
|
+
* - `conflict -> queued` (retry) is denied with `retries-exhausted` once
|
|
37
|
+
* `attempts >= maxAttempts`; the only legal exit is then `failed`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function transitionGitOperation(current: GitOperationProgress, to: GitOperationState, maxAttempts?: number): GitOperationTransitionResult;
|
|
40
|
+
/** Whether a conflicted operation may still be retried under the bound. */
|
|
41
|
+
export declare function canRetryGitOperation(current: GitOperationProgress, maxAttempts?: number): boolean;
|
|
42
|
+
/** Terminal states: no outgoing transitions remain. */
|
|
43
|
+
export declare function isGitOperationTerminal(state: GitOperationState): boolean;
|
|
44
|
+
//# sourceMappingURL=git-operation-state.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { denied } from "./decision.js";
|
|
2
|
+
/**
|
|
3
|
+
* Git operation state machine (design section 20.2, Phase 2 contract
|
|
4
|
+
* section 5): `queued -> preparing -> committing -> committed -> verified`,
|
|
5
|
+
* failures -> `conflict | failed`, with bounded retries (3).
|
|
6
|
+
*/
|
|
7
|
+
export const GIT_OPERATION_STATES = [
|
|
8
|
+
"queued",
|
|
9
|
+
"preparing",
|
|
10
|
+
"committing",
|
|
11
|
+
"committed",
|
|
12
|
+
"verified",
|
|
13
|
+
"conflict",
|
|
14
|
+
"failed",
|
|
15
|
+
];
|
|
16
|
+
/** Maximum commit attempts per operation (contract section 5: bounded retries, 3). */
|
|
17
|
+
export const MAX_GIT_ATTEMPTS = 3;
|
|
18
|
+
/**
|
|
19
|
+
* Legal transitions. `conflict -> queued` is the (bounded) retry edge;
|
|
20
|
+
* `preparing`/`committing` may fail into `conflict` (stale expected head,
|
|
21
|
+
* non-fast-forward) or `failed` (non-retryable error, retries exhausted).
|
|
22
|
+
*/
|
|
23
|
+
export const GIT_OPERATION_TRANSITIONS = Object.freeze({
|
|
24
|
+
queued: ["preparing"],
|
|
25
|
+
preparing: ["committing", "conflict", "failed"],
|
|
26
|
+
committing: ["committed", "conflict", "failed"],
|
|
27
|
+
committed: ["verified"],
|
|
28
|
+
verified: [],
|
|
29
|
+
conflict: ["queued", "failed"],
|
|
30
|
+
failed: [],
|
|
31
|
+
});
|
|
32
|
+
export function canTransitionGitOperation(from, to) {
|
|
33
|
+
return GIT_OPERATION_TRANSITIONS[from].includes(to);
|
|
34
|
+
}
|
|
35
|
+
export const INITIAL_GIT_OPERATION = Object.freeze({
|
|
36
|
+
state: "queued",
|
|
37
|
+
attempts: 0,
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Apply a state change with bounded-retry accounting:
|
|
41
|
+
* - `queued -> preparing` increments `attempts` (an attempt begins).
|
|
42
|
+
* - `conflict -> queued` (retry) is denied with `retries-exhausted` once
|
|
43
|
+
* `attempts >= maxAttempts`; the only legal exit is then `failed`.
|
|
44
|
+
*/
|
|
45
|
+
export function transitionGitOperation(current, to, maxAttempts = MAX_GIT_ATTEMPTS) {
|
|
46
|
+
if (!canTransitionGitOperation(current.state, to)) {
|
|
47
|
+
return denied("illegal-transition", `git operation cannot change from "${current.state}" to "${to}"`);
|
|
48
|
+
}
|
|
49
|
+
if (current.state === "conflict" && to === "queued" && current.attempts >= maxAttempts) {
|
|
50
|
+
return denied("retries-exhausted", `git operation already used ${current.attempts} of ${maxAttempts} attempts; it must fail`);
|
|
51
|
+
}
|
|
52
|
+
const attempts = current.state === "queued" && to === "preparing"
|
|
53
|
+
? current.attempts + 1
|
|
54
|
+
: current.attempts;
|
|
55
|
+
return { allowed: true, next: { state: to, attempts } };
|
|
56
|
+
}
|
|
57
|
+
/** Whether a conflicted operation may still be retried under the bound. */
|
|
58
|
+
export function canRetryGitOperation(current, maxAttempts = MAX_GIT_ATTEMPTS) {
|
|
59
|
+
return current.state === "conflict" && current.attempts < maxAttempts;
|
|
60
|
+
}
|
|
61
|
+
/** Terminal states: no outgoing transitions remain. */
|
|
62
|
+
export function isGitOperationTerminal(state) {
|
|
63
|
+
return GIT_OPERATION_TRANSITIONS[state].length === 0;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=git-operation-state.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { ALLOWED, denied } from "./decision.js";
|
|
2
|
+
export { ANNOTATION_POLICIES, DEFAULT_ANNOTATION_POLICY, checkAnnotationPolicy, isAnnotationPolicy, policyRequiresApproval, } from "./annotation-policy.js";
|
|
3
|
+
export type { AnnotationPolicy, AnnotationPolicyDenialReason, AnnotationPolicyRequest, PolicyCapability, } from "./annotation-policy.js";
|
|
4
|
+
export type { Allowed, Decision, Denied } from "./decision.js";
|
|
5
|
+
export { ROLES, ROLE_SCOPES, SCOPES, effectiveScopes, requireScope, requireScopes, roleSchema, roleScopes, scopeSchema, } from "./scopes.js";
|
|
6
|
+
export type { Role, Scope, ScopeDenialReason } from "./scopes.js";
|
|
7
|
+
export { ANNOTATION_STATUSES, ANNOTATION_TRANSITIONS, authorizeAnnotationWithdraw, canTransitionAnnotation, transitionAnnotation, } from "./annotation-state.js";
|
|
8
|
+
export type { AnnotationStatus, AnnotationTransitionDenialReason, WithdrawDenialReason, } from "./annotation-state.js";
|
|
9
|
+
export { GIT_OPERATION_STATES, GIT_OPERATION_TRANSITIONS, INITIAL_GIT_OPERATION, MAX_GIT_ATTEMPTS, canRetryGitOperation, canTransitionGitOperation, isGitOperationTerminal, transitionGitOperation, } from "./git-operation-state.js";
|
|
10
|
+
export type { GitOperationDenialReason, GitOperationProgress, GitOperationState, GitOperationTransitionResult, } from "./git-operation-state.js";
|
|
11
|
+
export { MAX_BODY_BYTES, MAX_TOKEN_NAME_LENGTH, bodySchema, normalizeBody, createAnnotationCommandSchema, createReplyCommandSchema, mintAgentTokenCommandSchema, orderedRangeTargetSchema, utf8ByteLength, withdrawAnnotationCommandSchema, } from "./commands.js";
|
|
12
|
+
export type { CreateAnnotationCommand, CreateReplyCommand, MintAgentTokenCommand, MintAgentTokenCommandInput, WithdrawAnnotationCommand, } from "./commands.js";
|
|
13
|
+
export { VOTE_VALUES, authorizeVote, castVoteCommandSchema, clearVoteCommandSchema, voteValueSchema, } from "./vote.js";
|
|
14
|
+
export type { CastVoteCommand, ClearVoteCommand, VoteDenialReason, VoteValue } from "./vote.js";
|
|
15
|
+
export { DECISION_RESULTS, DECISION_SUPPORT_CHANGED_EVENT, decisionResultSchema, resolveSupportChange, } from "./decision-support.js";
|
|
16
|
+
export type { DecisionResult, SupportChangeOutcome, SupportChangeTransition, } from "./decision-support.js";
|
|
17
|
+
export { PHASE3_WORK_ITEM_STATUSES, WORK_ITEM_STATUSES, WORK_ITEM_TRANSITIONS, canTransitionWorkItem, isPhase3WorkItemStatus, transitionWorkItem, } from "./work-item-state.js";
|
|
18
|
+
export type { WorkItemStatus, WorkItemTransitionDenialReason, } from "./work-item-state.js";
|
|
19
|
+
export { FORCE_CREATE_RULE_VERSION, MAX_OVERRIDE_REASON_LENGTH, MIN_OVERRIDE_REASON_LENGTH, authorizeCancelWorkItem, authorizeForceCreateWorkItem, authorizeRejectSuggestion, authorizeReopenSuggestion, cancelWorkItemCommandSchema, forceCreateWorkItemCommandSchema, overrideReasonSchema, rejectSuggestionCommandSchema, reopenSuggestionCommandSchema, } from "./overrides.js";
|
|
20
|
+
export type { CancelWorkItemCommand, ForceCreateWorkItemCommand, RejectSuggestionCommand, ReopenSuggestionCommand, SuggestionOverrideDenialReason, WorkItemOverrideDenialReason, } from "./overrides.js";
|
|
21
|
+
export { AGENT_TOKEN_PREFIX, AGENT_TOKEN_REGEX, AGENT_TOKEN_SECRET_LENGTH, DEFAULT_TOKEN_TTL_DAYS, LAST_USED_UPDATE_INTERVAL_MS, MAX_TOKEN_TTL_DAYS, SESSION_ID_LENGTH, SESSION_ID_REGEX, SESSION_TTL_DAYS, agentTokenSchema, checkTokenActive, isAgentTokenFormat, isSessionIdFormat, parseAgentToken, resolveSessionExpiry, resolveTokenExpiry, sessionIdSchema, shouldUpdateLastUsed, toTimestamp, } from "./token.js";
|
|
22
|
+
export type { AgentTokenParseFailure, AgentTokenParseResult, TokenExpiryResult, TokenInactiveReason, } from "./token.js";
|
|
23
|
+
export { DEFAULT_LEASE_CONFIG, LEASE_DURATION_MS, LEASE_MAX_TOTAL_DURATION_MS, LEASE_RENEWAL_DURATION_MS, LEASE_RENEWAL_PROMPT_BEFORE_MS, checkLeaseActive, checkLeaseRenewable, checkWorkItemClaimable, isLeaseExpired, leaseConfigSchema, parseIsoDuration, renewalPromptAt, resolveLeaseExpiry, shouldExpireLease, } from "./lease.js";
|
|
24
|
+
export type { ClaimCheckResult, ClaimDenialReason, IsoDurationParseResult, LeaseConfig, LeaseConfigInput, LeaseInactiveReason, LeaseSnapshot, RenewCheckResult, RenewDenialReason, } from "./lease.js";
|
|
25
|
+
export { LEASE_TOKEN_PREFIX, LEASE_TOKEN_REGEX, LEASE_TOKEN_SECRET_LENGTH, isLeaseTokenFormat, leaseTokenSchema, parseLeaseToken, } from "./lease-token.js";
|
|
26
|
+
export type { LeaseTokenParseFailure, LeaseTokenParseResult, } from "./lease-token.js";
|
|
27
|
+
export { PHASE4_WORK_ITEM_STATUSES, WORK_ITEM_TRIGGERS, WORK_ITEM_TRIGGER_EDGES, applyWorkItemTrigger, isWorkItemTerminal, transitionWorkItemPhase4, } from "./work-item-lifecycle.js";
|
|
28
|
+
export type { WorkItemLifecycleDenialReason, WorkItemTrigger, WorkItemTriggerResult, } from "./work-item-lifecycle.js";
|
|
29
|
+
export { CONTENT_HASH_REGEX, MAX_SUBMISSION_CONTENT_BYTES, SUBMISSION_SCHEMA_IDS, SUBMISSION_TYPES, WORK_ITEM_SUBMISSION_TYPES, checkSubmissionBase, checkSubmissionTypeMatches, contentHashSchema, requiredSubmissionType, submissionTypeSchema, submitWorkCommandSchema, } from "./submission.js";
|
|
30
|
+
export type { SubmissionBaseDenialReason, SubmissionType, SubmissionTypeDenialReason, SubmitWorkCommand, } from "./submission.js";
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { ALLOWED, denied } from "./decision.js";
|
|
2
|
+
export { ANNOTATION_POLICIES, DEFAULT_ANNOTATION_POLICY, checkAnnotationPolicy, isAnnotationPolicy, policyRequiresApproval, } from "./annotation-policy.js";
|
|
3
|
+
export { ROLES, ROLE_SCOPES, SCOPES, effectiveScopes, requireScope, requireScopes, roleSchema, roleScopes, scopeSchema, } from "./scopes.js";
|
|
4
|
+
export { ANNOTATION_STATUSES, ANNOTATION_TRANSITIONS, authorizeAnnotationWithdraw, canTransitionAnnotation, transitionAnnotation, } from "./annotation-state.js";
|
|
5
|
+
export { GIT_OPERATION_STATES, GIT_OPERATION_TRANSITIONS, INITIAL_GIT_OPERATION, MAX_GIT_ATTEMPTS, canRetryGitOperation, canTransitionGitOperation, isGitOperationTerminal, transitionGitOperation, } from "./git-operation-state.js";
|
|
6
|
+
export { MAX_BODY_BYTES, MAX_TOKEN_NAME_LENGTH, bodySchema, normalizeBody, createAnnotationCommandSchema, createReplyCommandSchema, mintAgentTokenCommandSchema, orderedRangeTargetSchema, utf8ByteLength, withdrawAnnotationCommandSchema, } from "./commands.js";
|
|
7
|
+
export { VOTE_VALUES, authorizeVote, castVoteCommandSchema, clearVoteCommandSchema, voteValueSchema, } from "./vote.js";
|
|
8
|
+
export { DECISION_RESULTS, DECISION_SUPPORT_CHANGED_EVENT, decisionResultSchema, resolveSupportChange, } from "./decision-support.js";
|
|
9
|
+
export { PHASE3_WORK_ITEM_STATUSES, WORK_ITEM_STATUSES, WORK_ITEM_TRANSITIONS, canTransitionWorkItem, isPhase3WorkItemStatus, transitionWorkItem, } from "./work-item-state.js";
|
|
10
|
+
export { FORCE_CREATE_RULE_VERSION, MAX_OVERRIDE_REASON_LENGTH, MIN_OVERRIDE_REASON_LENGTH, authorizeCancelWorkItem, authorizeForceCreateWorkItem, authorizeRejectSuggestion, authorizeReopenSuggestion, cancelWorkItemCommandSchema, forceCreateWorkItemCommandSchema, overrideReasonSchema, rejectSuggestionCommandSchema, reopenSuggestionCommandSchema, } from "./overrides.js";
|
|
11
|
+
export { AGENT_TOKEN_PREFIX, AGENT_TOKEN_REGEX, AGENT_TOKEN_SECRET_LENGTH, DEFAULT_TOKEN_TTL_DAYS, LAST_USED_UPDATE_INTERVAL_MS, MAX_TOKEN_TTL_DAYS, SESSION_ID_LENGTH, SESSION_ID_REGEX, SESSION_TTL_DAYS, agentTokenSchema, checkTokenActive, isAgentTokenFormat, isSessionIdFormat, parseAgentToken, resolveSessionExpiry, resolveTokenExpiry, sessionIdSchema, shouldUpdateLastUsed, toTimestamp, } from "./token.js";
|
|
12
|
+
export { DEFAULT_LEASE_CONFIG, LEASE_DURATION_MS, LEASE_MAX_TOTAL_DURATION_MS, LEASE_RENEWAL_DURATION_MS, LEASE_RENEWAL_PROMPT_BEFORE_MS, checkLeaseActive, checkLeaseRenewable, checkWorkItemClaimable, isLeaseExpired, leaseConfigSchema, parseIsoDuration, renewalPromptAt, resolveLeaseExpiry, shouldExpireLease, } from "./lease.js";
|
|
13
|
+
export { LEASE_TOKEN_PREFIX, LEASE_TOKEN_REGEX, LEASE_TOKEN_SECRET_LENGTH, isLeaseTokenFormat, leaseTokenSchema, parseLeaseToken, } from "./lease-token.js";
|
|
14
|
+
export { PHASE4_WORK_ITEM_STATUSES, WORK_ITEM_TRIGGERS, WORK_ITEM_TRIGGER_EDGES, applyWorkItemTrigger, isWorkItemTerminal, transitionWorkItemPhase4, } from "./work-item-lifecycle.js";
|
|
15
|
+
export { CONTENT_HASH_REGEX, MAX_SUBMISSION_CONTENT_BYTES, SUBMISSION_SCHEMA_IDS, SUBMISSION_TYPES, WORK_ITEM_SUBMISSION_TYPES, checkSubmissionBase, checkSubmissionTypeMatches, contentHashSchema, requiredSubmissionType, submissionTypeSchema, submitWorkCommandSchema, } from "./submission.js";
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Lease-token FORMAT helpers only (Phase 4 contract section 2): parse and
|
|
4
|
+
* shape-check, no crypto. Generating the 256-bit random secret, hashing it
|
|
5
|
+
* (SHA-256), storing only the hash, and the constant-time compare are the
|
|
6
|
+
* API layer's job — exactly the split used for agent tokens in `token.ts`.
|
|
7
|
+
* Failure results carry a reason only and never echo any part of the value;
|
|
8
|
+
* lease tokens are never logged.
|
|
9
|
+
*
|
|
10
|
+
* Format (resolved ambiguity — the contracts pin "opaque 256-bit token" but
|
|
11
|
+
* no encoding): `authorbot_lease_` + 43 base64url chars, matching the agent
|
|
12
|
+
* token secret encoding. The distinct prefix makes a leaked lease token
|
|
13
|
+
* identifiable and unconfusable with an agent credential (an agent token is
|
|
14
|
+
* `authorbot_` + exactly 43 chars, so the two spaces are disjoint).
|
|
15
|
+
*/
|
|
16
|
+
export declare const LEASE_TOKEN_PREFIX = "authorbot_lease_";
|
|
17
|
+
export declare const LEASE_TOKEN_SECRET_LENGTH = 43;
|
|
18
|
+
export declare const LEASE_TOKEN_REGEX: RegExp;
|
|
19
|
+
export declare const leaseTokenSchema: z.ZodString;
|
|
20
|
+
export declare function isLeaseTokenFormat(value: string): boolean;
|
|
21
|
+
export type LeaseTokenParseFailure = "bad-prefix" | "bad-length" | "bad-charset";
|
|
22
|
+
export type LeaseTokenParseResult = {
|
|
23
|
+
readonly ok: true;
|
|
24
|
+
readonly secret: string;
|
|
25
|
+
} | {
|
|
26
|
+
readonly ok: false;
|
|
27
|
+
readonly reason: LeaseTokenParseFailure;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Shape-check a presented lease token and split off the secret part (which
|
|
31
|
+
* the API layer hashes for the constant-time comparison against the stored
|
|
32
|
+
* hash). Failures never contain any fragment of the presented value.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseLeaseToken(value: string): LeaseTokenParseResult;
|
|
35
|
+
//# sourceMappingURL=lease-token.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Lease-token FORMAT helpers only (Phase 4 contract section 2): parse and
|
|
4
|
+
* shape-check, no crypto. Generating the 256-bit random secret, hashing it
|
|
5
|
+
* (SHA-256), storing only the hash, and the constant-time compare are the
|
|
6
|
+
* API layer's job — exactly the split used for agent tokens in `token.ts`.
|
|
7
|
+
* Failure results carry a reason only and never echo any part of the value;
|
|
8
|
+
* lease tokens are never logged.
|
|
9
|
+
*
|
|
10
|
+
* Format (resolved ambiguity — the contracts pin "opaque 256-bit token" but
|
|
11
|
+
* no encoding): `authorbot_lease_` + 43 base64url chars, matching the agent
|
|
12
|
+
* token secret encoding. The distinct prefix makes a leaked lease token
|
|
13
|
+
* identifiable and unconfusable with an agent credential (an agent token is
|
|
14
|
+
* `authorbot_` + exactly 43 chars, so the two spaces are disjoint).
|
|
15
|
+
*/
|
|
16
|
+
export const LEASE_TOKEN_PREFIX = "authorbot_lease_";
|
|
17
|
+
export const LEASE_TOKEN_SECRET_LENGTH = 43;
|
|
18
|
+
const BASE64URL_CHAR = /^[A-Za-z0-9_-]+$/;
|
|
19
|
+
export const LEASE_TOKEN_REGEX = /^authorbot_lease_[A-Za-z0-9_-]{43}$/;
|
|
20
|
+
export const leaseTokenSchema = z
|
|
21
|
+
.string()
|
|
22
|
+
.regex(LEASE_TOKEN_REGEX, "must be 'authorbot_lease_' followed by 43 base64url characters");
|
|
23
|
+
export function isLeaseTokenFormat(value) {
|
|
24
|
+
return LEASE_TOKEN_REGEX.test(value);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Shape-check a presented lease token and split off the secret part (which
|
|
28
|
+
* the API layer hashes for the constant-time comparison against the stored
|
|
29
|
+
* hash). Failures never contain any fragment of the presented value.
|
|
30
|
+
*/
|
|
31
|
+
export function parseLeaseToken(value) {
|
|
32
|
+
if (!value.startsWith(LEASE_TOKEN_PREFIX)) {
|
|
33
|
+
return { ok: false, reason: "bad-prefix" };
|
|
34
|
+
}
|
|
35
|
+
const secret = value.slice(LEASE_TOKEN_PREFIX.length);
|
|
36
|
+
if (secret.length !== LEASE_TOKEN_SECRET_LENGTH) {
|
|
37
|
+
return { ok: false, reason: "bad-length" };
|
|
38
|
+
}
|
|
39
|
+
if (!BASE64URL_CHAR.test(secret)) {
|
|
40
|
+
return { ok: false, reason: "bad-charset" };
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, secret };
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=lease-token.js.map
|
package/dist/lease.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type WorkItemStatus } from "@authorbot/schemas";
|
|
3
|
+
import { type Decision, type Denied } from "./decision.js";
|
|
4
|
+
/** Design section 25 defaults: duration PT30M. */
|
|
5
|
+
export declare const LEASE_DURATION_MS: number;
|
|
6
|
+
/** Design section 25 defaults: renewal_duration PT30M. */
|
|
7
|
+
export declare const LEASE_RENEWAL_DURATION_MS: number;
|
|
8
|
+
/** Design section 25 defaults: maximum_total_duration PT4H. */
|
|
9
|
+
export declare const LEASE_MAX_TOTAL_DURATION_MS: number;
|
|
10
|
+
/** Design section 25 defaults: renewal_prompt_before PT5M (UI concern). */
|
|
11
|
+
export declare const LEASE_RENEWAL_PROMPT_BEFORE_MS: number;
|
|
12
|
+
export type IsoDurationParseResult = {
|
|
13
|
+
readonly ok: true;
|
|
14
|
+
readonly ms: number;
|
|
15
|
+
} | {
|
|
16
|
+
readonly ok: false;
|
|
17
|
+
readonly reason: "bad-format" | "zero-duration";
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Parse the ISO-8601 duration subset the contracts use (`PT30M`, `PT4H`,
|
|
21
|
+
* `P1D`, combinations with integer designators; no years/months/weeks —
|
|
22
|
+
* calendar units are ambiguous in milliseconds). Used to validate `LEASE_*`
|
|
23
|
+
* env overrides at boot (Phase 4 contract section 2).
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseIsoDuration(value: string): IsoDurationParseResult;
|
|
26
|
+
/**
|
|
27
|
+
* Lease timing configuration in milliseconds, with design section 25
|
|
28
|
+
* defaults. Boot-time validation of env overrides (contract section 2):
|
|
29
|
+
* every duration positive, renewal prompt strictly inside the duration, and
|
|
30
|
+
* the initial duration within the max total (otherwise a lease would be born
|
|
31
|
+
* beyond its own cap).
|
|
32
|
+
*/
|
|
33
|
+
export declare const leaseConfigSchema: z.ZodObject<{
|
|
34
|
+
durationMs: z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
renewalDurationMs: z.ZodDefault<z.ZodNumber>;
|
|
36
|
+
maxTotalDurationMs: z.ZodDefault<z.ZodNumber>;
|
|
37
|
+
renewalPromptBeforeMs: z.ZodDefault<z.ZodNumber>;
|
|
38
|
+
}, z.core.$strict>;
|
|
39
|
+
export type LeaseConfig = z.infer<typeof leaseConfigSchema>;
|
|
40
|
+
export type LeaseConfigInput = z.input<typeof leaseConfigSchema>;
|
|
41
|
+
/** The design section 25 defaults as a parsed config. */
|
|
42
|
+
export declare const DEFAULT_LEASE_CONFIG: LeaseConfig;
|
|
43
|
+
/**
|
|
44
|
+
* The clock-relevant columns of a `leases` row (contract section 2). The
|
|
45
|
+
* token hash deliberately does not appear: comparing presented tokens is the
|
|
46
|
+
* API layer's constant-time job.
|
|
47
|
+
*/
|
|
48
|
+
export interface LeaseSnapshot {
|
|
49
|
+
readonly expiresAt: string;
|
|
50
|
+
readonly maxExpiresAt: string;
|
|
51
|
+
readonly releasedAt?: string | null;
|
|
52
|
+
readonly revokedAt?: string | null;
|
|
53
|
+
}
|
|
54
|
+
/** Timestamps for a freshly issued lease (claim step; design section 12.2). */
|
|
55
|
+
export declare function resolveLeaseExpiry(now: Date, config?: LeaseConfig): {
|
|
56
|
+
expiresAt: string;
|
|
57
|
+
maxExpiresAt: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Expiry is inclusive at exactly `expires_at` (matching `checkTokenActive`):
|
|
61
|
+
* no submission is accepted merely because a countdown still shows 0:00.
|
|
62
|
+
*/
|
|
63
|
+
export declare function isLeaseExpired(lease: LeaseSnapshot, now: Date): boolean;
|
|
64
|
+
export type LeaseInactiveReason = "expired" | "released" | "revoked";
|
|
65
|
+
/**
|
|
66
|
+
* Whether a lease still backs commands at `now`. Check order follows the
|
|
67
|
+
* contract section 4 verification order ("not expired / not released"):
|
|
68
|
+
* expired first, then released, then revoked. Used by renew, release, and
|
|
69
|
+
* submission checks alike — an expired, released, or revoked lease can do
|
|
70
|
+
* nothing further (contract section 2: renewing an expired lease is a 409).
|
|
71
|
+
*/
|
|
72
|
+
export declare function checkLeaseActive(lease: LeaseSnapshot, now: Date): Decision<LeaseInactiveReason>;
|
|
73
|
+
export type ClaimDenialReason = "not-claimable" | "lease-held";
|
|
74
|
+
export type ClaimCheckResult = {
|
|
75
|
+
readonly allowed: true;
|
|
76
|
+
/**
|
|
77
|
+
* True when the item is still `leased` but its lease is no longer
|
|
78
|
+
* active: the claim command must expire the stale lease in the same
|
|
79
|
+
* serialized batch before issuing the new one (contract section 2).
|
|
80
|
+
*/
|
|
81
|
+
readonly priorLeaseExpired: boolean;
|
|
82
|
+
} | Denied<ClaimDenialReason>;
|
|
83
|
+
/**
|
|
84
|
+
* Claimability (design section 12.2 step 1, contract section 2): the item is
|
|
85
|
+
* `ready`, or it is `leased` and no LIVE lease occupies its slot. Only a
|
|
86
|
+
* `leased` item whose recorded active lease is still live denies with
|
|
87
|
+
* `lease-held` (the 409 the losing claimant sees; holder-safe messaging is
|
|
88
|
+
* the API's concern). Scope (`work:claim`) and per-type capability checks are
|
|
89
|
+
* separate (`requireScope`, `requiredSubmissionType`).
|
|
90
|
+
*
|
|
91
|
+
* A `leased` item with NO active lease row at all is claimable, not held: the
|
|
92
|
+
* slot is provably free (the unique index admits exactly one active lease, so
|
|
93
|
+
* "none" is unambiguous). That combination is what an interrupted expiry or a
|
|
94
|
+
* bare administrative revocation leaves behind, and treating it as
|
|
95
|
+
* `lease-held` made such items permanently unclaimable, unreleasable, and
|
|
96
|
+
* unsweepable — recoverable only by direct database surgery. Claiming
|
|
97
|
+
* self-heals it instead: `priorLeaseExpired` sends the caller down the
|
|
98
|
+
* expire-then-claim path, whose compare-and-swap starts from `leased`.
|
|
99
|
+
*/
|
|
100
|
+
export declare function checkWorkItemClaimable(status: WorkItemStatus, activeLease: LeaseSnapshot | null, now: Date): ClaimCheckResult;
|
|
101
|
+
export type RenewDenialReason = LeaseInactiveReason | "max-total-exceeded";
|
|
102
|
+
export type RenewCheckResult = {
|
|
103
|
+
readonly allowed: true;
|
|
104
|
+
readonly expiresAt: string;
|
|
105
|
+
} | Denied<RenewDenialReason>;
|
|
106
|
+
/**
|
|
107
|
+
* Renewal (design section 12.3, contract section 2): only an active lease
|
|
108
|
+
* renews; the new expiry is the current `expires_at` plus the renewal
|
|
109
|
+
* duration ("extends by"), clamped to `max_expires_at`. When the lease
|
|
110
|
+
* already sits at its max-total cap so no extension is possible, the renewal
|
|
111
|
+
* is rejected with `max-total-exceeded` rather than succeeding as a no-op —
|
|
112
|
+
* a partially clamped extension is still allowed. Verifying the presented
|
|
113
|
+
* token against the stored hash happens before this in the API layer.
|
|
114
|
+
*/
|
|
115
|
+
export declare function checkLeaseRenewable(lease: LeaseSnapshot, now: Date, config?: LeaseConfig): RenewCheckResult;
|
|
116
|
+
/**
|
|
117
|
+
* When the UI should prompt for renewal (design section 12.3: five minutes
|
|
118
|
+
* before expiration). Purely advisory; expiry itself never depends on it.
|
|
119
|
+
*/
|
|
120
|
+
export declare function renewalPromptAt(lease: LeaseSnapshot, config?: LeaseConfig): string;
|
|
121
|
+
/**
|
|
122
|
+
* Whether a sweep (`sweepExpiredLeases`) or lazy check should expire this
|
|
123
|
+
* lease now: it is past `expires_at` and has not already been ended some
|
|
124
|
+
* other way (released/revoked leases were already dealt with).
|
|
125
|
+
*/
|
|
126
|
+
export declare function shouldExpireLease(lease: LeaseSnapshot, now: Date): boolean;
|
|
127
|
+
//# sourceMappingURL=lease.d.ts.map
|
package/dist/lease.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {} from "@authorbot/schemas";
|
|
3
|
+
import { ALLOWED, denied } from "./decision.js";
|
|
4
|
+
import { toTimestamp } from "./token.js";
|
|
5
|
+
/**
|
|
6
|
+
* Lease VALUE rules (Phase 4 contract section 2, design sections 12, 25).
|
|
7
|
+
* Pure logic only: claimability, renewability under the max-total cap, and
|
|
8
|
+
* expiry given an injected clock. Token generation, SHA-256 hashing, and the
|
|
9
|
+
* constant-time compare are the API layer's job (as with agent tokens); the
|
|
10
|
+
* serialized compare-and-set that makes two simultaneous claims produce
|
|
11
|
+
* exactly one success is the database's job. Nothing here ever logs or
|
|
12
|
+
* embeds a lease token.
|
|
13
|
+
*/
|
|
14
|
+
const MS_PER_SECOND = 1000;
|
|
15
|
+
const MS_PER_MINUTE = 60 * MS_PER_SECOND;
|
|
16
|
+
const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
17
|
+
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
18
|
+
/** Design section 25 defaults: duration PT30M. */
|
|
19
|
+
export const LEASE_DURATION_MS = 30 * MS_PER_MINUTE;
|
|
20
|
+
/** Design section 25 defaults: renewal_duration PT30M. */
|
|
21
|
+
export const LEASE_RENEWAL_DURATION_MS = 30 * MS_PER_MINUTE;
|
|
22
|
+
/** Design section 25 defaults: maximum_total_duration PT4H. */
|
|
23
|
+
export const LEASE_MAX_TOTAL_DURATION_MS = 4 * MS_PER_HOUR;
|
|
24
|
+
/** Design section 25 defaults: renewal_prompt_before PT5M (UI concern). */
|
|
25
|
+
export const LEASE_RENEWAL_PROMPT_BEFORE_MS = 5 * MS_PER_MINUTE;
|
|
26
|
+
const ISO_DURATION_REGEX = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
|
|
27
|
+
/**
|
|
28
|
+
* Parse the ISO-8601 duration subset the contracts use (`PT30M`, `PT4H`,
|
|
29
|
+
* `P1D`, combinations with integer designators; no years/months/weeks —
|
|
30
|
+
* calendar units are ambiguous in milliseconds). Used to validate `LEASE_*`
|
|
31
|
+
* env overrides at boot (Phase 4 contract section 2).
|
|
32
|
+
*/
|
|
33
|
+
export function parseIsoDuration(value) {
|
|
34
|
+
const match = ISO_DURATION_REGEX.exec(value);
|
|
35
|
+
if (match === null) {
|
|
36
|
+
return { ok: false, reason: "bad-format" };
|
|
37
|
+
}
|
|
38
|
+
const [, days, hours, minutes, seconds] = match;
|
|
39
|
+
if (days === undefined &&
|
|
40
|
+
hours === undefined &&
|
|
41
|
+
minutes === undefined &&
|
|
42
|
+
seconds === undefined) {
|
|
43
|
+
return { ok: false, reason: "bad-format" };
|
|
44
|
+
}
|
|
45
|
+
const ms = Number(days ?? 0) * MS_PER_DAY +
|
|
46
|
+
Number(hours ?? 0) * MS_PER_HOUR +
|
|
47
|
+
Number(minutes ?? 0) * MS_PER_MINUTE +
|
|
48
|
+
Number(seconds ?? 0) * MS_PER_SECOND;
|
|
49
|
+
if (ms === 0) {
|
|
50
|
+
return { ok: false, reason: "zero-duration" };
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, ms };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Lease timing configuration in milliseconds, with design section 25
|
|
56
|
+
* defaults. Boot-time validation of env overrides (contract section 2):
|
|
57
|
+
* every duration positive, renewal prompt strictly inside the duration, and
|
|
58
|
+
* the initial duration within the max total (otherwise a lease would be born
|
|
59
|
+
* beyond its own cap).
|
|
60
|
+
*/
|
|
61
|
+
export const leaseConfigSchema = z
|
|
62
|
+
.strictObject({
|
|
63
|
+
durationMs: z.number().int().positive().default(LEASE_DURATION_MS),
|
|
64
|
+
renewalDurationMs: z.number().int().positive().default(LEASE_RENEWAL_DURATION_MS),
|
|
65
|
+
maxTotalDurationMs: z.number().int().positive().default(LEASE_MAX_TOTAL_DURATION_MS),
|
|
66
|
+
renewalPromptBeforeMs: z
|
|
67
|
+
.number()
|
|
68
|
+
.int()
|
|
69
|
+
.positive()
|
|
70
|
+
.default(LEASE_RENEWAL_PROMPT_BEFORE_MS),
|
|
71
|
+
})
|
|
72
|
+
.refine((config) => config.renewalPromptBeforeMs < config.durationMs, {
|
|
73
|
+
path: ["renewalPromptBeforeMs"],
|
|
74
|
+
message: "renewal prompt must fire strictly before the lease duration elapses",
|
|
75
|
+
})
|
|
76
|
+
.refine((config) => config.durationMs <= config.maxTotalDurationMs, {
|
|
77
|
+
path: ["durationMs"],
|
|
78
|
+
message: "initial duration must not exceed the maximum total duration",
|
|
79
|
+
});
|
|
80
|
+
/** The design section 25 defaults as a parsed config. */
|
|
81
|
+
export const DEFAULT_LEASE_CONFIG = Object.freeze(leaseConfigSchema.parse({}));
|
|
82
|
+
/** Timestamps for a freshly issued lease (claim step; design section 12.2). */
|
|
83
|
+
export function resolveLeaseExpiry(now, config = DEFAULT_LEASE_CONFIG) {
|
|
84
|
+
return {
|
|
85
|
+
expiresAt: toTimestamp(new Date(now.getTime() + config.durationMs)),
|
|
86
|
+
maxExpiresAt: toTimestamp(new Date(now.getTime() + config.maxTotalDurationMs)),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Expiry is inclusive at exactly `expires_at` (matching `checkTokenActive`):
|
|
91
|
+
* no submission is accepted merely because a countdown still shows 0:00.
|
|
92
|
+
*/
|
|
93
|
+
export function isLeaseExpired(lease, now) {
|
|
94
|
+
return now.getTime() >= Date.parse(lease.expiresAt);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether a lease still backs commands at `now`. Check order follows the
|
|
98
|
+
* contract section 4 verification order ("not expired / not released"):
|
|
99
|
+
* expired first, then released, then revoked. Used by renew, release, and
|
|
100
|
+
* submission checks alike — an expired, released, or revoked lease can do
|
|
101
|
+
* nothing further (contract section 2: renewing an expired lease is a 409).
|
|
102
|
+
*/
|
|
103
|
+
export function checkLeaseActive(lease, now) {
|
|
104
|
+
if (isLeaseExpired(lease, now)) {
|
|
105
|
+
return denied("expired", "lease has expired");
|
|
106
|
+
}
|
|
107
|
+
if (lease.releasedAt !== undefined && lease.releasedAt !== null) {
|
|
108
|
+
return denied("released", "lease has been released");
|
|
109
|
+
}
|
|
110
|
+
if (lease.revokedAt !== undefined && lease.revokedAt !== null) {
|
|
111
|
+
return denied("revoked", "lease has been revoked");
|
|
112
|
+
}
|
|
113
|
+
return ALLOWED;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Claimability (design section 12.2 step 1, contract section 2): the item is
|
|
117
|
+
* `ready`, or it is `leased` and no LIVE lease occupies its slot. Only a
|
|
118
|
+
* `leased` item whose recorded active lease is still live denies with
|
|
119
|
+
* `lease-held` (the 409 the losing claimant sees; holder-safe messaging is
|
|
120
|
+
* the API's concern). Scope (`work:claim`) and per-type capability checks are
|
|
121
|
+
* separate (`requireScope`, `requiredSubmissionType`).
|
|
122
|
+
*
|
|
123
|
+
* A `leased` item with NO active lease row at all is claimable, not held: the
|
|
124
|
+
* slot is provably free (the unique index admits exactly one active lease, so
|
|
125
|
+
* "none" is unambiguous). That combination is what an interrupted expiry or a
|
|
126
|
+
* bare administrative revocation leaves behind, and treating it as
|
|
127
|
+
* `lease-held` made such items permanently unclaimable, unreleasable, and
|
|
128
|
+
* unsweepable — recoverable only by direct database surgery. Claiming
|
|
129
|
+
* self-heals it instead: `priorLeaseExpired` sends the caller down the
|
|
130
|
+
* expire-then-claim path, whose compare-and-swap starts from `leased`.
|
|
131
|
+
*/
|
|
132
|
+
export function checkWorkItemClaimable(status, activeLease, now) {
|
|
133
|
+
if (status === "ready") {
|
|
134
|
+
return { allowed: true, priorLeaseExpired: false };
|
|
135
|
+
}
|
|
136
|
+
if (status !== "leased") {
|
|
137
|
+
return denied("not-claimable", `work item in status "${status}" cannot be claimed`);
|
|
138
|
+
}
|
|
139
|
+
if (activeLease !== null && checkLeaseActive(activeLease, now).allowed) {
|
|
140
|
+
return denied("lease-held", "work item is already leased");
|
|
141
|
+
}
|
|
142
|
+
return { allowed: true, priorLeaseExpired: true };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Renewal (design section 12.3, contract section 2): only an active lease
|
|
146
|
+
* renews; the new expiry is the current `expires_at` plus the renewal
|
|
147
|
+
* duration ("extends by"), clamped to `max_expires_at`. When the lease
|
|
148
|
+
* already sits at its max-total cap so no extension is possible, the renewal
|
|
149
|
+
* is rejected with `max-total-exceeded` rather than succeeding as a no-op —
|
|
150
|
+
* a partially clamped extension is still allowed. Verifying the presented
|
|
151
|
+
* token against the stored hash happens before this in the API layer.
|
|
152
|
+
*/
|
|
153
|
+
export function checkLeaseRenewable(lease, now, config = DEFAULT_LEASE_CONFIG) {
|
|
154
|
+
const active = checkLeaseActive(lease, now);
|
|
155
|
+
if (!active.allowed) {
|
|
156
|
+
return active;
|
|
157
|
+
}
|
|
158
|
+
const currentExpiry = Date.parse(lease.expiresAt);
|
|
159
|
+
const cap = Date.parse(lease.maxExpiresAt);
|
|
160
|
+
const extended = Math.min(currentExpiry + config.renewalDurationMs, cap);
|
|
161
|
+
if (extended <= currentExpiry) {
|
|
162
|
+
return denied("max-total-exceeded", "lease has reached its maximum total duration and cannot be renewed");
|
|
163
|
+
}
|
|
164
|
+
return { allowed: true, expiresAt: toTimestamp(new Date(extended)) };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* When the UI should prompt for renewal (design section 12.3: five minutes
|
|
168
|
+
* before expiration). Purely advisory; expiry itself never depends on it.
|
|
169
|
+
*/
|
|
170
|
+
export function renewalPromptAt(lease, config = DEFAULT_LEASE_CONFIG) {
|
|
171
|
+
return toTimestamp(new Date(Date.parse(lease.expiresAt) - config.renewalPromptBeforeMs));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether a sweep (`sweepExpiredLeases`) or lazy check should expire this
|
|
175
|
+
* lease now: it is past `expires_at` and has not already been ended some
|
|
176
|
+
* other way (released/revoked leases were already dealt with).
|
|
177
|
+
*/
|
|
178
|
+
export function shouldExpireLease(lease, now) {
|
|
179
|
+
return (isLeaseExpired(lease, now) &&
|
|
180
|
+
(lease.releasedAt === undefined || lease.releasedAt === null) &&
|
|
181
|
+
(lease.revokedAt === undefined || lease.revokedAt === null));
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=lease.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type AnnotationKind, type AnnotationStatus, type WorkItemStatus } from "@authorbot/schemas";
|
|
3
|
+
import { type Decision } from "./decision.js";
|
|
4
|
+
import type { Role } from "./scopes.js";
|
|
5
|
+
/**
|
|
6
|
+
* Maintainer overrides (Phase 3 contract section 4; design section 11.2).
|
|
7
|
+
* All four are maintainer-only, require a recorded `reason`, and are recorded
|
|
8
|
+
* as decisions with `override_reason`. Force-create bypasses the rule but
|
|
9
|
+
* respects the same uniqueness key `(source_annotation_id, action_type,
|
|
10
|
+
* rule_version)` with `rule_version: 0`.
|
|
11
|
+
*/
|
|
12
|
+
/** Minimum meaningful override reason length (after trimming). */
|
|
13
|
+
export declare const MIN_OVERRIDE_REASON_LENGTH = 3;
|
|
14
|
+
/** Bound chosen for storage sanity (not contract-pinned). */
|
|
15
|
+
export declare const MAX_OVERRIDE_REASON_LENGTH = 2000;
|
|
16
|
+
export declare const overrideReasonSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
17
|
+
/** `rule_version` recorded by force-created decisions (contract section 4). */
|
|
18
|
+
export declare const FORCE_CREATE_RULE_VERSION = 0;
|
|
19
|
+
/** Override 1: reject an open suggestion. */
|
|
20
|
+
export declare const rejectSuggestionCommandSchema: z.ZodObject<{
|
|
21
|
+
annotationId: z.ZodString;
|
|
22
|
+
reason: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
23
|
+
}, z.core.$strict>;
|
|
24
|
+
export type RejectSuggestionCommand = z.infer<typeof rejectSuggestionCommandSchema>;
|
|
25
|
+
/** Override 2: cancel a `ready` work item. */
|
|
26
|
+
export declare const cancelWorkItemCommandSchema: z.ZodObject<{
|
|
27
|
+
workItemId: z.ZodString;
|
|
28
|
+
reason: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
export type CancelWorkItemCommand = z.infer<typeof cancelWorkItemCommandSchema>;
|
|
31
|
+
/** Override 3: reopen a rejected suggestion. */
|
|
32
|
+
export declare const reopenSuggestionCommandSchema: z.ZodObject<{
|
|
33
|
+
annotationId: z.ZodString;
|
|
34
|
+
reason: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
export type ReopenSuggestionCommand = z.infer<typeof reopenSuggestionCommandSchema>;
|
|
37
|
+
/**
|
|
38
|
+
* Override 4: force-create a work item bypassing the rule. `work_type` is not
|
|
39
|
+
* part of the command — it resolves from the annotation scope exactly as
|
|
40
|
+
* rule-created items do (Phase 3 contract section 3).
|
|
41
|
+
*/
|
|
42
|
+
export declare const forceCreateWorkItemCommandSchema: z.ZodObject<{
|
|
43
|
+
annotationId: z.ZodString;
|
|
44
|
+
reason: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
45
|
+
}, z.core.$strict>;
|
|
46
|
+
export type ForceCreateWorkItemCommand = z.infer<typeof forceCreateWorkItemCommandSchema>;
|
|
47
|
+
export type SuggestionOverrideDenialReason = "not-maintainer" | "not-a-suggestion" | "illegal-transition";
|
|
48
|
+
/** Reject an open suggestion (`open -> rejected`). */
|
|
49
|
+
export declare function authorizeRejectSuggestion(input: {
|
|
50
|
+
actorRole: Role;
|
|
51
|
+
annotationKind: AnnotationKind;
|
|
52
|
+
annotationStatus: AnnotationStatus;
|
|
53
|
+
}): Decision<SuggestionOverrideDenialReason>;
|
|
54
|
+
/** Reopen a rejected suggestion (`rejected -> open`). */
|
|
55
|
+
export declare function authorizeReopenSuggestion(input: {
|
|
56
|
+
actorRole: Role;
|
|
57
|
+
annotationKind: AnnotationKind;
|
|
58
|
+
annotationStatus: AnnotationStatus;
|
|
59
|
+
}): Decision<SuggestionOverrideDenialReason>;
|
|
60
|
+
export type WorkItemOverrideDenialReason = "not-maintainer" | "illegal-transition" | "phase-not-enabled";
|
|
61
|
+
/** Cancel a `ready` work item (contract section 4: cancel before integration). */
|
|
62
|
+
export declare function authorizeCancelWorkItem(input: {
|
|
63
|
+
actorRole: Role;
|
|
64
|
+
workItemStatus: WorkItemStatus;
|
|
65
|
+
}): Decision<WorkItemOverrideDenialReason>;
|
|
66
|
+
/**
|
|
67
|
+
* Force-create a work item for an open suggestion, bypassing the rule. The
|
|
68
|
+
* annotation must still be able to make the `open -> work_item_created`
|
|
69
|
+
* transition; uniqueness (one item per annotation/action/rule-version) is the
|
|
70
|
+
* DB constraint's job.
|
|
71
|
+
*/
|
|
72
|
+
export declare function authorizeForceCreateWorkItem(input: {
|
|
73
|
+
actorRole: Role;
|
|
74
|
+
annotationKind: AnnotationKind;
|
|
75
|
+
annotationStatus: AnnotationStatus;
|
|
76
|
+
}): Decision<SuggestionOverrideDenialReason>;
|
|
77
|
+
//# sourceMappingURL=overrides.d.ts.map
|