@preventive/triage 1.0.0-alpha.4 → 1.0.0-alpha.40
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/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +2 -0
- package/client/dependency-paths.js +24 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +112 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +522 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-metadata.d.ts +14 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +310 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +23 -0
- package/common/dependency-reviews.js +81 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +56 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +108 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +254 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/server-info.ts +35 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +23 -0
- package/common/workspace-groups.js +176 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2KBF5TZ7.js +2 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-35VE6CDH.js +2 -0
- package/out/chunk-3BZ7JERB.js +1 -0
- package/out/chunk-4F6JSF57.js +3 -0
- package/out/chunk-4XCDFGDP.js +10 -0
- package/out/chunk-AQ75QITE.js +1 -0
- package/out/chunk-B2RQY6OP.js +2 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-CMQA4DM6.js +1 -0
- package/out/chunk-CVVJAXVY.js +2 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-GJ4IUCNS.js +1 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-IISEF2BZ.js +1 -0
- package/out/chunk-IU3CCINR.js +3 -0
- package/out/chunk-K3X5EJBQ.js +2 -0
- package/out/chunk-KEX2ATEW.js +2 -0
- package/out/chunk-KYCNJVB7.js +1 -0
- package/out/chunk-MYDDTA7W.js +225 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-RSHMVYNS.js +1 -0
- package/out/chunk-TGANXVTA.js +1 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-UN5ZSYLE.js +1 -0
- package/out/chunk-UNTRGE26.js +2 -0
- package/out/chunk-UXNUAIIC.js +33 -0
- package/out/chunk-UYAXVRVL.js +1 -0
- package/out/chunk-V3ME6FI5.js +2 -0
- package/out/chunk-V5O7M2YT.js +1 -0
- package/out/chunk-VIHGO77Y.js +1 -0
- package/out/chunk-VQJCXGIY.js +7 -0
- package/out/chunk-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-WZDTZE64.js +1 -0
- package/out/chunk-XYDGU6BM.js +51 -0
- package/out/chunk-XYFXXB2G.js +10 -0
- package/out/chunk-YVZH5SHP.js +1 -0
- package/out/chunk-ZBBOBQ43.js +1 -0
- package/out/chunk-ZMQQCGF6.js +2 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +37 -0
- package/out/client-sync.js +10 -13
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +6 -2
- package/out/stasis.svg +20 -0
- package/out/terminal.js +365 -39
- package/out/view.css +1 -1
- package/out/view.js +83 -117
- package/package.json +297 -59
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +171 -0
- package/risk/files/runs-code.js +358 -0
- package/risk/files/script.js +114 -0
- package/risk/files/stamps.js +46 -0
- package/risk/index.d.ts +142 -0
- package/risk/index.js +7 -0
- package/risk/npm/assess.js +219 -0
- package/risk/npm/combined.js +65 -0
- package/risk/npm/compare.js +228 -0
- package/risk/npm/dependencies.js +153 -0
- package/risk/npm/diff.js +43 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +34 -0
- package/risk/npm/held.js +60 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +30 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +282 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +113 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +486 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +56 -35
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +116 -18
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +35 -18
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +27 -26
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +102 -68
- package/{server → server-e2e}/sse-session.ts +28 -8
- package/{server → server-e2e}/static.ts +31 -21
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +245 -0
- package/server-managed/avatar-store.ts +12 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +111 -0
- package/server-managed/bundle-build.ts +198 -0
- package/server-managed/bundle-cache.ts +199 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +120 -0
- package/server-managed/cache-storage.ts +19 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +158 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1725 -0
- package/server-managed/db-neon.ts +367 -0
- package/server-managed/db-schema.ts +227 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +132 -0
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-app.ts +391 -0
- package/server-managed/github-errors.ts +68 -0
- package/server-managed/github-issue-links.ts +127 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +235 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/http-response.ts +50 -0
- package/server-managed/http.ts +3472 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +187 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +158 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +75 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-insights.ts +440 -0
- package/server-managed/npm-kept.ts +65 -0
- package/server-managed/npm-loads.ts +167 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +378 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +177 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +74 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +220 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +47 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +153 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-risk.ts +418 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/session.ts +92 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +69 -0
- package/server-managed/sql-postgres.ts +31 -0
- package/server-managed/sql.ts +76 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +175 -0
- package/server-managed/storage-encryption.ts +279 -0
- package/server-managed/storage-maintenance.ts +256 -0
- package/server-managed/storage-payload.ts +103 -0
- package/server-managed/storage-stores.ts +85 -0
- package/server-managed/storage.ts +78 -0
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +173 -0
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/team-reports.ts +228 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +86 -0
- package/server-managed/upstream-cache.ts +215 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +166 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -477
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -320
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Storage envelope v1: header, then 64 KiB plaintext chunks with full AEAD tags.
|
|
2
|
+
// The nonce layout follows age's STREAM construction (counter || final flag).
|
|
3
|
+
// Every write derives a new key, including rewrites of the same logical object.
|
|
4
|
+
import { Buffer } from 'node:buffer'
|
|
5
|
+
import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes } from 'node:crypto'
|
|
6
|
+
import { Readable } from 'node:stream'
|
|
7
|
+
|
|
8
|
+
export const STORAGE_MAGIC = Buffer.from('DeepView.storage')
|
|
9
|
+
const VERSION = 1
|
|
10
|
+
export const STORAGE_CHUNK_BYTES = 65_536
|
|
11
|
+
export const STORAGE_HEADER_BYTES = 57
|
|
12
|
+
const TAG_BYTES = 16
|
|
13
|
+
const UNKNOWN_SIZE = 0xffff_ffff_ffff_ffffn
|
|
14
|
+
|
|
15
|
+
export interface StorageKey { bytes: Buffer; id: string }
|
|
16
|
+
|
|
17
|
+
export function parseStorageKey(value: string | null | undefined): StorageKey | null {
|
|
18
|
+
if (value == null || value === '') return null
|
|
19
|
+
const bytes = Buffer.from(value, 'base64')
|
|
20
|
+
if (bytes.length !== 32 || bytes.toString('base64') !== value) {
|
|
21
|
+
throw new Error('MANAGED_STORAGE_ENCRYPTION_KEY must be canonical base64 encoding of 32 random bytes')
|
|
22
|
+
}
|
|
23
|
+
const id = createHash('sha256').update('deepview.storage.key.v1\0').update(bytes).digest('hex').slice(0, 32)
|
|
24
|
+
return { bytes, id }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Re-chunk without accumulating a file, regardless of the input's read sizes.
|
|
28
|
+
async function* chunks(source: AsyncIterable<Uint8Array>, size: number): AsyncGenerator<Buffer> {
|
|
29
|
+
let length = 0, pending: Buffer[] = []
|
|
30
|
+
for await (const value of source) {
|
|
31
|
+
const input = Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
32
|
+
for (let offset = 0; offset < input.length;) {
|
|
33
|
+
const n = Math.min(size - length, input.length - offset)
|
|
34
|
+
pending.push(input.subarray(offset, offset + n)); length += n; offset += n
|
|
35
|
+
if (length === size) { yield Buffer.concat(pending, length); pending = []; length = 0 }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (length) yield Buffer.concat(pending, length)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function* frames(source: AsyncIterable<Uint8Array>, size: number) {
|
|
42
|
+
const iterator = chunks(source, size)[Symbol.asyncIterator]()
|
|
43
|
+
try {
|
|
44
|
+
let current = await iterator.next()
|
|
45
|
+
if (current.done) { yield { bytes: Buffer.alloc(0), last: true }; return }
|
|
46
|
+
while (!current.done) {
|
|
47
|
+
const next = await iterator.next()
|
|
48
|
+
yield { bytes: current.value, last: next.done === true }
|
|
49
|
+
current = next
|
|
50
|
+
}
|
|
51
|
+
} finally { await iterator.return(undefined) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nonce(index: number, last: boolean): Buffer {
|
|
55
|
+
if (!Number.isSafeInteger(index) || index < 0 || index > 0xffff_ffff) throw new Error('Storage object has too many chunks')
|
|
56
|
+
const value = Buffer.alloc(12)
|
|
57
|
+
value.writeUInt32BE(index, 7)
|
|
58
|
+
value[11] = last ? 1 : 0
|
|
59
|
+
return value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function objectKey(key: Uint8Array, header: Buffer, identity: string): Buffer {
|
|
63
|
+
return Buffer.from(hkdfSync('sha256', key, header.subarray(17, 49),
|
|
64
|
+
Buffer.from(JSON.stringify(['deepview.storage.v1', identity])), 32))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeHeader(size: number | null): Buffer {
|
|
68
|
+
if (size !== null && (!Number.isSafeInteger(size) || size < 0)) throw new Error('Invalid storage size')
|
|
69
|
+
const header = Buffer.alloc(STORAGE_HEADER_BYTES)
|
|
70
|
+
STORAGE_MAGIC.copy(header); header[16] = VERSION
|
|
71
|
+
randomBytes(32).copy(header, 17)
|
|
72
|
+
header.writeBigUInt64BE(size === null ? UNKNOWN_SIZE : BigInt(size), 49)
|
|
73
|
+
return header
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function encryptStorageStream(source: Readable, key: Uint8Array, identity: string, size: number | null): Readable {
|
|
77
|
+
async function* encrypt() {
|
|
78
|
+
const header = makeHeader(size)
|
|
79
|
+
const derived = objectKey(key, header, identity)
|
|
80
|
+
let index = 0, length = 0
|
|
81
|
+
try {
|
|
82
|
+
yield header
|
|
83
|
+
for await (const { bytes, last } of frames(source, STORAGE_CHUNK_BYTES)) {
|
|
84
|
+
length += bytes.length
|
|
85
|
+
if (last && size !== null && length !== size) throw new Error('Storage size changed while encrypting')
|
|
86
|
+
const cipher = createCipheriv('chacha20-poly1305', derived, nonce(index++, last), { authTagLength: TAG_BYTES })
|
|
87
|
+
cipher.setAAD(header)
|
|
88
|
+
yield Buffer.concat([cipher.update(bytes), cipher.final(), cipher.getAuthTag()])
|
|
89
|
+
}
|
|
90
|
+
} finally { derived.fill(0); source.destroy() }
|
|
91
|
+
}
|
|
92
|
+
const stream = Readable.from(encrypt(), { objectMode: false })
|
|
93
|
+
stream.once('close', () => source.destroy())
|
|
94
|
+
return stream
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Read just the envelope and authenticate the first frame before returning.
|
|
98
|
+
// In particular, never trust a corrupted size or release update() output
|
|
99
|
+
// before final() authenticates the complete chunk.
|
|
100
|
+
export async function decryptStorageStream(source: Readable, key: Uint8Array, identity: string) {
|
|
101
|
+
const iterator = source[Symbol.asyncIterator]()
|
|
102
|
+
let done = false
|
|
103
|
+
let peek: Buffer = Buffer.alloc(0), remainder: Buffer = Buffer.alloc(0)
|
|
104
|
+
async function read(size: number): Promise<Buffer> {
|
|
105
|
+
const parts: Buffer[] = peek.length > 0 ? [peek] : []
|
|
106
|
+
let length = peek.length
|
|
107
|
+
peek = Buffer.alloc(0)
|
|
108
|
+
while (length < size) {
|
|
109
|
+
if (remainder.length === 0 && !done) {
|
|
110
|
+
const next = await iterator.next(); done = next.done === true
|
|
111
|
+
remainder = done ? Buffer.alloc(0) : next.value
|
|
112
|
+
}
|
|
113
|
+
if (remainder.length === 0 && done) break
|
|
114
|
+
const n = Math.min(size - length, remainder.length)
|
|
115
|
+
parts.push(remainder.subarray(0, n)); remainder = remainder.subarray(n); length += n
|
|
116
|
+
}
|
|
117
|
+
return Buffer.concat(parts, length)
|
|
118
|
+
}
|
|
119
|
+
let derived: Buffer | undefined
|
|
120
|
+
try {
|
|
121
|
+
const header = await read(STORAGE_HEADER_BYTES)
|
|
122
|
+
if (header.length !== STORAGE_HEADER_BYTES || !header.subarray(0, 16).equals(STORAGE_MAGIC) || header[16] !== VERSION) {
|
|
123
|
+
throw new Error('Invalid encrypted storage envelope')
|
|
124
|
+
}
|
|
125
|
+
const rawSize = header.readBigUInt64BE(49)
|
|
126
|
+
if (rawSize !== UNKNOWN_SIZE && rawSize > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('Invalid encrypted storage size')
|
|
127
|
+
const size = rawSize === UNKNOWN_SIZE ? null : Number(rawSize)
|
|
128
|
+
derived = objectKey(key, header, identity)
|
|
129
|
+
const derivedKey = derived
|
|
130
|
+
let index = 0, length = 0
|
|
131
|
+
const nextFrame = async () => {
|
|
132
|
+
const bytes = await read(STORAGE_CHUNK_BYTES + TAG_BYTES)
|
|
133
|
+
// Peek beyond the frame: EOF authenticates the final flag, including
|
|
134
|
+
// exact chunk boundaries. Keep the byte for the next read.
|
|
135
|
+
peek = await read(1)
|
|
136
|
+
const last = peek.length === 0
|
|
137
|
+
if (bytes.length < TAG_BYTES || (index > 0 && bytes.length === TAG_BYTES)) throw new Error('Truncated encrypted storage object')
|
|
138
|
+
const decipher = createDecipheriv('chacha20-poly1305', derivedKey, nonce(index++, last), { authTagLength: TAG_BYTES })
|
|
139
|
+
decipher.setAAD(header); decipher.setAuthTag(bytes.subarray(-TAG_BYTES))
|
|
140
|
+
const plain = Buffer.concat([decipher.update(bytes.subarray(0, -TAG_BYTES)), decipher.final()])
|
|
141
|
+
length += plain.length
|
|
142
|
+
if (size !== null && (length > size || (last && length !== size))) throw new Error('Encrypted storage size mismatch')
|
|
143
|
+
return { plain, last }
|
|
144
|
+
}
|
|
145
|
+
const first = await nextFrame()
|
|
146
|
+
const decrypt = async function* () {
|
|
147
|
+
try {
|
|
148
|
+
yield first.plain
|
|
149
|
+
let last = first.last
|
|
150
|
+
while (!last) { const frame = await nextFrame(); last = frame.last; yield frame.plain }
|
|
151
|
+
} finally { derivedKey.fill(0); await iterator.return?.(); source.destroy() }
|
|
152
|
+
}
|
|
153
|
+
const stream = Readable.from(decrypt(), { objectMode: false })
|
|
154
|
+
stream.once('close', () => { derivedKey.fill(0); source.destroy() })
|
|
155
|
+
return { size, stream }
|
|
156
|
+
} catch (err) { derived?.fill(0); source.destroy(); throw err }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Small SQL values (data keys, OAuth tokens and the installation sentinel).
|
|
160
|
+
// Fresh salt derives a one-use wrapping key, so the nonce can be all zeroes.
|
|
161
|
+
// The version and master-key ID are authenticated along with the row/field.
|
|
162
|
+
export function wrapStorageValue(key: StorageKey, identity: string, value: Uint8Array): string {
|
|
163
|
+
const header = Buffer.concat([Buffer.from([1]), Buffer.from(key.id, 'hex'), randomBytes(32)])
|
|
164
|
+
const derived = Buffer.from(hkdfSync('sha256', key.bytes, header.subarray(17), 'deepview.wrap.v1', 32))
|
|
165
|
+
try {
|
|
166
|
+
const cipher = createCipheriv('chacha20-poly1305', derived, Buffer.alloc(12), { authTagLength: TAG_BYTES })
|
|
167
|
+
cipher.setAAD(Buffer.concat([header, Buffer.from(identity)]))
|
|
168
|
+
return Buffer.concat([header, cipher.update(value), cipher.final(), cipher.getAuthTag()]).toString('base64')
|
|
169
|
+
} finally { derived.fill(0) }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function unwrapStorageValue(key: StorageKey, identity: string, value: string): Buffer {
|
|
173
|
+
const bytes = Buffer.from(value, 'base64'), header = bytes.subarray(0, 49)
|
|
174
|
+
if (bytes.length < 65 || bytes.toString('base64') !== value || header[0] !== 1
|
|
175
|
+
|| header.subarray(1, 17).toString('hex') !== key.id) throw new Error('Invalid wrapped storage value or encryption key')
|
|
176
|
+
const derived = Buffer.from(hkdfSync('sha256', key.bytes, header.subarray(17), 'deepview.wrap.v1', 32))
|
|
177
|
+
try {
|
|
178
|
+
const decipher = createDecipheriv('chacha20-poly1305', derived, Buffer.alloc(12), { authTagLength: TAG_BYTES })
|
|
179
|
+
decipher.setAAD(Buffer.concat([header, Buffer.from(identity)])); decipher.setAuthTag(bytes.subarray(-TAG_BYTES))
|
|
180
|
+
return Buffer.concat([decipher.update(bytes.subarray(49, -TAG_BYTES)), decipher.final()])
|
|
181
|
+
} finally { derived.fill(0) }
|
|
182
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path'
|
|
2
|
+
|
|
3
|
+
type DatabaseConfig = { dbPath: string; neonUrl?: string | null }
|
|
4
|
+
|
|
5
|
+
function databaseLocation({ dbPath, neonUrl }: DatabaseConfig): string {
|
|
6
|
+
if (!neonUrl) return `SQLite ${dbPath === ':memory:' ? dbPath : resolve(dbPath)}`
|
|
7
|
+
// Report the destination, never URL credentials or query-string secrets.
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(neonUrl)
|
|
10
|
+
return `Neon Postgres ${url.host}${url.pathname}`
|
|
11
|
+
} catch { return 'Neon Postgres' }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function e2eStorageLines(config: DatabaseConfig & { objstoreDir: string }): string[] {
|
|
15
|
+
return [
|
|
16
|
+
` E2E database: ${databaseLocation(config)}`,
|
|
17
|
+
` E2E objects: ${config.neonUrl ? 'Vercel Blob (private), {workspaceTag}/' : resolve(config.objstoreDir)}`,
|
|
18
|
+
` E2E staging: ${config.neonUrl ? 'Vercel Blob (private), {workspaceTag}/.staging/' : resolve(config.objstoreDir, '{workspaceTag}', '.staging')}`,
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function managedStorageLines(config: DatabaseConfig & { storageEncryptionKey?: string | null }): string[] {
|
|
23
|
+
const dir = dirname(config.dbPath)
|
|
24
|
+
const location = (path: string) => config.neonUrl ? `Vercel Blob (private), .managed/${path}/` : resolve(dir, path)
|
|
25
|
+
return [
|
|
26
|
+
` Managed database: ${databaseLocation(config)}`,
|
|
27
|
+
...(config.storageEncryptionKey ? [
|
|
28
|
+
' Managed storage encryption: ChaCha20-Poly1305',
|
|
29
|
+
` Managed encrypted caches: ${location('cache-encrypted-v1')}`,
|
|
30
|
+
] : []),
|
|
31
|
+
` Managed reports: ${location('reports')}`,
|
|
32
|
+
` Managed bundles: ${location('bundles')}`,
|
|
33
|
+
` Managed avatars: ${location('avatars')}`,
|
|
34
|
+
` Managed bundle cache: ${location('cache/bundles')}`,
|
|
35
|
+
` Managed report sources cache: ${location('cache/report-sources')}`,
|
|
36
|
+
...(config.neonUrl ? [` Managed upload parts: ${location('uploads')}`] : []),
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Shared optional Vercel Blob SDK boundary for both server modes.
|
|
2
|
+
import type { Readable } from 'node:stream'
|
|
3
|
+
import type { Buffer } from 'node:buffer'
|
|
4
|
+
|
|
5
|
+
// Minimal structural shape of the bits of `@vercel/blob` we use.
|
|
6
|
+
// Shared so the optional peer dep doesn't have to type-resolve
|
|
7
|
+
// for SQLite-only deployments. Real type details live in the
|
|
8
|
+
// installed package; the fields/parameters we touch here are stable
|
|
9
|
+
// per the SDK v2 public surface.
|
|
10
|
+
//
|
|
11
|
+
// `put` accepts the SDK's full `PutBody` union (string | Readable |
|
|
12
|
+
// Buffer | Blob | ArrayBuffer | ReadableStream | File). We only ever
|
|
13
|
+
// pass a Node `PassThrough` (a Readable), but the wider type lets
|
|
14
|
+
// callers reuse this signature for future buffer/blob bodies without
|
|
15
|
+
// type gymnastics. `copy` accepts `allowOverwrite` — REQUIRED for
|
|
16
|
+
// version bumps since the live pathname is reused on re-upload.
|
|
17
|
+
|
|
18
|
+
type VercelBlobBody = Readable | Buffer | string | Blob | ArrayBuffer | ReadableStream<Uint8Array>
|
|
19
|
+
export type VercelBlobSdk = {
|
|
20
|
+
BlobNotFoundError: new () => Error
|
|
21
|
+
BlobPreconditionFailedError?: new () => Error
|
|
22
|
+
put: (
|
|
23
|
+
pathname: string,
|
|
24
|
+
body: VercelBlobBody,
|
|
25
|
+
options: {
|
|
26
|
+
access: 'private' | 'public'
|
|
27
|
+
addRandomSuffix?: boolean
|
|
28
|
+
allowOverwrite?: boolean
|
|
29
|
+
contentType?: string
|
|
30
|
+
token?: string
|
|
31
|
+
ifMatch?: string
|
|
32
|
+
multipart?: boolean
|
|
33
|
+
abortSignal?: AbortSignal
|
|
34
|
+
cacheControlMaxAge?: number
|
|
35
|
+
},
|
|
36
|
+
) => Promise<{ url: string; pathname: string }>
|
|
37
|
+
head: (
|
|
38
|
+
pathname: string,
|
|
39
|
+
options?: { token?: string; abortSignal?: AbortSignal },
|
|
40
|
+
) => Promise<{ size: number; pathname: string; url: string; etag?: string; uploadedAt?: Date | string | number }>
|
|
41
|
+
get: (
|
|
42
|
+
pathname: string,
|
|
43
|
+
options: {
|
|
44
|
+
access: 'private' | 'public'
|
|
45
|
+
token?: string
|
|
46
|
+
useCache?: boolean
|
|
47
|
+
headers?: Record<string, string>
|
|
48
|
+
abortSignal?: AbortSignal
|
|
49
|
+
},
|
|
50
|
+
) => Promise<{
|
|
51
|
+
statusCode: 200 | 304
|
|
52
|
+
stream: ReadableStream<Uint8Array> | null
|
|
53
|
+
blob: { size: number | null; etag?: string; uploadedAt?: Date | string | number }
|
|
54
|
+
} | null>
|
|
55
|
+
copy: (
|
|
56
|
+
fromPathname: string,
|
|
57
|
+
toPathname: string,
|
|
58
|
+
options: {
|
|
59
|
+
access: 'private' | 'public'
|
|
60
|
+
addRandomSuffix?: boolean
|
|
61
|
+
allowOverwrite?: boolean
|
|
62
|
+
token?: string
|
|
63
|
+
contentType?: string
|
|
64
|
+
cacheControlMaxAge?: number
|
|
65
|
+
},
|
|
66
|
+
) => Promise<{ url: string; pathname: string }>
|
|
67
|
+
del: (
|
|
68
|
+
urlOrPathname: string | string[],
|
|
69
|
+
options?: { token?: string; abortSignal?: AbortSignal; ifMatch?: string },
|
|
70
|
+
) => Promise<void>
|
|
71
|
+
list: (options: {
|
|
72
|
+
prefix?: string
|
|
73
|
+
cursor?: string
|
|
74
|
+
limit?: number
|
|
75
|
+
mode?: 'expanded' | 'folded'
|
|
76
|
+
token?: string
|
|
77
|
+
abortSignal?: AbortSignal
|
|
78
|
+
}) => Promise<{
|
|
79
|
+
// `uploadedAt` (a Date per the SDK v2 surface) is the blob's
|
|
80
|
+
// creation time — used by the reaper's GC grace window. Optional
|
|
81
|
+
// in the type because the `folded` listing path ignores it (only
|
|
82
|
+
// `listLiveBlobs` reads it, and it uses the default expanded mode).
|
|
83
|
+
blobs: Array<{ pathname: string; size: number; uploadedAt?: Date | string | number }>
|
|
84
|
+
folders?: string[]
|
|
85
|
+
cursor?: string
|
|
86
|
+
hasMore: boolean
|
|
87
|
+
}>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Recognise "blob is gone" errors uniformly across read/write/delete
|
|
91
|
+
// paths so callers can treat them as success (delete) or not-found (read).
|
|
92
|
+
// SDK error subclasses retain `.name === 'Error'`; identify the missing-blob
|
|
93
|
+
// class from the same lazily loaded SDK instance that performs the request.
|
|
94
|
+
// Store/configuration failures must propagate, never masquerade as cache misses.
|
|
95
|
+
export function isNotFound(err: unknown, sdk: Pick<VercelBlobSdk, 'BlobNotFoundError'>): boolean {
|
|
96
|
+
return err instanceof sdk.BlobNotFoundError
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function loadVercelBlobSdk(): Promise<VercelBlobSdk> {
|
|
100
|
+
// @ts-ignore optional peer dep: '@vercel/blob'
|
|
101
|
+
const mod = (await import('@vercel/blob')) as VercelBlobSdk
|
|
102
|
+
return mod
|
|
103
|
+
}
|