@stacksjs/defaults 0.75.62 → 0.75.64

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.
@@ -1,13 +1,40 @@
1
- import { createRateLimitMiddleware, Middleware, parseThrottleString } from '@stacksjs/router'
1
+ import { clientAddress, createRateLimitMiddleware, Middleware, parseThrottleString } from '@stacksjs/router'
2
2
 
3
3
  // Cache for rate limiters to avoid creating new instances for each request
4
4
  const limiterCache = new Map<string, ReturnType<typeof createRateLimitMiddleware>>()
5
5
 
6
+ const RATE_LIMIT_HEADERS = ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'] as const
7
+
8
+ /**
9
+ * Whose budget a request spends.
10
+ *
11
+ * A signed-in request spends its user's: the Auth middleware stamps
12
+ * `_authenticatedUser`, and Throttle runs after it (priority 2 to Auth's 1),
13
+ * so people sharing an address - an office, a phone carrier's NAT, every test
14
+ * user on 127.0.0.1 - do not exhaust each other. bun-router's own default
15
+ * reads `request.user.id`, which on a Stacks request is the async `user()`
16
+ * accessor, so it never saw a user and every request fell through to an
17
+ * address.
18
+ *
19
+ * Anything else spends its client address's, as `clientAddress()` resolves
20
+ * it: the socket peer, or - only when that peer is a trusted proxy - the
21
+ * forwarding headers it vouches for (`CF-Connecting-IP` only when the hop
22
+ * that delivered the request is Cloudflare). A client-written
23
+ * `X-Forwarded-For` no longer buys a fresh budget.
24
+ */
25
+ function throttleKey(request: { _authenticatedUser?: unknown }): string {
26
+ const user = request._authenticatedUser as { id?: unknown } | null | undefined
27
+ const id = user?.id
28
+ if ((typeof id === 'number' && Number.isFinite(id)) || (typeof id === 'string' && id !== ''))
29
+ return `user:${id}`
30
+ return `ip:${clientAddress(request as unknown as Request) ?? 'unknown'}`
31
+ }
32
+
6
33
  /**
7
34
  * Throttle Middleware
8
35
  *
9
- * Rate limits requests based on IP address or authenticated user.
10
- * Uses Laravel-style throttle patterns.
36
+ * Rate limits requests per authenticated user, or per client address for
37
+ * anonymous requests (see `throttleKey`). Uses Laravel-style throttle patterns.
11
38
  *
12
39
  * Usage:
13
40
  * .middleware('throttle:60,1') // 60 requests per 1 minute
@@ -29,7 +56,7 @@ const limiterCache = new Map<string, ReturnType<typeof createRateLimitMiddleware
29
56
  * "retryAfter": X
30
57
  * }
31
58
  *
32
- * Headers added to all responses:
59
+ * Headers added to all responses (successful ones via `_responseHeaders`):
33
60
  * - X-RateLimit-Limit: Maximum requests allowed
34
61
  * - X-RateLimit-Remaining: Remaining requests in current window
35
62
  * - X-RateLimit-Reset: Unix timestamp when the limit resets
@@ -37,7 +64,9 @@ const limiterCache = new Map<string, ReturnType<typeof createRateLimitMiddleware
37
64
  */
38
65
  export default new Middleware({
39
66
  name: 'throttle',
40
- priority: 1, // Run early, after maintenance but before auth
67
+ // After Auth (1), so an authenticated request is keyed on its user rather
68
+ // than on an address it may share with strangers.
69
+ priority: 2,
41
70
 
42
71
  async handle(request) {
43
72
  // Get throttle params from middleware params (e.g., '60,1' from 'throttle:60,1')
@@ -48,24 +77,25 @@ export default new Middleware({
48
77
  if (!limiter) {
49
78
  try {
50
79
  const config = parseThrottleString(params as Parameters<typeof parseThrottleString>[0])
51
- limiter = createRateLimitMiddleware(config, `throttle:${params}`)
80
+ limiter = createRateLimitMiddleware({ ...config, keyGenerator: throttleKey }, `throttle:${params}`)
52
81
  limiterCache.set(params, limiter)
53
82
  }
54
83
  catch (error) {
55
84
  console.error(`[Throttle] Invalid throttle pattern: ${params}`, error)
56
85
  // Use default if pattern is invalid
57
86
  const defaultConfig = parseThrottleString('60,1')
58
- limiter = createRateLimitMiddleware(defaultConfig, 'throttle:default')
87
+ limiter = createRateLimitMiddleware({ ...defaultConfig, keyGenerator: throttleKey }, 'throttle:default')
59
88
  limiterCache.set(params, limiter)
60
89
  }
61
90
  }
62
91
 
63
- // Create a next function that continues the request
64
- // The middleware will handle rate limiting and return 429 if needed
65
- const result = await limiter(request as any, async () => null)
92
+ // The limiter stamps its X-RateLimit-* headers onto whatever `next()`
93
+ // answers, so hand it a placeholder and read them back. Passing `null`
94
+ // here meant a request that got through never learned its budget.
95
+ const result = await limiter(request as any, async () => new Response(null, { status: 204 }))
66
96
 
67
- // If result is a Response (429), throw it to short-circuit the request
68
- if (result instanceof Response) {
97
+ // A 429 short-circuits the request
98
+ if (result instanceof Response && result.status === 429) {
69
99
  // Transform the response to match stacks format
70
100
  const retryAfter = result.headers.get('Retry-After') || '60'
71
101
 
@@ -90,6 +120,15 @@ export default new Middleware({
90
120
  })
91
121
  }
92
122
 
93
- // Continue normally - rate limit not exceeded
123
+ // Within the limit: ask the router to put the budget on the real response.
124
+ if (result instanceof Response) {
125
+ const headers: Record<string, string> = { ...request._responseHeaders }
126
+ for (const name of RATE_LIMIT_HEADERS) {
127
+ const value = result.headers.get(name)
128
+ if (value !== null)
129
+ headers[name] = value
130
+ }
131
+ request._responseHeaders = headers
132
+ }
94
133
  },
95
134
  })
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.62",
5
+ "version": "0.75.64",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.62",
5
+ "version": "0.75.64",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.35",
58
- "@stacksjs/mobile": "^0.75.62",
58
+ "@stacksjs/mobile": "^0.75.64",
59
59
  "@stacksjs/sanitizer": "^0.2.348",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }
@@ -1,52 +0,0 @@
1
- /**
2
- * Builds the extension into dist/:
3
- *
4
- * extension.js the extension, for VS Code's Node host; only `vscode` is external
5
- * pickier-worker.js the Bun process that runs the project's pickier (src/pickier-worker.ts)
6
- * pickier-worker.bunfig.toml an empty bunfig, so the worker skips the project's preloads
7
- * stx/ grammar, language configuration and snippets from @stacksjs/stx-vscode
8
- * stx-typescript-plugin/ the stx TypeScript server plugin from @stacksjs/stx-vscode
9
- *
10
- * `vsce package` runs this first (`vscode:prepublish`), and `files` in
11
- * package.json ships exactly these. tsserver loads the plugin from
12
- * node_modules, so a forwarder to it is written there as well, which is what
13
- * a development host (`--extensionDevelopmentPath`) loads; scripts/package.ts
14
- * adds the same forwarder to the VSIX (see scripts/ts-plugin.ts).
15
- */
16
- import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
17
- import { dirname, join } from 'node:path'
18
- import process from 'node:process'
19
- import { copyTsPlugin, tsPluginName, writeTsPluginForwarder } from './scripts/ts-plugin'
20
-
21
- const here = import.meta.dir
22
- const dist = join(here, 'dist')
23
-
24
- rmSync(dist, { recursive: true, force: true })
25
- mkdirSync(dist, { recursive: true })
26
-
27
- const builds = await Promise.all([
28
- // bunfig stays out: the stx utility-class engine imports it only to load a
29
- // config file, which the extension never asks for, and its module-scope
30
- // `await` and `import.meta.require` would make the CommonJS bundle a syntax
31
- // error in VS Code's Node host. tests/build.test.ts parses the bundle.
32
- Bun.build({ entrypoints: [join(here, 'src/extension.ts')], outdir: dist, target: 'node', format: 'cjs', external: ['vscode', 'bunfig'], minify: true }),
33
- Bun.build({ entrypoints: [join(here, 'src/pickier-worker.ts')], outdir: dist, target: 'bun', format: 'esm' }),
34
- ])
35
-
36
- for (const build of builds) {
37
- if (!build.success) {
38
- console.error(build.logs)
39
- process.exit(1)
40
- }
41
- }
42
-
43
- writeFileSync(join(dist, 'pickier-worker.bunfig.toml'), '# Intentionally empty: the pickier worker must not run the project\'s bunfig preloads.\n')
44
-
45
- const stxAssets = join(dirname(Bun.resolveSync('@stacksjs/stx-vscode/contributes.json', here)), 'assets')
46
- cpSync(stxAssets, join(dist, 'stx'), { recursive: true })
47
-
48
- copyTsPlugin(here)
49
- const { version } = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8'))
50
- writeTsPluginForwarder(here, tsPluginName(here), version)
51
-
52
- console.log('Built dist/')
@@ -1,71 +0,0 @@
1
- /**
2
- * Package (and with `--publish`, publish) the extension.
3
- *
4
- * bun scripts/package.ts [--out <file.vsix>] [--publish]
5
- *
6
- * `vsce package --no-dependencies` keeps the VSIX to what `files` lists, and it
7
- * leaves out everything under node_modules - including the one package the
8
- * extension needs there: the stx TypeScript server plugin, which tsserver will
9
- * only load by package name from `<extension>/node_modules` (see ts-plugin.ts).
10
- * So its forwarder is added to the VSIX after vsce writes it, and a publish
11
- * uploads that VSIX rather than letting vsce package a second time.
12
- */
13
- import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
14
- import { tmpdir } from 'node:os'
15
- import { join, resolve } from 'node:path'
16
- import process from 'node:process'
17
- import { TS_PLUGIN_DIST, tsPluginModuleDir, tsPluginName, writeTsPluginForwarder } from './ts-plugin'
18
-
19
- const ROOT = join(import.meta.dir, '..')
20
-
21
- function run(command: string[], cwd = ROOT, capture = false): string {
22
- const result = Bun.spawnSync(command, { cwd, stdout: capture ? 'pipe' : 'inherit', stderr: 'inherit', env: process.env })
23
- if (result.exitCode !== 0)
24
- throw new Error(`${command.join(' ')} exited with ${result.exitCode}`)
25
- return capture ? String(result.stdout ?? '') : ''
26
- }
27
-
28
- /** Build the VSIX at `out`, with the TypeScript plugin's forwarder in it. */
29
- export function packageExtension(out: string): string {
30
- const manifest = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
31
- const name = tsPluginName(ROOT)
32
-
33
- // vsce runs `vscode:prepublish` (the build) first.
34
- run(['bun', 'x', '--bun', 'vsce', 'package', '--no-dependencies', '--out', out])
35
-
36
- // A staging tree whose layout is the VSIX's, so zip records the paths VS
37
- // Code extracts: extension/node_modules/<package>/...
38
- const stage = mkdtempSync(join(tmpdir(), 'stacks-vsix-'))
39
- try {
40
- writeTsPluginForwarder(join(stage, 'extension'), name, manifest.version)
41
- run(['zip', '-q', '-r', '-X', '-D', out, `extension/${tsPluginModuleDir(name)}`], stage)
42
- }
43
- finally {
44
- rmSync(stage, { recursive: true, force: true })
45
- }
46
-
47
- const listing = run(['unzip', '-Z1', out], ROOT, true).split('\n')
48
- for (const file of [`extension/${tsPluginModuleDir(name)}/index.js`, `extension/${TS_PLUGIN_DIST}/index.js`, `extension/${TS_PLUGIN_DIST}/typescript-stx-plugin.js`]) {
49
- if (!listing.includes(file))
50
- throw new Error(`${out} is missing ${file}`)
51
- }
52
-
53
- return out
54
- }
55
-
56
- if (import.meta.main) {
57
- const args = process.argv.slice(2)
58
- const outFlag = args.indexOf('--out')
59
- const manifest = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
60
- const out = resolve(outFlag === -1 ? join(ROOT, `${manifest.name}-${manifest.version}.vsix`) : args[outFlag + 1]!)
61
-
62
- packageExtension(out)
63
- console.log(`Packaged ${out}`)
64
-
65
- if (args.includes('--publish')) {
66
- if (!existsSync(out))
67
- throw new Error(`${out} was not written`)
68
- run(['bun', 'x', '--bun', 'vsce', 'publish', '--packagePath', out])
69
- console.log(`Published ${manifest.publisher}.${manifest.name}@${manifest.version}`)
70
- }
71
- }
@@ -1,78 +0,0 @@
1
- /**
2
- * Keeps the stx half of package.json's `contributes` equal to what
3
- * `@stacksjs/stx-vscode` ships.
4
- *
5
- * VS Code reads languages, grammars, commands and settings only from the
6
- * manifest, so the built-in stx support needs them declared here, pointing at
7
- * dist/stx/ (copied there by build.ts). The library describes them in
8
- * `contributes.json`; this derives the manifest from it rather than keeping a
9
- * second copy by hand. That includes the TypeScript server plugin entry, which
10
- * type-checks `.stx` files (see ts-plugin.ts). Snippets are left out on purpose: the extension offers
11
- * them as completions only when the stx extension is not installed, because
12
- * a manifest entry would show every snippet twice next to it.
13
- *
14
- * bun scripts/stx-contributes.ts --write update package.json (`bun run sync:stx`)
15
- *
16
- * tests/manifest.test.ts fails while the two disagree.
17
- */
18
- import { readFileSync, writeFileSync } from 'node:fs'
19
- import { join } from 'node:path'
20
- import process from 'node:process'
21
-
22
- export interface StxContributes {
23
- languages: Array<Record<string, unknown> & { id: string, configuration: string }>
24
- grammars: Array<Record<string, unknown> & { language: string, path: string }>
25
- commands: Array<{ command: string, title: string, category?: string }>
26
- configuration: { title: string, properties: Record<string, unknown> }
27
- typescriptServerPlugins: Array<Record<string, unknown> & { name: string, languages?: string[] }>
28
- }
29
-
30
- const STX_DIR = './dist/stx/'
31
-
32
- function fromStx(path: string): string {
33
- return STX_DIR + path.replace(/^\.\//, '')
34
- }
35
-
36
- /** The namespace of the extension's own commands; every other one is stx's. */
37
- export const OWN_COMMAND_PREFIX = 'stacks.'
38
-
39
- /**
40
- * `manifest` with its stx contributions replaced by the library's.
41
- *
42
- * Commands are told apart by namespace, not by the library's current list: a
43
- * command stx renamed or dropped is no longer in that list, and filtering by
44
- * it kept the old entry as if it were this extension's own (`css.reload`
45
- * outlived its rename to `stx.reloadUtilityClasses` that way).
46
- */
47
- export function withStxContributes(manifest: any, stx: StxContributes): any {
48
- const contributes = manifest.contributes
49
- const sections: Array<{ title: string }> = Array.isArray(contributes.configuration) ? contributes.configuration : [contributes.configuration]
50
-
51
- return {
52
- ...manifest,
53
- contributes: {
54
- ...contributes,
55
- languages: stx.languages.map(language => ({ ...language, configuration: fromStx(language.configuration) })),
56
- grammars: stx.grammars.map(grammar => ({ ...grammar, path: fromStx(grammar.path) })),
57
- commands: [...contributes.commands.filter((command: { command: string }) => command.command.startsWith(OWN_COMMAND_PREFIX)), ...stx.commands],
58
- configuration: [...sections.filter(section => section.title !== stx.configuration.title), stx.configuration],
59
- typescriptServerPlugins: stx.typescriptServerPlugins,
60
- },
61
- }
62
- }
63
-
64
- export function libraryContributes(root: string): StxContributes {
65
- return JSON.parse(readFileSync(Bun.resolveSync('@stacksjs/stx-vscode/contributes.json', root), 'utf8'))
66
- }
67
-
68
- if (import.meta.main) {
69
- const root = join(import.meta.dir, '..')
70
- const path = join(root, 'package.json')
71
- const manifest = JSON.parse(readFileSync(path, 'utf8'))
72
- const next = `${JSON.stringify(withStxContributes(manifest, libraryContributes(root)), null, 2)}\n`
73
-
74
- if (process.argv.includes('--write'))
75
- writeFileSync(path, next)
76
- else
77
- process.stdout.write(next)
78
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * The stx TypeScript server plugin, as this extension ships it.
3
- *
4
- * It is what type-checks the TypeScript in `.stx` files (script blocks and
5
- * template expressions) in the editor. `@stacksjs/stx-vscode` publishes it as a
6
- * complete package, `dist/typescript-plugin/`, and its `contributes.json`
7
- * carries the `typescriptServerPlugins` entry that `bun run sync:stx` copies
8
- * into package.json.
9
- *
10
- * tsserver loads a plugin only by package name, resolved from
11
- * `<extension>/node_modules`, the probe location VS Code passes for every
12
- * extension that contributes one. This extension is packaged with
13
- * `vsce --no-dependencies`, which leaves node_modules out of the VSIX, so:
14
- *
15
- * dist/stx-typescript-plugin/ the package itself, copied by build.ts
16
- * (shipped through `files`, like the rest of dist/)
17
- * node_modules/<name>/{package.json,index.js}
18
- * a forwarder to it, written by build.ts for
19
- * a development host and added to the VSIX
20
- * by scripts/package.ts after vsce writes it
21
- *
22
- * The stx extension (Stacks.vscode-stx) contributes the same plugin under the
23
- * same name. With both installed tsserver loads it twice, and the plugin
24
- * decorates each project only the first time (stacksjs/stx#2028).
25
- */
26
- import { cpSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
27
- import { dirname, join } from 'node:path'
28
- import { libraryContributes } from './stx-contributes'
29
-
30
- /** Where build.ts puts the plugin package, relative to the extension root. */
31
- export const TS_PLUGIN_DIST = 'dist/stx-typescript-plugin'
32
-
33
- /** The name the plugin is contributed and configured under. */
34
- export function tsPluginName(root: string): string {
35
- const [plugin] = libraryContributes(root).typescriptServerPlugins
36
- if (!plugin)
37
- throw new Error('@stacksjs/stx-vscode contributes no TypeScript server plugin')
38
- return plugin.name
39
- }
40
-
41
- /** Where tsserver resolves the plugin from, relative to the extension root. */
42
- export function tsPluginModuleDir(name: string): string {
43
- return `node_modules/${name}`
44
- }
45
-
46
- /** The forwarder's files, relative to its directory under node_modules. */
47
- export function tsPluginForwarder(name: string, version: string): Record<string, string> {
48
- const up = '../'.repeat(1 + name.split('/').length)
49
- return {
50
- 'package.json': `${JSON.stringify({ name, version, private: true, main: 'index.js' }, null, 2)}\n`,
51
- // tsserver calls `module.exports` as the factory, unwrapped.
52
- 'index.js': `module.exports = require('${up}${TS_PLUGIN_DIST}/index.js')\n`,
53
- }
54
- }
55
-
56
- /** Write the forwarder under `<root>/node_modules`, returning its directory. */
57
- export function writeTsPluginForwarder(root: string, name: string, version: string): string {
58
- const dir = join(root, tsPluginModuleDir(name))
59
- mkdirSync(dir, { recursive: true })
60
- for (const [file, content] of Object.entries(tsPluginForwarder(name, version)))
61
- writeFileSync(join(dir, file), content)
62
- return dir
63
- }
64
-
65
- /** Copy the plugin package from `@stacksjs/stx-vscode` into `<root>/dist`. */
66
- export function copyTsPlugin(root: string): string {
67
- const source = dirname(Bun.resolveSync('@stacksjs/stx-vscode/typescript-plugin/package.json', root))
68
- const target = join(root, TS_PLUGIN_DIST)
69
- rmSync(target, { recursive: true, force: true })
70
- cpSync(source, target, { recursive: true })
71
- return target
72
- }
@@ -1,103 +0,0 @@
1
- /**
2
- * Env files, read the way Stacks reads them - the pure half of ./env.ts.
3
- *
4
- * Decryption and key lookup are not reimplemented here: they are
5
- * `@stacksjs/env`'s own (core/env/src), bundled into the extension, so a value
6
- * the hover decrypts is exactly what `./buddy env:get` prints.
7
- */
8
- import { basename } from 'node:path'
9
- import { envPrivateKeyNames } from '../../../../core/env/src/cli'
10
- import { decryptValue } from '../../../../core/env/src/crypto'
11
- import { parseEnvFile } from './project'
12
-
13
- /** `.env`, `.env.local`, `.env.production`, ... - not `.env.keys`, which holds the private keys themselves. */
14
- export function isEnvFile(path: string): boolean {
15
- const name = basename(path)
16
- return /^\.env(?:\..+)?$/.test(name) && name !== '.env.keys'
17
- }
18
-
19
- export function isEncrypted(value: string): boolean {
20
- return value.startsWith('encrypted:')
21
- }
22
-
23
- export interface EnvEntry {
24
- key: string
25
- value: string
26
- /** Character offsets of the value on its line, quotes excluded. */
27
- start: number
28
- end: number
29
- }
30
-
31
- /** The `KEY=value` on one line, with where its value sits, by `parseEnvFile`'s rules. */
32
- export function envEntryAt(line: string): EnvEntry | undefined {
33
- const match = line.match(/^(\s*(?:export\s+)?([A-Z_a-z][\w.-]*)\s*=\s*)(.*)$/)
34
- if (!match)
35
- return undefined
36
-
37
- const key = match[2] as string
38
- const value = parseEnvFile(line)[key]
39
- if (value === undefined)
40
- return undefined
41
-
42
- const rest = match[3] as string
43
- const quoted = rest[0] === '"' || rest[0] === '\'' || rest[0] === '`'
44
- const start = (match[1] as string).length + (quoted ? 1 : 0)
45
- return { key, value, start, end: start + value.length }
46
- }
47
-
48
- /** Keys `.env.example` declares that the env file does not set, in example order. */
49
- export function missingKeys(example: string, actual: string): string[] {
50
- const present = parseEnvFile(actual)
51
- return Object.keys(parseEnvFile(example)).filter(key => !(key in present))
52
- }
53
-
54
- export interface PrivateKey {
55
- name: string
56
- key: string
57
- from: '.env.keys' | 'environment'
58
- }
59
-
60
- /**
61
- * The private key for an env file, looked up as `./buddy env:get` does:
62
- * `envPrivateKeyNames` (`DOTENV_PRIVATE_KEY_<ENV>`, then `DOTENV_PRIVATE_KEY`)
63
- * in `.env.keys`, then in the environment, the dotenvx convention for CI and
64
- * servers where `.env.keys` must not exist.
65
- */
66
- export function findPrivateKey(envFile: string, keysFile: string | undefined, environment: Record<string, string | undefined>): PrivateKey | undefined {
67
- const names = envPrivateKeyNames(basename(envFile))
68
- const keys = keysFile === undefined ? {} : parseEnvFile(keysFile)
69
-
70
- for (const name of names) {
71
- if (keys[name])
72
- return { name, key: keys[name] as string, from: '.env.keys' }
73
- }
74
- for (const name of names) {
75
- if (environment[name])
76
- return { name, key: environment[name] as string, from: 'environment' }
77
- }
78
- return undefined
79
- }
80
-
81
- export type Decryption =
82
- | { ok: true, value: string, key: PrivateKey }
83
- | { ok: false, reason: 'no-key', looked: string[] }
84
- | { ok: false, reason: 'failed', key: PrivateKey }
85
-
86
- /** Decrypt one value in memory. The result is never cached or written anywhere. */
87
- export function decryptEnvValue(
88
- envFile: string,
89
- value: string,
90
- keysFile: string | undefined,
91
- environment: Record<string, string | undefined>,
92
- ): Decryption {
93
- const key = findPrivateKey(envFile, keysFile, environment)
94
- if (!key)
95
- return { ok: false, reason: 'no-key', looked: envPrivateKeyNames(basename(envFile)) }
96
-
97
- try {
98
- return { ok: true, value: decryptValue(value, key.key), key }
99
- }
100
- catch {
101
- return { ok: false, reason: 'failed', key }
102
- }
103
- }