@townco/secret 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/env-file.d.ts +105 -0
- package/dist/env-file.js +205 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +130 -0
- package/dist/onepassword.d.ts +102 -0
- package/dist/onepassword.js +203 -0
- package/package.json +33 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a line in an .env file
|
|
3
|
+
*/
|
|
4
|
+
type EnvLine = {
|
|
5
|
+
type: "comment";
|
|
6
|
+
content: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: "blank";
|
|
9
|
+
} | {
|
|
10
|
+
type: "entry";
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
raw: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A data structure that represents a parsed .env file while preserving
|
|
17
|
+
* all original content including comments, blank lines, and order.
|
|
18
|
+
*/
|
|
19
|
+
export declare class EnvFile {
|
|
20
|
+
private lines;
|
|
21
|
+
constructor(lines?: EnvLine[]);
|
|
22
|
+
/**
|
|
23
|
+
* Parse a .env file string into an EnvFile structure
|
|
24
|
+
*/
|
|
25
|
+
static parse(content: string): EnvFile;
|
|
26
|
+
/**
|
|
27
|
+
* Serialize the EnvFile back to a string
|
|
28
|
+
*/
|
|
29
|
+
toString(): string;
|
|
30
|
+
/**
|
|
31
|
+
* Get all entries as a key-value record
|
|
32
|
+
*/
|
|
33
|
+
toRecord(): Record<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* Find an entry line by key
|
|
36
|
+
*/
|
|
37
|
+
private findEntry;
|
|
38
|
+
/**
|
|
39
|
+
* Find the index of an entry by key
|
|
40
|
+
*/
|
|
41
|
+
private findEntryIndex;
|
|
42
|
+
/**
|
|
43
|
+
* Create an entry line from key and value
|
|
44
|
+
*/
|
|
45
|
+
private createEntry;
|
|
46
|
+
/**
|
|
47
|
+
* Normalize a value by ensuring it's properly quoted
|
|
48
|
+
*/
|
|
49
|
+
private normalizeValue;
|
|
50
|
+
/**
|
|
51
|
+
* Find the insertion index for a new entry (before the final blank line if present)
|
|
52
|
+
*/
|
|
53
|
+
private findInsertionIndex;
|
|
54
|
+
/**
|
|
55
|
+
* Get the value for a specific key
|
|
56
|
+
*/
|
|
57
|
+
get(key: string): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Set a value for a key. If the key exists, updates it in place.
|
|
60
|
+
* If it doesn't exist, appends it above the final newline (if present).
|
|
61
|
+
* Values are always double quoted.
|
|
62
|
+
*/
|
|
63
|
+
set(key: string, value: string): this;
|
|
64
|
+
/**
|
|
65
|
+
* Delete a key-value entry
|
|
66
|
+
*/
|
|
67
|
+
delete(key: string): this;
|
|
68
|
+
/**
|
|
69
|
+
* Check if a key exists
|
|
70
|
+
*/
|
|
71
|
+
has(key: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Get all keys
|
|
74
|
+
*/
|
|
75
|
+
keys(): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Iterate over all entries
|
|
78
|
+
*/
|
|
79
|
+
entries(): IterableIterator<[string, string]>;
|
|
80
|
+
/**
|
|
81
|
+
* Apply a function to all entries and return a new EnvFile
|
|
82
|
+
*/
|
|
83
|
+
map(fn: (key: string, value: string) => [string, string]): EnvFile;
|
|
84
|
+
/**
|
|
85
|
+
* Filter entries based on a predicate
|
|
86
|
+
*/
|
|
87
|
+
filter(fn: (key: string, value: string) => boolean): EnvFile;
|
|
88
|
+
/**
|
|
89
|
+
* Add a comment line
|
|
90
|
+
*/
|
|
91
|
+
addComment(content: string): this;
|
|
92
|
+
/**
|
|
93
|
+
* Add a blank line
|
|
94
|
+
*/
|
|
95
|
+
addBlank(): this;
|
|
96
|
+
/**
|
|
97
|
+
* Get the raw lines array for custom operations
|
|
98
|
+
*/
|
|
99
|
+
getLines(): readonly EnvLine[];
|
|
100
|
+
/**
|
|
101
|
+
* Create a clone of this EnvFile
|
|
102
|
+
*/
|
|
103
|
+
clone(): EnvFile;
|
|
104
|
+
}
|
|
105
|
+
export {};
|
package/dist/env-file.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A data structure that represents a parsed .env file while preserving
|
|
3
|
+
* all original content including comments, blank lines, and order.
|
|
4
|
+
*/
|
|
5
|
+
export class EnvFile {
|
|
6
|
+
lines;
|
|
7
|
+
constructor(lines = []) {
|
|
8
|
+
this.lines = lines;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse a .env file string into an EnvFile structure
|
|
12
|
+
*/
|
|
13
|
+
static parse(content) {
|
|
14
|
+
const lines = content.split("\n").map((line) => {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (trimmed === "") {
|
|
17
|
+
return { type: "blank" };
|
|
18
|
+
}
|
|
19
|
+
if (trimmed.startsWith("#")) {
|
|
20
|
+
return { type: "comment", content: line };
|
|
21
|
+
}
|
|
22
|
+
const equalsIndex = line.indexOf("=");
|
|
23
|
+
if (equalsIndex === -1) {
|
|
24
|
+
// Malformed line, treat as comment
|
|
25
|
+
return { type: "comment", content: line };
|
|
26
|
+
}
|
|
27
|
+
const key = line.substring(0, equalsIndex).trim();
|
|
28
|
+
const value = line.substring(equalsIndex + 1);
|
|
29
|
+
return { type: "entry", key, value, raw: line };
|
|
30
|
+
});
|
|
31
|
+
return new EnvFile(lines);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Serialize the EnvFile back to a string
|
|
35
|
+
*/
|
|
36
|
+
toString() {
|
|
37
|
+
return this.lines
|
|
38
|
+
.map((line) => {
|
|
39
|
+
switch (line.type) {
|
|
40
|
+
case "blank":
|
|
41
|
+
return "";
|
|
42
|
+
case "comment":
|
|
43
|
+
return line.content;
|
|
44
|
+
case "entry":
|
|
45
|
+
return line.raw;
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Unknown line type`);
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.join("\n");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get all entries as a key-value record
|
|
54
|
+
*/
|
|
55
|
+
toRecord() {
|
|
56
|
+
return Object.fromEntries(this.entries());
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find an entry line by key
|
|
60
|
+
*/
|
|
61
|
+
findEntry(key) {
|
|
62
|
+
const line = this.lines.find((l) => l.type === "entry" && l.key === key);
|
|
63
|
+
return line;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Find the index of an entry by key
|
|
67
|
+
*/
|
|
68
|
+
findEntryIndex(key) {
|
|
69
|
+
return this.lines.findIndex((l) => l.type === "entry" && l.key === key);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create an entry line from key and value
|
|
73
|
+
*/
|
|
74
|
+
createEntry(key, value) {
|
|
75
|
+
const quotedValue = this.normalizeValue(value);
|
|
76
|
+
return {
|
|
77
|
+
type: "entry",
|
|
78
|
+
key,
|
|
79
|
+
value: quotedValue,
|
|
80
|
+
raw: `${key}=${quotedValue}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Normalize a value by ensuring it's properly quoted
|
|
85
|
+
*/
|
|
86
|
+
normalizeValue(value) {
|
|
87
|
+
// Strip existing quotes if present, then add quotes
|
|
88
|
+
const unquoted = value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
|
|
89
|
+
return `"${unquoted}"`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Find the insertion index for a new entry (before the final blank line if present)
|
|
93
|
+
*/
|
|
94
|
+
findInsertionIndex() {
|
|
95
|
+
const lastLine = this.lines[this.lines.length - 1];
|
|
96
|
+
return lastLine?.type === "blank"
|
|
97
|
+
? this.lines.length - 1
|
|
98
|
+
: this.lines.length;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get the value for a specific key
|
|
102
|
+
*/
|
|
103
|
+
get(key) {
|
|
104
|
+
return this.findEntry(key)?.value;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Set a value for a key. If the key exists, updates it in place.
|
|
108
|
+
* If it doesn't exist, appends it above the final newline (if present).
|
|
109
|
+
* Values are always double quoted.
|
|
110
|
+
*/
|
|
111
|
+
set(key, value) {
|
|
112
|
+
const index = this.findEntryIndex(key);
|
|
113
|
+
const entry = this.createEntry(key, value);
|
|
114
|
+
if (index !== -1) {
|
|
115
|
+
this.lines[index] = entry;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
this.lines.splice(this.findInsertionIndex(), 0, entry);
|
|
119
|
+
}
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Delete a key-value entry
|
|
124
|
+
*/
|
|
125
|
+
delete(key) {
|
|
126
|
+
this.lines = this.lines.filter((l) => !(l.type === "entry" && l.key === key));
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check if a key exists
|
|
131
|
+
*/
|
|
132
|
+
has(key) {
|
|
133
|
+
return this.findEntry(key) !== undefined;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get all keys
|
|
137
|
+
*/
|
|
138
|
+
keys() {
|
|
139
|
+
return this.lines
|
|
140
|
+
.filter((l) => l.type === "entry")
|
|
141
|
+
.map((l) => l.key);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Iterate over all entries
|
|
145
|
+
*/
|
|
146
|
+
*entries() {
|
|
147
|
+
for (const line of this.lines) {
|
|
148
|
+
if (line.type === "entry") {
|
|
149
|
+
yield [line.key, line.value];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Apply a function to all entries and return a new EnvFile
|
|
155
|
+
*/
|
|
156
|
+
map(fn) {
|
|
157
|
+
const newLines = this.lines.map((line) => {
|
|
158
|
+
if (line.type === "entry") {
|
|
159
|
+
const [newKey, newValue] = fn(line.key, line.value);
|
|
160
|
+
return this.createEntry(newKey, newValue);
|
|
161
|
+
}
|
|
162
|
+
return line;
|
|
163
|
+
});
|
|
164
|
+
return new EnvFile(newLines);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Filter entries based on a predicate
|
|
168
|
+
*/
|
|
169
|
+
filter(fn) {
|
|
170
|
+
const newLines = this.lines.filter((line) => {
|
|
171
|
+
if (line.type === "entry") {
|
|
172
|
+
return fn(line.key, line.value);
|
|
173
|
+
}
|
|
174
|
+
return true; // Keep comments and blank lines
|
|
175
|
+
});
|
|
176
|
+
return new EnvFile(newLines);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Add a comment line
|
|
180
|
+
*/
|
|
181
|
+
addComment(content) {
|
|
182
|
+
const comment = content.startsWith("#") ? content : `# ${content}`;
|
|
183
|
+
this.lines.push({ type: "comment", content: comment });
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Add a blank line
|
|
188
|
+
*/
|
|
189
|
+
addBlank() {
|
|
190
|
+
this.lines.push({ type: "blank" });
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get the raw lines array for custom operations
|
|
195
|
+
*/
|
|
196
|
+
getLines() {
|
|
197
|
+
return this.lines;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Create a clone of this EnvFile
|
|
201
|
+
*/
|
|
202
|
+
clone() {
|
|
203
|
+
return new EnvFile([...this.lines]);
|
|
204
|
+
}
|
|
205
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { EnvFile } from "./env-file";
|
|
2
|
+
export { type OnePassword, OnePassword as OpItem } from "./onepassword";
|
|
3
|
+
export type SecretValidation = {
|
|
4
|
+
key: string;
|
|
5
|
+
value: string;
|
|
6
|
+
valid: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const listSecrets: () => Promise<Promise<SecretValidation[]>>;
|
|
10
|
+
export declare const createSecret: (name: string, value: string) => Promise<Promise<void>>;
|
|
11
|
+
export declare const deleteSecret: (name: string) => Promise<Promise<void>>;
|
|
12
|
+
export declare const genenv: () => Promise<Promise<void>>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { EnvFile } from "./env-file";
|
|
3
|
+
import { OnePassword } from "./onepassword";
|
|
4
|
+
export { EnvFile } from "./env-file";
|
|
5
|
+
export { OnePassword as OpItem } from "./onepassword";
|
|
6
|
+
const ROOT_MARKER = ".root";
|
|
7
|
+
const SECRET_FILE = ".env.in";
|
|
8
|
+
const OP_VAULT = "app";
|
|
9
|
+
const OP_ITEM = "dev";
|
|
10
|
+
const findRoot = async (start = ".") => {
|
|
11
|
+
const startResolved = path.resolve(start);
|
|
12
|
+
const rootPath = path.join(startResolved, ROOT_MARKER);
|
|
13
|
+
if (await Bun.file(rootPath).exists())
|
|
14
|
+
return startResolved;
|
|
15
|
+
const parent = path.dirname(startResolved);
|
|
16
|
+
if (parent === "/") {
|
|
17
|
+
throw new Error("No root found");
|
|
18
|
+
}
|
|
19
|
+
return await findRoot(parent);
|
|
20
|
+
};
|
|
21
|
+
const withOpSignIn = (fn) => async (...args) => {
|
|
22
|
+
await OnePassword.signin();
|
|
23
|
+
return fn(...args);
|
|
24
|
+
};
|
|
25
|
+
const readEnvFile = async (filePath) => {
|
|
26
|
+
const content = await Bun.file(filePath).text();
|
|
27
|
+
return EnvFile.parse(content);
|
|
28
|
+
};
|
|
29
|
+
const writeEnvFile = async (filePath, envFile) => {
|
|
30
|
+
await Bun.write(filePath, envFile.toString());
|
|
31
|
+
};
|
|
32
|
+
const withEnvFile = (fn) => async (...args) => {
|
|
33
|
+
const filePath = path.join(await findRoot(), SECRET_FILE);
|
|
34
|
+
const envFile = await readEnvFile(filePath);
|
|
35
|
+
const originalRecord = envFile.toRecord();
|
|
36
|
+
const result = fn({ ...originalRecord }, ...args);
|
|
37
|
+
// If the result is a Record<string, string>, write it back
|
|
38
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
39
|
+
const resultRecord = result;
|
|
40
|
+
// Check if this looks like a secrets record (all string values)
|
|
41
|
+
const isSecretsRecord = Object.values(resultRecord).every((v) => typeof v === "string");
|
|
42
|
+
if (isSecretsRecord) {
|
|
43
|
+
const resultSecrets = resultRecord;
|
|
44
|
+
// Delete keys that are in original but not in result
|
|
45
|
+
for (const key of Object.keys(originalRecord)) {
|
|
46
|
+
if (!(key in resultSecrets)) {
|
|
47
|
+
envFile.delete(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Update or add keys from result
|
|
51
|
+
for (const [key, value] of Object.entries(resultSecrets)) {
|
|
52
|
+
envFile.set(key, value);
|
|
53
|
+
}
|
|
54
|
+
await writeEnvFile(filePath, envFile);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
export const listSecrets = withOpSignIn(async () => {
|
|
60
|
+
const root = await findRoot();
|
|
61
|
+
const filePath = path.join(root, SECRET_FILE);
|
|
62
|
+
const envFile = await readEnvFile(filePath);
|
|
63
|
+
const secrets = envFile.toRecord();
|
|
64
|
+
// Fetch the dev item using OpItem
|
|
65
|
+
const opItem = await OnePassword.fetch(OP_VAULT, OP_ITEM);
|
|
66
|
+
const validations = [];
|
|
67
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
68
|
+
// Strip quotes from value if present
|
|
69
|
+
let unquotedValue = value;
|
|
70
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
71
|
+
unquotedValue = value.slice(1, -1);
|
|
72
|
+
}
|
|
73
|
+
const validation = {
|
|
74
|
+
key,
|
|
75
|
+
value: unquotedValue,
|
|
76
|
+
valid: true,
|
|
77
|
+
};
|
|
78
|
+
if (!validation.value.startsWith("op://")) {
|
|
79
|
+
validations.push(validation);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// Check if field exists in 1Password item
|
|
83
|
+
if (opItem.has(key)) {
|
|
84
|
+
validation.valid = false;
|
|
85
|
+
validation.error = `Field '${key}' not found in 1Password item`;
|
|
86
|
+
validations.push(validation);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Build expected op:// reference
|
|
90
|
+
const expectedRef = opItem.getReference(key);
|
|
91
|
+
// Compare the value from .env.in with expected reference
|
|
92
|
+
if (unquotedValue !== expectedRef) {
|
|
93
|
+
validation.valid = false;
|
|
94
|
+
validation.error = `Reference mismatch: expected '${expectedRef}', got '${unquotedValue}'`;
|
|
95
|
+
validations.push(validation);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
validations.push(validation);
|
|
99
|
+
}
|
|
100
|
+
return validations;
|
|
101
|
+
});
|
|
102
|
+
export const createSecret = withOpSignIn(async (name, value) => {
|
|
103
|
+
// Fetch the item, update it, and sync
|
|
104
|
+
const opItem = await OnePassword.fetch(OP_VAULT, OP_ITEM);
|
|
105
|
+
opItem.set(name, value);
|
|
106
|
+
await opItem.sync();
|
|
107
|
+
// Update the env file with the secret reference
|
|
108
|
+
await withEnvFile((secrets) => {
|
|
109
|
+
secrets[name] = opItem.getReference(name);
|
|
110
|
+
return secrets;
|
|
111
|
+
})();
|
|
112
|
+
});
|
|
113
|
+
export const deleteSecret = withOpSignIn(async (name) => {
|
|
114
|
+
// Fetch the item, delete the field, and sync
|
|
115
|
+
const opItem = await OnePassword.fetch(OP_VAULT, OP_ITEM);
|
|
116
|
+
opItem.delete(name);
|
|
117
|
+
await opItem.sync();
|
|
118
|
+
// Update the env file by removing the secret
|
|
119
|
+
await withEnvFile((secrets, fieldName) => {
|
|
120
|
+
delete secrets[fieldName];
|
|
121
|
+
return secrets;
|
|
122
|
+
})(name);
|
|
123
|
+
});
|
|
124
|
+
export const genenv = withOpSignIn(async () => {
|
|
125
|
+
const root = await findRoot();
|
|
126
|
+
const inputPath = path.join(root, SECRET_FILE);
|
|
127
|
+
const outputPath = path.join(root, ".env");
|
|
128
|
+
// Use OpItem to inject and resolve secret references
|
|
129
|
+
await OnePassword.inject(inputPath, outputPath);
|
|
130
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { FullItem } from "@1password/connect";
|
|
2
|
+
/**
|
|
3
|
+
* Represents a change to be made to a 1Password item
|
|
4
|
+
*/
|
|
5
|
+
type OnePasswordChange = {
|
|
6
|
+
type: "add";
|
|
7
|
+
key: string;
|
|
8
|
+
value: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: "update";
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: "delete";
|
|
15
|
+
key: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* A data structure that represents a 1Password item and provides
|
|
19
|
+
* methods to detect changes and generate appropriate `op` CLI commands.
|
|
20
|
+
*/
|
|
21
|
+
export declare class OnePassword {
|
|
22
|
+
private vault;
|
|
23
|
+
private item;
|
|
24
|
+
private fields;
|
|
25
|
+
private originalFields;
|
|
26
|
+
constructor(vault: string, item: string, fields?: Map<string, string>);
|
|
27
|
+
/**
|
|
28
|
+
* Create an OpItem from a 1Password FullItem JSON response
|
|
29
|
+
*/
|
|
30
|
+
static fromFullItem(vault: string, itemName: string, fullItem: FullItem): OnePassword;
|
|
31
|
+
/**
|
|
32
|
+
* Fetch an OpItem from 1Password using the CLI
|
|
33
|
+
*/
|
|
34
|
+
static fetch(vault: string, item: string): Promise<OnePassword>;
|
|
35
|
+
/**
|
|
36
|
+
* Sign in to 1Password CLI
|
|
37
|
+
*/
|
|
38
|
+
static signin(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Get a field value
|
|
41
|
+
*/
|
|
42
|
+
get(key: string): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Set a field value (marks as changed)
|
|
45
|
+
*/
|
|
46
|
+
set(key: string, value: string): this;
|
|
47
|
+
/**
|
|
48
|
+
* Delete a field (marks as deleted)
|
|
49
|
+
*/
|
|
50
|
+
delete(key: string): this;
|
|
51
|
+
/**
|
|
52
|
+
* Check if a field exists
|
|
53
|
+
*/
|
|
54
|
+
has(key: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Get all field keys
|
|
57
|
+
*/
|
|
58
|
+
keys(): string[];
|
|
59
|
+
/**
|
|
60
|
+
* Get all fields as a record
|
|
61
|
+
*/
|
|
62
|
+
toRecord(): Record<string, string>;
|
|
63
|
+
/**
|
|
64
|
+
* Get all field entries
|
|
65
|
+
*/
|
|
66
|
+
entries(): IterableIterator<[string, string]>;
|
|
67
|
+
/**
|
|
68
|
+
* Build an op:// reference for a field
|
|
69
|
+
*/
|
|
70
|
+
getReference(key: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Detect changes between original and current state
|
|
73
|
+
*/
|
|
74
|
+
detectChanges(): OnePasswordChange[];
|
|
75
|
+
/**
|
|
76
|
+
* Generate op CLI arguments for a single change
|
|
77
|
+
*/
|
|
78
|
+
private buildEditArgs;
|
|
79
|
+
/**
|
|
80
|
+
* Apply all changes to 1Password using the op CLI
|
|
81
|
+
* Returns the number of changes applied
|
|
82
|
+
*/
|
|
83
|
+
sync(): Promise<number>;
|
|
84
|
+
/**
|
|
85
|
+
* Inject secrets from a template file to an output file
|
|
86
|
+
* This resolves op:// references to actual values
|
|
87
|
+
*/
|
|
88
|
+
static inject(inputPath: string, outputPath: string): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Create a clone of this OpItem
|
|
91
|
+
*/
|
|
92
|
+
clone(): OnePassword;
|
|
93
|
+
/**
|
|
94
|
+
* Reset to original state (discard changes)
|
|
95
|
+
*/
|
|
96
|
+
reset(): this;
|
|
97
|
+
/**
|
|
98
|
+
* Check if there are unsaved changes
|
|
99
|
+
*/
|
|
100
|
+
hasChanges(): boolean;
|
|
101
|
+
}
|
|
102
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A data structure that represents a 1Password item and provides
|
|
3
|
+
* methods to detect changes and generate appropriate `op` CLI commands.
|
|
4
|
+
*/
|
|
5
|
+
export class OnePassword {
|
|
6
|
+
vault;
|
|
7
|
+
item;
|
|
8
|
+
fields;
|
|
9
|
+
originalFields;
|
|
10
|
+
constructor(vault, item, fields = new Map()) {
|
|
11
|
+
this.vault = vault;
|
|
12
|
+
this.item = item;
|
|
13
|
+
this.fields = new Map(fields);
|
|
14
|
+
this.originalFields = new Map(fields);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create an OpItem from a 1Password FullItem JSON response
|
|
18
|
+
*/
|
|
19
|
+
static fromFullItem(vault, itemName, fullItem) {
|
|
20
|
+
const fields = new Map();
|
|
21
|
+
if (fullItem.fields) {
|
|
22
|
+
for (const field of fullItem.fields) {
|
|
23
|
+
if (field.label && field.value) {
|
|
24
|
+
fields.set(field.label, field.value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return new OnePassword(vault, itemName, fields);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fetch an OpItem from 1Password using the CLI
|
|
32
|
+
*/
|
|
33
|
+
static async fetch(vault, item) {
|
|
34
|
+
try {
|
|
35
|
+
const proc = Bun.spawn([
|
|
36
|
+
"op",
|
|
37
|
+
"--format=json",
|
|
38
|
+
"item",
|
|
39
|
+
"get",
|
|
40
|
+
"--vault",
|
|
41
|
+
vault,
|
|
42
|
+
item,
|
|
43
|
+
]);
|
|
44
|
+
const output = await new Response(proc.stdout).text();
|
|
45
|
+
await proc.exited;
|
|
46
|
+
const fullItem = JSON.parse(output);
|
|
47
|
+
return OnePassword.fromFullItem(vault, item, fullItem);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new Error(`Failed to fetch item '${item}' from vault '${vault}': ${error}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Sign in to 1Password CLI
|
|
55
|
+
*/
|
|
56
|
+
static async signin() {
|
|
57
|
+
await Bun.spawn(["op", "signin"]).exited;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get a field value
|
|
61
|
+
*/
|
|
62
|
+
get(key) {
|
|
63
|
+
return this.fields.get(key);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Set a field value (marks as changed)
|
|
67
|
+
*/
|
|
68
|
+
set(key, value) {
|
|
69
|
+
this.fields.set(key, value);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Delete a field (marks as deleted)
|
|
74
|
+
*/
|
|
75
|
+
delete(key) {
|
|
76
|
+
this.fields.delete(key);
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Check if a field exists
|
|
81
|
+
*/
|
|
82
|
+
has(key) {
|
|
83
|
+
return this.fields.has(key);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get all field keys
|
|
87
|
+
*/
|
|
88
|
+
keys() {
|
|
89
|
+
return Array.from(this.fields.keys());
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get all fields as a record
|
|
93
|
+
*/
|
|
94
|
+
toRecord() {
|
|
95
|
+
return Object.fromEntries(this.fields);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get all field entries
|
|
99
|
+
*/
|
|
100
|
+
entries() {
|
|
101
|
+
return this.fields.entries();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build an op:// reference for a field
|
|
105
|
+
*/
|
|
106
|
+
getReference(key) {
|
|
107
|
+
return `op://${this.vault}/${this.item}/${key}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Detect changes between original and current state
|
|
111
|
+
*/
|
|
112
|
+
detectChanges() {
|
|
113
|
+
const changes = [];
|
|
114
|
+
// Check for deletions
|
|
115
|
+
for (const key of this.originalFields.keys()) {
|
|
116
|
+
if (!this.fields.has(key)) {
|
|
117
|
+
changes.push({ type: "delete", key });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Check for additions and updates
|
|
121
|
+
for (const [key, value] of this.fields.entries()) {
|
|
122
|
+
const originalValue = this.originalFields.get(key);
|
|
123
|
+
if (originalValue === undefined) {
|
|
124
|
+
changes.push({ type: "add", key, value });
|
|
125
|
+
}
|
|
126
|
+
else if (originalValue !== value) {
|
|
127
|
+
changes.push({ type: "update", key, value });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return changes;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Generate op CLI arguments for a single change
|
|
134
|
+
*/
|
|
135
|
+
buildEditArgs(change) {
|
|
136
|
+
switch (change.type) {
|
|
137
|
+
case "add":
|
|
138
|
+
case "update":
|
|
139
|
+
return [`${change.key}[password]=${change.value}`];
|
|
140
|
+
case "delete":
|
|
141
|
+
return [`${change.key}[delete]`];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Apply all changes to 1Password using the op CLI
|
|
146
|
+
* Returns the number of changes applied
|
|
147
|
+
*/
|
|
148
|
+
async sync() {
|
|
149
|
+
const changes = this.detectChanges();
|
|
150
|
+
if (changes.length === 0) {
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
// Build all edit arguments
|
|
154
|
+
const editArgs = changes.flatMap((change) => this.buildEditArgs(change));
|
|
155
|
+
// Execute a single op item edit command with all changes
|
|
156
|
+
await Bun.spawn([
|
|
157
|
+
"op",
|
|
158
|
+
"item",
|
|
159
|
+
"edit",
|
|
160
|
+
this.item,
|
|
161
|
+
"--vault",
|
|
162
|
+
this.vault,
|
|
163
|
+
...editArgs,
|
|
164
|
+
]).exited;
|
|
165
|
+
// Update originalFields to match current state
|
|
166
|
+
this.originalFields = new Map(this.fields);
|
|
167
|
+
return changes.length;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Inject secrets from a template file to an output file
|
|
171
|
+
* This resolves op:// references to actual values
|
|
172
|
+
*/
|
|
173
|
+
static async inject(inputPath, outputPath) {
|
|
174
|
+
await Bun.spawn([
|
|
175
|
+
"op",
|
|
176
|
+
"inject",
|
|
177
|
+
"-i",
|
|
178
|
+
inputPath,
|
|
179
|
+
"-o",
|
|
180
|
+
outputPath,
|
|
181
|
+
"--force",
|
|
182
|
+
]).exited;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Create a clone of this OpItem
|
|
186
|
+
*/
|
|
187
|
+
clone() {
|
|
188
|
+
return new OnePassword(this.vault, this.item, new Map(this.fields));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Reset to original state (discard changes)
|
|
192
|
+
*/
|
|
193
|
+
reset() {
|
|
194
|
+
this.fields = new Map(this.originalFields);
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Check if there are unsaved changes
|
|
199
|
+
*/
|
|
200
|
+
hasChanges() {
|
|
201
|
+
return this.detectChanges().length > 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@townco/secret",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/federicoweber/agent_hub.git"
|
|
14
|
+
},
|
|
15
|
+
"author": "Federico Weber",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"check": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@1password/connect": "^1.4.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@townco/tsconfig": "^0.1.0",
|
|
31
|
+
"@types/bun": "^1.3.1"
|
|
32
|
+
}
|
|
33
|
+
}
|