@webdecoy/ai-protection 0.1.0-alpha.2 → 0.1.0-alpha.3
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/README.md +12 -1
- package/action-runtime.mjs +41 -0
- package/actions.d.mts +70 -0
- package/actions.mjs +168 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Bot and abuse protection for AI-powered applications. A small Node.js SDK that
|
|
|
4
4
|
checks requests before your application invokes a model. Customer-defined rules run
|
|
5
5
|
locally; proprietary bot detection runs in WebDecoy. See [architecture](ARCHITECTURE.md).
|
|
6
6
|
|
|
7
|
-
**Alpha release: `0.1.0-alpha.
|
|
7
|
+
**Alpha release: `0.1.0-alpha.3`.** Integration mechanics are tested;
|
|
8
8
|
real-world detection accuracy and provider cost savings have not been established.
|
|
9
9
|
Requires a WebDecoy property, a property-scoped API key, and a compatible WebDecoy
|
|
10
10
|
service deployment. This repository contains the SDK, not the detection service.
|
|
@@ -317,3 +317,14 @@ snapshot, not current quota state.
|
|
|
317
317
|
|
|
318
318
|
Only admission is deduplicated. Repeated application/model calls still require
|
|
319
319
|
application-level idempotency. The hosted runtime must support quota schema 2 before enabling this option.
|
|
320
|
+
|
|
321
|
+
## Action authorization (Alpha)
|
|
322
|
+
|
|
323
|
+
Version `0.1.0-alpha.3` includes an action boundary at `@webdecoy/ai-protection/actions`. See the
|
|
324
|
+
[record-action example and integration contract](examples/actions/README.md).
|
|
325
|
+
This API requires your server authentication and application authorization.
|
|
326
|
+
An [Auth0 access-token example](examples/auth0/README.md) verifies signed tokens
|
|
327
|
+
and maps tenant membership. The [TypeScript MCP integration](examples/mcp/README.md)
|
|
328
|
+
protects stateless HTTP tool dispatch in a source-only example; an exported MCP
|
|
329
|
+
package entrypoint remains a follow-up. Optional shared quotas, concurrency and hosted
|
|
330
|
+
action events are documented in the action guide and use the hosted AI Protection runtime and dashboard.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {prepareQuota,quotaHash} from './quota.mjs';
|
|
2
|
+
import {prepareConcurrency} from './concurrency.mjs';
|
|
3
|
+
import {validPropertyID} from './account.mjs';
|
|
4
|
+
import {createReporter} from './reporting.mjs';
|
|
5
|
+
|
|
6
|
+
export function prepareActionRuntime(options, definitions) {
|
|
7
|
+
const config=options.sharedRuntime;
|
|
8
|
+
if(!config){if([...definitions.values()].some(d=>d.limits))throw Error('Action limits require sharedRuntime');return null;}
|
|
9
|
+
const url=new URL(config.webdecoyUrl);
|
|
10
|
+
if(!['https:','http:'].includes(url.protocol)||url.username||url.password||url.pathname!=='/'||url.search||url.hash||
|
|
11
|
+
(url.protocol==='http:'&&!['localhost','127.0.0.1','[::1]'].includes(url.hostname))||!validPropertyID(config.propertyId)||
|
|
12
|
+
typeof config.webdecoyKey!=='string'||!config.webdecoyKey||/[^\x21-\x7e]/.test(config.webdecoyKey)||
|
|
13
|
+
typeof config.subjectSecret!=='string'||Buffer.byteLength(config.subjectSecret)<32)throw Error('Invalid action runtime');
|
|
14
|
+
const c={...config};const limits=new Map(),ruleIDs=new Set();
|
|
15
|
+
const subject=(ctx,tenant)=>({accountId:tenant?quotaHash(c.subjectSecret,'webdecoy.actions.tenant.v1',ctx.caller.tenant):quotaHash(c.subjectSecret,'webdecoy.actions.caller.v1',ctx.caller.issuer,ctx.caller.tenant,ctx.caller.subject)});
|
|
16
|
+
for(const [name,d] of definitions){
|
|
17
|
+
const l=d.limits??{},gates=[];
|
|
18
|
+
for(const [key,tenant]of [['callerQuota',false],['tenantQuota',true]])if(l[key]){
|
|
19
|
+
const q=l[key];if(ruleIDs.has(q.ruleId))throw Error('Action limits require distinct rule IDs');ruleIDs.add(q.ruleId);
|
|
20
|
+
const gate=prepareQuota({...c,accountQuota:{...q,idempotency:false,operationId:undefined,sessionLimit:0,subject:ctx=>subject(ctx,tenant)}});
|
|
21
|
+
gates.push(async(ctx,signal)=>{const r=await gate(ctx,signal);if(r.check)r.check.id=tenant?'tenant_quota':'caller_quota';return r;});
|
|
22
|
+
}
|
|
23
|
+
let concurrency=null;
|
|
24
|
+
if(l.concurrency){if(ruleIDs.has(l.concurrency.ruleId))throw Error('Action limits require distinct rule IDs');ruleIDs.add(l.concurrency.ruleId);
|
|
25
|
+
concurrency=prepareConcurrency({...c,concurrency:{...l.concurrency,subject:ctx=>subject(ctx,false)}});}
|
|
26
|
+
limits.set(name,{gates,concurrency});
|
|
27
|
+
}
|
|
28
|
+
const reporter=createReporter({reportingTimeoutMs:c.reportingTimeoutMs??1000,maxPendingReports:c.maxPendingReports??100,
|
|
29
|
+
onObservation:async(event,{signal})=>{
|
|
30
|
+
const checks=[{id:'action_boundary',source:'local',mode:'enforce',decision:event.decision,reason:event.reason,duration_ms:0},
|
|
31
|
+
...event.checks.map(check=>({id:check.id,source:check.source,mode:check.mode,decision:check.decision,reason:check.reason,duration_ms:check.durationMs}))];
|
|
32
|
+
const payload={schema:2,request_id:event.eventId,timestamp:event.timestamp,decision:event.decision,reason:event.reason,
|
|
33
|
+
degraded:event.checks.some(c=>c.decision==='unavailable'),checks,handler_attempted:event.attempted,
|
|
34
|
+
action:event.decision==='deny'?'denied':event.outcome==='unknown'?'handler_error':'forwarded',
|
|
35
|
+
tool_action:{action_id:event.actionId,name:event.action,policy_version:event.policyVersion,outcome:event.outcome}};
|
|
36
|
+
const response=await fetch(new URL('/api/v1/sdk/ai-abuse/reports',url),{method:'POST',redirect:'error',signal,
|
|
37
|
+
headers:{Authorization:`Bearer ${c.webdecoyKey}`,'X-WebDecoy-Property-ID':c.propertyId,'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
|
38
|
+
await response.body?.cancel();if(!response.ok)throw Error('Action reporting unavailable');
|
|
39
|
+
}});
|
|
40
|
+
return {limits,report:event=>reporter.send(event),flush:()=>reporter.flush()};
|
|
41
|
+
}
|
package/actions.d.mts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Contract supplied by verified server authentication, never parsed from tool arguments. */
|
|
2
|
+
export interface TrustedCaller {
|
|
3
|
+
readonly schema: 1;
|
|
4
|
+
readonly subject: string;
|
|
5
|
+
readonly tenant: string;
|
|
6
|
+
readonly issuer: string;
|
|
7
|
+
readonly authenticationMethod: string;
|
|
8
|
+
/** Unix milliseconds; checked again immediately before execution. */
|
|
9
|
+
readonly expiresAt: number;
|
|
10
|
+
readonly scopes: readonly string[];
|
|
11
|
+
/** OAuth client identity, separate from subject; not an agent signer. */
|
|
12
|
+
readonly clientId?: string;
|
|
13
|
+
}
|
|
14
|
+
export type ActionInput = null | boolean | number | string | readonly ActionInput[] | {readonly [key:string]: ActionInput};
|
|
15
|
+
export interface ActionContext {
|
|
16
|
+
readonly caller: TrustedCaller;
|
|
17
|
+
readonly args: ActionInput;
|
|
18
|
+
readonly signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
export interface ActionEvent {
|
|
21
|
+
readonly schema: 1;
|
|
22
|
+
readonly eventId: string;
|
|
23
|
+
readonly timestamp: string;
|
|
24
|
+
readonly checks: readonly {id:string;source:string;mode:string;decision:string;reason:string;durationMs:number}[];
|
|
25
|
+
readonly actionId: string;
|
|
26
|
+
readonly action: string;
|
|
27
|
+
readonly policyVersion: string;
|
|
28
|
+
readonly evaluation: 'local';
|
|
29
|
+
readonly decision: 'allow' | 'deny';
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
readonly attempted: boolean;
|
|
32
|
+
readonly outcome: 'not_attempted' | 'attempted' | 'completed' | 'unknown';
|
|
33
|
+
}
|
|
34
|
+
export interface ActionQuota {ruleId:string;limit:number;windowSeconds:number;mode?:'observe'|'enforce';failureMode?:'open'|'closed';timeoutMs?:number}
|
|
35
|
+
export interface ActionLimits {
|
|
36
|
+
callerQuota?: ActionQuota;
|
|
37
|
+
tenantQuota?: ActionQuota;
|
|
38
|
+
concurrency?: {ruleId:string;accountLimit:number;featureLimit:number;mode?:'observe'|'enforce';failureMode?:'open'|'closed';ttlSeconds?:number;maxSeconds?:number;timeoutMs?:number};
|
|
39
|
+
}
|
|
40
|
+
export interface ActionRuntime {
|
|
41
|
+
webdecoyUrl:string;webdecoyKey:string;propertyId:string;subjectSecret:string;
|
|
42
|
+
reportingTimeoutMs?:number;maxPendingReports?:number;
|
|
43
|
+
}
|
|
44
|
+
export interface ActionDefinition {
|
|
45
|
+
limits?: ActionLimits;
|
|
46
|
+
requiredScopes: readonly string[];
|
|
47
|
+
validate(args: ActionInput): boolean | Promise<boolean>;
|
|
48
|
+
authorize(context: ActionContext): boolean | Promise<boolean>;
|
|
49
|
+
/** Additional restrictive policy; cannot override application authorization. */
|
|
50
|
+
policy?(context: ActionContext): boolean | Promise<boolean>;
|
|
51
|
+
/** Await all protected work. Do not return a live stream or detached task. */
|
|
52
|
+
execute(context: ActionContext): unknown | Promise<unknown>;
|
|
53
|
+
}
|
|
54
|
+
export class ActionDenied extends Error {
|
|
55
|
+
readonly reason: string;
|
|
56
|
+
readonly status: number;
|
|
57
|
+
readonly actionId: string;
|
|
58
|
+
readonly retryAfterSeconds?: number;
|
|
59
|
+
constructor(reason: string, status: number, actionId: string);
|
|
60
|
+
}
|
|
61
|
+
export function createActionProtection<T>(options: {
|
|
62
|
+
policyVersion: string;
|
|
63
|
+
sharedRuntime?: ActionRuntime;
|
|
64
|
+
/** Bounds local admission checks; shared controls have their own RPC deadlines. Default 1000 ms, max 10000. */
|
|
65
|
+
admissionTimeoutMs?: number;
|
|
66
|
+
authenticate(context: T, options: {signal?: AbortSignal}): TrustedCaller | Promise<TrustedCaller>;
|
|
67
|
+
actions: Record<string, ActionDefinition>;
|
|
68
|
+
/** Local best-effort observer. sharedRuntime enables independent hosted reporting. */
|
|
69
|
+
onEvent?(event: ActionEvent): void | Promise<void>;
|
|
70
|
+
}): {flush():Promise<void>;run(action: string, args: ActionInput, authenticationContext: T, options?: {signal?: AbortSignal}): Promise<unknown>};
|
package/actions.mjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
2
|
+
import {abortable} from './transport.mjs';
|
|
3
|
+
import {prepareActionRuntime} from './action-runtime.mjs';
|
|
4
|
+
|
|
5
|
+
const token = /^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,95}$/;
|
|
6
|
+
const bounded = value => typeof value === 'string' && value.length > 0 && value.length <= 512 && !/[\x00-\x1f\x7f]/.test(value);
|
|
7
|
+
const freeze = value => {
|
|
8
|
+
if (value && typeof value === 'object') { for (const child of Object.values(value)) freeze(child); Object.freeze(value); }
|
|
9
|
+
return value;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class ActionDenied extends Error {
|
|
13
|
+
constructor(reason, status, actionId) {
|
|
14
|
+
super(reason); this.name = 'ActionDenied'; this.reason = reason; this.status = status; this.actionId = actionId;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Authentication is owned by the application's server integration. This validates
|
|
19
|
+
// its contract; it does not verify a bearer token, signer, or model-supplied claim.
|
|
20
|
+
function callerSnapshot(value) {
|
|
21
|
+
if (!value || value.schema !== 1 || !bounded(value.subject) || !bounded(value.tenant) ||
|
|
22
|
+
!bounded(value.issuer) || !bounded(value.authenticationMethod) ||
|
|
23
|
+
!Number.isSafeInteger(value.expiresAt) || value.expiresAt <= Date.now() ||
|
|
24
|
+
!Array.isArray(value.scopes) || value.scopes.length > 64 || value.scopes.some(s => !bounded(s)) ||
|
|
25
|
+
(value.clientId !== undefined && !bounded(value.clientId))) throw Error('Invalid authenticated caller');
|
|
26
|
+
return freeze({schema:1, subject:value.subject, tenant:value.tenant, issuer:value.issuer,
|
|
27
|
+
authenticationMethod:value.authenticationMethod, expiresAt:value.expiresAt,
|
|
28
|
+
scopes:[...new Set(value.scopes)], ...(value.clientId === undefined ? {} : {clientId:value.clientId})});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// No accessors, class instances, cycles, non-finite numbers, or unbounded trees.
|
|
32
|
+
// Copy before any await so a caller cannot swap arguments while policy runs.
|
|
33
|
+
function inputSnapshot(input) {
|
|
34
|
+
let nodes = 0;
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
function copy(value, depth) {
|
|
37
|
+
if (++nodes > 2048 || depth > 12) throw Error('Input too complex');
|
|
38
|
+
if (value === null || typeof value === 'boolean') return value;
|
|
39
|
+
if (typeof value === 'string') { if (value.length > 16384) throw Error('Input too large'); return value; }
|
|
40
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
41
|
+
if (!value || typeof value !== 'object' || seen.has(value) ||
|
|
42
|
+
(!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) throw Error('Invalid JSON input');
|
|
43
|
+
if (Array.isArray(value) && value.length > 2048) throw Error('Input array too large');
|
|
44
|
+
const keys = Object.keys(value);
|
|
45
|
+
if (keys.length > 2048) throw Error('Input too complex');
|
|
46
|
+
seen.add(value);
|
|
47
|
+
const out = Array.isArray(value) ? [] : Object.create(null);
|
|
48
|
+
for (const key of keys) {
|
|
49
|
+
if (Array.isArray(value) && !/^(0|[1-9][0-9]*)$/.test(key)) throw Error('Invalid array key');
|
|
50
|
+
const d = Object.getOwnPropertyDescriptor(value, key);
|
|
51
|
+
if (!d || !('value' in d) || key.length > 512 || ['__proto__','constructor','prototype'].includes(key)) throw Error('Invalid input key');
|
|
52
|
+
out[key] = copy(d.value, depth + 1);
|
|
53
|
+
}
|
|
54
|
+
seen.delete(value);
|
|
55
|
+
return Object.freeze(out);
|
|
56
|
+
}
|
|
57
|
+
const result = copy(input, 0);
|
|
58
|
+
if (Buffer.byteLength(JSON.stringify(result)) > 16384) throw Error('Input too large');
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Local protected dispatch. No network calls, cloud override, or automatic retry. */
|
|
63
|
+
export function createActionProtection(options) {
|
|
64
|
+
if (!options || typeof options.authenticate !== 'function' || (typeof options.policyVersion !== 'string' || !token.test(options.policyVersion))) throw Error('Invalid action configuration');
|
|
65
|
+
const authenticate = options.authenticate, policyVersion = options.policyVersion, sink = options.onEvent;
|
|
66
|
+
if (sink !== undefined && typeof sink !== 'function') throw Error('Invalid event sink');
|
|
67
|
+
const admissionTimeoutMs = options.admissionTimeoutMs ?? 1000;
|
|
68
|
+
if (!Number.isInteger(admissionTimeoutMs) || admissionTimeoutMs < 1 || admissionTimeoutMs > 10000) throw Error('Invalid admission timeout');
|
|
69
|
+
const actions = new Map();
|
|
70
|
+
for (const [name, definition] of Object.entries(options.actions ?? {})) {
|
|
71
|
+
if (!token.test(name) || !definition || !Array.isArray(definition.requiredScopes) || definition.requiredScopes.length > 64 ||
|
|
72
|
+
definition.requiredScopes.some(s => !bounded(s)) ||
|
|
73
|
+
['validate','authorize','execute'].some(k => typeof definition[k] !== 'function') ||
|
|
74
|
+
(definition.policy !== undefined && typeof definition.policy !== 'function')) throw Error('Invalid action definition');
|
|
75
|
+
actions.set(name, Object.freeze({...definition,requiredScopes:Object.freeze([...definition.requiredScopes])}));
|
|
76
|
+
}
|
|
77
|
+
if (!actions.size || actions.size > 128) throw Error('Expected 1–128 actions');
|
|
78
|
+
const runtime=prepareActionRuntime(options,actions);
|
|
79
|
+
let pendingEvents = 0;
|
|
80
|
+
return Object.freeze({flush:async()=>{await runtime?.flush();},async run(name, input, authenticationContext, {signal} = {}) {
|
|
81
|
+
const actionId = randomUUID();
|
|
82
|
+
// Unknown caller-controlled action strings are never placed in evidence.
|
|
83
|
+
const action = actions.get(name), eventAction = action ? name : 'unregistered';
|
|
84
|
+
let attempted = false,completed=false,lease;
|
|
85
|
+
const checks=[];
|
|
86
|
+
const deadline = new AbortController();
|
|
87
|
+
const admissionSignal = signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal;
|
|
88
|
+
const timer = setTimeout(() => deadline.abort(new DOMException('Action admission timed out','TimeoutError')), admissionTimeoutMs);
|
|
89
|
+
const evaluate = fn => abortable(Promise.resolve().then(() => { admissionSignal.throwIfAborted(); return fn(); }), admissionSignal);
|
|
90
|
+
function emit(decision, reason, outcome) {
|
|
91
|
+
|
|
92
|
+
const event = Object.freeze({schema:1, eventId:randomUUID(), timestamp:new Date().toISOString(), actionId, action:eventAction, policyVersion,
|
|
93
|
+
evaluation:'local', decision, reason, attempted, outcome,checks:Object.freeze(checks.map(c=>Object.freeze({...c})))});
|
|
94
|
+
if(runtime)void runtime.report(event);
|
|
95
|
+
if(!sink||pendingEvents>=100)return;
|
|
96
|
+
pendingEvents++;
|
|
97
|
+
Promise.resolve().then(() => sink(event)).catch(() => {}).finally(() => { pendingEvents--; });
|
|
98
|
+
}
|
|
99
|
+
function deny(reason, status, retryAfterSeconds) { emit('deny', reason, 'not_attempted'); const e=new ActionDenied(reason,status,actionId);if(retryAfterSeconds)e.retryAfterSeconds=retryAfterSeconds;throw e; }
|
|
100
|
+
function cancelled() { (attempted ? signal : admissionSignal)?.throwIfAborted(); }
|
|
101
|
+
try {
|
|
102
|
+
cancelled();
|
|
103
|
+
if (!action) deny('action_not_registered',403);
|
|
104
|
+
let args;
|
|
105
|
+
try { args = inputSnapshot(input); } catch { deny('invalid_arguments',400); }
|
|
106
|
+
let caller;
|
|
107
|
+
try { caller = callerSnapshot(await evaluate(() => authenticate(authenticationContext, {signal:admissionSignal}))); }
|
|
108
|
+
catch { cancelled(); deny('authentication_required',401); }
|
|
109
|
+
cancelled();
|
|
110
|
+
if (action.requiredScopes.some(scope => !caller.scopes.includes(scope))) deny('missing_scope',403);
|
|
111
|
+
const context = Object.freeze({caller, args, signal:admissionSignal});
|
|
112
|
+
let valid;
|
|
113
|
+
try { valid = await evaluate(() => action.validate(args)); } catch { cancelled(); deny('invalid_arguments',400); }
|
|
114
|
+
cancelled();
|
|
115
|
+
if (valid !== true) deny('invalid_arguments',400);
|
|
116
|
+
let authorized;
|
|
117
|
+
try { authorized = await evaluate(() => action.authorize(context)); } catch { cancelled(); deny('authorization_unavailable',503); }
|
|
118
|
+
cancelled();
|
|
119
|
+
if (authorized !== true) deny('permission_denied',403);
|
|
120
|
+
if (action.policy) {
|
|
121
|
+
let permitted;
|
|
122
|
+
try { permitted = await evaluate(() => action.policy(context)); } catch { cancelled(); deny('policy_unavailable',503); }
|
|
123
|
+
cancelled();
|
|
124
|
+
if (permitted !== true) deny('policy_denied',403);
|
|
125
|
+
}
|
|
126
|
+
// Shared limits run only after application permission checks. Their own
|
|
127
|
+
// RPC deadlines are separate from local admission and detector availability.
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
const controls=runtime?.limits.get(name);
|
|
130
|
+
for(const gate of controls?.gates??[]){
|
|
131
|
+
const r=await gate(context,admissionSignal);checks.push(r.check);
|
|
132
|
+
if(r.denial)deny(r.denial.reason,r.denial.status,r.denial.retryAfterSeconds);
|
|
133
|
+
}
|
|
134
|
+
if(controls?.concurrency){
|
|
135
|
+
lease=await controls.concurrency(context,admissionSignal);checks.push(lease.check);
|
|
136
|
+
if(lease.denial)deny(lease.denial.reason,lease.denial.status,lease.denial.retryAfterSeconds);
|
|
137
|
+
lease.signal.throwIfAborted();
|
|
138
|
+
}
|
|
139
|
+
// Authentication may expire while ownership/policy checks are running.
|
|
140
|
+
if (caller.expiresAt <= Date.now()) deny('authentication_expired',401);
|
|
141
|
+
cancelled();
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
attempted = true;
|
|
144
|
+
emit('allow','authorized','attempted');
|
|
145
|
+
const execution=Object.freeze({...context,signal:lease?.signal??context.signal});
|
|
146
|
+
const result = await action.execute(execution);
|
|
147
|
+
execution.signal.throwIfAborted();
|
|
148
|
+
// Resolution is application completion, not proof of an external side effect.
|
|
149
|
+
cancelled();
|
|
150
|
+
completed=true;
|
|
151
|
+
if(lease&&!lease.denial){
|
|
152
|
+
const began=performance.now();
|
|
153
|
+
try{await lease.finish(true);}catch{checks.push({id:'concurrency_release',source:'shared',mode:lease.check.mode,decision:'unavailable',reason:'concurrency_release_unavailable',durationMs:performance.now()-began});}
|
|
154
|
+
}
|
|
155
|
+
emit('allow','authorized','completed');
|
|
156
|
+
return result;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (!attempted && deadline.signal.aborted && !signal?.aborted) deny('admission_timeout',503);
|
|
159
|
+
if (attempted) emit('allow',signal?.aborted ? 'execution_cancelled' : 'execution_failed','unknown');
|
|
160
|
+
else if (!(error instanceof ActionDenied)) emit('deny','admission_cancelled','not_attempted');
|
|
161
|
+
throw error;
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
// Only confirmed completion (or no callback) releases; uncertain work holds.
|
|
165
|
+
if(lease&&!lease.denial)await lease.finish(!attempted||completed).catch(()=>{});
|
|
166
|
+
}
|
|
167
|
+
}});
|
|
168
|
+
}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"check:package": "node scripts/check-package.mjs",
|
|
11
11
|
"test:types": "tsc --strict --noEmit --module nodenext --target es2022 test/types.mts"
|
|
12
12
|
},
|
|
13
|
-
"version": "0.1.0-alpha.
|
|
13
|
+
"version": "0.1.0-alpha.3",
|
|
14
14
|
"exports": {
|
|
15
15
|
"./fetch": {
|
|
16
16
|
"types": "./fetch.d.mts",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"./browser": {
|
|
24
24
|
"types": "./browser.d.mts",
|
|
25
25
|
"import": "./browser.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./actions": {
|
|
28
|
+
"types": "./actions.d.mts",
|
|
29
|
+
"import": "./actions.mjs"
|
|
26
30
|
}
|
|
27
31
|
},
|
|
28
32
|
"files": [
|
|
@@ -46,7 +50,10 @@
|
|
|
46
50
|
"browser-evidence.mjs",
|
|
47
51
|
"usage.mjs",
|
|
48
52
|
"transport.mjs",
|
|
49
|
-
"NOTICE"
|
|
53
|
+
"NOTICE",
|
|
54
|
+
"actions.mjs",
|
|
55
|
+
"actions.d.mts",
|
|
56
|
+
"action-runtime.mjs"
|
|
50
57
|
],
|
|
51
58
|
"description": "Bot and abuse protection for AI-powered applications. Server-side request admission for Node.js.",
|
|
52
59
|
"license": "Apache-2.0",
|