@relaycast/engine 8.10.0 → 8.11.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/dist/adapters/node/kv.d.ts +1 -1
- package/dist/adapters/node/kv.d.ts.map +1 -1
- package/dist/adapters/node/kv.js +9 -4
- package/dist/adapters/node/kv.js.map +1 -1
- package/dist/adapters/node/rate-limit.d.ts.map +1 -1
- package/dist/adapters/node/rate-limit.js +10 -5
- package/dist/adapters/node/rate-limit.js.map +1 -1
- package/dist/db/migrations/0060_durable_task_invocations.sql +7 -0
- package/dist/db/migrations/0061_messages_workspace_length_id_index.sql +7 -0
- package/dist/db/schema.d.ts +51 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +3 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/engine/action.d.ts +30 -0
- package/dist/engine/action.d.ts.map +1 -1
- package/dist/engine/action.js +33 -3
- package/dist/engine/action.js.map +1 -1
- package/dist/engine/inbox.d.ts.map +1 -1
- package/dist/engine/inbox.js +73 -27
- package/dist/engine/inbox.js.map +1 -1
- package/dist/engine/node.d.ts +10 -0
- package/dist/engine/node.d.ts.map +1 -1
- package/dist/engine/node.js +20 -0
- package/dist/engine/node.js.map +1 -1
- package/dist/engine/nodeProvider.d.ts.map +1 -1
- package/dist/engine/nodeProvider.js +5 -1
- package/dist/engine/nodeProvider.js.map +1 -1
- package/dist/engine/taskInvocation.d.ts +110 -0
- package/dist/engine/taskInvocation.d.ts.map +1 -0
- package/dist/engine/taskInvocation.js +141 -0
- package/dist/engine/taskInvocation.js.map +1 -0
- package/dist/engine/usage.d.ts +17 -8
- package/dist/engine/usage.d.ts.map +1 -1
- package/dist/engine/usage.js +35 -18
- package/dist/engine/usage.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/throttle.d.ts +20 -0
- package/dist/lib/throttle.d.ts.map +1 -0
- package/dist/lib/throttle.js +28 -0
- package/dist/lib/throttle.js.map +1 -0
- package/dist/middleware/planLimits.d.ts.map +1 -1
- package/dist/middleware/planLimits.js +22 -1
- package/dist/middleware/planLimits.js.map +1 -1
- package/dist/middleware/rateLimit.d.ts.map +1 -1
- package/dist/middleware/rateLimit.js +17 -3
- package/dist/middleware/rateLimit.js.map +1 -1
- package/dist/ports/entitlements.d.ts +8 -0
- package/dist/ports/entitlements.d.ts.map +1 -1
- package/dist/ports/kv.d.ts +7 -1
- package/dist/ports/kv.d.ts.map +1 -1
- package/dist/ports/rate-limit.d.ts +5 -0
- package/dist/ports/rate-limit.d.ts.map +1 -1
- package/dist/providers/static-entitlements.d.ts +4 -2
- package/dist/providers/static-entitlements.d.ts.map +1 -1
- package/dist/providers/static-entitlements.js +8 -4
- package/dist/providers/static-entitlements.js.map +1 -1
- package/dist/routes/workspace.d.ts.map +1 -1
- package/dist/routes/workspace.js +25 -22
- package/dist/routes/workspace.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { and, eq, inArray, isNotNull, sql } from 'drizzle-orm';
|
|
2
|
+
import { FleetTaskContextSchema } from '@relaycast/types';
|
|
3
|
+
import { actionInvocations } from '../db/schema.js';
|
|
4
|
+
import { codedError } from '../lib/httpError.js';
|
|
5
|
+
const liveStates = ['pending', 'dispatched', 'running'];
|
|
6
|
+
/** Stored in the same insert as the idempotent invocation claim. */
|
|
7
|
+
export function createTaskState(input) {
|
|
8
|
+
const parsed = FleetTaskContextSchema.safeParse(input.task_context);
|
|
9
|
+
if (!parsed.success)
|
|
10
|
+
throw codedError('Task actions require valid input.task_context', 'invalid_task_context', 400);
|
|
11
|
+
return { ...parsed.data, deadline: new Date(Date.now() + parsed.data.timeout_ms).toISOString() };
|
|
12
|
+
}
|
|
13
|
+
export function taskExecution(row) {
|
|
14
|
+
const task = row.taskState;
|
|
15
|
+
if (!task)
|
|
16
|
+
return undefined;
|
|
17
|
+
return {
|
|
18
|
+
execution_id: task.execution_id ?? `${row.id}/${row.dispatchAttempts}`,
|
|
19
|
+
run_id: task.run_id,
|
|
20
|
+
step_id: task.step_id,
|
|
21
|
+
dispatch_id: task.dispatch_id,
|
|
22
|
+
deadline: task.deadline,
|
|
23
|
+
...(task.worker_generation ? { worker_generation: task.worker_generation } : {}),
|
|
24
|
+
...(task.accepted_at ? { accepted_at: task.accepted_at } : {}),
|
|
25
|
+
...(task.accounting ? { accounting: task.accounting } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function receipt(row) {
|
|
29
|
+
return {
|
|
30
|
+
invocation_id: row.id,
|
|
31
|
+
action_name: row.actionName,
|
|
32
|
+
status: row.status,
|
|
33
|
+
task_execution: taskExecution(row),
|
|
34
|
+
output: row.output,
|
|
35
|
+
error: row.error,
|
|
36
|
+
completed_at: row.completedAt?.toISOString() ?? null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function read(db, workspaceId, invocationId) {
|
|
40
|
+
const [row] = await db.select().from(actionInvocations).where(and(eq(actionInvocations.workspaceId, workspaceId), eq(actionInvocations.id, invocationId)));
|
|
41
|
+
return row;
|
|
42
|
+
}
|
|
43
|
+
function owner(row, nodeId, provider) {
|
|
44
|
+
if (!row?.taskState || row.dispatchedNodeId !== nodeId || row.dispatchedProvider !== provider) {
|
|
45
|
+
throw codedError('Task execution is not owned by this provider', 'task_not_found', 404);
|
|
46
|
+
}
|
|
47
|
+
return row;
|
|
48
|
+
}
|
|
49
|
+
function fence(row, executionId, generation) {
|
|
50
|
+
if (taskExecution(row).execution_id !== executionId
|
|
51
|
+
|| (row.taskState.worker_generation !== undefined && row.taskState.worker_generation !== generation)) {
|
|
52
|
+
throw codedError('Task execution fence or worker generation changed', 'stale_task_execution', 409);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function expireTaskInvocation(db, row) {
|
|
56
|
+
if (!row.taskState || !liveStates.includes(row.status) || row.taskState.deadline > new Date().toISOString())
|
|
57
|
+
return row;
|
|
58
|
+
const [updated] = await db.update(actionInvocations).set({
|
|
59
|
+
status: 'failed', error: 'task_deadline_exceeded', completedAt: new Date(),
|
|
60
|
+
}).where(and(eq(actionInvocations.id, row.id), eq(actionInvocations.workspaceId, row.workspaceId), inArray(actionInvocations.status, liveStates), sql `json_extract(${actionInvocations.taskState}, '$.deadline') <= ${new Date().toISOString()}`)).returning();
|
|
61
|
+
return updated ?? (await read(db, row.workspaceId, row.id));
|
|
62
|
+
}
|
|
63
|
+
/** Replaying accept reconciles the same generation, including its terminal receipt. */
|
|
64
|
+
export async function acceptTaskInvocation(db, workspaceId, nodeId, provider, message) {
|
|
65
|
+
let row = owner(await read(db, workspaceId, message.invocation_id), nodeId, provider);
|
|
66
|
+
fence(row, message.execution_id, message.worker_generation);
|
|
67
|
+
row = await expireTaskInvocation(db, row);
|
|
68
|
+
if (row.status === 'completed' || row.status === 'failed' || row.status === 'running') {
|
|
69
|
+
return { ...receipt(row), newly_accepted: false };
|
|
70
|
+
}
|
|
71
|
+
const state = {
|
|
72
|
+
...row.taskState, execution_id: message.execution_id,
|
|
73
|
+
worker_generation: message.worker_generation, accepted_at: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
const [accepted] = await db.update(actionInvocations).set({ status: 'running', taskState: state }).where(and(eq(actionInvocations.id, row.id), eq(actionInvocations.workspaceId, workspaceId), eq(actionInvocations.status, 'dispatched'), eq(actionInvocations.dispatchedNodeId, nodeId), eq(actionInvocations.dispatchedProvider, provider), eq(actionInvocations.dispatchAttempts, row.dispatchAttempts), sql `json_extract(${actionInvocations.taskState}, '$.worker_generation') IS NULL`, sql `json_extract(${actionInvocations.taskState}, '$.deadline') > ${new Date().toISOString()}`)).returning();
|
|
76
|
+
if (accepted)
|
|
77
|
+
return { ...receipt(accepted), newly_accepted: true };
|
|
78
|
+
row = owner(await read(db, workspaceId, row.id), nodeId, provider);
|
|
79
|
+
fence(row, message.execution_id, message.worker_generation);
|
|
80
|
+
if (['running', 'completed', 'failed'].includes(row.status))
|
|
81
|
+
return { ...receipt(row), newly_accepted: false };
|
|
82
|
+
throw codedError('Task is not in the dispatched execution', 'task_not_dispatched', 409);
|
|
83
|
+
}
|
|
84
|
+
function canonical(value) {
|
|
85
|
+
if (Array.isArray(value))
|
|
86
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
87
|
+
if (value !== null && typeof value === 'object')
|
|
88
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
89
|
+
return JSON.stringify(value) ?? 'null';
|
|
90
|
+
}
|
|
91
|
+
/** Returns undefined only for a legacy short action. A task always needs a durable reply. */
|
|
92
|
+
export async function completeTaskInvocation(db, workspaceId, nodeId, provider, message) {
|
|
93
|
+
const candidate = await read(db, workspaceId, message.invocation_id);
|
|
94
|
+
if (!candidate?.taskState) {
|
|
95
|
+
if (message.execution_id || message.worker_generation || message.final !== undefined || message.accounting) {
|
|
96
|
+
throw codedError('Task result fields require a task invocation', 'task_not_found', 404);
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
if (!message.id || !message.execution_id || !message.worker_generation || message.final === undefined) {
|
|
101
|
+
throw codedError('Task results require id, execution_id, worker_generation and final', 'invalid_task_result', 400);
|
|
102
|
+
}
|
|
103
|
+
let row = owner(candidate, nodeId, provider);
|
|
104
|
+
fence(row, message.execution_id, message.worker_generation);
|
|
105
|
+
row = await expireTaskInvocation(db, row);
|
|
106
|
+
if (!row.taskState.accepted_at)
|
|
107
|
+
throw codedError('Task must be accepted before reporting results', 'task_not_accepted', 409);
|
|
108
|
+
if (!message.final)
|
|
109
|
+
return { receipt: receipt(row), completed: undefined };
|
|
110
|
+
if (message.error !== undefined && !message.error.trim())
|
|
111
|
+
throw codedError('Task failure must name a reason', 'invalid_task_result', 400);
|
|
112
|
+
const output = message.output ?? null;
|
|
113
|
+
const error = message.error ?? null;
|
|
114
|
+
const status = error === null ? 'completed' : 'failed';
|
|
115
|
+
const matches = (value) => value.status === status
|
|
116
|
+
&& canonical(value.output) === canonical(output) && value.error === error
|
|
117
|
+
&& canonical(value.taskState?.accounting) === canonical(message.accounting);
|
|
118
|
+
if (row.status === 'completed' || row.status === 'failed') {
|
|
119
|
+
if (!matches(row))
|
|
120
|
+
throw codedError('Task already has a different terminal result', 'task_result_conflict', 409);
|
|
121
|
+
return { receipt: receipt(row), completed: undefined };
|
|
122
|
+
}
|
|
123
|
+
const [completed] = await db.update(actionInvocations).set({
|
|
124
|
+
status, output, error, completedAt: new Date(),
|
|
125
|
+
taskState: { ...row.taskState, ...(message.accounting ? { accounting: message.accounting } : {}) },
|
|
126
|
+
}).where(and(eq(actionInvocations.id, row.id), eq(actionInvocations.workspaceId, workspaceId), eq(actionInvocations.status, 'running'), eq(actionInvocations.dispatchedNodeId, nodeId), eq(actionInvocations.dispatchedProvider, provider), eq(actionInvocations.dispatchAttempts, row.dispatchAttempts), sql `json_extract(${actionInvocations.taskState}, '$.execution_id') = ${message.execution_id}`, sql `json_extract(${actionInvocations.taskState}, '$.worker_generation') = ${message.worker_generation}`, sql `json_extract(${actionInvocations.taskState}, '$.deadline') > ${new Date().toISOString()}`)).returning();
|
|
127
|
+
if (completed)
|
|
128
|
+
return { receipt: receipt(completed), completed };
|
|
129
|
+
row = owner(await read(db, workspaceId, row.id), nodeId, provider);
|
|
130
|
+
fence(row, message.execution_id, message.worker_generation);
|
|
131
|
+
if (matches(row))
|
|
132
|
+
return { receipt: receipt(row), completed: undefined };
|
|
133
|
+
throw codedError('Task completion lost its state transition', 'task_result_conflict', 409);
|
|
134
|
+
}
|
|
135
|
+
/** Absolute deadline bounds both never-accepted dispatches and accepted tasks with no final result. */
|
|
136
|
+
export async function expireTaskInvocations(db) {
|
|
137
|
+
await db.update(actionInvocations).set({
|
|
138
|
+
status: 'failed', error: 'task_deadline_exceeded', completedAt: new Date(),
|
|
139
|
+
}).where(and(isNotNull(actionInvocations.taskState), inArray(actionInvocations.status, liveStates), sql `json_extract(${actionInvocations.taskState}, '$.deadline') <= ${new Date().toISOString()}`));
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=taskInvocation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taskInvocation.js","sourceRoot":"","sources":["../../src/engine/taskInvocation.ts"],"names":[],"mappings":"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"}
|
package/dist/engine/usage.d.ts
CHANGED
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
* All functions accept a {@link KeyValueStore} port.
|
|
4
4
|
*/
|
|
5
5
|
import type { KeyValueStore } from '../ports/kv.js';
|
|
6
|
+
/**
|
|
7
|
+
* Usage counters are scoped to a UTC calendar month, because plan quotas
|
|
8
|
+
* (`api_calls`) are *per billing period*. An unscoped counter accumulates for
|
|
9
|
+
* the lifetime of the workspace, so once it passes the plan ceiling every
|
|
10
|
+
* authenticated request — including the `GET /v1/workspace` identity read a
|
|
11
|
+
* client needs to diagnose the failure — returns 429 forever, with no window
|
|
12
|
+
* that ever clears it and no retry policy that can absorb it.
|
|
13
|
+
*
|
|
14
|
+
* The period lives in the key, so rollover is the reset: nothing has to run on
|
|
15
|
+
* a schedule and no adapter has to support expiry for the quota to be correct.
|
|
16
|
+
*/
|
|
17
|
+
export declare function usagePeriod(at?: Date): string;
|
|
18
|
+
export declare function usageCounterKey(workspaceId: string, metric: string, at?: Date): string;
|
|
19
|
+
/** Unix ms at which the current usage period rolls over (start of the next UTC month). */
|
|
20
|
+
export declare function usagePeriodResetAt(at?: Date): number;
|
|
21
|
+
/** TTL for a period counter — storage hygiene only; the key drives the reset. */
|
|
22
|
+
export declare function usagePeriodTtlSeconds(at?: Date): number;
|
|
6
23
|
export declare function incrementUsage(kv: KeyValueStore, workspaceId: string, metric: string, amount?: number): Promise<number>;
|
|
7
|
-
export declare function getUsageCounters(kv: KeyValueStore, workspaceId: string): Promise<{
|
|
8
|
-
messages: number;
|
|
9
|
-
api_calls: number;
|
|
10
|
-
files: number;
|
|
11
|
-
file_bytes: number;
|
|
12
|
-
ws_minutes: number;
|
|
13
|
-
}>;
|
|
14
|
-
export declare function resetUsageCounters(kv: KeyValueStore, workspaceId: string): Promise<void>;
|
|
15
24
|
export declare function getUsageMetric(kv: KeyValueStore, workspaceId: string, metric: string): Promise<number>;
|
|
16
25
|
//# sourceMappingURL=usage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/engine/usage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/engine/usage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,EAAE,GAAE,IAAiB,GAAG,MAAM,CAEzD;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAE,IAAiB,GAAG,MAAM,CAElG;AAED,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,EAAE,GAAE,IAAiB,GAAG,MAAM,CAEhE;AASD,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,EAAE,GAAE,IAAiB,GAAG,MAAM,CAEnE;AAED,wBAAsB,cAAc,CAAC,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,MAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAIhI;AAED,wBAAsB,cAAc,CAAC,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5G"}
|
package/dist/engine/usage.js
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Usage counters are scoped to a UTC calendar month, because plan quotas
|
|
3
|
+
* (`api_calls`) are *per billing period*. An unscoped counter accumulates for
|
|
4
|
+
* the lifetime of the workspace, so once it passes the plan ceiling every
|
|
5
|
+
* authenticated request — including the `GET /v1/workspace` identity read a
|
|
6
|
+
* client needs to diagnose the failure — returns 429 forever, with no window
|
|
7
|
+
* that ever clears it and no retry policy that can absorb it.
|
|
8
|
+
*
|
|
9
|
+
* The period lives in the key, so rollover is the reset: nothing has to run on
|
|
10
|
+
* a schedule and no adapter has to support expiry for the quota to be correct.
|
|
11
|
+
*/
|
|
12
|
+
export function usagePeriod(at = new Date()) {
|
|
13
|
+
return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
14
|
+
}
|
|
15
|
+
export function usageCounterKey(workspaceId, metric, at = new Date()) {
|
|
16
|
+
return `usage:${workspaceId}:${metric}:${usagePeriod(at)}`;
|
|
4
17
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return {
|
|
9
|
-
messages: parseInt(values[0] || '0', 10),
|
|
10
|
-
api_calls: parseInt(values[1] || '0', 10),
|
|
11
|
-
files: parseInt(values[2] || '0', 10),
|
|
12
|
-
file_bytes: parseInt(values[3] || '0', 10),
|
|
13
|
-
ws_minutes: parseInt(values[4] || '0', 10),
|
|
14
|
-
};
|
|
18
|
+
/** Unix ms at which the current usage period rolls over (start of the next UTC month). */
|
|
19
|
+
export function usagePeriodResetAt(at = new Date()) {
|
|
20
|
+
return Date.UTC(at.getUTCFullYear(), at.getUTCMonth() + 1, 1);
|
|
15
21
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A day of slack past rollover, so a counter written at the very end of a
|
|
24
|
+
* period is still readable by a concurrent request that resolved the period a
|
|
25
|
+
* moment earlier.
|
|
26
|
+
*/
|
|
27
|
+
const PERIOD_TTL_GRACE_SECONDS = 86_400;
|
|
28
|
+
/** TTL for a period counter — storage hygiene only; the key drives the reset. */
|
|
29
|
+
export function usagePeriodTtlSeconds(at = new Date()) {
|
|
30
|
+
return Math.ceil((usagePeriodResetAt(at) - at.getTime()) / 1000) + PERIOD_TTL_GRACE_SECONDS;
|
|
31
|
+
}
|
|
32
|
+
export async function incrementUsage(kv, workspaceId, metric, amount = 1) {
|
|
33
|
+
const at = new Date();
|
|
34
|
+
// Atomic increment — avoids the lost-update race of a get→parse→put round-trip.
|
|
35
|
+
return kv.increment(usageCounterKey(workspaceId, metric, at), amount, usagePeriodTtlSeconds(at));
|
|
19
36
|
}
|
|
20
37
|
export async function getUsageMetric(kv, workspaceId, metric) {
|
|
21
|
-
const value = await kv.get(
|
|
22
|
-
return parseInt(value || '0', 10);
|
|
38
|
+
const value = await kv.get(usageCounterKey(workspaceId, metric));
|
|
39
|
+
return parseInt(value || '0', 10) || 0;
|
|
23
40
|
}
|
|
24
41
|
//# sourceMappingURL=usage.js.map
|
package/dist/engine/usage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/engine/usage.ts"],"names":[],"mappings":"AAMA,MAAM,
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/engine/usage.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,KAAW,IAAI,IAAI,EAAE;IAC/C,OAAO,GAAG,EAAE,CAAC,cAAc,EAAE,IAAI,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACnF,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,MAAc,EAAE,KAAW,IAAI,IAAI,EAAE;IACxF,OAAO,SAAS,WAAW,IAAI,MAAM,IAAI,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;AAC7D,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,kBAAkB,CAAC,KAAW,IAAI,IAAI,EAAE;IACtD,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,cAAc,EAAE,EAAE,EAAE,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAChE,CAAC;AAED;;;;GAIG;AACH,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAExC,iFAAiF;AACjF,MAAM,UAAU,qBAAqB,CAAC,KAAW,IAAI,IAAI,EAAE;IACzD,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,wBAAwB,CAAC;AAC9F,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,EAAiB,EAAE,WAAmB,EAAE,MAAc,EAAE,SAAiB,CAAC;IAC7G,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC;IACtB,gFAAgF;IAChF,OAAO,EAAE,CAAC,SAAS,CAAC,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC;AACnG,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,EAAiB,EAAE,WAAmB,EAAE,MAAc;IACzF,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IACjE,OAAO,QAAQ,CAAC,KAAK,IAAI,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type { AppEnv, AppVariables, EngineRuntime } from './env.js';
|
|
|
4
4
|
export type { EngineDeps, EnginePorts, EngineProviders, EngineConfig, EngineDb, EngineEvent, RealtimeBus, ConnectionRegistry, UpgradeArgs, PresenceTracker, RateLimiter, RateLimitResult, FileStorage, KeyValueStore, EventQueue, QueuedEvent, AuthProvider, AuthResult, AuthRequire, Workspace, Agent, EntitlementsProvider, PlanLimits, UsageMetric, TelemetrySink, TelemetryEvent, } from './ports/index.js';
|
|
5
5
|
export { SqliteApiKeyAuthProvider, hashToken } from './auth/index.js';
|
|
6
6
|
export { StaticEntitlementsProvider, PLAN_LIMITS } from './providers/static-entitlements.js';
|
|
7
|
+
export { getUsageMetric, usageCounterKey, usagePeriod, usagePeriodResetAt, } from './engine/usage.js';
|
|
7
8
|
export { NoopTelemetrySink } from './providers/noop-telemetry.js';
|
|
8
9
|
export { getDb, healthCheck as dbHealthCheck } from './db/index.js';
|
|
9
10
|
export type { Db } from './db/index.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,EAAG,OAAgB,CAAC;AAG/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGpE,YAAY,EACV,UAAU,EACV,WAAW,EACX,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,WAAW,EACX,eAAe,EACf,WAAW,EACX,aAAa,EACb,UAAU,EACV,WAAW,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,SAAS,EACT,KAAK,EACL,oBAAoB,EACpB,UAAU,EACV,WAAW,EACX,aAAa,EACb,cAAc,GACf,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,wBAAwB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,0BAA0B,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,10 @@ export { createEngine } from './engine.js';
|
|
|
4
4
|
// OSS default providers (self-host). Cloud injects its own.
|
|
5
5
|
export { SqliteApiKeyAuthProvider, hashToken } from './auth/index.js';
|
|
6
6
|
export { StaticEntitlementsProvider, PLAN_LIMITS } from './providers/static-entitlements.js';
|
|
7
|
+
// Usage-counter key contract. An `EntitlementsProvider` reads the counters the
|
|
8
|
+
// engine writes, so it has to agree with the engine on the billing period the
|
|
9
|
+
// key is scoped to — build the key with these rather than by hand.
|
|
10
|
+
export { getUsageMetric, usageCounterKey, usagePeriod, usagePeriodResetAt, } from './engine/usage.js';
|
|
7
11
|
export { NoopTelemetrySink } from './providers/noop-telemetry.js';
|
|
8
12
|
// Database helpers + schema for adapters and migrations.
|
|
9
13
|
export { getDb, healthCheck as dbHealthCheck } from './db/index.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG,OAAgB,CAAC;AAE/C,2DAA2D;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAiC3C,4DAA4D;AAC5D,OAAO,EAAE,wBAAwB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,0BAA0B,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAC7F,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,yDAAyD;AACzD,OAAO,EAAE,KAAK,EAAE,WAAW,IAAI,aAAa,EAAE,MAAM,eAAe,CAAC;AAEpE,OAAO,KAAK,MAAM,MAAM,gBAAgB,CAAC;AAEzC,+EAA+E;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EACL,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EACpB,uBAAuB,EACvB,8BAA8B,GAC/B,MAAM,qCAAqC,CAAC;AAM7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,wEAAwE;AACxE,4EAA4E;AAC5E,gFAAgF;AAChF,8EAA8E;AAC9E,gFAAgF;AAChF,yCAAyC;AACzC,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACjG,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACvF,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAElG,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAG5D,6EAA6E;AAC7E,4EAA4E;AAC5E,yEAAyE;AACzE,4CAA4C;AAC5C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,aAAa,EACb,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAGhC,yEAAyE;AACzE,8EAA8E;AAC9E,4EAA4E;AAC5E,qBAAqB;AACrB,OAAO,EACL,YAAY,EACZ,gCAAgC,EAChC,yBAAyB,EACzB,4BAA4B,EAC5B,gCAAgC,GACjC,MAAM,uBAAuB,CAAC;AAK/B,yEAAyE;AACzE,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,uBAAuB,CAAC;AAE/B,2EAA2E;AAC3E,+EAA+E;AAC/E,8BAA8B;AAC9B,OAAO,EACL,oBAAoB,EACpB,8BAA8B,EAC9B,mBAAmB,EACnB,kCAAkC,EAClC,8BAA8B,GAC/B,MAAM,6BAA6B,CAAC;AAGrC,iBAAiB;AACjB,OAAO,EACL,UAAU,EACV,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG,OAAgB,CAAC;AAE/C,2DAA2D;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAiC3C,4DAA4D;AAC5D,OAAO,EAAE,wBAAwB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,0BAA0B,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAC7F,+EAA+E;AAC/E,8EAA8E;AAC9E,mEAAmE;AACnE,OAAO,EACL,cAAc,EACd,eAAe,EACf,WAAW,EACX,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,yDAAyD;AACzD,OAAO,EAAE,KAAK,EAAE,WAAW,IAAI,aAAa,EAAE,MAAM,eAAe,CAAC;AAEpE,OAAO,KAAK,MAAM,MAAM,gBAAgB,CAAC;AAEzC,+EAA+E;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EACL,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EACpB,uBAAuB,EACvB,8BAA8B,GAC/B,MAAM,qCAAqC,CAAC;AAM7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,wEAAwE;AACxE,4EAA4E;AAC5E,gFAAgF;AAChF,8EAA8E;AAC9E,gFAAgF;AAChF,yCAAyC;AACzC,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACjG,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACvF,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAElG,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAG5D,6EAA6E;AAC7E,4EAA4E;AAC5E,yEAAyE;AACzE,4CAA4C;AAC5C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,aAAa,EACb,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAGhC,yEAAyE;AACzE,8EAA8E;AAC9E,4EAA4E;AAC5E,qBAAqB;AACrB,OAAO,EACL,YAAY,EACZ,gCAAgC,EAChC,yBAAyB,EACzB,4BAA4B,EAC5B,gCAAgC,GACjC,MAAM,uBAAuB,CAAC;AAK/B,yEAAyE;AACzE,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,uBAAuB,CAAC;AAE/B,2EAA2E;AAC3E,+EAA+E;AAC/E,8BAA8B;AAC9B,OAAO,EACL,oBAAoB,EACpB,8BAA8B,EAC9B,mBAAmB,EACnB,kCAAkC,EAClC,8BAA8B,GAC/B,MAAM,6BAA6B,CAAC;AAGrC,iBAAiB;AACjB,OAAO,EACL,UAAU,EACV,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
/** Fixed window used by the per-minute rate limiter. */
|
|
3
|
+
export declare const RATE_LIMIT_WINDOW_MS = 60000;
|
|
4
|
+
/** Index of the fixed window containing `now` — the minute bucket in a bucket key. */
|
|
5
|
+
export declare function rateLimitWindow(now: number): number;
|
|
6
|
+
/** Unix ms at which the fixed window containing `now` rolls over. */
|
|
7
|
+
export declare function rateLimitWindowResetAt(now: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* Whole seconds to wait before retrying. Floored at 1: a `Retry-After: 0`
|
|
10
|
+
* invites an immediate retry that is certain to be throttled again.
|
|
11
|
+
*/
|
|
12
|
+
export declare function retryAfterSeconds(resetAtMs: number, now?: number): number;
|
|
13
|
+
/**
|
|
14
|
+
* Attach the retry contract to a 429. Without it a caller can only guess a
|
|
15
|
+
* backoff, and — worse — cannot distinguish a per-minute throttle that clears
|
|
16
|
+
* in seconds from an exhausted plan quota that no bounded retry will outlast,
|
|
17
|
+
* so it burns its whole retry budget on a condition retrying cannot fix.
|
|
18
|
+
*/
|
|
19
|
+
export declare function setRetryContract(c: Context, resetAtMs: number, now?: number): void;
|
|
20
|
+
//# sourceMappingURL=throttle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"throttle.d.ts","sourceRoot":"","sources":["../../src/lib/throttle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,wDAAwD;AACxD,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,qEAAqE;AACrE,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CAErF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,IAAI,CAG9F"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Fixed window used by the per-minute rate limiter. */
|
|
2
|
+
export const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
3
|
+
/** Index of the fixed window containing `now` — the minute bucket in a bucket key. */
|
|
4
|
+
export function rateLimitWindow(now) {
|
|
5
|
+
return Math.floor(now / RATE_LIMIT_WINDOW_MS);
|
|
6
|
+
}
|
|
7
|
+
/** Unix ms at which the fixed window containing `now` rolls over. */
|
|
8
|
+
export function rateLimitWindowResetAt(now) {
|
|
9
|
+
return (rateLimitWindow(now) + 1) * RATE_LIMIT_WINDOW_MS;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Whole seconds to wait before retrying. Floored at 1: a `Retry-After: 0`
|
|
13
|
+
* invites an immediate retry that is certain to be throttled again.
|
|
14
|
+
*/
|
|
15
|
+
export function retryAfterSeconds(resetAtMs, now = Date.now()) {
|
|
16
|
+
return Math.max(1, Math.ceil((resetAtMs - now) / 1000));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Attach the retry contract to a 429. Without it a caller can only guess a
|
|
20
|
+
* backoff, and — worse — cannot distinguish a per-minute throttle that clears
|
|
21
|
+
* in seconds from an exhausted plan quota that no bounded retry will outlast,
|
|
22
|
+
* so it burns its whole retry budget on a condition retrying cannot fix.
|
|
23
|
+
*/
|
|
24
|
+
export function setRetryContract(c, resetAtMs, now = Date.now()) {
|
|
25
|
+
c.header('Retry-After', String(retryAfterSeconds(resetAtMs, now)));
|
|
26
|
+
c.header('X-RateLimit-Reset', String(Math.ceil(resetAtMs / 1000)));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=throttle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"throttle.js","sourceRoot":"","sources":["../../src/lib/throttle.ts"],"names":[],"mappings":"AAEA,wDAAwD;AACxD,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE3C,sFAAsF;AACtF,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,oBAAoB,CAAC,CAAC;AAChD,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,oBAAoB,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IAC3E,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,CAAU,EAAE,SAAiB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IACtF,CAAC,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,iBAAiB,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACnE,CAAC,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AACrE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planLimits.d.ts","sourceRoot":"","sources":["../../src/middleware/planLimits.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"planLimits.d.ts","sourceRoot":"","sources":["../../src/middleware/planLimits.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAwB,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAMlF,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAmBlE,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,kEA+BjD"}
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { createMiddleware } from 'hono/factory';
|
|
2
|
+
import { usagePeriodResetAt } from '../engine/usage.js';
|
|
2
3
|
import { jsonError } from '../lib/httpResponse.js';
|
|
4
|
+
import { setRetryContract } from '../lib/throttle.js';
|
|
3
5
|
// Re-exported for back-compat with callers/tests that imported the table here.
|
|
4
6
|
export { PLAN_LIMITS } from '../providers/static-entitlements.js';
|
|
7
|
+
/**
|
|
8
|
+
* A provider billing on its own cycle reports that cycle's end. An absent or
|
|
9
|
+
* failing hook falls back to the engine's UTC-month period, so how the reset is
|
|
10
|
+
* *reported* can never change the throttling decision itself.
|
|
11
|
+
*/
|
|
12
|
+
async function resolveUsageResetAt(entitlements, workspaceId, metric) {
|
|
13
|
+
try {
|
|
14
|
+
const reported = await entitlements.getUsageResetAt?.(workspaceId, metric);
|
|
15
|
+
if (typeof reported === 'number' && Number.isFinite(reported))
|
|
16
|
+
return reported;
|
|
17
|
+
}
|
|
18
|
+
catch { /* fall through to the engine period */ }
|
|
19
|
+
return usagePeriodResetAt();
|
|
20
|
+
}
|
|
5
21
|
export function checkPlanLimit(metric) {
|
|
6
22
|
return createMiddleware(async (c, next) => {
|
|
7
23
|
const workspace = c.get('workspace');
|
|
@@ -27,7 +43,12 @@ export function checkPlanLimit(metric) {
|
|
|
27
43
|
const current = await entitlements.getUsage(workspace.id, metric);
|
|
28
44
|
if (current >= limit) {
|
|
29
45
|
const plan = workspace.plan || 'free';
|
|
30
|
-
|
|
46
|
+
// Quota clears on the billing period, not on a backoff. Advertising the
|
|
47
|
+
// real reset lets a caller with a bounded retry budget fail fast instead
|
|
48
|
+
// of spending it on a condition retrying cannot fix.
|
|
49
|
+
const resetAt = await resolveUsageResetAt(entitlements, workspace.id, metric);
|
|
50
|
+
setRetryContract(c, resetAt);
|
|
51
|
+
return jsonError(c, 'plan_limit_exceeded', `Plan limit exceeded for ${metric}. Current plan: ${plan}. Limit: ${limit} per usage period. Current usage: ${current}. Quota resets at ${new Date(resetAt).toISOString()}. Upgrade your plan to increase limits.`, 429);
|
|
31
52
|
}
|
|
32
53
|
}
|
|
33
54
|
catch { /* fail open */ }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planLimits.js","sourceRoot":"","sources":["../../src/middleware/planLimits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"planLimits.js","sourceRoot":"","sources":["../../src/middleware/planLimits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAElE;;;;GAIG;AACH,KAAK,UAAU,mBAAmB,CAChC,YAAkC,EAClC,WAAmB,EACnB,MAAmB;IAEnB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC3E,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,QAAQ,CAAC;IACjF,CAAC;IAAC,MAAM,CAAC,CAAC,uCAAuC,CAAC,CAAC;IACnD,OAAO,kBAAkB,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAmB;IAChD,OAAO,gBAAgB,CAAS,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QAChD,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACrC,IAAI,CAAC,SAAS,EAAE,CAAC;YAAC,MAAM,IAAI,EAAE,CAAC;YAAC,OAAO;QAAC,CAAC;QAEzC,MAAM,EAAE,YAAY,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,MAAM,CAAC;QACX,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;YAAC,MAAM,IAAI,EAAE,CAAC;YAAC,OAAO;QAAC,CAAC;QAE9E,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;YAClE,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;gBACrB,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,IAAI,MAAM,CAAC;gBACtC,wEAAwE;gBACxE,yEAAyE;gBACzE,qDAAqD;gBACrD,MAAM,OAAO,GAAG,MAAM,mBAAmB,CAAC,YAAY,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;gBAC9E,gBAAgB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;gBAC7B,OAAO,SAAS,CAAC,CAAC,EAAE,qBAAqB,EAAE,2BAA2B,MAAM,mBAAmB,IAAI,YAAY,KAAK,qCAAqC,OAAO,qBAAqB,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,yCAAyC,EAAE,GAAG,CAAC,CAAC;YACtQ,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC;QAE3B,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AA0CxC,eAAO,MAAM,SAAS,gEAuDpB,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createMiddleware } from 'hono/factory';
|
|
2
2
|
import { jsonError } from '../lib/httpResponse.js';
|
|
3
|
+
import { RATE_LIMIT_WINDOW_MS, rateLimitWindow, rateLimitWindowResetAt, setRetryContract } from '../lib/throttle.js';
|
|
3
4
|
import { checkPlanLimit } from './planLimits.js';
|
|
4
5
|
import { presenceRefresh } from './presenceRefresh.js';
|
|
5
6
|
import { usageTracker } from './usageTracker.js';
|
|
@@ -16,6 +17,14 @@ const ROUTE_MULTIPLIERS = {
|
|
|
16
17
|
'POST:/messages/*/reactions': 0.4,
|
|
17
18
|
'GET:/channels/*/messages': 1.0,
|
|
18
19
|
'GET:/agents/presence': 0.3,
|
|
20
|
+
// Workspace identity is the control-plane read a client makes before it can
|
|
21
|
+
// do anything else — credential probes, launch preflight, and the lookup a
|
|
22
|
+
// caller uses to find out *why* it is being throttled. In the shared `global`
|
|
23
|
+
// bucket that one request competes with all of the workspace's data-plane
|
|
24
|
+
// traffic, so a busy workspace starves it and no client-side retry helps:
|
|
25
|
+
// every attempt lands in the same saturated bucket. Its own bucket keeps
|
|
26
|
+
// identity reachable while the data plane is at its ceiling.
|
|
27
|
+
'GET:/workspace': 1.0,
|
|
19
28
|
};
|
|
20
29
|
function getRouteKey(method, path) {
|
|
21
30
|
// Normalize path: /v1/channels/foo/messages -> /channels/*/messages
|
|
@@ -45,8 +54,8 @@ export const rateLimit = createMiddleware(async (c, next) => {
|
|
|
45
54
|
const routeKey = getRouteKey(c.req.method, c.req.path);
|
|
46
55
|
// The rate limiter port is not workspace-scoped, so the workspace id is part
|
|
47
56
|
// of the bucket key (the Cloudflare adapter previously scoped via the DO id).
|
|
48
|
-
const
|
|
49
|
-
const bucketKey = `${workspace.id}:${routeKey ?? 'global'}:${
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const bucketKey = `${workspace.id}:${routeKey ?? 'global'}:${rateLimitWindow(now)}`;
|
|
50
59
|
// Resolve the per-minute limit. If entitlements are unavailable, fall back to
|
|
51
60
|
// a conservative default and still enforce it — an entitlements outage must
|
|
52
61
|
// NOT translate into unlimited throughput.
|
|
@@ -59,10 +68,15 @@ export const rateLimit = createMiddleware(async (c, next) => {
|
|
|
59
68
|
}
|
|
60
69
|
const limit = routeKey ? Math.ceil(globalLimit * ROUTE_MULTIPLIERS[routeKey]) : globalLimit;
|
|
61
70
|
try {
|
|
62
|
-
const { allowed, count, remaining } = await rateLimiter.check({ bucketKey, limit, windowMs:
|
|
71
|
+
const { allowed, count, remaining } = await rateLimiter.check({ bucketKey, limit, windowMs: RATE_LIMIT_WINDOW_MS });
|
|
72
|
+
const resetAt = rateLimitWindowResetAt(now);
|
|
63
73
|
c.header('X-RateLimit-Limit', String(limit));
|
|
64
74
|
c.header('X-RateLimit-Remaining', String(remaining ?? Math.max(0, limit - count)));
|
|
75
|
+
c.header('X-RateLimit-Reset', String(Math.ceil(resetAt / 1000)));
|
|
65
76
|
if (!allowed) {
|
|
77
|
+
// Transient by construction: the bucket is keyed to this minute, so the
|
|
78
|
+
// advertised wait is the real one.
|
|
79
|
+
setRetryContract(c, resetAt, now);
|
|
66
80
|
return jsonError(c, 'rate_limit_exceeded', `Rate limit exceeded. ${limit} requests per minute allowed for ${workspace.plan} plan.`, 429);
|
|
67
81
|
}
|
|
68
82
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,8EAA8E;AAC9E,6EAA6E;AAC7E,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,qBAAqB,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC;AAE1D,8EAA8E;AAC9E,+DAA+D;AAC/D,MAAM,iBAAiB,GAA2B;IAChD,2BAA2B,EAAE,GAAG,EAAE,sCAAsC;IACxE,UAAU,EAAE,GAAG;IACf,gBAAgB,EAAE,GAAG;IACrB,4BAA4B,EAAE,GAAG;IACjC,0BAA0B,EAAE,GAAG;IAC/B,sBAAsB,EAAE,GAAG;
|
|
1
|
+
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,5 +23,13 @@ export interface EntitlementsProvider {
|
|
|
23
23
|
getLimits(workspace: Workspace): Promise<PlanLimits>;
|
|
24
24
|
/** Current usage for a metric, compared against the limit before allowing an action. */
|
|
25
25
|
getUsage(workspaceId: string, metric: UsageMetric): Promise<number>;
|
|
26
|
+
/**
|
|
27
|
+
* Unix ms at which `metric`'s usage window rolls over, used to put an honest
|
|
28
|
+
* `Retry-After` on a quota 429 so callers can tell a wait-and-retry condition
|
|
29
|
+
* from one that no retry will clear. Optional: a provider billing on its own
|
|
30
|
+
* cycle reports that cycle's end; omitting it falls back to the engine's
|
|
31
|
+
* UTC-month period.
|
|
32
|
+
*/
|
|
33
|
+
getUsageResetAt?(workspaceId: string, metric: UsageMetric): Promise<number>;
|
|
26
34
|
}
|
|
27
35
|
//# sourceMappingURL=entitlements.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entitlements.d.ts","sourceRoot":"","sources":["../../src/ports/entitlements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC;AAE7E,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB;IACnC,sDAAsD;IACtD,SAAS,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAErD,wFAAwF;IACxF,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"entitlements.d.ts","sourceRoot":"","sources":["../../src/ports/entitlements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC;AAE7E,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB;IACnC,sDAAsD;IACtD,SAAS,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAErD,wFAAwF;IACxF,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEpE;;;;;;OAMG;IACH,eAAe,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7E"}
|
package/dist/ports/kv.d.ts
CHANGED
|
@@ -19,7 +19,13 @@ export interface KeyValueStore {
|
|
|
19
19
|
* counters were previously a racy get→parse→put read-modify-write that lost
|
|
20
20
|
* concurrent increments. Adapters must implement this without a lost-update
|
|
21
21
|
* window (the Cloudflare adapter routes it through a Durable Object).
|
|
22
|
+
*
|
|
23
|
+
* `ttlSeconds` sets the expiry when the key is first created (an existing
|
|
24
|
+
* key keeps its expiry, so a counter isn't extended by its own traffic).
|
|
25
|
+
* Adapters that can't express a TTL may ignore it: period-scoped counters
|
|
26
|
+
* carry their period in the key, so ignoring it only leaks a dead key rather
|
|
27
|
+
* than breaking the reset.
|
|
22
28
|
*/
|
|
23
|
-
increment(key: string, delta: number): Promise<number>;
|
|
29
|
+
increment(key: string, delta: number, ttlSeconds?: number): Promise<number>;
|
|
24
30
|
}
|
|
25
31
|
//# sourceMappingURL=kv.d.ts.map
|
package/dist/ports/kv.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kv.d.ts","sourceRoot":"","sources":["../../src/ports/kv.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC
|
|
1
|
+
{"version":3,"file":"kv.d.ts","sourceRoot":"","sources":["../../src/ports/kv.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7E"}
|
|
@@ -11,6 +11,11 @@ export interface RateLimitResult {
|
|
|
11
11
|
remaining: number;
|
|
12
12
|
}
|
|
13
13
|
export interface RateLimiter {
|
|
14
|
+
/**
|
|
15
|
+
* Count this request against `bucketKey` and report whether it is allowed.
|
|
16
|
+
* A rejected request must not consume the bucket, so a throttled client's
|
|
17
|
+
* own retries cannot push `count` past `limit` or extend its window.
|
|
18
|
+
*/
|
|
14
19
|
check(args: {
|
|
15
20
|
bucketKey: string;
|
|
16
21
|
limit: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/ports/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAC/F"}
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/ports/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAC/F"}
|
|
@@ -10,8 +10,9 @@ export declare const PLAN_LIMITS: Record<string, PlanLimits>;
|
|
|
10
10
|
/**
|
|
11
11
|
* Static, table-driven entitlements — the self-host default. Resolves limits
|
|
12
12
|
* from {@link PLAN_LIMITS} keyed by `workspace.plan`, and reads usage counters
|
|
13
|
-
* from the key/value store (the same `usage:<wid>:<metric>` keys the
|
|
14
|
-
* tracker increments
|
|
13
|
+
* from the key/value store (the same `usage:<wid>:<metric>:<period>` keys the
|
|
14
|
+
* usage tracker increments, so both agree on the current billing period). The
|
|
15
|
+
* cloud product injects a billing-backed provider.
|
|
15
16
|
*/
|
|
16
17
|
export declare class StaticEntitlementsProvider implements EntitlementsProvider {
|
|
17
18
|
private readonly kv?;
|
|
@@ -19,5 +20,6 @@ export declare class StaticEntitlementsProvider implements EntitlementsProvider
|
|
|
19
20
|
constructor(kv?: KeyValueStore | undefined, defaultPlan?: string);
|
|
20
21
|
getLimits(workspace: Workspace): Promise<PlanLimits>;
|
|
21
22
|
getUsage(workspaceId: string, metric: UsageMetric): Promise<number>;
|
|
23
|
+
getUsageResetAt(): Promise<number>;
|
|
22
24
|
}
|
|
23
25
|
//# sourceMappingURL=static-entitlements.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"static-entitlements.d.ts","sourceRoot":"","sources":["../../src/providers/static-entitlements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAClG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"static-entitlements.d.ts","sourceRoot":"","sources":["../../src/providers/static-entitlements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAClG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAKlD,CAAC;AAEF;;;;;;GAMG;AACH,qBAAa,0BAA2B,YAAW,oBAAoB;IAEnE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;IACpB,OAAO,CAAC,QAAQ,CAAC,WAAW;gBADX,EAAE,CAAC,EAAE,aAAa,YAAA,EAClB,WAAW,GAAE,MAAmB;IAG7C,SAAS,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAKpD,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IASnE,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;CAGzC"}
|