@preventive/triage 1.0.0-alpha.26 → 1.0.0-alpha.28
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/LICENSE-APACHE +202 -0
- package/client/linked-findings.js +1 -1
- package/common/bundle-metadata.js +12 -10
- package/common/code-language.js +17 -2
- package/common/default-scan-models.ts +3 -3
- package/common/finding-id.js +1 -1
- package/common/managed/report-content.ts +1 -1
- package/common/managed/report-filter.ts +1 -1
- package/out/client-managed-import.js +24 -24
- package/out/client-managed.js +93 -68
- package/out/graph.js +11 -11
- package/out/index.html +1 -1
- package/out/prism.js +2 -2
- package/out/stasis.svg +0 -25
- package/out/terminal.js +163 -163
- package/out/view.css +1 -1
- package/out/view.js +223 -190
- package/package.json +29 -29
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +5 -1
- package/server-common/vercel-blob.ts +6 -3
- package/server-e2e/static.ts +3 -4
- package/server-managed/avatar-store.ts +1 -40
- package/server-managed/blob-store.ts +3 -52
- package/server-managed/bundle-advisories.ts +53 -0
- package/server-managed/bundle-advisory-inventory.ts +75 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +44 -0
- package/server-managed/bundle-build.ts +141 -0
- package/server-managed/bundle-cache.ts +22 -41
- package/server-managed/bundle-store.ts +3 -7
- package/server-managed/bundle-summary-cache.ts +27 -9
- package/server-managed/bundle.ts +66 -1
- package/server-managed/cache-storage.ts +4 -25
- package/server-managed/cli.js +10 -2
- package/server-managed/config.ts +12 -1
- package/server-managed/content-access.ts +1 -1
- package/server-managed/db-methods.ts +163 -40
- package/server-managed/db-neon.ts +124 -30
- package/server-managed/db-schema.ts +2 -0
- package/server-managed/db.ts +11 -1
- package/server-managed/github-app.ts +10 -15
- package/server-managed/github-errors.ts +21 -0
- package/server-managed/github-issues.ts +1 -1
- package/server-managed/github-metadata.ts +34 -1
- package/server-managed/github-pulls.ts +142 -30
- package/server-managed/http.ts +278 -167
- package/server-managed/index.ts +26 -9
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/object-storage-disk.ts +144 -0
- package/server-managed/object-storage-vercel.ts +60 -0
- package/server-managed/object-storage.ts +71 -0
- package/server-managed/public-workspace.ts +20 -14
- package/server-managed/report-migration.ts +1 -1
- package/server-managed/report-sources.ts +14 -4
- package/server-managed/repository-browser.ts +8 -1
- package/server-managed/repository-discovery.ts +1 -1
- package/server-managed/revisions.ts +35 -0
- package/server-managed/solidity-entry-points.ts +69 -0
- package/server-managed/sql-postgres.ts +1 -0
- package/server-managed/sql.ts +14 -8
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +175 -0
- package/server-managed/storage-encryption.ts +211 -0
- package/server-managed/storage-maintenance.ts +197 -0
- package/server-managed/storage-payload.ts +72 -0
- package/server-managed/storage-stores.ts +81 -0
- package/server-managed/storage.ts +48 -24
- package/server-managed/team-feed.ts +50 -5
- package/server-managed/team-reports.ts +85 -22
- package/server-managed/uploads.ts +2 -2
- package/server-managed/workspace-shares.ts +15 -3
- package/report/index.js +0 -260
- package/report/src/finding-id.js +0 -80
- package/report/src/finding.js +0 -314
- package/report/src/labels.js +0 -35
- package/report/src/md-structure.js +0 -471
- package/report/src/md-text.js +0 -167
- package/report/src/meta.js +0 -76
- package/report/src/parse-codex.js +0 -149
- package/report/src/parse-deepsec.js +0 -199
- package/report/src/parse-deepview-fields.js +0 -375
- package/report/src/parse-deepview-md.js +0 -186
- package/report/src/parse-md-id.js +0 -137
- package/report/src/parse-md.js +0 -323
- package/report/src/parse-piolium-id.js +0 -79
- package/report/src/parse-piolium-rows.js +0 -126
- package/report/src/parse-piolium-tokens.js +0 -185
- package/report/src/parse-piolium.js +0 -399
- package/report/src/security.js +0 -63
- package/report/src/severity.js +0 -18
- package/report/src/utf8.js +0 -21
- package/report/src/write-md-finding.js +0 -273
- package/report/src/write-md.js +0 -291
- package/server-managed/blob-vercel.ts +0 -127
- /package/{LICENSE → LICENSE-MIT} +0 -0
|
@@ -8,10 +8,14 @@ import { type CommentStore, commentMethods } from './comments.ts'
|
|
|
8
8
|
import { type ActivityStore, activityMethods } from './activity.ts'
|
|
9
9
|
import { type GithubMetadataStore, githubMetadataMethods } from './github-metadata.ts'
|
|
10
10
|
import { type ManagedIssueStore, managedIssueMethods } from './managed-issues.ts'
|
|
11
|
+
import { type BundleBuildLeaseStore, bundleBuildLeaseMethods } from './bundle-build-leases.ts'
|
|
11
12
|
import { type ImportTriageStore, importTriageMethods } from './import-triage.ts'
|
|
12
13
|
import type { ManagedSql } from './sql.ts'
|
|
13
14
|
import { type WorkspaceShareStore, workspaceShareMethods } from './workspace-shares.ts'
|
|
14
15
|
import { ManagedMutationError, type ManagementStore, managementMethods } from './management.ts'
|
|
16
|
+
import { type ManagementCatalogStore, managementCatalogMethods } from './management-catalog.ts'
|
|
17
|
+
import { type StorageKey } from '../server-common/storage-crypto.ts'
|
|
18
|
+
import { type StorageDb, type StoredTokens, decodeStorageTokens, encodeStorageTokens, storageMethods, storageState, validateStorageUpload } from './storage-db.ts'
|
|
15
19
|
|
|
16
20
|
// A managed user identity (the subset of GitHub's `GET /user` we keep). Input
|
|
17
21
|
// to the upsert; `githubUserId` is the provider lookup key, never exposed to
|
|
@@ -70,6 +74,9 @@ export interface SelectedRepo {
|
|
|
70
74
|
private: boolean
|
|
71
75
|
installationId: number | null
|
|
72
76
|
defaultBranch: string
|
|
77
|
+
// Last live default observed by the repository browser, independent of the
|
|
78
|
+
// selection/discovery snapshot above. NULL means unknown or unavailable.
|
|
79
|
+
cachedDefaultBranch: string | null
|
|
73
80
|
htmlUrl: string
|
|
74
81
|
addedBy: string | null
|
|
75
82
|
addedAt: number
|
|
@@ -84,7 +91,7 @@ export interface ManagedRepo extends SelectedRepo {
|
|
|
84
91
|
|
|
85
92
|
// What a caller supplies to select (upsert) a repo; the store stamps the
|
|
86
93
|
// timestamps.
|
|
87
|
-
export type SelectedRepoInput = Omit<SelectedRepo, 'addedAt'>
|
|
94
|
+
export type SelectedRepoInput = Omit<SelectedRepo, 'addedAt' | 'cachedDefaultBranch'>
|
|
88
95
|
|
|
89
96
|
// A stored report's metadata. The bytes live in the blob-store keyed by `id`;
|
|
90
97
|
// `contentType` + `filename` ride here so a download can label them, `sha256`
|
|
@@ -131,6 +138,7 @@ export interface ReportAccessSnapshot {
|
|
|
131
138
|
// bundle links; `bundleIntegrity` is the report's declared primary bundle (kept
|
|
132
139
|
// so a later bundle upload of that integrity re-links it).
|
|
133
140
|
export interface ReportRecordInput {
|
|
141
|
+
dataKey?: string | null
|
|
134
142
|
id: string
|
|
135
143
|
filename: string
|
|
136
144
|
contentType: string
|
|
@@ -228,7 +236,7 @@ export interface ManagedBundle {
|
|
|
228
236
|
|
|
229
237
|
// What the upload handler supplies to record a bundle; the store stamps
|
|
230
238
|
// uploaded_at. `uploadedByLogin` is the durable uploader-login snapshot.
|
|
231
|
-
export type BundleInput = Omit<ManagedBundle, 'uploadedAt' | 'slug' | 'repoDirectory'> & { uploadedByLogin: string | null; repoDirectory?: string }
|
|
239
|
+
export type BundleInput = Omit<ManagedBundle, 'uploadedAt' | 'slug' | 'repoDirectory'> & { uploadedByLogin: string | null; repoDirectory?: string; dataKey?: string | null }
|
|
232
240
|
|
|
233
241
|
// A bundle row for the "Manage bundles" list — adds the uploader login + repo
|
|
234
242
|
// full name display joins (null when absent / since removed).
|
|
@@ -310,7 +318,12 @@ export interface UserTeam {
|
|
|
310
318
|
}
|
|
311
319
|
|
|
312
320
|
// Backend-agnostic store surface (SQLite + PostgreSQL implementations).
|
|
313
|
-
export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataStore, ManagedIssueStore, WorkspaceShareStore, ImportTriageStore, ManagementStore {
|
|
321
|
+
export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataStore, ManagedIssueStore, BundleBuildLeaseStore, WorkspaceShareStore, ImportTriageStore, ManagementStore, ManagementCatalogStore, StorageDb {
|
|
322
|
+
claimMaintenanceLease(owner: string, now: number, until: number, migration?: boolean): Promise<boolean>
|
|
323
|
+
finishMaintenanceLease(owner: string, until: number): Promise<void>
|
|
324
|
+
getFeedState(sessionId: string, now: number): Promise<{ user: Pick<StoredUser, 'id' | 'role'>; catalog: number; annotations: number } | null>
|
|
325
|
+
// Reuse connections within a finite request or one feed polling iteration.
|
|
326
|
+
withRequest?<T>(work: () => Promise<T>): Promise<T>
|
|
314
327
|
// Upsert the identity; returns the user's opaque id (stable across logins).
|
|
315
328
|
// Initial-admin approval comes only from trusted login configuration. It
|
|
316
329
|
// promotes a matching No access identity only while it is the sole user.
|
|
@@ -330,6 +343,7 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
330
343
|
// mutable context while keeping the original added_by/added_at; deselectRepo
|
|
331
344
|
// resolves true iff a row was removed.
|
|
332
345
|
selectRepo(repo: SelectedRepoInput, now: number): Promise<void>
|
|
346
|
+
cacheRepoDefaultBranch(repo: SelectedRepo, branch: string | null): Promise<boolean>
|
|
333
347
|
connectRepoInstallation(repo: SelectedRepo, installationId: number, sessionId: string, now: number): Promise<boolean>
|
|
334
348
|
deselectRepo(repoId: number): Promise<boolean>
|
|
335
349
|
listSelectedRepos(): Promise<SelectedRepo[]>
|
|
@@ -358,6 +372,8 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
358
372
|
listReports(userId?: string): Promise<AdminReport[]>
|
|
359
373
|
listFindingCatalogReports(after: string, limit: number): Promise<Pick<ReportRecord, 'id' | 'byteSize'>[]>
|
|
360
374
|
getReport(id: string): Promise<ReportRecord | null>
|
|
375
|
+
// Writer-locked reconciliation waits for an uncertain earlier insert.
|
|
376
|
+
resolveReportUpload(id: string): Promise<ReportRecord | null>
|
|
361
377
|
getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string): Promise<TeamReportAccessSnapshot | null>
|
|
362
378
|
getReportAccessSnapshot(sessionId: string, now: number, ids: readonly string[]): Promise<ReportAccessSnapshot | null>
|
|
363
379
|
listReportFilenamesWithBundleHash(bundleId: string, sha256: string): Promise<string[]>
|
|
@@ -378,6 +394,7 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
378
394
|
// stamp nor the trail. Every change also appends to managed_finding_triage_event;
|
|
379
395
|
// listTriageHistory walks one finding's trail, newest first.
|
|
380
396
|
listTriage(findingIds: readonly string[]): Promise<TriageRow[]>
|
|
397
|
+
getAnnotations(findingIds: readonly string[]): Promise<{ triage: TriageRow[]; comments: Awaited<ReturnType<CommentStore['listComments']>> }>
|
|
381
398
|
// A content-free fingerprint of current annotations, read in one snapshot.
|
|
382
399
|
getAnnotationRevision(findingIds: readonly string[]): Promise<string>
|
|
383
400
|
setTriage(findingId: string, entry: TriageEntryPatch | null, updatedBy: string | null, updatedByLogin: string | null, now: number): Promise<void>
|
|
@@ -392,8 +409,15 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
392
409
|
// declared its integrity.
|
|
393
410
|
insertBundle(bundle: BundleInput, now: number, sessionId?: string): Promise<void>
|
|
394
411
|
getBundleByIntegrity(integrity: string): Promise<ManagedBundle | null>
|
|
412
|
+
resolveBundleUpload(integrity: string): Promise<ManagedBundle | null>
|
|
395
413
|
getBundle(id: string): Promise<ManagedBundle | null>
|
|
414
|
+
// One read snapshot of the live session, bundle and grants. Passing a team
|
|
415
|
+
// (or null for any team) also checks advisory security access; omission skips it.
|
|
416
|
+
getBundleAccessSnapshot(sessionId: string, now: number, id: string, advisoriesTeamId?: string | null): Promise<{
|
|
417
|
+
bundle: ManagedBundle | null; canReadAdvisories: boolean
|
|
418
|
+
} | null>
|
|
396
419
|
listBundles(userId?: string): Promise<AdminBundle[]>
|
|
420
|
+
listReadableBundleIds(userId: string, ids: readonly string[]): Promise<string[]>
|
|
397
421
|
userCanReadBundle(userId: string, id: string): Promise<boolean>
|
|
398
422
|
userCanReadRepo(userId: string, repoId: number): Promise<boolean>
|
|
399
423
|
userCanReadBundleAdvisories(userId: string, bundleId: string, teamId: string | null): Promise<boolean>
|
|
@@ -424,7 +448,7 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
424
448
|
// bundles attached to that team's repos — for that user's own sidebar Teams section.
|
|
425
449
|
// Any user; only their own memberships.
|
|
426
450
|
listTeamsForUser(userId: string): Promise<UserTeam[]>
|
|
427
|
-
getUserTeamFeedSnapshot(sessionId: string, now: number): Promise<{ user: Pick<StoredUser, 'id' | 'role'>; revision: string } | null>
|
|
451
|
+
getUserTeamFeedSnapshot(sessionId: string, now: number): Promise<{ user: Pick<StoredUser, 'id' | 'role'>; revision: string; teams: UserTeam[]; catalog: number } | null>
|
|
428
452
|
// Whether `userId` may read `reportId`: true iff the report's repo belongs to
|
|
429
453
|
// a team the user is a member of. Backs the team-scoped report view endpoint.
|
|
430
454
|
userCanReadReport(userId: string, reportId: string): Promise<boolean>
|
|
@@ -499,11 +523,11 @@ function prepareStatements(db: ManagedSql) {
|
|
|
499
523
|
updateRoleStmt: db.prepare(`UPDATE managed_user SET role = ?, updated_at = ? WHERE id = ?`),
|
|
500
524
|
updateTokensStmt: db.prepare(
|
|
501
525
|
`UPDATE managed_user
|
|
502
|
-
SET gh_access_token = ?, gh_refresh_token = ?, gh_token_expires_at = ?, updated_at = ?
|
|
526
|
+
SET gh_access_token = ?, gh_refresh_token = ?, gh_token_expires_at = ?, gh_tokens_encrypted = ?, updated_at = ?
|
|
503
527
|
WHERE id = ?`,
|
|
504
528
|
),
|
|
505
529
|
selectTokensStmt: db.prepare(
|
|
506
|
-
`SELECT gh_access_token AS access, gh_refresh_token AS refresh, gh_token_expires_at AS exp
|
|
530
|
+
`SELECT gh_access_token AS access, gh_refresh_token AS refresh, gh_token_expires_at AS exp, gh_tokens_encrypted AS encrypted
|
|
507
531
|
FROM managed_user WHERE id = ?`,
|
|
508
532
|
),
|
|
509
533
|
deleteSessionStmt: db.prepare(`DELETE FROM managed_session WHERE id = ?`),
|
|
@@ -522,6 +546,8 @@ function prepareStatements(db: ManagedSql) {
|
|
|
522
546
|
WHERE repo_id = ? AND full_name = ? AND added_at = ? AND installation_id IS NULL
|
|
523
547
|
AND EXISTS (SELECT 1 FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
524
548
|
WHERE s.id = ? AND s.expires_at > ? AND u.role = 'admin')`),
|
|
549
|
+
cacheRepoDefaultBranchStmt: db.prepare(`UPDATE managed_selected_repo SET cached_default_branch = ?
|
|
550
|
+
WHERE repo_id = ? AND full_name = ? AND added_at = ? AND active = 1`),
|
|
525
551
|
deleteRepoStmt: db.prepare(`DELETE FROM managed_selected_repo WHERE repo_id = ?`),
|
|
526
552
|
deactivateRepoStmt: db.prepare(`UPDATE managed_selected_repo SET active = 0 WHERE repo_id = ? AND active = 1`),
|
|
527
553
|
reactivateRepoStmt: db.prepare(`UPDATE managed_selected_repo SET active = 1 WHERE repo_id = ? AND active = 0`),
|
|
@@ -539,21 +565,21 @@ function prepareStatements(db: ManagedSql) {
|
|
|
539
565
|
UNION SELECT finding_id FROM managed_finding_comment_event WHERE finding_id IN (SELECT value FROM json_each(?))) AS annotations`),
|
|
540
566
|
selectReposStmt: db.prepare(
|
|
541
567
|
`SELECT repo_id AS repoId, full_name AS fullName, is_private AS priv,
|
|
542
|
-
installation_id AS installId, default_branch AS branch, html_url AS htmlUrl,
|
|
568
|
+
installation_id AS installId, default_branch AS branch, cached_default_branch AS cachedDefaultBranch, html_url AS htmlUrl,
|
|
543
569
|
added_by AS addedBy, added_at AS addedAt, active AS active
|
|
544
570
|
FROM managed_selected_repo WHERE active = 1 ORDER BY full_name ASC`,
|
|
545
571
|
),
|
|
546
572
|
selectAllReposStmt: db.prepare(
|
|
547
573
|
`SELECT repo_id AS repoId, full_name AS fullName, is_private AS priv,
|
|
548
|
-
installation_id AS installId, default_branch AS branch, html_url AS htmlUrl,
|
|
574
|
+
installation_id AS installId, default_branch AS branch, cached_default_branch AS cachedDefaultBranch, html_url AS htmlUrl,
|
|
549
575
|
added_by AS addedBy, added_at AS addedAt, active AS active
|
|
550
576
|
FROM managed_selected_repo ORDER BY full_name ASC`,
|
|
551
577
|
),
|
|
552
578
|
insertReportStmt: db.prepare(
|
|
553
579
|
`WITH candidate(id, slug) AS (VALUES (?, ?))
|
|
554
|
-
INSERT INTO managed_report (id, slug, filename, content_type, byte_size, sha256, uploaded_by, uploaded_by_login, repo_id, repo_directory, repo_embedded, analyzer, visible, bundle_id, bundle_integrity, uploaded_at)
|
|
580
|
+
INSERT INTO managed_report (id, slug, filename, content_type, byte_size, sha256, uploaded_by, uploaded_by_login, repo_id, repo_directory, repo_embedded, analyzer, visible, bundle_id, bundle_integrity, uploaded_at, data_key, storage_encrypted)
|
|
555
581
|
SELECT id, CASE WHEN EXISTS (SELECT 1 FROM managed_report WHERE slug = candidate.slug) THEN id ELSE slug END,
|
|
556
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM candidate`,
|
|
582
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM candidate`,
|
|
557
583
|
),
|
|
558
584
|
// LEFT JOINs so a report whose uploader / repo / bundle was removed (the FK
|
|
559
585
|
// nulled) still lists, with null display fields.
|
|
@@ -680,9 +706,9 @@ function prepareStatements(db: ManagedSql) {
|
|
|
680
706
|
WHERE finding_id IN (SELECT value FROM json_each(?)) ORDER BY id`),
|
|
681
707
|
insertBundleStmt: db.prepare(
|
|
682
708
|
`WITH candidate(id, slug) AS (VALUES (?, ?))
|
|
683
|
-
INSERT INTO managed_bundle (id, slug, integrity, filename, kind, byte_size, uploaded_by, uploaded_by_login, repo_id, repo_directory, uploaded_at)
|
|
709
|
+
INSERT INTO managed_bundle (id, slug, integrity, filename, kind, byte_size, uploaded_by, uploaded_by_login, repo_id, repo_directory, uploaded_at, data_key, storage_encrypted)
|
|
684
710
|
SELECT id, CASE WHEN EXISTS (SELECT 1 FROM managed_bundle WHERE slug = candidate.slug) THEN id ELSE slug END,
|
|
685
|
-
?, ?, ?, ?, ?, ?, ?, ?, ? FROM candidate`,
|
|
711
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM candidate`,
|
|
686
712
|
),
|
|
687
713
|
selectBundleByIntegrityStmt: db.prepare(
|
|
688
714
|
`SELECT id, slug, integrity, filename, kind, byte_size AS byteSize,
|
|
@@ -711,6 +737,11 @@ function prepareStatements(db: ManagedSql) {
|
|
|
711
737
|
SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
712
738
|
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = ?))`,
|
|
713
739
|
),
|
|
740
|
+
selectReadableBundleIdsStmt: db.prepare(
|
|
741
|
+
`SELECT b.id FROM managed_bundle b WHERE b.id IN (SELECT value FROM json_each(?)) AND (b.uploaded_by = ? OR EXISTS (
|
|
742
|
+
SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
743
|
+
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = ?))`,
|
|
744
|
+
),
|
|
714
745
|
selectRepoReadableStmt: db.prepare(
|
|
715
746
|
`SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
716
747
|
WHERE tr.repo_id = ? AND tu.user_id = ? LIMIT 1`,
|
|
@@ -836,7 +867,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
836
867
|
|
|
837
868
|
type RepoRow = {
|
|
838
869
|
repoId: number; fullName: string; priv: number; installId: number | null
|
|
839
|
-
branch: string; htmlUrl: string; addedBy: string | null; addedAt: number; active: number
|
|
870
|
+
branch: string; cachedDefaultBranch: string | null; htmlUrl: string; addedBy: string | null; addedAt: number; active: number
|
|
840
871
|
}
|
|
841
872
|
|
|
842
873
|
// The repo-selection slice of ManagedDb, split out to keep openSqliteManagedDb
|
|
@@ -846,7 +877,7 @@ function selectedRepoMethods(stmts: ReturnType<typeof prepareStatements>) {
|
|
|
846
877
|
selectReportsForRepoStmt, selectBundlesForRepoStmt, deleteReportsForRepoStmt, deleteBundlesForRepoStmt } = stmts
|
|
847
878
|
const readRepo = (r: RepoRow): SelectedRepo => ({
|
|
848
879
|
repoId: r.repoId, fullName: r.fullName, private: r.priv === 1,
|
|
849
|
-
installationId: r.installId, defaultBranch: r.branch, htmlUrl: r.htmlUrl,
|
|
880
|
+
installationId: r.installId, defaultBranch: r.branch, cachedDefaultBranch: r.cachedDefaultBranch, htmlUrl: r.htmlUrl,
|
|
850
881
|
addedBy: r.addedBy, addedAt: r.addedAt,
|
|
851
882
|
})
|
|
852
883
|
const readManagedRepo = (r: RepoRow): ManagedRepo => ({ ...readRepo(r), active: r.active === 1 })
|
|
@@ -857,6 +888,9 @@ function selectedRepoMethods(stmts: ReturnType<typeof prepareStatements>) {
|
|
|
857
888
|
repo.defaultBranch, repo.htmlUrl, repo.addedBy, now, now,
|
|
858
889
|
)
|
|
859
890
|
},
|
|
891
|
+
async cacheRepoDefaultBranch(repo: SelectedRepo, branch: string | null): Promise<boolean> {
|
|
892
|
+
return Number((await stmts.cacheRepoDefaultBranchStmt.run(branch, repo.repoId, repo.fullName, repo.addedAt)).changes) > 0
|
|
893
|
+
},
|
|
860
894
|
async deselectRepo(repoId: number): Promise<boolean> {
|
|
861
895
|
return Number((await deleteRepoStmt.run(repoId)).changes) > 0
|
|
862
896
|
},
|
|
@@ -924,7 +958,7 @@ async function authorizeUpload(stmts: ReturnType<typeof prepareStatements>, sess
|
|
|
924
958
|
}
|
|
925
959
|
|
|
926
960
|
// The report slice of ManagedDb closes over its prepared statements.
|
|
927
|
-
function reportMethods(stmts: ReturnType<typeof prepareStatements
|
|
961
|
+
function reportMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql, key: StorageKey | null) {
|
|
928
962
|
const { insertReportStmt, selectReportsStmt, selectReportStmt, deleteReportStmt, setReportRepoStmt, setReportVisibleStmt } = stmts
|
|
929
963
|
async function getReport(id: string): Promise<ReportRecord | null> {
|
|
930
964
|
const row = (await selectReportStmt.get(id)) as ReportRow | undefined
|
|
@@ -936,16 +970,18 @@ function reportMethods(stmts: ReturnType<typeof prepareStatements>) {
|
|
|
936
970
|
return row ? getReport(row.id) : null
|
|
937
971
|
}
|
|
938
972
|
async function insertReport(report: ReportRecordInput, now: number): Promise<void> {
|
|
973
|
+
await validateStorageUpload(db, key, 'report', report.id, report.dataKey)
|
|
939
974
|
await insertReportStmt.run(
|
|
940
975
|
report.id, preferredSlug(report.id), report.filename, report.contentType, report.byteSize,
|
|
941
976
|
report.sha256, report.uploadedBy, report.uploadedByLogin ?? null, report.repoId,
|
|
942
977
|
report.repoDirectory ?? '', report.repoEmbedded ? 1 : 0, report.analyzer ?? null, report.visible == null ? 0 : report.visible ? 1 : 0, report.bundleId ?? null,
|
|
943
|
-
report.bundleIntegrity ?? null, now,
|
|
978
|
+
report.bundleIntegrity ?? null, now, report.dataKey ?? null, report.dataKey ? 1 : 0,
|
|
944
979
|
)
|
|
945
980
|
}
|
|
946
981
|
return {
|
|
947
982
|
insertReport,
|
|
948
983
|
getReport,
|
|
984
|
+
resolveReportUpload: getReport,
|
|
949
985
|
getReportByHash,
|
|
950
986
|
async insertOrReuseReport(report: ReportRecordInput, now: number, sessionId?: string): Promise<ReportRecord> {
|
|
951
987
|
await authorizeUpload(stmts, sessionId, report)
|
|
@@ -1038,7 +1074,7 @@ type TriageEventDbRow = TriageStateDbRow & {
|
|
|
1038
1074
|
// current row is skipped altogether, so a client re-pushing what already
|
|
1039
1075
|
// stands neither re-stamps the writer nor echoes into the trail. `historyLimit`
|
|
1040
1076
|
// > 0 keeps only that many events per finding; 0 keeps everything.
|
|
1041
|
-
function triageMethods(
|
|
1077
|
+
function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit: number, db: ManagedSql) {
|
|
1042
1078
|
const { upsertTriageStmt, selectTriageStmt, selectTriageStateStmt, insertTriageEventStmt, trimTriageEventsStmt, selectTriageHistoryStmt,
|
|
1043
1079
|
deleteTriageStmt, deleteTriageHistoryStmt, deleteCommentsStmt, deleteCommentHistoryStmt, countAnnotationsStmt } = stmts
|
|
1044
1080
|
async function writeEntry(findingId: string, entry: TriageEntryPatch | null, batchId: string, updatedBy: string | null, updatedByLogin: string | null, now: number, reportId: string | null = null): Promise<void> {
|
|
@@ -1104,7 +1140,43 @@ function triageMethods( stmts: ReturnType<typeof prepareStatements>, historyLimi
|
|
|
1104
1140
|
// (and costs one fsync under synchronous = FULL, not one per row); one
|
|
1105
1141
|
// batch id groups its rows in the trail.
|
|
1106
1142
|
const batchId = randomUUID()
|
|
1107
|
-
|
|
1143
|
+
if (new Set(entries.map(([id]) => id)).size !== entries.length) {
|
|
1144
|
+
// Preserve the ordered history of callers that supply repeated IDs.
|
|
1145
|
+
for (const [id, entry] of entries) await writeEntry(id, entry, batchId, updatedBy, updatedByLogin, now, reportId)
|
|
1146
|
+
return
|
|
1147
|
+
}
|
|
1148
|
+
const current = new Map((await selectTriageStmt.all(JSON.stringify(entries.map(([id]) => id))) as TriageDbRow[])
|
|
1149
|
+
.map(row => [row.findingId, row]))
|
|
1150
|
+
const changed = entries.flatMap(([id, entry]) => {
|
|
1151
|
+
const e = entry ?? {}, previous = current.get(id)
|
|
1152
|
+
const next = { color: e.color ?? null, triage: e.triage ?? null, comment: e.comment ?? null,
|
|
1153
|
+
fix: e.fix ?? null, flagged: e.flagged == null ? null : e.flagged ? 1 : 0 }
|
|
1154
|
+
if (previous && Object.entries(next).every(([key, value]) => previous[key as keyof TriageStateDbRow] === value)) return []
|
|
1155
|
+
return [[id, next.color, next.triage, next.comment, next.fix, next.flagged]]
|
|
1156
|
+
})
|
|
1157
|
+
// Bound SQL parameters while keeping the entire batch/history atomic.
|
|
1158
|
+
for (let offset = 0; offset < changed.length; offset += 200) {
|
|
1159
|
+
const rows = changed.slice(offset, offset + 200), values = rows.flat()
|
|
1160
|
+
const input = `WITH changes(finding_id, color, triage, comment, fix, flagged, ordinal) AS
|
|
1161
|
+
(VALUES ${rows.map((_, index) => `(?, ?, ?, ?, ?, CAST(? AS INTEGER), ${index})`).join(',')})`
|
|
1162
|
+
await db.prepare(`${input}
|
|
1163
|
+
INSERT INTO managed_finding_triage (finding_id, color, triage, comment, fix, flagged, updated_by, updated_by_login, updated_at)
|
|
1164
|
+
SELECT finding_id, color, triage, comment, fix, flagged, ?, ?, CAST(? AS BIGINT) FROM changes WHERE TRUE
|
|
1165
|
+
ON CONFLICT(finding_id) DO UPDATE SET color = excluded.color, triage = excluded.triage,
|
|
1166
|
+
comment = excluded.comment, fix = excluded.fix, flagged = excluded.flagged, updated_by = excluded.updated_by,
|
|
1167
|
+
updated_by_login = excluded.updated_by_login, updated_at = excluded.updated_at`).run(...values, updatedBy, updatedByLogin, now)
|
|
1168
|
+
await db.prepare(`${input}
|
|
1169
|
+
INSERT INTO managed_finding_triage_event (finding_id, batch_id, color, triage, comment, fix, flagged, actor_id, actor_login, at, report_id, report, repo)
|
|
1170
|
+
SELECT c.finding_id, ?, c.color, c.triage, c.comment, c.fix, c.flagged, ?, ?, CAST(? AS BIGINT), ?, r.filename, p.full_name
|
|
1171
|
+
FROM changes c LEFT JOIN managed_report r ON r.id = ? LEFT JOIN managed_selected_repo p ON p.repo_id = r.repo_id ORDER BY c.ordinal`)
|
|
1172
|
+
.run(...values, batchId, updatedBy, updatedByLogin, now, reportId ?? null, reportId ?? null)
|
|
1173
|
+
if (historyLimit > 0) {
|
|
1174
|
+
await db.prepare(`DELETE FROM managed_finding_triage_event WHERE seq IN (
|
|
1175
|
+
SELECT seq FROM (SELECT seq, ROW_NUMBER() OVER (PARTITION BY finding_id ORDER BY seq DESC) AS position
|
|
1176
|
+
FROM managed_finding_triage_event WHERE finding_id IN (SELECT value FROM json_each(?))) ranked WHERE position > ?)`)
|
|
1177
|
+
.run(JSON.stringify(rows.map(row => row[0])), historyLimit)
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1108
1180
|
},
|
|
1109
1181
|
}
|
|
1110
1182
|
}
|
|
@@ -1126,27 +1198,48 @@ function mapBundle(r: BundleRow): ManagedBundle {
|
|
|
1126
1198
|
}
|
|
1127
1199
|
|
|
1128
1200
|
// The bundle slice of ManagedDb. Closes over its prepared statements.
|
|
1129
|
-
function bundleMethods(stmts: ReturnType<typeof prepareStatements
|
|
1201
|
+
function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql, key: StorageKey | null) {
|
|
1130
1202
|
const {
|
|
1131
1203
|
insertBundleStmt, selectBundleByIntegrityStmt, selectBundleStmt,
|
|
1132
1204
|
selectBundlesStmt, deleteBundleStmt, setBundleRepoStmt, linkReportsToBundleStmt, selectBundleReadableStmt, selectRepoReadableStmt, selectRepoPathReadableStmt,
|
|
1133
1205
|
} = stmts
|
|
1206
|
+
async function getBundleByIntegrity(integrity: string): Promise<ManagedBundle | null> {
|
|
1207
|
+
const row = (await selectBundleByIntegrityStmt.get(integrity)) as BundleRow | undefined
|
|
1208
|
+
return row == null ? null : mapBundle(row)
|
|
1209
|
+
}
|
|
1134
1210
|
return {
|
|
1135
1211
|
async insertBundle(bundle: BundleInput, now: number, sessionId?: string): Promise<void> {
|
|
1136
1212
|
await authorizeUpload(stmts, sessionId, bundle)
|
|
1213
|
+
await validateStorageUpload(db, key, 'bundle', bundle.id, bundle.dataKey)
|
|
1137
1214
|
await insertBundleStmt.run(
|
|
1138
1215
|
bundle.id, preferredSlug(bundle.id), bundle.integrity, bundle.filename, bundle.kind,
|
|
1139
|
-
bundle.byteSize, bundle.uploadedBy, bundle.uploadedByLogin ?? null, bundle.repoId, bundle.repoId == null ? '' : bundle.repoDirectory ?? '', now,
|
|
1216
|
+
bundle.byteSize, bundle.uploadedBy, bundle.uploadedByLogin ?? null, bundle.repoId, bundle.repoId == null ? '' : bundle.repoDirectory ?? '', now, bundle.dataKey ?? null, bundle.dataKey ? 1 : 0,
|
|
1140
1217
|
)
|
|
1141
1218
|
},
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
},
|
|
1219
|
+
getBundleByIntegrity,
|
|
1220
|
+
// The distinct method name selects a writer-locked reconciliation scope.
|
|
1221
|
+
resolveBundleUpload: getBundleByIntegrity,
|
|
1146
1222
|
async getBundle(id: string): Promise<ManagedBundle | null> {
|
|
1147
1223
|
const row = (await selectBundleStmt.get(id)) as BundleRow | undefined
|
|
1148
1224
|
return row == null ? null : mapBundle(row)
|
|
1149
1225
|
},
|
|
1226
|
+
async getBundleAccessSnapshot(sessionId: string, now: number, id: string, advisoriesTeamId?: string | null) {
|
|
1227
|
+
const session = await stmts.selectSessionStmt.get(sessionId, now) as SessionRow | undefined
|
|
1228
|
+
if (!session) return null
|
|
1229
|
+
const denied = { bundle: null, canReadAdvisories: false }
|
|
1230
|
+
if (!roleAtLeast(session.role, 'view')) return denied
|
|
1231
|
+
const row = await selectBundleStmt.get(id) as BundleRow | undefined
|
|
1232
|
+
if (!row) return denied
|
|
1233
|
+
const manager = roleAtLeast(session.role, 'manage')
|
|
1234
|
+
if (session.role !== 'admin') {
|
|
1235
|
+
const allowed = manager ? await selectBundleReadableStmt.get(id, session.uid, session.uid)
|
|
1236
|
+
: row.repoId !== null && await selectRepoPathReadableStmt.get(row.repoDirectory, row.repoId, session.uid)
|
|
1237
|
+
if (!allowed) return denied
|
|
1238
|
+
}
|
|
1239
|
+
const canReadAdvisories = manager || (advisoriesTeamId !== undefined
|
|
1240
|
+
&& await stmts.selectBundleSecurityStmt.get(id, session.uid, advisoriesTeamId, advisoriesTeamId) != null)
|
|
1241
|
+
return { bundle: mapBundle(row), canReadAdvisories }
|
|
1242
|
+
},
|
|
1150
1243
|
async listBundles(userId?: string): Promise<AdminBundle[]> {
|
|
1151
1244
|
const rows = (await selectBundlesStmt.all(userId ?? null, userId ?? null, userId ?? null)) as BundleListRow[]
|
|
1152
1245
|
return rows.map((r) => ({
|
|
@@ -1158,6 +1251,11 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>) {
|
|
|
1158
1251
|
async userCanReadBundle(userId: string, id: string): Promise<boolean> {
|
|
1159
1252
|
return (await selectBundleReadableStmt.get(id, userId, userId)) != null
|
|
1160
1253
|
},
|
|
1254
|
+
async listReadableBundleIds(userId: string, ids: readonly string[]): Promise<string[]> {
|
|
1255
|
+
if (ids.length === 0) return []
|
|
1256
|
+
const rows = await stmts.selectReadableBundleIdsStmt.all(JSON.stringify([...new Set(ids)]), userId, userId) as { id: string }[]
|
|
1257
|
+
return rows.map(row => row.id)
|
|
1258
|
+
},
|
|
1161
1259
|
async userCanReadRepo(userId: string, repoId: number): Promise<boolean> {
|
|
1162
1260
|
return (await selectRepoReadableStmt.get(repoId, userId)) != null
|
|
1163
1261
|
},
|
|
@@ -1183,7 +1281,7 @@ type TeamMemberRow = { teamId: string; userId: string; login: string; viewDepend
|
|
|
1183
1281
|
// The team slice of ManagedDb. listTeams reads the three tables in full and
|
|
1184
1282
|
// groups in JS (3 queries, not N+1) — fine for the handful of teams a managed
|
|
1185
1283
|
// workspace has.
|
|
1186
|
-
function teamMethods(
|
|
1284
|
+
function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql) {
|
|
1187
1285
|
const {
|
|
1188
1286
|
insertTeamStmt, selectTeamByNameStmt, renameTeamStmt, deleteTeamStmt, selectTeamStmt,
|
|
1189
1287
|
selectUserRepoScopesStmt, selectTeamsStmt, selectTeamsForUserStmt, selectUserTeamReportsStmt, selectUserTeamBundlesStmt, selectReportReadableStmt,
|
|
@@ -1246,15 +1344,19 @@ function teamMethods( stmts: ReturnType<typeof prepareStatements>) {
|
|
|
1246
1344
|
bundles: bundlesByTeam.get(t.id) ?? [],
|
|
1247
1345
|
}))
|
|
1248
1346
|
},
|
|
1347
|
+
async getFeedState(sessionId: string, now: number) {
|
|
1348
|
+
const row = await db.prepare(`SELECT u.id, u.role, r.catalog, r.annotations FROM managed_session s
|
|
1349
|
+
JOIN managed_user u ON u.id = s.user_id JOIN managed_change_revision r ON r.id = 1
|
|
1350
|
+
WHERE s.id = ? AND s.expires_at > ?`).get(sessionId, now) as { id: string; role: Role; catalog: number; annotations: number } | undefined
|
|
1351
|
+
return row ? { user: { id: row.id, role: row.role }, catalog: row.catalog, annotations: row.annotations } : null
|
|
1352
|
+
},
|
|
1249
1353
|
async getUserTeamFeedSnapshot(sessionId: string, now: number) {
|
|
1250
1354
|
// One short read-only transaction, shared by SQLite and Postgres. Never
|
|
1251
1355
|
// touch last-seen timestamps or include other users' memberships.
|
|
1252
|
-
const
|
|
1253
|
-
if (!
|
|
1254
|
-
const teams =
|
|
1255
|
-
return { user:
|
|
1256
|
-
revision: teamCatalogRevision(teams),
|
|
1257
|
-
}
|
|
1356
|
+
const state = await methods.getFeedState(sessionId, now)
|
|
1357
|
+
if (!state) return null
|
|
1358
|
+
const teams = state.user.role === 'none' ? [] : await methods.listTeamsForUser(state.user.id)
|
|
1359
|
+
return { user: state.user, catalog: state.catalog, teams, revision: teamCatalogRevision(teams) }
|
|
1258
1360
|
},
|
|
1259
1361
|
async userCanReadReport(userId: string, reportId: string): Promise<boolean> {
|
|
1260
1362
|
return (await selectReportReadableStmt.get(reportId, userId)) != null
|
|
@@ -1309,21 +1411,33 @@ function teamMethods( stmts: ReturnType<typeof prepareStatements>) {
|
|
|
1309
1411
|
// `triageHistoryLimit`: events kept per finding in the triage trail; 0 (the
|
|
1310
1412
|
// default) keeps everything. See ManagedConfig.
|
|
1311
1413
|
export interface ManagedDbOptions {
|
|
1414
|
+
storageEncryptionKey?: StorageKey | null
|
|
1312
1415
|
triageHistoryLimit?: number
|
|
1313
1416
|
}
|
|
1314
1417
|
|
|
1315
1418
|
export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions = {}): ManagedDb {
|
|
1419
|
+
const key = options.storageEncryptionKey ?? null
|
|
1316
1420
|
const comments = commentMethods(db)
|
|
1317
1421
|
const activity = activityMethods(db)
|
|
1318
1422
|
const stmts = prepareStatements(db)
|
|
1319
|
-
const reports = reportMethods(stmts)
|
|
1320
|
-
const triage = triageMethods(stmts, options.triageHistoryLimit ?? 0)
|
|
1423
|
+
const reports = reportMethods(stmts, db, key)
|
|
1424
|
+
const triage = triageMethods(stmts, options.triageHistoryLimit ?? 0, db)
|
|
1321
1425
|
const {
|
|
1322
1426
|
upsertUserStmt, selectUserIdStmt, promoteInitialAdminStmt, selectGithubIdStmt, insertSessionStmt, selectSessionStmt, selectUsersStmt,
|
|
1323
1427
|
touchUserSeenStmt, updateRoleStmt, updateTokensStmt, selectTokensStmt, deleteSessionStmt, deleteExpiredStmt,
|
|
1324
1428
|
} = stmts
|
|
1325
1429
|
|
|
1326
|
-
const methods: Omit<ManagedDb, keyof ManagementStore> = {
|
|
1430
|
+
const methods: Omit<ManagedDb, keyof ManagementStore | keyof ManagementCatalogStore> = {
|
|
1431
|
+
...storageMethods(db, key),
|
|
1432
|
+
...bundleBuildLeaseMethods(db),
|
|
1433
|
+
async claimMaintenanceLease(owner, now, until, migration = false) {
|
|
1434
|
+
return !!await db.prepare(`INSERT INTO managed_maintenance_lease (id, owner, expires_at) VALUES (?, ?, ?)
|
|
1435
|
+
ON CONFLICT(id) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at
|
|
1436
|
+
WHERE managed_maintenance_lease.expires_at <= ? RETURNING id`).get(migration ? 2 : 1, owner, until, now)
|
|
1437
|
+
},
|
|
1438
|
+
async finishMaintenanceLease(owner, until) {
|
|
1439
|
+
await db.prepare('UPDATE managed_maintenance_lease SET expires_at = ? WHERE owner = ?').run(until, owner)
|
|
1440
|
+
},
|
|
1327
1441
|
async upsertUser(user, now, initialAdminGithubId = null) {
|
|
1328
1442
|
// New row → a fresh id; ON CONFLICT(github_user_id) keeps an existing
|
|
1329
1443
|
// user's id (DO UPDATE leaves it untouched), so re-read to return it.
|
|
@@ -1364,12 +1478,18 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1364
1478
|
return Number((await updateRoleStmt.run(role, Date.now(), id)).changes) > 0
|
|
1365
1479
|
},
|
|
1366
1480
|
async setUserTokens(id, tokens) {
|
|
1367
|
-
|
|
1481
|
+
const state = await storageState(db, key)
|
|
1482
|
+
const value = state ? encodeStorageTokens(key!, id, tokens.accessToken, tokens.refreshToken) : { access: tokens.accessToken, refresh: tokens.refreshToken }
|
|
1483
|
+
await updateTokensStmt.run(value.access, value.refresh, tokens.expiresAt, state ? 1 : 0, Date.now(), id)
|
|
1368
1484
|
},
|
|
1369
1485
|
async getUserTokens(id) {
|
|
1370
|
-
const
|
|
1486
|
+
const state = await storageState(db, key)
|
|
1487
|
+
const row = (await selectTokensStmt.get(id)) as StoredTokens | undefined
|
|
1371
1488
|
if (row == null || row.access == null) return null
|
|
1372
|
-
|
|
1489
|
+
if (row.encrypted && !state) throw new Error('Encrypted tokens without storage encryption state')
|
|
1490
|
+
if (!row.encrypted && state?.complete) throw new Error('Unexpected plaintext GitHub tokens')
|
|
1491
|
+
const value = row.encrypted ? decodeStorageTokens(key!, id, row) : row
|
|
1492
|
+
return { accessToken: value.access!, refreshToken: value.refresh, expiresAt: row.exp }
|
|
1373
1493
|
},
|
|
1374
1494
|
async getUserGithubId(id) {
|
|
1375
1495
|
const row = (await selectGithubIdStmt.get(id)) as { githubId: number } | undefined
|
|
@@ -1380,15 +1500,18 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1380
1500
|
...selectedRepoMethods(stmts),
|
|
1381
1501
|
...activity,
|
|
1382
1502
|
...comments,
|
|
1503
|
+
async getAnnotations(ids) {
|
|
1504
|
+
return { triage: await triage.listTriage(ids), comments: await comments.listComments(ids) }
|
|
1505
|
+
},
|
|
1383
1506
|
...workspaceShareMethods(db),
|
|
1384
1507
|
...reports,
|
|
1385
1508
|
...triage,
|
|
1386
1509
|
...importTriageMethods({ getReport: reports.getReport, ...triage, ...comments }),
|
|
1387
|
-
...bundleMethods(stmts),
|
|
1388
|
-
...teamMethods(stmts),
|
|
1510
|
+
...bundleMethods(stmts, db, key),
|
|
1511
|
+
...teamMethods(stmts, db),
|
|
1389
1512
|
async close() {
|
|
1390
1513
|
await db.close()
|
|
1391
1514
|
},
|
|
1392
1515
|
}
|
|
1393
|
-
return { ...methods, ...managementMethods(methods) }
|
|
1516
|
+
return { ...methods, ...managementMethods(methods), ...managementCatalogMethods(methods) }
|
|
1394
1517
|
}
|