@retinue/agentkit 0.1.0 → 0.3.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/README.md +59 -277
- package/dist/adapters/audio/openai.d.ts +36 -0
- package/dist/adapters/audio/openai.js +136 -0
- package/dist/adapters/bullmq/lock.d.ts +26 -2
- package/dist/adapters/embeddings/openai.d.ts +45 -0
- package/dist/adapters/embeddings/openai.js +109 -0
- package/dist/adapters/memory/connections.d.ts +10 -0
- package/dist/adapters/memory/connections.js +87 -0
- package/dist/adapters/memory/graph.d.ts +17 -0
- package/dist/adapters/memory/graph.js +267 -0
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.js +3 -0
- package/dist/adapters/memory/rate-limit.d.ts +14 -0
- package/dist/adapters/memory/rate-limit.js +28 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/postgres/connections.d.ts +15 -0
- package/dist/adapters/postgres/connections.js +134 -0
- package/dist/adapters/postgres/graph.d.ts +24 -0
- package/dist/adapters/postgres/graph.js +325 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.js +198 -0
- package/dist/adapters/postgres/run-store.js +26 -2
- package/dist/adapters/postgres/schema.d.ts +61 -2
- package/dist/adapters/postgres/schema.js +95 -10
- package/dist/adapters/redis/index.d.ts +1 -0
- package/dist/adapters/redis/index.js +1 -0
- package/dist/adapters/redis/rate-limit.d.ts +46 -0
- package/dist/adapters/redis/rate-limit.js +55 -0
- package/dist/adapters/supabase/index.d.ts +15 -0
- package/dist/adapters/supabase/index.js +15 -0
- package/dist/adapters/supabase/rls.js +33 -0
- package/dist/agents/agent.d.ts +99 -1
- package/dist/agents/agent.js +215 -16
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +48 -0
- package/dist/agents/engine.js +370 -13
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/capabilities/index.d.ts +5 -1
- package/dist/capabilities/index.js +23 -0
- package/dist/capabilities/runtime.d.ts +8 -0
- package/dist/connections/cipher.d.ts +103 -0
- package/dist/connections/cipher.js +141 -0
- package/dist/connections/index.d.ts +156 -0
- package/dist/connections/index.js +72 -0
- package/dist/connections/oauth/client.d.ts +84 -0
- package/dist/connections/oauth/client.js +141 -0
- package/dist/connections/oauth/index.d.ts +166 -0
- package/dist/connections/oauth/index.js +226 -0
- package/dist/connections/oauth/service.d.ts +90 -0
- package/dist/connections/oauth/service.js +132 -0
- package/dist/connections/pause.d.ts +68 -0
- package/dist/connections/pause.js +97 -0
- package/dist/connections/resolver.d.ts +59 -0
- package/dist/connections/resolver.js +138 -0
- package/dist/connections/resume.d.ts +64 -0
- package/dist/connections/resume.js +99 -0
- package/dist/connections/vault-cipher.d.ts +95 -0
- package/dist/connections/vault-cipher.js +206 -0
- package/dist/core/budget.d.ts +55 -0
- package/dist/core/budget.js +56 -0
- package/dist/core/content-parts.d.ts +26 -2
- package/dist/core/content-parts.js +1 -0
- package/dist/core/context.d.ts +25 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +15 -0
- package/dist/core/events.d.ts +110 -2
- package/dist/core/events.js +33 -2
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/validation.js +19 -0
- package/dist/documents/index.d.ts +14 -0
- package/dist/documents/parsers/text.d.ts +16 -0
- package/dist/documents/parsers/text.js +54 -2
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/guardrails.d.ts +14 -0
- package/dist/entries/guardrails.js +14 -0
- package/dist/entries/knowledge.d.ts +17 -0
- package/dist/entries/knowledge.js +16 -0
- package/dist/entries/mcp-server.d.ts +9 -0
- package/dist/entries/mcp-server.js +9 -0
- package/dist/entries/runtime.d.ts +10 -0
- package/dist/entries/runtime.js +8 -0
- package/dist/entries/testing.d.ts +28 -0
- package/dist/entries/testing.js +28 -0
- package/dist/graphql/resolvers.d.ts +7 -1
- package/dist/graphql/resolvers.js +20 -0
- package/dist/graphql/schema.d.ts +1 -1
- package/dist/graphql/schema.js +44 -0
- package/dist/guardrails/index.d.ts +115 -0
- package/dist/guardrails/index.js +108 -0
- package/dist/guardrails/moderation.d.ts +53 -0
- package/dist/guardrails/moderation.js +75 -0
- package/dist/guardrails/pii.d.ts +75 -0
- package/dist/guardrails/pii.js +193 -0
- package/dist/knowledge/communities.d.ts +166 -0
- package/dist/knowledge/communities.js +377 -0
- package/dist/knowledge/graph-global.d.ts +153 -0
- package/dist/knowledge/graph-global.js +246 -0
- package/dist/knowledge/graph-retrieval.d.ts +150 -0
- package/dist/knowledge/graph-retrieval.js +303 -0
- package/dist/knowledge/graph.d.ts +223 -0
- package/dist/knowledge/graph.js +369 -0
- package/dist/knowledge/index.d.ts +25 -0
- package/dist/knowledge/index.js +37 -1
- package/dist/knowledge/navigate.d.ts +89 -0
- package/dist/knowledge/navigate.js +107 -0
- package/dist/knowledge/retrieval.d.ts +98 -5
- package/dist/knowledge/retrieval.js +158 -28
- package/dist/mcp-server/index.d.ts +168 -0
- package/dist/mcp-server/index.js +175 -0
- package/dist/models/extraction.d.ts +73 -0
- package/dist/models/extraction.js +118 -0
- package/dist/models/index.d.ts +40 -1
- package/dist/models/index.js +22 -1
- package/dist/models/pricing.d.ts +2 -0
- package/dist/models/pricing.js +17 -1
- package/dist/models/provider-factory.js +9 -7
- package/dist/models/streaming.d.ts +111 -1
- package/dist/models/streaming.js +184 -3
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +2 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/worker.d.ts +36 -1
- package/dist/runtime/worker.js +37 -4
- package/dist/security/checklist.js +55 -0
- package/dist/security/findings.js +18 -9
- package/dist/server/bin.d.ts +19 -0
- package/dist/server/bin.js +226 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +7 -4
- package/dist/server/doctor.d.ts +74 -0
- package/dist/server/doctor.js +280 -0
- package/dist/skills/catalogue.d.ts +49 -0
- package/dist/skills/catalogue.js +61 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/telemetry/spans.js +17 -0
- package/dist/testing/conformance/artifact-exports.d.ts +20 -0
- package/dist/testing/conformance/artifact-exports.js +172 -0
- package/dist/testing/conformance/artifacts.d.ts +11 -0
- package/dist/testing/conformance/artifacts.js +316 -0
- package/dist/testing/conformance/audio.d.ts +38 -0
- package/dist/testing/conformance/audio.js +145 -0
- package/dist/testing/conformance/capability.d.ts +23 -0
- package/dist/testing/conformance/capability.js +27 -0
- package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
- package/dist/testing/conformance/checkpoint-store.js +72 -0
- package/dist/testing/conformance/connections.d.ts +14 -0
- package/dist/testing/conformance/connections.js +171 -0
- package/dist/testing/conformance/conversation-store.d.ts +10 -0
- package/dist/testing/conformance/conversation-store.js +60 -0
- package/dist/testing/conformance/evaluation.d.ts +10 -0
- package/dist/testing/conformance/evaluation.js +295 -0
- package/dist/testing/conformance/files.d.ts +19 -0
- package/dist/testing/conformance/files.js +454 -0
- package/dist/testing/conformance/flows.d.ts +16 -0
- package/dist/testing/conformance/flows.js +193 -0
- package/dist/testing/conformance/graph.d.ts +22 -0
- package/dist/testing/conformance/graph.js +500 -0
- package/dist/testing/conformance/hitl.d.ts +25 -0
- package/dist/testing/conformance/hitl.js +523 -0
- package/dist/testing/conformance/index.d.ts +163 -0
- package/dist/testing/conformance/index.js +321 -0
- package/dist/testing/conformance/invariants.d.ts +23 -0
- package/dist/testing/conformance/invariants.js +80 -0
- package/dist/testing/conformance/knowledge.d.ts +41 -0
- package/dist/testing/conformance/knowledge.js +592 -0
- package/dist/testing/conformance/parents.d.ts +50 -0
- package/dist/testing/conformance/parents.js +39 -0
- package/dist/testing/conformance/rate-limit.d.ts +25 -0
- package/dist/testing/conformance/rate-limit.js +71 -0
- package/dist/testing/conformance/records.d.ts +36 -0
- package/dist/testing/conformance/records.js +400 -0
- package/dist/testing/conformance/rollups.d.ts +30 -0
- package/dist/testing/conformance/rollups.js +425 -0
- package/dist/testing/conformance/run-coordinator.d.ts +27 -0
- package/dist/testing/conformance/run-coordinator.js +150 -0
- package/dist/testing/conformance/run-event-log.d.ts +9 -0
- package/dist/testing/conformance/run-event-log.js +138 -0
- package/dist/testing/conformance/run-store.d.ts +12 -0
- package/dist/testing/conformance/run-store.js +294 -0
- package/dist/testing/conformance/session-state.d.ts +38 -0
- package/dist/testing/conformance/session-state.js +198 -0
- package/dist/testing/conformance/usage-limits.d.ts +10 -0
- package/dist/testing/conformance/usage-limits.js +309 -0
- package/dist/testing/conformance.d.ts +8 -0
- package/dist/testing/conformance.js +8 -0
- package/dist/testing/memory-backend.d.ts +39 -0
- package/dist/testing/memory-backend.js +41 -0
- package/dist/testing/pglite.d.ts +55 -0
- package/dist/testing/pglite.js +78 -0
- package/dist/testing/stub-model.d.ts +58 -0
- package/dist/testing/stub-model.js +71 -0
- package/dist/testing/supabase-storage-double.d.ts +35 -0
- package/dist/testing/supabase-storage-double.js +101 -0
- package/dist/toolkit/files.d.ts +125 -0
- package/dist/toolkit/files.js +320 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +31 -1
- package/dist/toolkit/index.js +26 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/sandbox.d.ts +119 -0
- package/dist/toolkit/sandbox.js +239 -0
- package/dist/toolkit/ssrf.d.ts +139 -0
- package/dist/toolkit/ssrf.js +349 -0
- package/dist/toolkit/vendor.d.ts +91 -0
- package/dist/toolkit/vendor.js +153 -0
- package/dist/toolkit/web.d.ts +13 -0
- package/dist/toolkit/web.js +7 -1
- package/dist/tools/budget.d.ts +28 -0
- package/dist/tools/budget.js +35 -0
- package/dist/tools/credentials.d.ts +316 -0
- package/dist/tools/credentials.js +369 -0
- package/dist/tools/define.d.ts +45 -0
- package/dist/tools/define.js +26 -0
- package/dist/tools/find.d.ts +109 -0
- package/dist/tools/find.js +210 -0
- package/dist/tools/index.d.ts +35 -2
- package/dist/tools/index.js +11 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/fs.d.ts +24 -0
- package/dist/tools/library/fs.js +102 -0
- package/dist/tools/library/index.d.ts +37 -2
- package/dist/tools/library/index.js +47 -0
- package/dist/tools/library/shell.d.ts +45 -0
- package/dist/tools/library/shell.js +70 -0
- package/dist/tools/meta-tools.js +8 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +250 -4
- package/dist/usage/index.d.ts +21 -0
- package/dist/usage/index.js +1 -0
- package/dist/usage/rate-limit.d.ts +122 -0
- package/dist/usage/rate-limit.js +131 -0
- package/dist/usage/recorder.js +3 -0
- package/package.json +33 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An OpenAI-compatible embedding adapter — REQ-050 (#209), task #219.
|
|
3
|
+
*
|
|
4
|
+
* The `EmbeddingProvider` port has existed since #136 with **no adapter of any kind**, which means the semantic
|
|
5
|
+
* half of hybrid retrieval has never run against a real model in this repository: every test supplies a stub, and
|
|
6
|
+
* a stub measures the stub. That is why this exists before the eval does — a retrieval score computed over
|
|
7
|
+
* hash-based pseudo-vectors is a number about nothing.
|
|
8
|
+
*
|
|
9
|
+
* OpenAI's shape rather than OpenAI specifically: the same request works against Azure OpenAI, Together, a local
|
|
10
|
+
* `llama.cpp` server and anything else that copied the endpoint, so `baseUrl` is the whole configuration story.
|
|
11
|
+
*
|
|
12
|
+
* ## Two properties worth stating
|
|
13
|
+
*
|
|
14
|
+
* **Order is checked, not assumed.** The API returns objects carrying an `index`, and the port's contract is one
|
|
15
|
+
* vector per input *in order*. A provider that returned them out of order — or dropped one — would silently pair
|
|
16
|
+
* every chunk with its neighbour's vector, and retrieval would still work well enough to look fine. So the
|
|
17
|
+
* response is sorted by `index` and the count is verified.
|
|
18
|
+
*
|
|
19
|
+
* **The model reference records a version.** Providers change what a model id returns without renaming it, and a
|
|
20
|
+
* corpus embedded across such a change has two incomparable halves. The port carries the ref per chunk precisely
|
|
21
|
+
* so that is detectable; this passes the caller's version through rather than inventing one.
|
|
22
|
+
*/
|
|
23
|
+
import { AgentPlatformError } from "../../core/errors.js";
|
|
24
|
+
export const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
|
|
25
|
+
export const DEFAULT_EMBEDDING_BATCH = 96;
|
|
26
|
+
export const createOpenAiEmbeddings = (config) => {
|
|
27
|
+
const modelId = config.modelId ?? DEFAULT_EMBEDDING_MODEL;
|
|
28
|
+
const model = {
|
|
29
|
+
modelId,
|
|
30
|
+
version: config.version ?? "1",
|
|
31
|
+
dimensions: config.dimensions ?? 1536,
|
|
32
|
+
};
|
|
33
|
+
const doFetch = config.fetchImpl ?? fetch;
|
|
34
|
+
const base = (config.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
|
|
35
|
+
const batchSize = config.batchSize ?? DEFAULT_EMBEDDING_BATCH;
|
|
36
|
+
const embedBatch = async (texts) => {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => controller.abort(), config.timeoutMs ?? 60_000);
|
|
39
|
+
let response;
|
|
40
|
+
try {
|
|
41
|
+
response = await doFetch(`${base}/embeddings`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
headers: { authorization: `Bearer ${config.apiKey}`, "content-type": "application/json" },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
model: modelId,
|
|
47
|
+
input: texts,
|
|
48
|
+
// Asked for explicitly, because the 3-series models support shortening and a deployment that pinned
|
|
49
|
+
// 1536 in its column must get 1536 rather than whatever the default becomes.
|
|
50
|
+
...(model.dimensions === 1536 ? {} : { dimensions: model.dimensions }),
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
throw new AgentPlatformError({
|
|
56
|
+
code: error.name === "AbortError" ? "timeout" : "provider_unavailable",
|
|
57
|
+
message: `The embedding endpoint did not respond: ${error.message}`,
|
|
58
|
+
retryable: true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
}
|
|
64
|
+
const payload = (await response.json().catch(() => ({})));
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const rateLimited = response.status === 429;
|
|
67
|
+
throw new AgentPlatformError({
|
|
68
|
+
code: rateLimited ? "rate_limited" : response.status >= 500 ? "provider_unavailable" : "provider_error",
|
|
69
|
+
message: `The embedding endpoint returned ${response.status}: ${payload.error?.message ?? "no message"}`,
|
|
70
|
+
// A 5xx and a rate limit are worth retrying; a 400 means the request is wrong and will stay wrong.
|
|
71
|
+
retryable: rateLimited || response.status >= 500,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const data = [...(payload.data ?? [])].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
75
|
+
if (data.length !== texts.length) {
|
|
76
|
+
throw new AgentPlatformError({
|
|
77
|
+
code: "provider_error",
|
|
78
|
+
message: `Asked for ${texts.length} embeddings and received ${data.length}. Pairing them would attach every chunk to the wrong vector.`,
|
|
79
|
+
retryable: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return data.map((entry, at) => {
|
|
83
|
+
const vector = entry.embedding;
|
|
84
|
+
if (vector === undefined || vector.length !== model.dimensions) {
|
|
85
|
+
throw new AgentPlatformError({
|
|
86
|
+
code: "provider_error",
|
|
87
|
+
message: `Embedding ${at} has ${vector?.length ?? 0} dimensions; this deployment records ${model.dimensions}.`,
|
|
88
|
+
retryable: false,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return vector;
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
model,
|
|
96
|
+
async embed(texts) {
|
|
97
|
+
if (texts.length === 0)
|
|
98
|
+
return [];
|
|
99
|
+
const out = [];
|
|
100
|
+
// Sequential batches, deliberately: parallel ones hit the rate limit on a first index of a real corpus,
|
|
101
|
+
// and the failure arrives as a 429 in the middle of a job rather than as a slower job.
|
|
102
|
+
for (let at = 0; at < texts.length; at += batchSize) {
|
|
103
|
+
out.push(...(await embedBatch(texts.slice(at, at + batchSize))));
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference `ConnectionStore` — task #261.
|
|
3
|
+
*
|
|
4
|
+
* The baseline the other adapters are compared against, and the one the conformance suite runs first. It holds
|
|
5
|
+
* `SealedSecret`s exactly as Postgres does: an in-memory adapter that stored plaintext would make the raw-row
|
|
6
|
+
* assertion pass for the wrong reason, and the suite would then be asserting a different guarantee per adapter.
|
|
7
|
+
*/
|
|
8
|
+
import type { ConnectionStore } from "../../connections/index.js";
|
|
9
|
+
export declare const createMemoryConnectionStore: (now?: () => string) => ConnectionStore;
|
|
10
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference `ConnectionStore` — task #261.
|
|
3
|
+
*
|
|
4
|
+
* The baseline the other adapters are compared against, and the one the conformance suite runs first. It holds
|
|
5
|
+
* `SealedSecret`s exactly as Postgres does: an in-memory adapter that stored plaintext would make the raw-row
|
|
6
|
+
* assertion pass for the wrong reason, and the suite would then be asserting a different guarantee per adapter.
|
|
7
|
+
*/
|
|
8
|
+
import { AgentPlatformError } from "../../core/errors.js";
|
|
9
|
+
const notFound = (id) => new AgentPlatformError({ code: "not_found", message: `no connection "${id}" for this tenant`, retryable: false });
|
|
10
|
+
export const createMemoryConnectionStore = (now = () => new Date().toISOString()) => {
|
|
11
|
+
// Keyed by tenant, so a missing tenant filter is a missing map rather than a leak.
|
|
12
|
+
const byTenant = new Map();
|
|
13
|
+
const of = (tenantId) => {
|
|
14
|
+
const existing = byTenant.get(tenantId);
|
|
15
|
+
if (existing !== undefined)
|
|
16
|
+
return existing;
|
|
17
|
+
const created = new Map();
|
|
18
|
+
byTenant.set(tenantId, created);
|
|
19
|
+
return created;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
async create({ tenantId, connection }) {
|
|
23
|
+
const rows = of(tenantId);
|
|
24
|
+
if (rows.has(connection.id))
|
|
25
|
+
throw new AgentPlatformError({
|
|
26
|
+
code: "conflict",
|
|
27
|
+
message: `connection "${connection.id}" already exists for this tenant`,
|
|
28
|
+
retryable: false,
|
|
29
|
+
});
|
|
30
|
+
const at = now();
|
|
31
|
+
const row = { ...connection, createdAt: at, updatedAt: at };
|
|
32
|
+
rows.set(connection.id, row);
|
|
33
|
+
return row;
|
|
34
|
+
},
|
|
35
|
+
async get({ tenantId, id, kind }) {
|
|
36
|
+
const row = of(tenantId).get(id);
|
|
37
|
+
// A caller that did not ask for an app registration must not receive one — see `ConnectionKind`.
|
|
38
|
+
if (row !== undefined && (row.kind ?? "connection") !== (kind ?? "connection"))
|
|
39
|
+
return null;
|
|
40
|
+
// A revoked connection reads as absent. Anything else would let a caller resolve a credential somebody
|
|
41
|
+
// deliberately withdrew.
|
|
42
|
+
return row === undefined || row.revokedAt !== undefined ? null : row;
|
|
43
|
+
},
|
|
44
|
+
async list({ tenantId, provider, kind }) {
|
|
45
|
+
return [...of(tenantId).values()]
|
|
46
|
+
.filter((c) => c.revokedAt === undefined &&
|
|
47
|
+
(provider === undefined || c.provider === provider) &&
|
|
48
|
+
(c.kind ?? "connection") === (kind ?? "connection"))
|
|
49
|
+
.sort((a, b) => (a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt.localeCompare(b.createdAt)));
|
|
50
|
+
},
|
|
51
|
+
async update({ tenantId, id, patch }) {
|
|
52
|
+
const rows = of(tenantId);
|
|
53
|
+
const existing = rows.get(id);
|
|
54
|
+
if (existing === undefined || existing.revokedAt !== undefined)
|
|
55
|
+
throw notFound(id);
|
|
56
|
+
const updated = {
|
|
57
|
+
...existing,
|
|
58
|
+
...(patch.label === undefined ? {} : { label: patch.label }),
|
|
59
|
+
...(patch.sealed === undefined ? {} : { sealed: patch.sealed }),
|
|
60
|
+
...(patch.metadata === undefined ? {} : { metadata: patch.metadata }),
|
|
61
|
+
...(patch.grantedScopes === undefined ? {} : { grantedScopes: patch.grantedScopes }),
|
|
62
|
+
...(patch.expiresAt === undefined ? {} : { expiresAt: patch.expiresAt }),
|
|
63
|
+
updatedAt: now(),
|
|
64
|
+
};
|
|
65
|
+
rows.set(id, updated);
|
|
66
|
+
return updated;
|
|
67
|
+
},
|
|
68
|
+
async revoke({ tenantId, id }) {
|
|
69
|
+
const rows = of(tenantId);
|
|
70
|
+
const existing = rows.get(id);
|
|
71
|
+
if (existing === undefined)
|
|
72
|
+
throw notFound(id);
|
|
73
|
+
// Idempotent: revoking twice is not an error, because a caller retrying a disconnect should not have to
|
|
74
|
+
// distinguish "already gone" from "failed".
|
|
75
|
+
if (existing.revokedAt !== undefined)
|
|
76
|
+
return;
|
|
77
|
+
rows.set(id, { ...existing, revokedAt: now(), updatedAt: now() });
|
|
78
|
+
},
|
|
79
|
+
async purge({ tenantId }) {
|
|
80
|
+
const rows = byTenant.get(tenantId);
|
|
81
|
+
const count = rows?.size ?? 0;
|
|
82
|
+
byTenant.delete(tenantId);
|
|
83
|
+
return count;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=connections.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory `GraphStore` — the reference implementation (REQ-064 #270, task #271).
|
|
3
|
+
*
|
|
4
|
+
* The same standard the in-memory `KnowledgeStore` holds itself to: this is not a stub that makes tests pass,
|
|
5
|
+
* it is the definition of the contract that the Postgres adapter is then held to by the same conformance suite.
|
|
6
|
+
* Three things it takes as seriously as a database would:
|
|
7
|
+
*
|
|
8
|
+
* - **Tenant isolation is structural.** Every map is keyed by tenant first, so cross-tenant reads are not
|
|
9
|
+
* prevented by a filter somebody could forget — there is nowhere for another tenant's rows to be.
|
|
10
|
+
* - **Provenance is enforced, not assumed.** A row with no chunks is refused here, so the refusal is part of
|
|
11
|
+
* the contract rather than a Postgres constraint that the reference adapter quietly tolerates.
|
|
12
|
+
* - **A source's contribution is what is replaced.** Entities are shared between sources; re-indexing one
|
|
13
|
+
* document withdraws its claims and prunes only what nothing else asserts.
|
|
14
|
+
*/
|
|
15
|
+
import type { GraphStore } from "../../persistence/index.js";
|
|
16
|
+
export declare const createMemoryGraphStore: () => GraphStore;
|
|
17
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory `GraphStore` — the reference implementation (REQ-064 #270, task #271).
|
|
3
|
+
*
|
|
4
|
+
* The same standard the in-memory `KnowledgeStore` holds itself to: this is not a stub that makes tests pass,
|
|
5
|
+
* it is the definition of the contract that the Postgres adapter is then held to by the same conformance suite.
|
|
6
|
+
* Three things it takes as seriously as a database would:
|
|
7
|
+
*
|
|
8
|
+
* - **Tenant isolation is structural.** Every map is keyed by tenant first, so cross-tenant reads are not
|
|
9
|
+
* prevented by a filter somebody could forget — there is nowhere for another tenant's rows to be.
|
|
10
|
+
* - **Provenance is enforced, not assumed.** A row with no chunks is refused here, so the refusal is part of
|
|
11
|
+
* the contract rather than a Postgres constraint that the reference adapter quietly tolerates.
|
|
12
|
+
* - **A source's contribution is what is replaced.** Entities are shared between sources; re-indexing one
|
|
13
|
+
* document withdraws its claims and prunes only what nothing else asserts.
|
|
14
|
+
*/
|
|
15
|
+
import { AgentPlatformError } from "../../core/errors.js";
|
|
16
|
+
const sourceKey = (sourceType, sourceId) => `${sourceType} ${sourceId}`;
|
|
17
|
+
const DEFAULT_SETTINGS = { enabled: false, updatedAt: "1970-01-01T00:00:00.000Z" };
|
|
18
|
+
const uniqueSorted = (values) => [...new Set(values)].sort();
|
|
19
|
+
const byId = (a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
20
|
+
/**
|
|
21
|
+
* Every contribution, merged into the graph a reader sees.
|
|
22
|
+
*
|
|
23
|
+
* Recomputed on read rather than maintained on write. That is the right trade for a reference adapter: the
|
|
24
|
+
* merge rule then exists in exactly one place, and a bug in it cannot leave a stored graph subtly inconsistent
|
|
25
|
+
* with the contributions it came from. Postgres maintains merged rows for query speed and is held to producing
|
|
26
|
+
* the same answer by `fingerprint`.
|
|
27
|
+
*/
|
|
28
|
+
const mergeAll = (source) => {
|
|
29
|
+
/**
|
|
30
|
+
* Materialised, because this function walks it **twice** — entities, then relationships.
|
|
31
|
+
*
|
|
32
|
+
* The first version took the `Iterable` and looped it directly, which works for an array and is silently
|
|
33
|
+
* wrong for `Map.values()`: an iterator is exhausted by the first pass, so the second saw nothing and *every
|
|
34
|
+
* edge was dropped*. Entities were correct, so the graph looked plausible; only a traversal returned empty.
|
|
35
|
+
* Found by the neighbour-ordering test, and worth the parameter rename so the two loops cannot drift back.
|
|
36
|
+
*/
|
|
37
|
+
const contributions = [...source];
|
|
38
|
+
const entities = new Map();
|
|
39
|
+
const relationships = new Map();
|
|
40
|
+
for (const contribution of contributions) {
|
|
41
|
+
for (const entity of contribution.entities) {
|
|
42
|
+
const existing = entities.get(entity.id);
|
|
43
|
+
if (existing === undefined) {
|
|
44
|
+
entities.set(entity.id, entity);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const description = (entity.description ?? "").length > (existing.description ?? "").length ? entity.description : existing.description;
|
|
48
|
+
entities.set(entity.id, {
|
|
49
|
+
id: entity.id,
|
|
50
|
+
// Lexicographically first, matching `mergeContributions`. "First written" would depend on source order,
|
|
51
|
+
// and source order is a thing that changes without the corpus changing.
|
|
52
|
+
name: existing.name < entity.name ? existing.name : entity.name,
|
|
53
|
+
type: existing.type,
|
|
54
|
+
...(description === undefined ? {} : { description }),
|
|
55
|
+
surfaceForms: uniqueSorted([...existing.surfaceForms, ...entity.surfaceForms]),
|
|
56
|
+
provenance: uniqueSorted([...existing.provenance, ...entity.provenance]),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const contribution of contributions) {
|
|
61
|
+
for (const edge of contribution.relationships) {
|
|
62
|
+
const existing = relationships.get(edge.id);
|
|
63
|
+
if (existing === undefined) {
|
|
64
|
+
relationships.set(edge.id, edge);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const description = (edge.description ?? "").length > (existing.description ?? "").length ? edge.description : existing.description;
|
|
68
|
+
const provenance = uniqueSorted([...existing.provenance, ...edge.provenance]);
|
|
69
|
+
relationships.set(edge.id, {
|
|
70
|
+
...existing,
|
|
71
|
+
...(description === undefined ? {} : { description }),
|
|
72
|
+
weight: provenance.length,
|
|
73
|
+
provenance,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// An edge whose endpoints are gone goes with them — the state after a source is removed.
|
|
78
|
+
const kept = [...relationships.values()].filter((edge) => entities.has(edge.fromId) && entities.has(edge.toId));
|
|
79
|
+
return { entities: [...entities.values()].sort(byId), relationships: kept.sort(byId) };
|
|
80
|
+
};
|
|
81
|
+
/** Refuses an untraceable claim. See `GraphStore` — provenance is structural. */
|
|
82
|
+
const assertProvenance = (contribution) => {
|
|
83
|
+
for (const entity of contribution.entities) {
|
|
84
|
+
if (entity.provenance.length === 0)
|
|
85
|
+
throw new AgentPlatformError({
|
|
86
|
+
code: "invalid_input",
|
|
87
|
+
message: `entity ${entity.id} has no provenance; a graph claim with no chunk behind it is one the model would cite`,
|
|
88
|
+
retryable: false,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
for (const edge of contribution.relationships) {
|
|
92
|
+
if (edge.provenance.length === 0)
|
|
93
|
+
throw new AgentPlatformError({
|
|
94
|
+
code: "invalid_input",
|
|
95
|
+
message: `relationship ${edge.id} has no provenance; a graph claim with no chunk behind it is one the model would cite`,
|
|
96
|
+
retryable: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
export const createMemoryGraphStore = () => {
|
|
101
|
+
const tenants = new Map();
|
|
102
|
+
const graphOf = (tenantId) => {
|
|
103
|
+
let graph = tenants.get(tenantId);
|
|
104
|
+
if (!graph) {
|
|
105
|
+
tenants.set(tenantId, (graph = { settings: DEFAULT_SETTINGS, sourceFlags: new Map(), contributions: new Map(), communities: new Map() }));
|
|
106
|
+
}
|
|
107
|
+
return graph;
|
|
108
|
+
};
|
|
109
|
+
const page = (items, limit, cursor) => {
|
|
110
|
+
const offset = cursor === undefined ? 0 : Number.parseInt(cursor, 10) || 0;
|
|
111
|
+
const slice = items.slice(offset, offset + limit);
|
|
112
|
+
const next = offset + slice.length;
|
|
113
|
+
return next < items.length ? { items: slice, nextCursor: String(next) } : { items: slice };
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
async getSettings({ tenantId }) {
|
|
117
|
+
return graphOf(tenantId).settings;
|
|
118
|
+
},
|
|
119
|
+
async setEnabled({ tenantId, enabled, at }) {
|
|
120
|
+
const graph = graphOf(tenantId);
|
|
121
|
+
graph.settings = { enabled, updatedAt: at };
|
|
122
|
+
return graph.settings;
|
|
123
|
+
},
|
|
124
|
+
async setSourceEnabled({ tenantId, sourceType, sourceId, enabled }) {
|
|
125
|
+
// Stored regardless of the tenant switch — AC-2. Marking a handbook today and enabling the tenant next
|
|
126
|
+
// week must not require re-marking anything, so this never consults `settings`.
|
|
127
|
+
graphOf(tenantId).sourceFlags.set(sourceKey(sourceType, sourceId), enabled);
|
|
128
|
+
},
|
|
129
|
+
async isSourceEnabled({ tenantId, sourceType, sourceId }) {
|
|
130
|
+
return graphOf(tenantId).sourceFlags.get(sourceKey(sourceType, sourceId)) === true;
|
|
131
|
+
},
|
|
132
|
+
async listEnabledSources({ tenantId, limit, cursor }) {
|
|
133
|
+
const rows = [...graphOf(tenantId).sourceFlags.entries()]
|
|
134
|
+
.filter(([, enabled]) => enabled)
|
|
135
|
+
.map(([key]) => {
|
|
136
|
+
const at = key.indexOf(" ");
|
|
137
|
+
return { sourceType: key.slice(0, at), sourceId: key.slice(at + 1) };
|
|
138
|
+
})
|
|
139
|
+
.sort((a, b) => (sourceKey(a.sourceType, a.sourceId) < sourceKey(b.sourceType, b.sourceId) ? -1 : 1));
|
|
140
|
+
return page(rows, limit, cursor);
|
|
141
|
+
},
|
|
142
|
+
async replaceSourceGraph({ tenantId, sourceType, sourceId, contribution }) {
|
|
143
|
+
assertProvenance(contribution);
|
|
144
|
+
const graph = graphOf(tenantId);
|
|
145
|
+
const before = mergeAll(graph.contributions.values()).entities.length;
|
|
146
|
+
const key = sourceKey(sourceType, sourceId);
|
|
147
|
+
if (contribution.entities.length === 0 && contribution.relationships.length === 0) {
|
|
148
|
+
graph.contributions.delete(key);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
graph.contributions.set(key, { entities: contribution.entities, relationships: contribution.relationships });
|
|
152
|
+
}
|
|
153
|
+
const after = mergeAll(graph.contributions.values());
|
|
154
|
+
return {
|
|
155
|
+
entities: after.entities.length,
|
|
156
|
+
relationships: after.relationships.length,
|
|
157
|
+
// What this replacement removed from the graph, not what it removed from the source. An operator asking
|
|
158
|
+
// "did re-indexing lose anything" is asking the first question.
|
|
159
|
+
pruned: Math.max(0, before - after.entities.length),
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
async deleteSourceGraph({ tenantId, sourceType, sourceId }) {
|
|
163
|
+
const graph = graphOf(tenantId);
|
|
164
|
+
const before = mergeAll(graph.contributions.values()).entities.length;
|
|
165
|
+
graph.contributions.delete(sourceKey(sourceType, sourceId));
|
|
166
|
+
const after = mergeAll(graph.contributions.values()).entities.length;
|
|
167
|
+
return { pruned: Math.max(0, before - after) };
|
|
168
|
+
},
|
|
169
|
+
async getEntity({ tenantId, id }) {
|
|
170
|
+
return mergeAll(graphOf(tenantId).contributions.values()).entities.find((entity) => entity.id === id) ?? null;
|
|
171
|
+
},
|
|
172
|
+
async getEntities({ tenantId, ids }) {
|
|
173
|
+
const wanted = new Set(ids);
|
|
174
|
+
return mergeAll(graphOf(tenantId).contributions.values()).entities.filter((entity) => wanted.has(entity.id));
|
|
175
|
+
},
|
|
176
|
+
async resolveEntities({ tenantId, normalisedNames }) {
|
|
177
|
+
if (normalisedNames.length === 0)
|
|
178
|
+
return [];
|
|
179
|
+
// The name is everything after the first colon; `normaliseName` cannot produce one, so the split is safe.
|
|
180
|
+
const wanted = new Set(normalisedNames);
|
|
181
|
+
return mergeAll(graphOf(tenantId).contributions.values()).entities.filter((entity) => wanted.has(entity.id.slice(entity.id.indexOf(":") + 1)));
|
|
182
|
+
},
|
|
183
|
+
async listEntities({ tenantId, limit, cursor, type }) {
|
|
184
|
+
const all = mergeAll(graphOf(tenantId).contributions.values()).entities;
|
|
185
|
+
return page(type === undefined ? all : all.filter((entity) => entity.type === type), limit, cursor);
|
|
186
|
+
},
|
|
187
|
+
async neighbours({ tenantId, entityIds, limit }) {
|
|
188
|
+
const wanted = new Set(entityIds);
|
|
189
|
+
const { relationships } = mergeAll(graphOf(tenantId).contributions.values());
|
|
190
|
+
return relationships
|
|
191
|
+
.filter((edge) => wanted.has(edge.fromId) || wanted.has(edge.toId))
|
|
192
|
+
// Heaviest first — the edge the most chunks asserted is the one most worth traversing. Ties broken by
|
|
193
|
+
// id so the order is total, which is what makes a truncated traversal reproducible.
|
|
194
|
+
.sort((a, b) => (b.weight - a.weight) || byId(a, b))
|
|
195
|
+
.slice(0, Math.max(0, limit));
|
|
196
|
+
},
|
|
197
|
+
async replaceCommunities({ tenantId, communities }) {
|
|
198
|
+
const graph = graphOf(tenantId);
|
|
199
|
+
const previous = new Map(graph.communities);
|
|
200
|
+
graph.communities.clear();
|
|
201
|
+
let summariesKept = 0;
|
|
202
|
+
for (const community of communities) {
|
|
203
|
+
const before = previous.get(community.id);
|
|
204
|
+
/**
|
|
205
|
+
* A summary survives a re-clustering **only when the membership is identical**.
|
|
206
|
+
*
|
|
207
|
+
* Carrying it over on id alone would attach a summary of the old members to the new ones — a
|
|
208
|
+
* confidently wrong description of a community that changed, which is worse than having none.
|
|
209
|
+
*/
|
|
210
|
+
const keep = before?.summary !== undefined && before.summaryFingerprint === community.fingerprint;
|
|
211
|
+
if (keep)
|
|
212
|
+
summariesKept += 1;
|
|
213
|
+
graph.communities.set(community.id, {
|
|
214
|
+
...community,
|
|
215
|
+
...(keep && before !== undefined
|
|
216
|
+
? {
|
|
217
|
+
summary: before.summary,
|
|
218
|
+
summaryFingerprint: before.summaryFingerprint,
|
|
219
|
+
...(before.summarisedAt === undefined ? {} : { summarisedAt: before.summarisedAt }),
|
|
220
|
+
}
|
|
221
|
+
: {}),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return { written: communities.length, summariesKept };
|
|
225
|
+
},
|
|
226
|
+
async listCommunities({ tenantId, limit, cursor, level }) {
|
|
227
|
+
const all = [...graphOf(tenantId).communities.values()]
|
|
228
|
+
.filter((community) => level === undefined || community.level === level)
|
|
229
|
+
.sort((a, b) => a.level - b.level || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
230
|
+
return page(all, limit, cursor);
|
|
231
|
+
},
|
|
232
|
+
async getCommunity({ tenantId, id }) {
|
|
233
|
+
return graphOf(tenantId).communities.get(id) ?? null;
|
|
234
|
+
},
|
|
235
|
+
async setCommunitySummary({ tenantId, id, summary, fingerprint, at }) {
|
|
236
|
+
const graph = graphOf(tenantId);
|
|
237
|
+
const community = graph.communities.get(id);
|
|
238
|
+
if (community === undefined)
|
|
239
|
+
return;
|
|
240
|
+
graph.communities.set(id, { ...community, summary, summaryFingerprint: fingerprint, summarisedAt: at });
|
|
241
|
+
},
|
|
242
|
+
async fingerprint({ tenantId }) {
|
|
243
|
+
const { entities, relationships } = mergeAll(graphOf(tenantId).contributions.values());
|
|
244
|
+
/**
|
|
245
|
+
* A stable serialisation, on the port rather than in a test — AC-6.
|
|
246
|
+
*
|
|
247
|
+
* Every field that a re-index could plausibly reorder is included: surface forms and provenance are the
|
|
248
|
+
* two that actually caught bugs, because both are sets that arrive in whatever order extraction produced.
|
|
249
|
+
* Leaving them out would let the fingerprint agree while the graph differed.
|
|
250
|
+
*/
|
|
251
|
+
const lines = [
|
|
252
|
+
...entities.map((entity) => [
|
|
253
|
+
"E",
|
|
254
|
+
entity.id,
|
|
255
|
+
entity.name,
|
|
256
|
+
entity.type,
|
|
257
|
+
entity.description ?? "",
|
|
258
|
+
entity.surfaceForms.join(","),
|
|
259
|
+
entity.provenance.join(","),
|
|
260
|
+
].join("")),
|
|
261
|
+
...relationships.map((edge) => ["R", edge.id, edge.type, String(edge.weight), edge.description ?? "", edge.provenance.join(",")].join("")),
|
|
262
|
+
];
|
|
263
|
+
return lines.join("\n");
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -20,6 +20,7 @@ export * from "./files.js";
|
|
|
20
20
|
export * from "./artifacts.js";
|
|
21
21
|
export * from "./artifact-exports.js";
|
|
22
22
|
export * from "./knowledge.js";
|
|
23
|
+
export * from "./graph.js";
|
|
23
24
|
export * from "./evaluation.js";
|
|
24
25
|
/** Idempotency store reference adapter. */
|
|
25
26
|
export * from "./idempotency.js";
|
|
@@ -36,4 +37,6 @@ export * from "./principal-memory.js";
|
|
|
36
37
|
/** Message + agent reference adapters (used by the embedded agent facade). */
|
|
37
38
|
export * from "./message-store.js";
|
|
38
39
|
export * from "./flows.js";
|
|
40
|
+
export * from "./rate-limit.js";
|
|
41
|
+
export * from "./connections.js";
|
|
39
42
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -88,6 +88,7 @@ export * from "./files.js";
|
|
|
88
88
|
export * from "./artifacts.js";
|
|
89
89
|
export * from "./artifact-exports.js";
|
|
90
90
|
export * from "./knowledge.js";
|
|
91
|
+
export * from "./graph.js";
|
|
91
92
|
export * from "./evaluation.js";
|
|
92
93
|
/** Idempotency store reference adapter. */
|
|
93
94
|
export * from "./idempotency.js";
|
|
@@ -104,4 +105,6 @@ export * from "./principal-memory.js";
|
|
|
104
105
|
/** Message + agent reference adapters (used by the embedded agent facade). */
|
|
105
106
|
export * from "./message-store.js";
|
|
106
107
|
export * from "./flows.js";
|
|
108
|
+
export * from "./rate-limit.js";
|
|
109
|
+
export * from "./connections.js";
|
|
107
110
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rate-limit counter in memory — task #248.
|
|
3
|
+
*
|
|
4
|
+
* **For tests and a single-process deployment only.** Two workers each hold their own `Map`, so each allows the
|
|
5
|
+
* full rate and a limit of 60/minute becomes 60×N. That is not a caveat to be worked around; it is why
|
|
6
|
+
* `createRedisRateLimitStore` exists, and why the cross-process test in this task's suite runs against a real
|
|
7
|
+
* Redis rather than this.
|
|
8
|
+
*
|
|
9
|
+
* Old windows are dropped on write rather than on a timer: a timer in a store would keep a process alive and
|
|
10
|
+
* would need clearing, and the number of live windows is one per tenant.
|
|
11
|
+
*/
|
|
12
|
+
import type { RateLimitStore } from "../../usage/index.js";
|
|
13
|
+
export declare const createMemoryRateLimitStore: () => RateLimitStore;
|
|
14
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rate-limit counter in memory — task #248.
|
|
3
|
+
*
|
|
4
|
+
* **For tests and a single-process deployment only.** Two workers each hold their own `Map`, so each allows the
|
|
5
|
+
* full rate and a limit of 60/minute becomes 60×N. That is not a caveat to be worked around; it is why
|
|
6
|
+
* `createRedisRateLimitStore` exists, and why the cross-process test in this task's suite runs against a real
|
|
7
|
+
* Redis rather than this.
|
|
8
|
+
*
|
|
9
|
+
* Old windows are dropped on write rather than on a timer: a timer in a store would keep a process alive and
|
|
10
|
+
* would need clearing, and the number of live windows is one per tenant.
|
|
11
|
+
*/
|
|
12
|
+
export const createMemoryRateLimitStore = () => {
|
|
13
|
+
const counts = new Map();
|
|
14
|
+
return {
|
|
15
|
+
async consume({ tenantId, windowStartMs }) {
|
|
16
|
+
const key = `${tenantId}:${windowStartMs}`;
|
|
17
|
+
// Every other window for this tenant is over, so nothing else can be incremented again.
|
|
18
|
+
for (const existing of counts.keys()) {
|
|
19
|
+
if (existing.startsWith(`${tenantId}:`) && existing !== key)
|
|
20
|
+
counts.delete(existing);
|
|
21
|
+
}
|
|
22
|
+
const next = (counts.get(key) ?? 0) + 1;
|
|
23
|
+
counts.set(key, next);
|
|
24
|
+
return next;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ConnectionStore` over Postgres — task #261.
|
|
3
|
+
*
|
|
4
|
+
* Pure SQL over `SqlExecutor`, like every other adapter here. The store never sees a plaintext secret: it
|
|
5
|
+
* writes the four columns of a `SealedSecret` and reads them back, so an adapter cannot leak something it does
|
|
6
|
+
* not have.
|
|
7
|
+
*
|
|
8
|
+
* Every query is tenant-scoped in its `WHERE`, and there is no `findById(id)` — governing principle 1. That is
|
|
9
|
+
* not belt-and-braces over RLS; it is the layer RLS is defence in depth *for*, and #91 is the reminder that a
|
|
10
|
+
* method accepting a tenant scope and not using it typechecks perfectly.
|
|
11
|
+
*/
|
|
12
|
+
import type { ConnectionStore } from "../../connections/index.js";
|
|
13
|
+
import type { SqlExecutor } from "./sql.js";
|
|
14
|
+
export declare const createPostgresConnectionStore: (sql: SqlExecutor) => ConnectionStore;
|
|
15
|
+
//# sourceMappingURL=connections.d.ts.map
|