Skip to content

Commit 0dec822

Browse files
authored
Merge pull request #6 from AegisJSProject/feature/file-import
Add `openSecretStoreFile` to open secret store from JSON file (node)
2 parents 74bd694 + 380fede commit 0dec822

File tree

10 files changed

+117
-110
lines changed

10 files changed

+117
-110
lines changed

.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
node_modules/
2+
secrets.json
3+
key.jwk
24
test/
35
.github/
46
test/

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [v1.0.1] - 2025-09-04
11+
12+
### Added
13+
- Add `openSecretStoreFile` to open secret store from JSON file (node)
14+
1015
## [v1.0.0] - 2025-09-04
1116

1217
Initial Release

README.md

Lines changed: 53 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Proxy-based wrapper for encrypting and decrypting data over any storage object
1616
![NPM Unpacked Size](https://img.shields.io/npm/unpacked-size/%40aegisjsproject%secret-store)
1717
[![npm](https://img.shields.io/npm/dw/@aegisjsproject/secret-store?logo=npm)](https://www.npmjs.com/package/@aegisjsproject/secret-store)
1818

19-
[![GitHub followers](https://img.shields.io/github/followers/AegisJSProject.svg?style=social)](https://github.com/AegisJSProoject)
19+
[![GitHub followers](https://img.shields.io/github/followers/AegisJSProject.svg?style=social)](https://github.com/AegisJSProject)
2020
![GitHub forks](https://img.shields.io/github/forks/AegisJSProject/secret-store.svg?style=social)
2121
![GitHub stars](https://img.shields.io/github/stars/AegisJSProject/secret-store.svg?style=social)
2222
[![Twitter Follow](https://img.shields.io/twitter/follow/shgysk8zer0.svg?style=social)](https://twitter.com/shgysk8zer0)
@@ -28,125 +28,73 @@ Proxy-based wrapper for encrypting and decrypting data over any storage object
2828
- [Contributing](./.github/CONTRIBUTING.md)
2929
<!-- - [Security Policy](./.github/SECURITY.md) -->
3030

31-
This is a [GitHub Template Repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository)
32-
to create components using [`@aegisjsproject/component`](https://npmjs.com/package/@aegisjsproject/component).
33-
It serves as a quick-start to creating light-weight, secure, web standards based
34-
components. It provides the essentials, such as:
31+
## Installation
3532

36-
- The essential packages:
37-
- [`@aegisjsproject/core`](https://github.com/AegisJSProject/aegis)
38-
- [`@aegisjsproject/styles`](https://github.com/AegisJSProject/styles)
39-
- [`@aegisjsproject/component`](https://github.com/AegisJSProject/component)
40-
- Build tools
41-
- [eslint](https://npmjs.com/eslint)
42-
- [rollup](https://npmjs.com/rollup)
43-
- [Dependabot](https://github.com/dependabot) & [CodeQL](https://github.com/github/codeql) config
44-
- Pull Request & Issue templates
45-
- Automated releases to npm on `git tag` (when pushed using `git push --tags`)
46-
- Provides GitHub Action for Package Provenance
33+
### npm
34+
```bash
35+
npm install @aegisjsproject/secret-store
36+
```
4737

48-
To start creating your own component, just go to the [GitHub repo](https://@github.com/AegisJSProject/secret-store)
49-
and click the "Use this template" button.
38+
### `<script type="importmap">`
5039

51-
## Steps to Create a Component
40+
```html
41+
<script type="importmap">
42+
{
43+
"imports": {
44+
"@aegisjsproject/secret-store": "https://unpkg.com/@aegisjsproject/secret-store/secret-store.min.js",
45+
"@shgysk8zer0/aes-gcm": "https://unpkg.com/@shgysk8zer0/aes-gcm/aes-gcm.min.js"
46+
}
47+
}
48+
</script>
49+
```
5250

53-
- Create Repository from the GitHub Template Repository
54-
- Clone your new Aegis Component Repository
55-
- Update the `README.md`, `package.json`, & `CHANGELOG.md` as needed (especially the name)
56-
- Create your component
57-
- Update `rollup.config.js` (don't forget to update `input` & `output.file`)
58-
- Publish (create and merge PR)
51+
## API
5952

60-
## Using Automated Releases
53+
### `useSecretStore(key, targetObject, handler)`
6154

62-
The following setup will create an automated GitHub Release and publish on npm
63-
for every signed git tag (`git tag -s vx.y.z`) pushed to GitHub:
55+
Creates an encrypted proxy around an object where values are automatically encrypted on set and decrypted on get.
6456

65-
- Create a "Classic Token" (Automation) on [npmjs.org](https://www.npmjs.com/)
66-
- Give that token a descriptive name name and copy it
67-
- Paste it into your repository's "Repository secrets" in "settings" -> "Secrets and variables" -> Actions
68-
- Done!
57+
**Parameters:**
58+
- `key` - CryptoKey with decrypt usage (encrypt usage required for setter)
59+
- `targetObject` - Object to wrap (defaults to `process.env`)
60+
- `handler` - ProxyHandler (defaults to `Reflect`)
6961

70-
Now, every time you create a new PGP signed tag on GitHub (don't forget to `git push --tags`)
71-
it will create a new GitHub Release and a new release on npm with Package Provenance.
62+
**Returns:** `[proxy, setter]` - Frozen array containing the proxy and async setter function
7263

73-
## Example Component:
64+
**Throws:** TypeError if key lacks decrypt usage
7465

75-
```js
76-
import { AegisComponent, SYMBOLS, TRIGGERS } from '@aegisjsproject/component';
77-
import { html, appendTo } from '@aegisjsproject/core';
78-
79-
class HTMLHelloWorldElement extends AegisComponent {
80-
async [SYMBOLS.render](type, { shadow }) {
81-
switch(type) {
82-
case TRIGGERS.constructed:
83-
appendTo(shadow, html`<h1>Hello, World!</h1>`);
84-
break;
85-
}
86-
}
87-
}
66+
### `openSecretStoreFile(key, path, config)`
8867

89-
HTMLHelloWorldElement.register('hello-world');
90-
```
68+
Node.js only. Loads and wraps a JSON file as an encrypted store.
9169

92-
## Package, Component, & Repository Requirements
70+
**Parameters:**
71+
- `key` - CryptoKey
72+
- `path` - File path string
73+
- `config.encoding` - File encoding (default: "utf8")
74+
- `config.handler` - ProxyHandler (default: `Reflect`)
75+
- `config.signal` - AbortSignal for cancellation
9376

94-
All packages **MUST** adhere to strict but fairly easy security guidelines:
77+
**Returns:** Promise resolving to `[proxy, setter]`
9578

96-
- All commits and tags **MUST** be [PGP/GPG signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
97-
- All web-based commits **MUST** be signed off by the contributor (a setting in GitHub repo settings)
98-
- All releases **MUST** use [Package Provenance](https://github.blog/2023-04-19-introducing-npm-package-provenance/)
99-
- Components **MUST** adhere to a strict [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
100-
- Components **MUST** comply with the provided [`TrustedTypesPolicy`](https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy) or add their own, which **SHOULD**:
101-
- Use `:component-name#html` for writing HTML as a string
102-
- Use `:component-name#script-url`
103-
- Components **MUST** be `import`able from a CDN (such as `unpkg.com`) without any build step/bundler
104-
- Component **SHOULD** provide a `<script type="importmap">` or `importmap.json` as necessary
105-
- Components **MUST NOT** use inline scripts (`script-src 'unsafe-inline`) or styles (`style-src 'unsafe-inline'`)
106-
- Components **MUST NOT** use `element.innerHTML` or similar
107-
- Components **MUST NOT** use `eval()` or `onclick` or anything similar
108-
- Components using [life cycle callbacks](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#using_the_lifecycle_callbacks) **MUST** have a `[Symbol.for('aegis:render')]` method
79+
## Usage
10980

110-
## Content-Security-Policy
81+
```js
82+
import { useSecretStore, openSecretStoreFile } from '@aegisjsproject/secret-store';
11183

112-
This is the base CSP (with comments):
84+
// Generate key
85+
const key = await crypto.subtle.generateKey(
86+
{ name: 'AES-GCM', length: 256 },
87+
false,
88+
['encrypt', 'decrypt']
89+
);
11390

114-
```
115-
default-src 'none'; // Block everything by default
116-
style-src 'self' blob:; // `blob:` essential for constructable stylesheet polyfill
117-
script-src 'self' https://unpkg.com/@aegisjsproject/; // Your script source may/should be added
118-
connect-src 'self'; // Add any data sources as needed, but as specific as possible
119-
trusted-types emtpy#html mepty#script sanitizer-raw#html; // Add to as necessary
120-
require-trusted-types-for 'script'; // This is required and currently the only option
121-
```
91+
// Create store
92+
const [store, set] = useSecretStore(key, {});
93+
94+
// Values are encrypted when set, decrypted when accessed
95+
await set('password', 'secret123');
96+
const password = await store.password; // 'secret123'
12297

123-
Components **MAY** differ from this in requiring additional `script-src` (other
124-
than `unnsafe-inline`, `unsafe-eval`, and additional `nonce-*` or `sha*` hashes
125-
(URL only permitted). Components **MAY** add to the `trusted-types`, if necessary,
126-
such as to add additional [`policy.createScriptURL()`](https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy/createScript)s
127-
as essential. Any such policy created for script URLs (namely, `<iframe>`s)
128-
**SHOULD** be of the form `:component-name#script-url`.
129-
130-
Components **SHOULD NOT** utilize methods or setters (such as `innerHTML`) which
131-
write HTML as strings and would require [`policy.createHTML()`](https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy/createHTML).
132-
Components **MAY** use the text alternatives, such as `textContent`. However,
133-
should the component require such things, the component **MUST** create an
134-
additional `TrsutedTypePolicy`, which **SHOULD** be named as `:component-name#html`:
135-
136-
Components **SHOULD NOT** include any [`policy.createScript()`](https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy/createScript)
137-
unless absolutely essential, as this would violate the strict CSP requirement and
138-
restriction from using `unsafe-*` in `script-src`. However, should your component
139-
absolutely and justifiably require this, it **SHOULD** use a `TrustedTypes` Policy
140-
name of the form `:component-name#script`.
141-
142-
Any component which requires more than one of `createHTML()`, `createScriptURL()`,
143-
or `createScript()` **MAY** instead combine the above simple into a single policy,
144-
which **SHOULD** be named `:component-name`.
145-
146-
Components **MUST** explicitly list any necessary changes to their CSP (including)
147-
`TrustedTypes` in their README.
148-
149-
To be listed in the upcoming Aegis component registry, components **MUST NOT**
150-
implement their own `createHTML()` (`:component-name#html`) or `createScript()`
151-
(`component-name#script`) methods, but **MAY** implement a `createScriptURL()`
152-
(`:component-name#script-url`) method.
98+
// Load from file (Node.js)
99+
const [fileStore] = await openSecretStoreFile(key, './secrets.json');
100+
```

key.jwk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"key_ops":["encrypt","decrypt"],"ext":true,"kty":"oct","k":"sehAb7rHKUE-w_6rYA_XqZaaLc70d8fCXe-JLwe-OCU","alg":"A256GCM"}

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aegisjsproject/secret-store",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "Proxy-based wrapper for encrypting and decrypting data over any storage object",
55
"keywords": [
66
"aegis",

rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import terser from '@rollup/plugin-terser';
2-
const externalPackages = ['@shgysk8zer0/aes-gcm'];
2+
const externalPackages = ['@shgysk8zer0/aes-gcm', 'node:'];
33

44
export default {
55
input: 'secret-store.js',

secret-store.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,35 @@ export function useSecretStore(key, targetObject = globalThis.process?.env ?? {}
4646
return Object.freeze([proxy, setter]);
4747
}
4848
}
49+
50+
/**
51+
* Opens a secret store from a JSON file. (Node only)
52+
*
53+
* @param {CryptoKey} key
54+
* @param {string} path
55+
* @param {object} config
56+
* @param {string} [config.encoding="utf8"]
57+
* @param {ProxyHandler} [config.handler=Reflect]
58+
* @param {AbortSignal} [config.signal]
59+
* @returns {Promise<[Proxy, (prop, val) => Promise<boolean>]>}
60+
* @throws {*} Any `signal.reason` and an aborted `AbortSignal`.
61+
* @throws {TypeError} If `key` is not a `CryptoKey`.
62+
* @throws {TypeError} If `path` is not a string/path.
63+
* @throws {Error} Any error from `readFile` or `JSON.parse` or `useSecretStore`.
64+
*/
65+
export async function openSecretStoreFile(key, path, { encoding = 'utf8', handler = Reflect, signal } = {}) {
66+
if (signal instanceof AbortSignal && signal.aborted) {
67+
throw signal.reason;
68+
} else if (! (key instanceof CryptoKey)) {
69+
throw new TypeError('Key must be a `CryptoKey`.');
70+
} else if (typeof path !== 'string') {
71+
throw new TypeError('Path must be a string.');
72+
} else {
73+
// Using this to keep the rest of the module compatible in other environments
74+
const { readFile } = await import('node:fs/promises');
75+
const text = await readFile(path, { encoding, signal });
76+
const data = JSON.parse(text);
77+
78+
return useSecretStore(key, data, handler);
79+
}
80+
}

secret-store.test.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import '@shgysk8zer0/polyfills';
22

33
import { generateSecretKey } from '@shgysk8zer0/aes-gcm';
4-
import { useSecretStore } from './secret-store.js';
4+
import { useSecretStore, openSecretStoreFile } from './secret-store.js';
55
import { test, describe } from 'node:test';
66
import { strictEqual, deepEqual, throws, rejects, doesNotReject, ok, notStrictEqual } from 'node:assert';
7+
import { readFile } from 'node:fs/promises';
8+
9+
async function getKey({ signal } = {}) {
10+
const keyData = await readFile('key.jwk', { encoding: 'utf8', signal });
11+
return await crypto.subtle.importKey('jwk', JSON.parse(keyData), 'AES-GCM', false, ['decrypt', 'encrypt']);
12+
}
713

814
describe('Test encrypted storage', async () => {
915
test('Check the things', async () => {
@@ -41,6 +47,16 @@ describe('Test encrypted storage', async () => {
4147
await promise;
4248
});
4349

50+
test('Check file version of secret store', async () => {
51+
const key = await getKey();
52+
const [store] = await openSecretStoreFile(key, 'secrets.json');
53+
strictEqual(await store.msg, 'Hello, World!', 'Should open secrets file and decrypt correctly.');
54+
55+
rejects(() => openSecretStoreFile([]), 'Should reject when not given a `CryptoKey`.');
56+
rejects(() => openSecretStoreFile(key, []), 'Should reject when not given a string for a file path.');
57+
rejects(() => openSecretStoreFile(key, 'secrets.json', { signal: AbortSignal.abort('Make me fail!')}), 'Should reject when given an aborted `AbortSignal`.');
58+
});
59+
4460
test('Check for things that should error.', async () => {
4561
const encryptKey = await generateSecretKey({ usages: ['encrypt'] });
4662
const decryptKey = await generateSecretKey({ usages: ['decrypt'] });

secrets.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"msg": "HHaIpdhZQRkqzEzgZ4QVpsVgJyPQvwS6L69EXwwh+m+QqUSvxsee7/I="
3+
}

0 commit comments

Comments
 (0)