@feltdb/core 0.8.0 → 0.8.1
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/application-development.d.ts +7 -2
- package/dist/application-development.d.ts.map +1 -1
- package/dist/application-development.js +7 -13
- package/dist/application-manifest.d.ts +1 -0
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/application-manifest.js +1 -1
- package/dist/application-understanding.d.ts +76 -0
- package/dist/application-understanding.d.ts.map +1 -0
- package/dist/application-understanding.js +282 -0
- package/dist/cli/ai-model-config.js +3 -3
- package/dist/cli/commands.js +131 -38
- package/dist/cli/proposal-client.js +5 -1
- package/dist/cli/publish-engine.js +1 -1
- package/dist/cli/repository-evidence.js +55 -0
- package/dist/cli/source-sync.js +190 -75
- package/dist/convergence.d.ts +55 -0
- package/dist/convergence.d.ts.map +1 -0
- package/dist/convergence.js +63 -0
- package/dist/create/managed-account.js +1 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +4 -0
- package/dist/create/server-source/crates/feltdb-server/src/connections.rs +14 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +174 -24
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +8 -1
- package/dist/create/template/default-project/src/context/AuthContext.tsx +9 -1
- package/dist/create/template/default-project/src/pages/SignUp.tsx +1 -1
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +6 -1
- package/dist/http-client.d.ts +32 -1
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +14 -2
- package/dist/http-db.d.ts +3 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +28 -3
- package/dist/index-core.d.ts +3 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +3 -0
- package/dist/intent.d.ts +98 -0
- package/dist/intent.d.ts.map +1 -0
- package/dist/intent.js +343 -0
- package/dist/module-intent.d.ts +19 -5
- package/dist/module-intent.d.ts.map +1 -1
- package/dist/module-intent.js +211 -62
- package/dist/module.d.ts +41 -12
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +543 -38
- package/dist/proposal.d.ts +13 -1
- package/dist/proposal.d.ts.map +1 -1
- package/dist/proposal.js +21 -4
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -1
- package/dist/studio/components/ProposalBrowser.d.ts +1 -1
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-CNSNrmA9.js → components-Duq2xQWt.js} +251 -193
- package/dist/studio/index.js +1 -1
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/proposal-api.d.ts +4 -1
- package/dist/studio/utils/proposal-api.d.ts.map +1 -1
- package/dist/studio/utils/proposal-api.js +3 -0
- package/dist/studio-app/assets/{feltdb_wasm-CSB6KVgw.js → feltdb_wasm-bIqcRzAr.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BE79okwX.wasm +0 -0
- package/dist/studio-app/assets/{index-yazaWMUN.css → index-Cd_NPw14.css} +1 -1
- package/dist/studio-app/assets/index-XGYdlElN.js +29 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/local-development-authority.d.ts +2 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +65 -2
- package/package.json +1 -1
- package/dist/studio-app/assets/dist-CCOk39Uc.js +0 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
- package/dist/studio-app/assets/index-Bncji3aN.js +0 -43
- package/dist/studio-app/assets/lib-B2_7fcn7.js +0 -69
- package/dist/studio-app/assets/worker-CARZ5g7K.js +0 -69
package/dist/cli/source-sync.js
CHANGED
|
@@ -1,58 +1,139 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { createHash } from
|
|
4
|
-
import { spawnSync } from
|
|
5
|
-
import { compileApplicationContract, createContractSnapshot, flowSourceHash, validateFlowProposal, } from
|
|
6
|
-
import { parseFlowSpec } from
|
|
7
|
-
const FLOW_FILE =
|
|
8
|
-
const GENERATED_FILE =
|
|
9
|
-
const SYNC_STATE =
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { compileApplicationContract, createContractSnapshot, flowSourceHash, validateFlowProposal, resolveFeltDBModule, } from "@feltdb/core";
|
|
6
|
+
import { parseFlowSpec } from "@feltdb/core";
|
|
7
|
+
const FLOW_FILE = "feltdb.flow";
|
|
8
|
+
const GENERATED_FILE = "src/feltdb/generated-contract.ts";
|
|
9
|
+
const SYNC_STATE = ".feltdb/source-sync.json";
|
|
10
10
|
function digest(value) {
|
|
11
|
-
return `sha256:${createHash(
|
|
11
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
12
12
|
}
|
|
13
13
|
function readJson(file) {
|
|
14
|
-
return fs.existsSync(file)
|
|
14
|
+
return fs.existsSync(file)
|
|
15
|
+
? JSON.parse(fs.readFileSync(file, "utf8"))
|
|
16
|
+
: undefined;
|
|
15
17
|
}
|
|
16
18
|
function gitDirty(cwd, files) {
|
|
17
|
-
const result = spawnSync(
|
|
19
|
+
const result = spawnSync("git", ["status", "--porcelain", "--", ...files], {
|
|
20
|
+
cwd,
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
});
|
|
18
23
|
if (result.status !== 0)
|
|
19
24
|
return [];
|
|
20
|
-
return result.stdout
|
|
25
|
+
return result.stdout
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.map((line) => line.slice(3));
|
|
21
29
|
}
|
|
22
30
|
export async function repositoryContext(cwd = process.cwd(), plannedFiles = [FLOW_FILE, GENERATED_FILE]) {
|
|
23
|
-
const git = (args) => spawnSync(
|
|
24
|
-
|
|
31
|
+
const git = (args) => spawnSync("git", args, {
|
|
32
|
+
cwd,
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
35
|
+
});
|
|
36
|
+
const rootResult = git(["rev-parse", "--show-toplevel"]);
|
|
25
37
|
const root = rootResult.status === 0 ? rootResult.stdout.trim() : path.resolve(cwd);
|
|
26
|
-
const branchResult = git([
|
|
27
|
-
const commitResult = git([
|
|
38
|
+
const branchResult = git(["branch", "--show-current"]);
|
|
39
|
+
const commitResult = git(["rev-parse", "HEAD"]);
|
|
28
40
|
const context = await loadLocalDevelopmentContract(root);
|
|
29
|
-
return {
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
return {
|
|
42
|
+
root,
|
|
43
|
+
branch: branchResult.status === 0
|
|
44
|
+
? branchResult.stdout.trim() || undefined
|
|
45
|
+
: undefined,
|
|
46
|
+
commit: commitResult.status === 0 ? commitResult.stdout.trim() : undefined,
|
|
47
|
+
dirty: gitDirty(root, plannedFiles),
|
|
48
|
+
flowPath: path.join(root, FLOW_FILE),
|
|
49
|
+
flowHash: context.flowHash,
|
|
50
|
+
};
|
|
32
51
|
}
|
|
33
52
|
export function assertProposalSourcePlan(sourcePlan) {
|
|
34
|
-
const allowed = new Set([FLOW_FILE, GENERATED_FILE]);
|
|
35
53
|
if (!sourcePlan?.files?.length)
|
|
36
|
-
throw new Error(
|
|
54
|
+
throw new Error("SOURCE_PLAN_BLOCKED: Proposal has no source operations");
|
|
37
55
|
for (const operation of sourcePlan.files) {
|
|
38
|
-
|
|
56
|
+
const allowed = operation.path === FLOW_FILE ||
|
|
57
|
+
operation.path === GENERATED_FILE ||
|
|
58
|
+
/^src\/integrations\/[a-z0-9-]+\/[A-Za-z0-9._-]+\.(?:ts|tsx)$/.test(operation.path);
|
|
59
|
+
if (!allowed || !["add", "modify"].includes(operation.operation))
|
|
39
60
|
throw new Error(`SOURCE_PLAN_BLOCKED: unsupported ${operation.operation} operation for ${operation.path}`);
|
|
40
61
|
}
|
|
41
|
-
if (!sourcePlan.files.some(value => value.path === FLOW_FILE))
|
|
62
|
+
if (!sourcePlan.files.some((value) => value.path === FLOW_FILE))
|
|
42
63
|
throw new Error(`SOURCE_PLAN_BLOCKED: ${FLOW_FILE} synchronization is required`);
|
|
43
64
|
}
|
|
65
|
+
function moduleSource(role, module, owner) {
|
|
66
|
+
const header = `/* Generated from an approved FeltDB ${module.id} module Proposal. Configuration references only; no credential values. */\n`;
|
|
67
|
+
if (role === "module-domain")
|
|
68
|
+
return (header +
|
|
69
|
+
`export type IntegrationOwner=${JSON.stringify(owner)};\nexport const MODULE_MODELS=${JSON.stringify(module.models.map((value) => ({ name: value.name, ownership: value.ownership })))} as const;\nexport interface IntegrationConnection { id:string; ownerType:IntegrationOwner; ownerId:string; externalId:string; status:string; synchronizedAt?:string }\n`);
|
|
70
|
+
if (role === "module-provider-boundary")
|
|
71
|
+
return (header +
|
|
72
|
+
`export const MODULE_ID=${JSON.stringify(module.id)};\nexport const SERVER_CONFIGURATION=${JSON.stringify(module.configuration.filter((value) => value.visibility !== "browser").map((value) => value.name))} as const;\nexport const SERVER_SECRETS=${JSON.stringify(module.secrets.map((value) => value.name))} as const;\nexport const BROWSER_CONFIGURATION=${JSON.stringify(module.configuration.filter((value) => value.visibility === "browser").map((value) => value.name))} as const;\nexport const CAPABILITIES=${JSON.stringify(module.capabilities)} as const;\nexport const EVENTS=${JSON.stringify(module.events)} as const;\nexport interface ModuleTransport { invoke<T>(capability:string,input:unknown):Promise<T>; verifyEvent(payload:string,signature:string,secret:string):Promise<{type:string;data:unknown}> }\nexport function requireServerConfiguration(value:Record<string,string|undefined>){for(const name of [...SERVER_CONFIGURATION,...SERVER_SECRETS]){if(name.startsWith('VITE_'))throw new Error('Server module configuration cannot use VITE_ names');if(!value[name])throw new Error(\`Missing required server configuration: \${name}\`)}return value}\nexport const invokeCapability=<T>(transport:ModuleTransport,capability:typeof CAPABILITIES[number],input:unknown)=>transport.invoke<T>(capability,input);\n`);
|
|
73
|
+
if (role === "module-reconciliation")
|
|
74
|
+
return (header +
|
|
75
|
+
`export const RECONCILIATION=${JSON.stringify(module.reconciliation)} as const;\nexport function reconciliationTrace(event:string){const flow=RECONCILIATION.find(value=>value.event===event);if(!flow)throw new Error(\`Unsupported ${module.id} event: \${event}\`);return [flow.event,flow.workflow,...flow.writes] as const}\n`);
|
|
76
|
+
if (role === "module-ui")
|
|
77
|
+
return (header +
|
|
78
|
+
`export interface IntegrationViewModel { module:${JSON.stringify(module.id)};ownerType:${JSON.stringify(owner)};title:string;connections:Array<{id:string;status:string}>;capabilities:string[] }\nexport function integrationViewModel(connections:Array<{id:string;status:string}>):IntegrationViewModel{return{module:${JSON.stringify(module.id)},ownerType:${JSON.stringify(owner)},title:${JSON.stringify(module.name)},connections,capabilities:${JSON.stringify(module.capabilities)}}}\n`);
|
|
79
|
+
if (role === "module-contract-test")
|
|
80
|
+
return (header +
|
|
81
|
+
`import { reconciliationTrace } from './reconcile.js';\nexport function verifyModuleContract(){const trace=reconciliationTrace(${JSON.stringify(module.reconciliation[0]?.event ?? "")});if(trace[1]!==${JSON.stringify(module.reconciliation[0]?.workflow ?? "")})throw new Error('Module reconciliation contract failed');return true}\n`);
|
|
82
|
+
throw new Error(`SOURCE_PLAN_BLOCKED: unsupported generated role ${role ?? "<missing>"}`);
|
|
83
|
+
}
|
|
84
|
+
function syncProposalSource(proposal, cwd, replace = false) {
|
|
85
|
+
const operations = proposal.source_plan?.files.filter((value) => value.role?.startsWith("module-")) ?? [];
|
|
86
|
+
if (!operations.length)
|
|
87
|
+
return [];
|
|
88
|
+
const spec = parseFlowSpec(proposal.proposedFlow);
|
|
89
|
+
const operationPath = operations[0]?.path;
|
|
90
|
+
const declaration = spec.modules
|
|
91
|
+
?.map((value) => resolveFeltDBModule(value))
|
|
92
|
+
.find((value) => value.source_artifacts.some((artifact) => artifact.path === operationPath));
|
|
93
|
+
if (!declaration)
|
|
94
|
+
throw new Error("SOURCE_PLAN_BLOCKED: source artifacts do not belong to a resolved module");
|
|
95
|
+
const primary = spec.collections.find((value) => value.name === declaration.placement.primary_application_model);
|
|
96
|
+
const owner = primary?.fields
|
|
97
|
+
.find((value) => value.name === declaration.placement.owner_field)
|
|
98
|
+
?.type.replace(/^ref /, "") ?? "User";
|
|
99
|
+
const statePath = path.join(cwd, SYNC_STATE);
|
|
100
|
+
const state = readJson(statePath) ?? { version: 1, files: {} };
|
|
101
|
+
const written = [];
|
|
102
|
+
for (const operation of operations) {
|
|
103
|
+
const target = path.join(cwd, operation.path);
|
|
104
|
+
if (fs.existsSync(target)) {
|
|
105
|
+
const actual = digest(fs.readFileSync(target));
|
|
106
|
+
const recorded = state.files?.[operation.path];
|
|
107
|
+
if ((!recorded || recorded !== actual) && !replace)
|
|
108
|
+
throw new Error(`SOURCE_SYNC_CONFLICT: ${operation.path} has developer changes; review it or rerun with --replace-generated`);
|
|
109
|
+
}
|
|
110
|
+
const source = moduleSource(operation.role, declaration, owner);
|
|
111
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
112
|
+
fs.writeFileSync(target, source);
|
|
113
|
+
state.files[operation.path] = digest(source);
|
|
114
|
+
written.push(operation.path);
|
|
115
|
+
}
|
|
116
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
117
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, {
|
|
118
|
+
mode: 0o600,
|
|
119
|
+
});
|
|
120
|
+
return written;
|
|
121
|
+
}
|
|
44
122
|
export async function loadLocalDevelopmentContract(cwd = process.cwd()) {
|
|
45
123
|
const flowPath = path.join(cwd, FLOW_FILE);
|
|
46
124
|
if (!fs.existsSync(flowPath))
|
|
47
125
|
throw new Error(`${FLOW_FILE} is required`);
|
|
48
|
-
const source = fs.readFileSync(flowPath,
|
|
49
|
-
const project = readJson(path.join(cwd,
|
|
50
|
-
const managed = readJson(path.join(cwd,
|
|
51
|
-
const published = readJson(path.join(cwd,
|
|
52
|
-
const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
126
|
+
const source = fs.readFileSync(flowPath, "utf8");
|
|
127
|
+
const project = readJson(path.join(cwd, "feltdb.config.json")) ?? {};
|
|
128
|
+
const managed = readJson(path.join(cwd, ".feltdb", "managed.json")) ?? {};
|
|
129
|
+
const published = readJson(path.join(cwd, ".feltdb", "last-published.json"));
|
|
130
|
+
const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID ||
|
|
131
|
+
managed.applicationId ||
|
|
132
|
+
published?.applicationId ||
|
|
133
|
+
project.applicationId ||
|
|
134
|
+
parseFlowSpec(source).app;
|
|
135
|
+
const tenantId = managed.tenantId || published?.tenantId || "local";
|
|
136
|
+
const environment = managed.environment || published?.environment || "local";
|
|
56
137
|
const contract = await compileApplicationContract(parseFlowSpec(source), tenantId, applicationId);
|
|
57
138
|
return {
|
|
58
139
|
source,
|
|
@@ -65,45 +146,60 @@ export async function loadLocalDevelopmentContract(cwd = process.cwd()) {
|
|
|
65
146
|
}
|
|
66
147
|
export async function reviewFlowProposal(proposal, cwd = process.cwd()) {
|
|
67
148
|
const current = await loadLocalDevelopmentContract(cwd);
|
|
68
|
-
return validateFlowProposal({
|
|
149
|
+
return validateFlowProposal({
|
|
150
|
+
proposal,
|
|
151
|
+
currentFlow: current.source,
|
|
152
|
+
snapshot: current.snapshot,
|
|
153
|
+
});
|
|
69
154
|
}
|
|
70
155
|
function generatedContractSource(review) {
|
|
71
156
|
const snapshot = createContractSnapshot(review.proposedContract, {
|
|
72
|
-
environment:
|
|
73
|
-
contractVersion: review.proposal.baseContractHash ===
|
|
157
|
+
environment: "local",
|
|
158
|
+
contractVersion: review.proposal.baseContractHash ===
|
|
159
|
+
review.proposedContract.hashes.application
|
|
160
|
+
? 1
|
|
161
|
+
: 2,
|
|
74
162
|
});
|
|
75
|
-
const collections = review.proposedSpec.collections.map(collection => ({
|
|
163
|
+
const collections = review.proposedSpec.collections.map((collection) => ({
|
|
76
164
|
name: collection.name,
|
|
77
|
-
fields: collection.fields.map(field => ({
|
|
165
|
+
fields: collection.fields.map((field) => ({
|
|
166
|
+
name: field.name,
|
|
167
|
+
type: field.type,
|
|
168
|
+
optional: field.optional,
|
|
169
|
+
})),
|
|
78
170
|
}));
|
|
79
171
|
const typeName = (value) => {
|
|
80
|
-
const cleaned = value.replace(/[^A-Za-z0-9_$]/g,
|
|
172
|
+
const cleaned = value.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
81
173
|
return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
82
174
|
};
|
|
83
175
|
const valueType = (value) => {
|
|
84
|
-
const normalized = value.toLowerCase().replace(/\?$/,
|
|
85
|
-
if ([
|
|
86
|
-
return
|
|
87
|
-
if ([
|
|
88
|
-
return
|
|
89
|
-
if ([
|
|
90
|
-
return
|
|
91
|
-
if (normalized.endsWith(
|
|
176
|
+
const normalized = value.toLowerCase().replace(/\?$/, "");
|
|
177
|
+
if (["int", "integer", "float", "number"].includes(normalized))
|
|
178
|
+
return "number";
|
|
179
|
+
if (["bool", "boolean"].includes(normalized))
|
|
180
|
+
return "boolean";
|
|
181
|
+
if (["json", "object"].includes(normalized))
|
|
182
|
+
return "unknown";
|
|
183
|
+
if (normalized.endsWith("[]"))
|
|
92
184
|
return `${valueType(normalized.slice(0, -2))}[]`;
|
|
93
|
-
return
|
|
185
|
+
return "string";
|
|
94
186
|
};
|
|
95
|
-
const types = review.proposedSpec.collections
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
187
|
+
const types = review.proposedSpec.collections
|
|
188
|
+
.map((collection) => {
|
|
189
|
+
const fields = collection.fields.map((field) => ` ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${valueType(field.type)};`);
|
|
190
|
+
return `export interface ${typeName(collection.name)}Record {\n id: string;\n${fields.join("\n")}\n}`;
|
|
191
|
+
})
|
|
192
|
+
.join("\n\n");
|
|
193
|
+
const bindings = review.proposedSpec.collections
|
|
194
|
+
.map((collection) => ` ${JSON.stringify(collection.name)}: database.collection<${typeName(collection.name)}Record>(${JSON.stringify(collection.name)}),`)
|
|
195
|
+
.join("\n");
|
|
196
|
+
return (`/* Generated by feltdb sync from feltdb.flow. Do not edit this file directly. */\n` +
|
|
197
|
+
`export const FELTDB_CONTRACT = ${JSON.stringify(snapshot, null, 2)} as const;\n\n` +
|
|
198
|
+
`export const FELTDB_COLLECTIONS = ${JSON.stringify(collections, null, 2)} as const;\n\n` +
|
|
199
|
+
`${types}\n\n` +
|
|
200
|
+
`export interface FeltDBCollectionBinder {\n collection<T>(name: string): unknown;\n}\n\n` +
|
|
201
|
+
`/** Bind the generated, typed collection map to the project's FeltDB instance. */\n` +
|
|
202
|
+
`export function bindGeneratedCollections(database: FeltDBCollectionBinder) {\n return {\n${bindings}\n };\n}\n`);
|
|
107
203
|
}
|
|
108
204
|
export function syncGeneratedSource(review, cwd = process.cwd(), options = {}) {
|
|
109
205
|
const target = path.join(cwd, GENERATED_FILE);
|
|
@@ -120,7 +216,7 @@ export function syncGeneratedSource(review, cwd = process.cwd(), options = {}) {
|
|
|
120
216
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
121
217
|
fs.writeFileSync(target, source);
|
|
122
218
|
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
123
|
-
fs.writeFileSync(statePath, `${JSON.stringify({ version: 1, contractHash: review.proposedContract.hashes.application, files: { [GENERATED_FILE]: digest(source) } }, null, 2)}\n`, { mode: 0o600 });
|
|
219
|
+
fs.writeFileSync(statePath, `${JSON.stringify({ version: 1, contractHash: review.proposedContract.hashes.application, files: { ...(state.files ?? {}), [GENERATED_FILE]: digest(source) } }, null, 2)}\n`, { mode: 0o600 });
|
|
124
220
|
return { written: [GENERATED_FILE] };
|
|
125
221
|
}
|
|
126
222
|
function assertGeneratedSourceSafe(cwd, replace = false) {
|
|
@@ -137,20 +233,37 @@ export async function applyFlowProposal(proposal, cwd = process.cwd(), options =
|
|
|
137
233
|
const review = await reviewFlowProposal(proposal, cwd);
|
|
138
234
|
const dirty = gitDirty(cwd, [FLOW_FILE, GENERATED_FILE]);
|
|
139
235
|
if (dirty.length && !options.allowDirty)
|
|
140
|
-
throw new Error(`DIRTY_SOURCE: uncommitted changes affect ${dirty.join(
|
|
236
|
+
throw new Error(`DIRTY_SOURCE: uncommitted changes affect ${dirty.join(", ")}`);
|
|
141
237
|
if (review.authorizationChanges.length && !options.approveAuthorization) {
|
|
142
|
-
throw new Error(`AUTHORIZATION_APPROVAL_REQUIRED: ${review.authorizationChanges.map(change => change.policy).join(
|
|
238
|
+
throw new Error(`AUTHORIZATION_APPROVAL_REQUIRED: ${review.authorizationChanges.map((change) => change.policy).join(", ")}`);
|
|
143
239
|
}
|
|
144
240
|
if (options.syncSource !== false)
|
|
145
241
|
assertGeneratedSourceSafe(cwd, options.replaceGenerated);
|
|
146
|
-
fs.writeFileSync(path.join(cwd, FLOW_FILE), proposal.proposedFlow.replace(/\r\n/g,
|
|
147
|
-
const synchronized = options.syncSource === false
|
|
148
|
-
|
|
242
|
+
fs.writeFileSync(path.join(cwd, FLOW_FILE), proposal.proposedFlow.replace(/\r\n/g, "\n"));
|
|
243
|
+
const synchronized = options.syncSource === false
|
|
244
|
+
? { written: [] }
|
|
245
|
+
: syncGeneratedSource(review, cwd, { replace: options.replaceGenerated });
|
|
246
|
+
const moduleWritten = options.syncSource === false
|
|
247
|
+
? []
|
|
248
|
+
: syncProposalSource(proposal, cwd, Boolean(options.replaceGenerated));
|
|
249
|
+
return {
|
|
250
|
+
...review,
|
|
251
|
+
written: [FLOW_FILE, ...synchronized.written, ...moduleWritten],
|
|
252
|
+
};
|
|
149
253
|
}
|
|
150
254
|
/** Apply and verify as one developer-visible transaction, restoring exact prior bytes on failure. */
|
|
151
255
|
export async function applyFlowProposalAtomic(proposal, cwd = process.cwd(), options = {}) {
|
|
152
|
-
const
|
|
153
|
-
const
|
|
256
|
+
const planned = proposal.source_plan?.files.map((value) => value.path) ?? [];
|
|
257
|
+
const files = [
|
|
258
|
+
...new Set([FLOW_FILE, GENERATED_FILE, SYNC_STATE, ...planned]),
|
|
259
|
+
].map((relative) => ({ relative, absolute: path.join(cwd, relative) }));
|
|
260
|
+
const before = files.map((file) => ({
|
|
261
|
+
...file,
|
|
262
|
+
existed: fs.existsSync(file.absolute),
|
|
263
|
+
bytes: fs.existsSync(file.absolute)
|
|
264
|
+
? fs.readFileSync(file.absolute)
|
|
265
|
+
: undefined,
|
|
266
|
+
}));
|
|
154
267
|
try {
|
|
155
268
|
const result = await applyFlowProposal(proposal, cwd, options);
|
|
156
269
|
options.verify?.();
|
|
@@ -170,14 +283,16 @@ export async function applyFlowProposalAtomic(proposal, cwd = process.cwd(), opt
|
|
|
170
283
|
}
|
|
171
284
|
export function describeProposal(review) {
|
|
172
285
|
return [
|
|
173
|
-
...review.diff.added.map(value => `+ ${value}`),
|
|
174
|
-
...review.diff.changed.map(value => `~ ${value}`),
|
|
175
|
-
...review.diff.removed.map(value => `- ${value}`),
|
|
176
|
-
...review.authorizationChanges.flatMap(value => [
|
|
177
|
-
`AUTHORIZATION CHANGE${value.widening ?
|
|
178
|
-
` before: ${(value.before ?? [
|
|
179
|
-
` after: ${(value.after ?? [
|
|
286
|
+
...review.diff.added.map((value) => `+ ${value}`),
|
|
287
|
+
...review.diff.changed.map((value) => `~ ${value}`),
|
|
288
|
+
...review.diff.removed.map((value) => `- ${value}`),
|
|
289
|
+
...review.authorizationChanges.flatMap((value) => [
|
|
290
|
+
`AUTHORIZATION CHANGE${value.widening ? " (possible widening)" : ""}: ${value.policy}`,
|
|
291
|
+
` before: ${(value.before ?? ["<absent>"]).join(" | ")}`,
|
|
292
|
+
` after: ${(value.after ?? ["<removed>"]).join(" | ")}`,
|
|
180
293
|
]),
|
|
181
|
-
...review.migration
|
|
294
|
+
...review.migration
|
|
295
|
+
.filter((value) => value.safety !== "safe")
|
|
296
|
+
.map((value) => `${value.safety.toUpperCase()}: ${value.target} — ${value.detail}`),
|
|
182
297
|
];
|
|
183
298
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type ApplicationUnderstanding, type ObservedEvidenceInput } from './application-understanding.js';
|
|
2
|
+
import type { FeltDBContractSnapshot } from './application-development.js';
|
|
3
|
+
import type { FeltDBProposalRecord } from './proposal.js';
|
|
4
|
+
export type ConvergenceFindingStatus = 'satisfied' | 'missing' | 'partial' | 'contradictory' | 'stale' | 'unverified' | 'unexpected';
|
|
5
|
+
export type ConvergenceStatus = 'pending' | 'checking' | 'converged' | 'partially_converged' | 'diverged' | 'blocked' | 'unverified';
|
|
6
|
+
export type ConvergenceSurface = 'contract' | 'flow' | 'schema' | 'relationship' | 'policy' | 'workflow' | 'capability' | 'module' | 'source' | 'test' | 'build' | 'runtime';
|
|
7
|
+
export interface ConvergenceFinding {
|
|
8
|
+
claim: string;
|
|
9
|
+
surface: ConvergenceSurface;
|
|
10
|
+
status: ConvergenceFindingStatus;
|
|
11
|
+
expected?: unknown;
|
|
12
|
+
observed?: unknown;
|
|
13
|
+
confidence: number;
|
|
14
|
+
evidence: string[];
|
|
15
|
+
missing?: string[];
|
|
16
|
+
reason: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SourcePlanObservation {
|
|
19
|
+
path: string;
|
|
20
|
+
status: 'implemented' | 'modified' | 'missing' | 'conflicted' | 'unexpected';
|
|
21
|
+
evidence?: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface VerificationObservation {
|
|
24
|
+
surface: 'test' | 'build' | 'runtime';
|
|
25
|
+
claim: string;
|
|
26
|
+
status: 'satisfied' | 'partial' | 'missing' | 'unverified';
|
|
27
|
+
evidence?: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface ConvergenceResult {
|
|
30
|
+
id?: string;
|
|
31
|
+
proposal_id: string;
|
|
32
|
+
application_id: string;
|
|
33
|
+
environment: string;
|
|
34
|
+
contract_hash: string;
|
|
35
|
+
flow_hash: string;
|
|
36
|
+
evidence_hash: string;
|
|
37
|
+
status: ConvergenceStatus;
|
|
38
|
+
findings: ConvergenceFinding[];
|
|
39
|
+
confidence: number;
|
|
40
|
+
created_at: string;
|
|
41
|
+
verifier_version: 1;
|
|
42
|
+
updated_understanding: ApplicationUnderstanding;
|
|
43
|
+
}
|
|
44
|
+
/** Verifies semantic satisfaction. Source layout is supporting evidence, never the primary convergence boundary. */
|
|
45
|
+
export declare function verifyProposalConvergence(input: {
|
|
46
|
+
proposal: FeltDBProposalRecord;
|
|
47
|
+
currentFlow: string;
|
|
48
|
+
snapshot: FeltDBContractSnapshot;
|
|
49
|
+
source?: SourcePlanObservation[];
|
|
50
|
+
verification?: VerificationObservation[];
|
|
51
|
+
observations?: ObservedEvidenceInput[];
|
|
52
|
+
now?: string;
|
|
53
|
+
}): Promise<ConvergenceResult>;
|
|
54
|
+
export declare function renderConvergenceDiagnostic(result: ConvergenceResult): string[];
|
|
55
|
+
//# sourceMappingURL=convergence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convergence.d.ts","sourceRoot":"","sources":["../src/convergence.ts"],"names":[],"mappings":"AACA,OAAO,EAA6D,KAAK,wBAAwB,EAAE,KAAK,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACtK,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAE3E,OAAO,KAAK,EAAE,oBAAoB,EAA2B,MAAM,eAAe,CAAC;AAEnF,MAAM,MAAM,wBAAwB,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,eAAe,GAAG,OAAO,GAAG,YAAY,GAAG,YAAY,CAAC;AACrI,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,qBAAqB,GAAG,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC;AACrI,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,cAAc,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAC7K,MAAM,WAAW,kBAAkB;IAAG,KAAK,EAAC,MAAM,CAAC;IAAC,OAAO,EAAC,kBAAkB,CAAC;IAAC,MAAM,EAAC,wBAAwB,CAAC;IAAC,QAAQ,CAAC,EAAC,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAC,OAAO,CAAC;IAAC,UAAU,EAAC,MAAM,CAAC;IAAC,QAAQ,EAAC,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAC,MAAM,EAAE,CAAC;IAAC,MAAM,EAAC,MAAM,CAAA;CAAE;AAC/N,MAAM,WAAW,qBAAqB;IAAG,IAAI,EAAC,MAAM,CAAC;IAAC,MAAM,EAAC,aAAa,GAAC,UAAU,GAAC,SAAS,GAAC,YAAY,GAAC,YAAY,CAAC;IAAC,QAAQ,CAAC,EAAC,MAAM,EAAE,CAAA;CAAE;AAC/I,MAAM,WAAW,uBAAuB;IAAG,OAAO,EAAC,MAAM,GAAC,OAAO,GAAC,SAAS,CAAC;IAAC,KAAK,EAAC,MAAM,CAAC;IAAC,MAAM,EAAC,WAAW,GAAC,SAAS,GAAC,SAAS,GAAC,YAAY,CAAC;IAAC,QAAQ,CAAC,EAAC,MAAM,EAAE,CAAA;CAAE;AACpK,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAC,MAAM,CAAC;IAAC,WAAW,EAAC,MAAM,CAAC;IAAC,cAAc,EAAC,MAAM,CAAC;IAAC,WAAW,EAAC,MAAM,CAAC;IAAC,aAAa,EAAC,MAAM,CAAC;IAAC,SAAS,EAAC,MAAM,CAAC;IAAC,aAAa,EAAC,MAAM,CAAC;IACxI,MAAM,EAAC,iBAAiB,CAAC;IAAC,QAAQ,EAAC,kBAAkB,EAAE,CAAC;IAAC,UAAU,EAAC,MAAM,CAAC;IAAC,UAAU,EAAC,MAAM,CAAC;IAAC,gBAAgB,EAAC,CAAC,CAAC;IAAC,qBAAqB,EAAC,wBAAwB,CAAC;CACnK;AAKD,oHAAoH;AACpH,wBAAsB,yBAAyB,CAAC,KAAK,EAAC;IAAE,QAAQ,EAAC,oBAAoB,CAAC;IAAC,WAAW,EAAC,MAAM,CAAC;IAAC,QAAQ,EAAC,sBAAsB,CAAC;IAAC,MAAM,CAAC,EAAC,qBAAqB,EAAE,CAAC;IAAC,YAAY,CAAC,EAAC,uBAAuB,EAAE,CAAC;IAAC,YAAY,CAAC,EAAC,qBAAqB,EAAE,CAAC;IAAC,GAAG,CAAC,EAAC,MAAM,CAAA;CAAE,GAAE,OAAO,CAAC,iBAAiB,CAAC,CAqBrS;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAC,iBAAiB,GAAE,MAAM,EAAE,CAA8e"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { canonicalApplicationBytes } from './canonical-application.js';
|
|
2
|
+
import { collectApplicationUnderstanding } from './application-understanding.js';
|
|
3
|
+
import { parseFlowSpec } from './flowspec.js';
|
|
4
|
+
async function digest(value) { const bytes = canonicalApplicationBytes(value); const result = await crypto.subtle.digest('SHA-256', bytes.slice().buffer); return `sha256:${Array.from(new Uint8Array(result), byte => byte.toString(16).padStart(2, '0')).join('')}`; }
|
|
5
|
+
const ev = (items, claim) => items.filter(value => value.claim === claim).map(value => value.id);
|
|
6
|
+
/** Verifies semantic satisfaction. Source layout is supporting evidence, never the primary convergence boundary. */
|
|
7
|
+
export async function verifyProposalConvergence(input) {
|
|
8
|
+
const expected = parseFlowSpec(input.proposal.proposed_flow);
|
|
9
|
+
const actual = parseFlowSpec(input.currentFlow);
|
|
10
|
+
const findings = [];
|
|
11
|
+
const understanding = await collectApplicationUnderstanding({ intent: `verify ${input.proposal.id}`, snapshot: input.snapshot, currentFlow: input.currentFlow, observations: input.observations, now: input.now });
|
|
12
|
+
const push = (finding) => findings.push(finding);
|
|
13
|
+
const actualCollections = new Map(actual.collections.map(value => [value.name, value]));
|
|
14
|
+
for (const collection of expected.collections) {
|
|
15
|
+
const observed = actualCollections.get(collection.name);
|
|
16
|
+
push({ claim: `schema.collection.${collection.name}`, surface: 'schema', status: observed ? 'satisfied' : 'missing', expected: collection.name, observed: observed?.name, confidence: 1, evidence: ev(understanding.evidence, `schema.collection.${collection.name}`), reason: observed ? 'Collection exists in the canonical contract.' : 'Required collection is absent from the canonical contract.' });
|
|
17
|
+
if (!observed)
|
|
18
|
+
continue;
|
|
19
|
+
for (const field of collection.fields.filter(value => value.type.startsWith('ref '))) {
|
|
20
|
+
const current = observed.fields.find(value => value.name === field.name);
|
|
21
|
+
const status = !current ? 'missing' : current.type === field.type ? 'satisfied' : 'contradictory';
|
|
22
|
+
push({ claim: `schema.relationship.${collection.name}.${field.name}`, surface: 'relationship', status, expected: field.type, observed: current?.type, confidence: 1, evidence: ev(understanding.evidence, `schema.relationship.${collection.name}.${field.name}`), reason: status === 'satisfied' ? 'Relationship matches the Proposal.' : status === 'contradictory' ? 'The canonical relationship points to a different target.' : 'Required relationship is absent.' });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const compareNamed = (surface, expectedValues, actualValues) => { const names = new Set(actualValues.map(value => value.name)); for (const value of expectedValues)
|
|
26
|
+
push({ claim: `${surface}.${value.name}`, surface, status: names.has(value.name) ? 'satisfied' : 'missing', expected: value.name, observed: names.has(value.name) ? value.name : undefined, confidence: 1, evidence: ev(understanding.evidence, `${surface}.${value.name}`), reason: names.has(value.name) ? `${surface} exists in the canonical contract.` : `Required ${surface} is absent from the canonical contract.` }); };
|
|
27
|
+
compareNamed('policy', expected.policies, actual.policies);
|
|
28
|
+
compareNamed('workflow', expected.workflows, actual.workflows);
|
|
29
|
+
compareNamed('capability', expected.capabilities, actual.capabilities);
|
|
30
|
+
compareNamed('module', expected.modules ?? [], actual.modules ?? []);
|
|
31
|
+
const unexpectedNamed = (surface, expectedValues, actualValues) => { const names = new Set(expectedValues.map(value => value.name)); for (const value of actualValues.filter(value => !names.has(value.name)))
|
|
32
|
+
push({ claim: `${surface}.${value.name}`, surface, status: 'unexpected', observed: value.name, confidence: 1, evidence: ev(understanding.evidence, `${surface}.${surface === 'schema' ? 'collection.' : ''}${value.name}`), reason: `${surface} exists outside the Proposal semantics and does not automatically block convergence.` }); };
|
|
33
|
+
unexpectedNamed('schema', expected.collections, actual.collections);
|
|
34
|
+
unexpectedNamed('policy', expected.policies, actual.policies);
|
|
35
|
+
unexpectedNamed('workflow', expected.workflows, actual.workflows);
|
|
36
|
+
unexpectedNamed('capability', expected.capabilities, actual.capabilities);
|
|
37
|
+
unexpectedNamed('module', expected.modules ?? [], actual.modules ?? []);
|
|
38
|
+
const sourceByPath = new Map((input.source ?? []).map(value => [value.path, value]));
|
|
39
|
+
for (const planned of input.proposal.source_plan.files) {
|
|
40
|
+
const observed = sourceByPath.get(planned.path);
|
|
41
|
+
const status = observed?.status === 'implemented' ? 'satisfied' : observed?.status === 'modified' ? 'partial' : observed?.status === 'conflicted' ? 'contradictory' : observed?.status === 'unexpected' ? 'unexpected' : 'missing';
|
|
42
|
+
push({ claim: `source.${planned.path}`, surface: 'source', status, expected: planned.operation, observed: observed?.status, confidence: observed ? 1 : .2, evidence: observed?.evidence ?? [], reason: observed ? `Planned source is ${observed.status}.` : 'Planned source path was not verified.' });
|
|
43
|
+
}
|
|
44
|
+
for (const observed of input.source ?? [])
|
|
45
|
+
if (!input.proposal.source_plan.files.some(value => value.path === observed.path) && observed.status === 'unexpected')
|
|
46
|
+
push({ claim: `source.${observed.path}`, surface: 'source', status: 'unexpected', observed: observed.status, confidence: 1, evidence: observed.evidence ?? [], reason: 'Repository change was outside the Proposal source plan.' });
|
|
47
|
+
for (const observed of input.verification ?? [])
|
|
48
|
+
push({ claim: observed.claim, surface: observed.surface, status: observed.status, confidence: observed.status === 'unverified' ? .4 : 1, evidence: observed.evidence ?? [], reason: observed.status === 'satisfied' ? `${observed.surface} verification passed.` : `${observed.surface} verification is ${observed.status}.` });
|
|
49
|
+
const semantic = findings.filter(value => ['schema', 'relationship', 'policy', 'workflow', 'capability', 'module'].includes(value.surface));
|
|
50
|
+
const contradictions = semantic.some(value => value.status === 'contradictory');
|
|
51
|
+
const missing = semantic.some(value => value.status === 'missing');
|
|
52
|
+
const partial = findings.some(value => value.status === 'partial' || value.status === 'unverified');
|
|
53
|
+
const status = contradictions || missing ? 'diverged' : partial ? 'partially_converged' : semantic.length ? 'converged' : 'unverified';
|
|
54
|
+
const assessed = findings.filter(value => value.status !== 'unexpected');
|
|
55
|
+
const verified = assessed.filter(value => value.status === 'satisfied').length;
|
|
56
|
+
const confidence = assessed.length ? Math.round((verified / assessed.length) * 100) / 100 : 0;
|
|
57
|
+
const convergenceObservations = findings.slice(0, 50).map(value => ({ source: 'proposal_history', claim: `convergence.${value.claim}`, value: { status: value.status, confidence: value.confidence }, source_ref: `proposal:${input.proposal.id}` }));
|
|
58
|
+
const updatedUnderstanding = await collectApplicationUnderstanding({ intent: `verify ${input.proposal.id}`, snapshot: input.snapshot, currentFlow: input.currentFlow, observations: [...(input.observations ?? []).slice(0, 25), ...convergenceObservations.slice(0, 25)], now: input.now });
|
|
59
|
+
const flowHash = await digest(input.currentFlow.replace(/\r\n/g, '\n'));
|
|
60
|
+
const evidenceHash = await digest({ proposal_id: input.proposal.id, contract_hash: input.snapshot.contract_hash, flow_hash: flowHash, findings });
|
|
61
|
+
return { proposal_id: input.proposal.id, application_id: input.proposal.application_id, environment: input.proposal.environment, contract_hash: input.snapshot.contract_hash, flow_hash: flowHash, evidence_hash: evidenceHash, status, findings, confidence, created_at: input.now ?? new Date().toISOString(), verifier_version: 1, updated_understanding: updatedUnderstanding };
|
|
62
|
+
}
|
|
63
|
+
export function renderConvergenceDiagnostic(result) { const icon = (status) => status === 'satisfied' ? '✓' : status === 'unverified' || status === 'partial' ? '⚠' : status === 'unexpected' ? '+' : '✗'; return [`Verification: ${result.status.toUpperCase().replace('_', ' ')}`, `Confidence: ${Math.round(result.confidence * 100)}%`, ...result.findings.map(value => `${icon(value.status)} ${value.surface.toUpperCase()} ${value.claim} — ${value.status}${value.observed !== undefined && value.status !== 'satisfied' ? ` (observed: ${String(value.observed)})` : ''}`)]; }
|
|
@@ -68,6 +68,7 @@ export async function configureManagedAccount(options) {
|
|
|
68
68
|
'application:read', 'application:write', 'application:revision:create',
|
|
69
69
|
'application:revision:read', 'application:revision:promote',
|
|
70
70
|
'application:environment:read', 'application:environment:write',
|
|
71
|
+
'connections:read',
|
|
71
72
|
],
|
|
72
73
|
}),
|
|
73
74
|
}), 'Could not create the managed control-plane key');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// One release train keeps generated applications installable. The repository
|
|
2
2
|
// validation script checks these values against every workspace manifest.
|
|
3
|
-
export const FELTDB_PACKAGE_VERSION = '0.8.
|
|
3
|
+
export const FELTDB_PACKAGE_VERSION = '0.8.1';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -211,6 +211,10 @@ pub struct ArtifactReference {
|
|
|
211
211
|
pub name: String,
|
|
212
212
|
pub uri: String,
|
|
213
213
|
pub media_type: String,
|
|
214
|
+
/// Immutable source bytes used by trusted control-plane services. The URI
|
|
215
|
+
/// remains the content identity; values are base64 and never interpreted by Rust.
|
|
216
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
217
|
+
pub content: Option<String>,
|
|
214
218
|
}
|
|
215
219
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
|
216
220
|
pub struct DeploymentIntent {
|
|
@@ -590,6 +590,20 @@ impl ConnectionStore {
|
|
|
590
590
|
.cloned()
|
|
591
591
|
.collect())
|
|
592
592
|
}
|
|
593
|
+
|
|
594
|
+
/// Internal inventory lookup used after the caller has already passed an
|
|
595
|
+
/// application-scoped runtime or promotion authorization check.
|
|
596
|
+
pub fn application_connections(&self, application_id: &str) -> Result<Vec<Connection>, String> {
|
|
597
|
+
Ok(self
|
|
598
|
+
.records
|
|
599
|
+
.read()
|
|
600
|
+
.map_err(|_| "connection store lock poisoned")?
|
|
601
|
+
.connections
|
|
602
|
+
.iter()
|
|
603
|
+
.filter(|connection| connection.application_id == application_id)
|
|
604
|
+
.cloned()
|
|
605
|
+
.collect())
|
|
606
|
+
}
|
|
593
607
|
pub fn disable(
|
|
594
608
|
&self,
|
|
595
609
|
actor: &str,
|