@suveren/gateway 0.32.0 → 0.34.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/content/integrations/erp.json +83 -1
- package/content/integrations/mollie.json +336 -80
- package/dist/control-plane/index.mjs +37 -1
- package/dist/mcp-server/http.mjs +206 -35
- package/dist/ui/assets/{index-BWCRFqkv.css → index-BiGnI2Tk.css} +1 -1
- package/dist/ui/assets/index-DKBwi4Yw.js +123 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/profiles/charge/0.7.profile.json +232 -0
- package/profiles/index.json +3 -1
- package/profiles/invoicing/0.1.profile.json +259 -0
- package/dist/ui/assets/index-BCAc8h7B.js +0 -123
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -1917,6 +1917,13 @@ var ArchiveWriteError = class extends Error {
|
|
|
1917
1917
|
}
|
|
1918
1918
|
};
|
|
1919
1919
|
var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
|
|
1920
|
+
var SENT_ARGS_MAX_BYTES = 64 * 1024;
|
|
1921
|
+
function sentArgsFields(args) {
|
|
1922
|
+
if (args === void 0) return {};
|
|
1923
|
+
const json = JSON.stringify(args);
|
|
1924
|
+
if (Buffer.byteLength(json, "utf8") > SENT_ARGS_MAX_BYTES) return { sentArgsOmitted: "too_large" };
|
|
1925
|
+
return { sentArgs: JSON.parse(json) };
|
|
1926
|
+
}
|
|
1920
1927
|
function stripPreviews(proposal) {
|
|
1921
1928
|
const args = proposal.toolArgs;
|
|
1922
1929
|
if (!args || typeof args !== "object") return proposal;
|
|
@@ -1977,7 +1984,8 @@ var ReceiptArchive = class {
|
|
|
1977
1984
|
asPublicKey: entry.asPublicKey,
|
|
1978
1985
|
receipt: entry.receipt,
|
|
1979
1986
|
...entry.proposal ? { proposal: stripPreviews(entry.proposal) } : {},
|
|
1980
|
-
...entry.boundContent !== void 0 ? { boundContent: entry.boundContent } : {}
|
|
1987
|
+
...entry.boundContent !== void 0 ? { boundContent: entry.boundContent } : {},
|
|
1988
|
+
...sentArgsFields(entry.sentArgs)
|
|
1981
1989
|
});
|
|
1982
1990
|
changed = true;
|
|
1983
1991
|
}
|
|
@@ -2289,12 +2297,16 @@ var ExecutionResultStore = class {
|
|
|
2289
2297
|
isLocked() {
|
|
2290
2298
|
return this.vaultKey === null && existsSync9(this.encryptedFilePath);
|
|
2291
2299
|
}
|
|
2292
|
-
/**
|
|
2300
|
+
/**
|
|
2301
|
+
* Record the outcome of one execution. Replaces any prior entry for the
|
|
2302
|
+
* same proposal — or, for an automatic execution (no proposalId), for the
|
|
2303
|
+
* same ticket.
|
|
2304
|
+
*/
|
|
2293
2305
|
record(input, now = nowSec2()) {
|
|
2294
2306
|
const capped = capResult(input.text, input.structuredContent);
|
|
2295
|
-
this.entries = this.entries.filter((e) => e.proposalId !== input.proposalId);
|
|
2307
|
+
this.entries = input.proposalId !== void 0 ? this.entries.filter((e) => e.proposalId !== input.proposalId) : this.entries.filter((e) => e.proposalId !== void 0 || e.ticketId !== input.ticketId);
|
|
2296
2308
|
this.entries.push({
|
|
2297
|
-
proposalId: input.proposalId,
|
|
2309
|
+
...input.proposalId !== void 0 ? { proposalId: input.proposalId } : {},
|
|
2298
2310
|
ticketId: input.ticketId,
|
|
2299
2311
|
ok: input.ok,
|
|
2300
2312
|
text: capped.text,
|
|
@@ -2309,6 +2321,13 @@ var ExecutionResultStore = class {
|
|
|
2309
2321
|
get(proposalId) {
|
|
2310
2322
|
return this.entries.find((e) => e.proposalId === proposalId);
|
|
2311
2323
|
}
|
|
2324
|
+
/** The retained outcome of the execution under a ticket (either path), newest first. */
|
|
2325
|
+
getByTicket(ticketId) {
|
|
2326
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
2327
|
+
if (this.entries[i].ticketId === ticketId) return this.entries[i];
|
|
2328
|
+
}
|
|
2329
|
+
return void 0;
|
|
2330
|
+
}
|
|
2312
2331
|
/** Diagnostic / tests. */
|
|
2313
2332
|
get size() {
|
|
2314
2333
|
return this.entries.length;
|
|
@@ -3334,6 +3353,7 @@ var SharedState = class {
|
|
|
3334
3353
|
asPublicKey,
|
|
3335
3354
|
proposal: opts.proposal,
|
|
3336
3355
|
boundContent: opts.boundContent,
|
|
3356
|
+
sentArgs: opts.sentArgs,
|
|
3337
3357
|
authorization: opts.profileId ? {
|
|
3338
3358
|
profileId: opts.profileId,
|
|
3339
3359
|
boundsHash: opts.boundsHash,
|
|
@@ -4296,11 +4316,45 @@ function invalidManifestContextReason(manifest) {
|
|
|
4296
4316
|
}
|
|
4297
4317
|
return null;
|
|
4298
4318
|
}
|
|
4299
|
-
function
|
|
4300
|
-
|
|
4301
|
-
|
|
4319
|
+
function overrideProfile(entry) {
|
|
4320
|
+
if (entry === null || typeof entry !== "object") return void 0;
|
|
4321
|
+
const p = entry.profile;
|
|
4322
|
+
return typeof p === "string" ? p : void 0;
|
|
4323
|
+
}
|
|
4324
|
+
function distinctProfilesOf(manifest) {
|
|
4325
|
+
const out = [manifest.profile];
|
|
4326
|
+
for (const entry of Object.values(manifest.toolGating?.overrides ?? {})) {
|
|
4327
|
+
const p = overrideProfile(entry);
|
|
4328
|
+
if (p && !out.includes(p)) out.push(p);
|
|
4329
|
+
}
|
|
4330
|
+
return out;
|
|
4331
|
+
}
|
|
4332
|
+
function invalidManifestOverrideProfilesReason(manifest) {
|
|
4302
4333
|
const overrides = manifest.toolGating?.overrides ?? {};
|
|
4303
|
-
|
|
4334
|
+
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
4335
|
+
const profile = overrideProfile(entry);
|
|
4336
|
+
if (!profile) continue;
|
|
4337
|
+
if (!getProfile2(manifest.profile) && !getProfile2(profile)) continue;
|
|
4338
|
+
if (!getProfile2(profile)) {
|
|
4339
|
+
return `override "${toolName}".profile "${profile}" does not resolve to a known profile (full id or short name) \u2014 area-per-tool overrides must name a real profile, same as the manifest's own "profile"`;
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
return null;
|
|
4343
|
+
}
|
|
4344
|
+
function invalidManifestReadGateReason(manifest) {
|
|
4345
|
+
const allOverrides = manifest.toolGating?.overrides ?? {};
|
|
4346
|
+
for (const profileId of distinctProfilesOf(manifest)) {
|
|
4347
|
+
const profileDef = getProfile2(profileId);
|
|
4348
|
+
if (!profileDef) continue;
|
|
4349
|
+
const ownOverrides = {};
|
|
4350
|
+
for (const [toolName, entry] of Object.entries(allOverrides)) {
|
|
4351
|
+
const resolved = overrideProfile(entry) ?? manifest.profile;
|
|
4352
|
+
if (resolved === profileId) ownOverrides[toolName] = entry;
|
|
4353
|
+
}
|
|
4354
|
+
const reason = uncoveredReadSwitchReason(ownOverrides, profileDef.boundsSchema);
|
|
4355
|
+
if (reason) return reason;
|
|
4356
|
+
}
|
|
4357
|
+
return null;
|
|
4304
4358
|
}
|
|
4305
4359
|
var manifests = /* @__PURE__ */ new Map();
|
|
4306
4360
|
function loadManifests(integrationsDir) {
|
|
@@ -4339,6 +4393,11 @@ function loadManifests(integrationsDir) {
|
|
|
4339
4393
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${contextReason}`);
|
|
4340
4394
|
continue;
|
|
4341
4395
|
}
|
|
4396
|
+
const overrideProfilesReason = invalidManifestOverrideProfilesReason(manifest);
|
|
4397
|
+
if (overrideProfilesReason) {
|
|
4398
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${overrideProfilesReason}`);
|
|
4399
|
+
continue;
|
|
4400
|
+
}
|
|
4342
4401
|
const readGateReason = invalidManifestReadGateReason(manifest);
|
|
4343
4402
|
if (readGateReason) {
|
|
4344
4403
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${readGateReason}`);
|
|
@@ -6016,7 +6075,12 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
6016
6075
|
attestations: auth.attestations,
|
|
6017
6076
|
// Automatic path has no proposal, so without this the archive
|
|
6018
6077
|
// would hold a hash of content it cannot reproduce.
|
|
6019
|
-
boundContent: binding?.boundContent
|
|
6078
|
+
boundContent: binding?.boundContent,
|
|
6079
|
+
// UI spec §14 — what this call sends, exactly the object the
|
|
6080
|
+
// journal's argsHash is computed from below (`hashToolArgs(args)`),
|
|
6081
|
+
// so the ticket view can show it and check it. Same write, so it
|
|
6082
|
+
// is as fail-closed and as encrypted as the rest of the entry.
|
|
6083
|
+
sentArgs: args
|
|
6020
6084
|
});
|
|
6021
6085
|
} catch (err) {
|
|
6022
6086
|
if (err instanceof AsKeyMismatchError || err instanceof TicketBindingMismatchError) {
|
|
@@ -6214,9 +6278,11 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.` + a
|
|
|
6214
6278
|
try {
|
|
6215
6279
|
const result = await integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
|
|
6216
6280
|
state2.executionJournal.complete(receiptId, "done");
|
|
6281
|
+
recordAutomaticOutcome(state2, receiptId, result);
|
|
6217
6282
|
return result;
|
|
6218
6283
|
} catch (err) {
|
|
6219
6284
|
state2.executionJournal.complete(receiptId, "failed");
|
|
6285
|
+
recordAutomaticOutcome(state2, receiptId, void 0, err);
|
|
6220
6286
|
throw err;
|
|
6221
6287
|
}
|
|
6222
6288
|
}
|
|
@@ -6224,6 +6290,20 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.` + a
|
|
|
6224
6290
|
}
|
|
6225
6291
|
});
|
|
6226
6292
|
}
|
|
6293
|
+
function recordAutomaticOutcome(state2, ticketId, result, thrown) {
|
|
6294
|
+
try {
|
|
6295
|
+
if (!result) {
|
|
6296
|
+
const msg = thrown instanceof Error ? thrown.message : String(thrown);
|
|
6297
|
+
state2.executionResults?.record({ ticketId, ok: false, text: `The tool call threw: ${msg}` });
|
|
6298
|
+
return;
|
|
6299
|
+
}
|
|
6300
|
+
const text = result.content?.[0]?.text ?? JSON.stringify(result);
|
|
6301
|
+
const structured = result.structuredContent && typeof result.structuredContent === "object" && !Array.isArray(result.structuredContent) ? result.structuredContent : void 0;
|
|
6302
|
+
state2.executionResults?.record({ ticketId, ok: result.isError !== true, text, structuredContent: structured });
|
|
6303
|
+
} catch (err) {
|
|
6304
|
+
console.error(`[Suveren MCP] Could not record the execution result for ticket ${ticketId}:`, err);
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6227
6307
|
function noMandateRefusal(toolName, profile) {
|
|
6228
6308
|
return `Refused: no mandate covers ${toolName} (${shortProfile(profile)}). Nothing was done. Ask the person to grant a ${shortProfile(profile)} mandate in the gateway, then try again. No ticket was requested.`;
|
|
6229
6309
|
}
|
|
@@ -6675,29 +6755,30 @@ function listIntegrationsHandler(state2, integrationManager2) {
|
|
|
6675
6755
|
}
|
|
6676
6756
|
const auths = state2.getEnrichedAuthorizations();
|
|
6677
6757
|
const statuses = integrationManager2.getStatus();
|
|
6678
|
-
const integrations = statuses.filter((s) => s.running).
|
|
6758
|
+
const integrations = statuses.filter((s) => s.running).flatMap((s) => {
|
|
6679
6759
|
const tools = integrationManager2.getAllTools().filter((t) => t.integrationId === s.id);
|
|
6680
|
-
const
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6760
|
+
const profiles = [...new Set(tools.map((t) => t.gating?.profile).filter((p) => !!p))];
|
|
6761
|
+
if (profiles.length === 0) {
|
|
6762
|
+
return [{ id: s.id, profile: null, status: "no authorization" }];
|
|
6763
|
+
}
|
|
6764
|
+
return profiles.map((profile) => {
|
|
6765
|
+
const matchingAuths = auths.filter((a) => a.complete && profileMatches(a.profileId, profile));
|
|
6766
|
+
let status;
|
|
6767
|
+
if (matchingAuths.length > 0) {
|
|
6768
|
+
const soonestExpiry = Math.min(
|
|
6769
|
+
...matchingAuths.flatMap(
|
|
6770
|
+
(a) => a.attestations.map((att) => att.expiresAt)
|
|
6771
|
+
)
|
|
6772
|
+
);
|
|
6773
|
+
const remainingMs = soonestExpiry * 1e3 - Date.now();
|
|
6774
|
+
const remainingMin = Math.ceil(remainingMs / 6e4);
|
|
6775
|
+
status = `authorized (${matchingAuths.length} path${matchingAuths.length > 1 ? "s" : ""}, ${remainingMin} min remaining)`;
|
|
6776
|
+
} else {
|
|
6777
|
+
const allProfileAuths = auths.filter((a) => profileMatches(a.profileId, profile));
|
|
6778
|
+
status = allProfileAuths.length > 0 ? "expired" : "no authorization";
|
|
6779
|
+
}
|
|
6780
|
+
return { id: s.id, profile: shortProfileName3(profile), status };
|
|
6781
|
+
});
|
|
6701
6782
|
});
|
|
6702
6783
|
const text = integrations.length === 0 ? "No integrations running." : integrations.map(
|
|
6703
6784
|
(i) => `${i.id} [${i.profile ?? "ungated"}] \u2014 ${i.status}`
|
|
@@ -8274,7 +8355,14 @@ var IntegrationManager = class {
|
|
|
8274
8355
|
getToolDisplay() {
|
|
8275
8356
|
const out = {};
|
|
8276
8357
|
for (const t of this.getAllTools()) {
|
|
8277
|
-
|
|
8358
|
+
const mapping = t.gating?.executionMapping;
|
|
8359
|
+
out[t.namespacedName] = {
|
|
8360
|
+
inputSchema: t.inputSchema,
|
|
8361
|
+
...t.gating?.approvalView ? { approvalView: t.gating.approvalView } : {},
|
|
8362
|
+
// UI spec §14.1 — which sent values the signed ticket also carries
|
|
8363
|
+
// (arg → executionContext field), for the "in the ticket" tag.
|
|
8364
|
+
...mapping && Object.keys(mapping).length > 0 ? { executionMapping: mapping } : {}
|
|
8365
|
+
};
|
|
8278
8366
|
}
|
|
8279
8367
|
return out;
|
|
8280
8368
|
}
|
|
@@ -8553,18 +8641,26 @@ var IntegrationManager = class {
|
|
|
8553
8641
|
/**
|
|
8554
8642
|
* Resolve gating config for a tool from the profile's toolGating section.
|
|
8555
8643
|
* Returns the runtime ToolGatingConfig that tool-proxy.ts consumes.
|
|
8644
|
+
*
|
|
8645
|
+
* Area per tool: an override may declare its OWN `profile`, gating that
|
|
8646
|
+
* one tool under a different area than the rest of the connector (e.g.
|
|
8647
|
+
* an ERP's `create_invoice` under `invoicing` while `create_quote` stays
|
|
8648
|
+
* under the manifest's own `sales`). `resolvedProfile` below is what every
|
|
8649
|
+
* downstream consumer reads as this tool's governing profile — falling
|
|
8650
|
+
* back to the manifest-level `profileId` when the override doesn't say.
|
|
8556
8651
|
*/
|
|
8557
8652
|
resolveToolGating(profileId, profileGating, toolName) {
|
|
8558
8653
|
if (!profileId || !profileGating) return null;
|
|
8559
8654
|
if (profileGating.overrides && toolName in profileGating.overrides) {
|
|
8560
8655
|
const override = profileGating.overrides[toolName];
|
|
8561
8656
|
const ext = override ?? {};
|
|
8657
|
+
const resolvedProfile = ext.profile ?? profileId;
|
|
8562
8658
|
if (ext.category === "disabled") {
|
|
8563
|
-
return { profile:
|
|
8659
|
+
return { profile: resolvedProfile, executionMapping: {}, category: "disabled" };
|
|
8564
8660
|
}
|
|
8565
8661
|
if (override === null || ext.category === "read") {
|
|
8566
8662
|
return {
|
|
8567
|
-
profile:
|
|
8663
|
+
profile: resolvedProfile,
|
|
8568
8664
|
executionMapping: {},
|
|
8569
8665
|
category: "read",
|
|
8570
8666
|
boundField: ext.boundField,
|
|
@@ -8579,7 +8675,7 @@ var IntegrationManager = class {
|
|
|
8579
8675
|
};
|
|
8580
8676
|
}
|
|
8581
8677
|
return {
|
|
8582
|
-
profile:
|
|
8678
|
+
profile: resolvedProfile,
|
|
8583
8679
|
executionMapping: override.executionMapping,
|
|
8584
8680
|
staticExecution: override.staticExecution,
|
|
8585
8681
|
// Which argument the receipt binds to. This resolver is an explicit
|
|
@@ -9999,6 +10095,52 @@ function isAlive(pid) {
|
|
|
9999
10095
|
}
|
|
10000
10096
|
}
|
|
10001
10097
|
|
|
10098
|
+
// src/lib/ticket-content.ts
|
|
10099
|
+
function archivedArgs(entry) {
|
|
10100
|
+
if (entry.sentArgs && typeof entry.sentArgs === "object") return { args: entry.sentArgs, source: "sent" };
|
|
10101
|
+
const toolArgs = entry.proposal?.toolArgs;
|
|
10102
|
+
if (toolArgs && typeof toolArgs === "object" && !Array.isArray(toolArgs)) {
|
|
10103
|
+
return { args: toolArgs, source: "proposal" };
|
|
10104
|
+
}
|
|
10105
|
+
return void 0;
|
|
10106
|
+
}
|
|
10107
|
+
function checkSentArgs(entry, journalRow) {
|
|
10108
|
+
const found = archivedArgs(entry);
|
|
10109
|
+
if (!found || !journalRow?.argsHash) return "unchecked";
|
|
10110
|
+
if (hashToolArgs(found.args) === journalRow.argsHash) return "match";
|
|
10111
|
+
if (found.source === "proposal" && typeof found.args.imagePath === "string") return "unchecked";
|
|
10112
|
+
return "mismatch";
|
|
10113
|
+
}
|
|
10114
|
+
function resultView(entry) {
|
|
10115
|
+
if (!entry) return null;
|
|
10116
|
+
return {
|
|
10117
|
+
ok: entry.ok,
|
|
10118
|
+
text: entry.text,
|
|
10119
|
+
...entry.structuredContent ? { structuredContent: entry.structuredContent } : {},
|
|
10120
|
+
...entry.truncated ? { truncated: true } : {}
|
|
10121
|
+
};
|
|
10122
|
+
}
|
|
10123
|
+
function trimForHeadline(value, max = 200) {
|
|
10124
|
+
if (typeof value === "string") return value.length > max ? value.slice(0, max) : value;
|
|
10125
|
+
if (Array.isArray(value)) return value.map((v) => trimForHeadline(v, max));
|
|
10126
|
+
if (value && typeof value === "object") {
|
|
10127
|
+
const out = {};
|
|
10128
|
+
for (const [k, v] of Object.entries(value)) out[k] = trimForHeadline(v, max);
|
|
10129
|
+
return out;
|
|
10130
|
+
}
|
|
10131
|
+
return value;
|
|
10132
|
+
}
|
|
10133
|
+
function headlineArgs(entries) {
|
|
10134
|
+
const out = {};
|
|
10135
|
+
for (const e of entries) {
|
|
10136
|
+
const id = e.receipt?.id;
|
|
10137
|
+
if (typeof id !== "string" || e.proposal) continue;
|
|
10138
|
+
const src = e.sentArgs ?? (e.boundContent && typeof e.boundContent === "object" ? e.boundContent : void 0);
|
|
10139
|
+
if (src) out[id] = trimForHeadline(src);
|
|
10140
|
+
}
|
|
10141
|
+
return out;
|
|
10142
|
+
}
|
|
10143
|
+
|
|
10002
10144
|
// bin/http.ts
|
|
10003
10145
|
import { homedir as homedir14 } from "os";
|
|
10004
10146
|
import { join as join23 } from "path";
|
|
@@ -10535,6 +10677,35 @@ app.get("/internal/evidence", internalOnly, (_req, res) => {
|
|
|
10535
10677
|
gates: state.gateStore.getAll()
|
|
10536
10678
|
});
|
|
10537
10679
|
});
|
|
10680
|
+
app.get("/internal/evidence/ticket/:id", internalOnly, (req, res) => {
|
|
10681
|
+
if (state.receiptArchive.isLocked() || state.executionResults.isLocked()) {
|
|
10682
|
+
res.status(503).json({ error: "Vault locked \u2014 the evidence archive is encrypted until sign-in." });
|
|
10683
|
+
return;
|
|
10684
|
+
}
|
|
10685
|
+
const id = String(req.params.id);
|
|
10686
|
+
const entry = state.receiptArchive.getReceipts().find((r) => r.receipt.id === id);
|
|
10687
|
+
if (!entry) {
|
|
10688
|
+
res.status(404).json({ error: "No local copy of this ticket" });
|
|
10689
|
+
return;
|
|
10690
|
+
}
|
|
10691
|
+
let journalRow;
|
|
10692
|
+
try {
|
|
10693
|
+
journalRow = state.executionJournal.get(id);
|
|
10694
|
+
} catch {
|
|
10695
|
+
journalRow = void 0;
|
|
10696
|
+
}
|
|
10697
|
+
res.json({
|
|
10698
|
+
sentArgsCheck: checkSentArgs(entry, journalRow),
|
|
10699
|
+
result: resultView(state.executionResults.getByTicket(id))
|
|
10700
|
+
});
|
|
10701
|
+
});
|
|
10702
|
+
app.get("/internal/evidence/headlines", internalOnly, (_req, res) => {
|
|
10703
|
+
if (state.receiptArchive.isLocked()) {
|
|
10704
|
+
res.status(503).json({ error: "Vault locked \u2014 the evidence archive is encrypted until sign-in." });
|
|
10705
|
+
return;
|
|
10706
|
+
}
|
|
10707
|
+
res.json({ headlines: headlineArgs(state.receiptArchive.getReceipts()) });
|
|
10708
|
+
});
|
|
10538
10709
|
function reportScope() {
|
|
10539
10710
|
return scopeReportSources(reportSources, {
|
|
10540
10711
|
authorizations: state.getEnrichedAuthorizations(),
|