@pokit/op 0.0.1 → 0.0.3
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/package.json +6 -6
- package/src/checks.ts +25 -0
- package/src/op.ts +239 -0
- package/src/resolver.ts +143 -0
- package/src/vault.ts +55 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pokit/op",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Operation utilities for pok CLI applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -25,9 +25,8 @@
|
|
|
25
25
|
"bugs": {
|
|
26
26
|
"url": "https://github.com/notation-dev/openpok/issues"
|
|
27
27
|
},
|
|
28
|
-
"main": "./
|
|
29
|
-
"
|
|
30
|
-
"types": "./src/index.ts",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
31
30
|
"exports": {
|
|
32
31
|
".": {
|
|
33
32
|
"bun": "./src/index.ts",
|
|
@@ -38,13 +37,14 @@
|
|
|
38
37
|
"files": [
|
|
39
38
|
"dist",
|
|
40
39
|
"README.md",
|
|
41
|
-
"LICENSE"
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"src"
|
|
42
42
|
],
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@pokit/core": "0.0.
|
|
47
|
+
"@pokit/core": "0.0.3"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/bun": "latest"
|
package/src/checks.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineCheck } from '@pokit/core';
|
|
2
|
+
import { isInstalled, isAuthenticated, getAuthErrorMessage } from './op';
|
|
3
|
+
|
|
4
|
+
export const opInstalled = defineCheck({
|
|
5
|
+
label: '1Password CLI installed',
|
|
6
|
+
check: async () => {
|
|
7
|
+
const installed = await isInstalled();
|
|
8
|
+
if (!installed) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
'1Password CLI is not installed. ' +
|
|
11
|
+
'Install from: https://developer.1password.com/docs/cli/get-started/'
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const opAuthenticated = defineCheck({
|
|
18
|
+
label: '1Password authenticated',
|
|
19
|
+
check: async () => {
|
|
20
|
+
const authenticated = await isAuthenticated();
|
|
21
|
+
if (!authenticated) {
|
|
22
|
+
throw new Error(getAuthErrorMessage());
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
});
|
package/src/op.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { $ } from 'bun';
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Input Validation
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Valid characters for 1Password identifiers (vault, item, field names).
|
|
9
|
+
* Allows alphanumeric, spaces, dashes, underscores, and periods.
|
|
10
|
+
*/
|
|
11
|
+
const VALID_IDENTIFIER_PATTERN = /^[a-zA-Z0-9 _.-]+$/;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate a 1Password identifier (vault name, item name, or field name).
|
|
15
|
+
* Throws if the identifier contains invalid characters.
|
|
16
|
+
*/
|
|
17
|
+
function validateIdentifier(value: string, type: 'vault' | 'item' | 'field'): void {
|
|
18
|
+
if (!value || value.trim().length === 0) {
|
|
19
|
+
throw new Error(`${type} name cannot be empty`);
|
|
20
|
+
}
|
|
21
|
+
if (!VALID_IDENTIFIER_PATTERN.test(value)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Invalid ${type} name: "${value}". Only alphanumeric characters, spaces, dashes, underscores, and periods are allowed.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Authentication
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if 1Password CLI is installed
|
|
34
|
+
*/
|
|
35
|
+
export async function isInstalled(): Promise<boolean> {
|
|
36
|
+
const result = await $`which op`.nothrow().quiet();
|
|
37
|
+
return result.exitCode === 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if authenticated with 1Password CLI
|
|
42
|
+
* Works for both desktop (app/signin) and CI (OP_SERVICE_ACCOUNT_TOKEN)
|
|
43
|
+
*/
|
|
44
|
+
export async function isAuthenticated(): Promise<boolean> {
|
|
45
|
+
const result = await $`op whoami`.nothrow().quiet();
|
|
46
|
+
return result.exitCode === 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get helpful error message for authentication failure
|
|
51
|
+
*/
|
|
52
|
+
export function getAuthErrorMessage(): string {
|
|
53
|
+
if (process.env.OP_SERVICE_ACCOUNT_TOKEN) {
|
|
54
|
+
return (
|
|
55
|
+
'1Password authentication failed. ' +
|
|
56
|
+
'The OP_SERVICE_ACCOUNT_TOKEN may be invalid or expired.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return (
|
|
60
|
+
'1Password authentication failed. ' +
|
|
61
|
+
'Either run `op signin` or ensure the 1Password app is running with CLI integration enabled.'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a vault exists
|
|
67
|
+
*/
|
|
68
|
+
export async function vaultExists(vault: string): Promise<boolean> {
|
|
69
|
+
validateIdentifier(vault, 'vault');
|
|
70
|
+
const result = await $`op vault get ${vault} --format=json`.nothrow().quiet();
|
|
71
|
+
return result.exitCode === 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a vault
|
|
76
|
+
*/
|
|
77
|
+
export async function createVault(vault: string): Promise<void> {
|
|
78
|
+
validateIdentifier(vault, 'vault');
|
|
79
|
+
await $`op vault create ${vault}`.quiet();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* List all vaults
|
|
84
|
+
*/
|
|
85
|
+
export async function listVaults(): Promise<string[]> {
|
|
86
|
+
const result = await $`op vault list --format=json`.quiet();
|
|
87
|
+
const vaults = JSON.parse(result.text()) as Array<{ name: string }>;
|
|
88
|
+
return vaults.map((v) => v.name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Item structure
|
|
93
|
+
*/
|
|
94
|
+
export interface OpItem {
|
|
95
|
+
id: string;
|
|
96
|
+
title: string;
|
|
97
|
+
vault: { id: string; name: string };
|
|
98
|
+
fields: Record<string, string>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if an item exists in a vault
|
|
103
|
+
*/
|
|
104
|
+
export async function itemExists(vault: string, item: string): Promise<boolean> {
|
|
105
|
+
validateIdentifier(vault, 'vault');
|
|
106
|
+
validateIdentifier(item, 'item');
|
|
107
|
+
const result = await $`op item get ${item} --vault=${vault} --format=json`.nothrow().quiet();
|
|
108
|
+
return result.exitCode === 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get a field value from a 1Password item
|
|
113
|
+
*/
|
|
114
|
+
export async function getField(vault: string, item: string, field: string): Promise<string | null> {
|
|
115
|
+
validateIdentifier(vault, 'vault');
|
|
116
|
+
validateIdentifier(item, 'item');
|
|
117
|
+
validateIdentifier(field, 'field');
|
|
118
|
+
const result = await $`op read op://${vault}/${item}/${field}`.nothrow().quiet();
|
|
119
|
+
if (result.exitCode !== 0) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return result.text().trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get all fields from a 1Password item
|
|
127
|
+
*/
|
|
128
|
+
export async function getItem(vault: string, item: string): Promise<OpItem | null> {
|
|
129
|
+
validateIdentifier(vault, 'vault');
|
|
130
|
+
validateIdentifier(item, 'item');
|
|
131
|
+
const result = await $`op item get ${item} --vault=${vault} --format=json`.nothrow().quiet();
|
|
132
|
+
|
|
133
|
+
if (result.exitCode !== 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const data = JSON.parse(result.text()) as {
|
|
138
|
+
id: string;
|
|
139
|
+
title: string;
|
|
140
|
+
vault: { id: string; name: string };
|
|
141
|
+
fields: Array<{ label: string; value?: string }>;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const fields: Record<string, string> = {};
|
|
145
|
+
for (const f of data.fields) {
|
|
146
|
+
if (f.value) {
|
|
147
|
+
fields[f.label] = f.value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
id: data.id,
|
|
153
|
+
title: data.title,
|
|
154
|
+
vault: data.vault,
|
|
155
|
+
fields,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get multiple items from a vault in a single batch
|
|
161
|
+
* More efficient than calling getItem multiple times
|
|
162
|
+
*/
|
|
163
|
+
export async function getItemsBatch(
|
|
164
|
+
vault: string,
|
|
165
|
+
itemNames: string[]
|
|
166
|
+
): Promise<Map<string, OpItem>> {
|
|
167
|
+
validateIdentifier(vault, 'vault');
|
|
168
|
+
for (const itemName of itemNames) {
|
|
169
|
+
validateIdentifier(itemName, 'item');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const results = new Map<string, OpItem>();
|
|
173
|
+
|
|
174
|
+
// Fetch items in parallel
|
|
175
|
+
await Promise.all(
|
|
176
|
+
itemNames.map(async (itemName) => {
|
|
177
|
+
const item = await getItem(vault, itemName);
|
|
178
|
+
if (item) {
|
|
179
|
+
results.set(itemName, item);
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Set a field on a 1Password item, creating the item if it doesn't exist
|
|
189
|
+
*/
|
|
190
|
+
export async function setField(
|
|
191
|
+
vault: string,
|
|
192
|
+
item: string,
|
|
193
|
+
field: string,
|
|
194
|
+
value: string
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
return setFieldsBatch(vault, item, { [field]: value });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Set multiple fields on a 1Password item in a single operation.
|
|
201
|
+
* Batches all field updates into a single CLI call for efficiency.
|
|
202
|
+
*/
|
|
203
|
+
export async function setFieldsBatch(
|
|
204
|
+
vault: string,
|
|
205
|
+
item: string,
|
|
206
|
+
fields: Record<string, string>
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
validateIdentifier(vault, 'vault');
|
|
209
|
+
validateIdentifier(item, 'item');
|
|
210
|
+
for (const fieldName of Object.keys(fields)) {
|
|
211
|
+
validateIdentifier(fieldName, 'field');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const exists = await itemExists(vault, item);
|
|
215
|
+
|
|
216
|
+
// Build field arguments for all fields
|
|
217
|
+
const fieldArgs = Object.entries(fields).map(
|
|
218
|
+
([fieldName, fieldValue]) => `${fieldName}[concealed]=${fieldValue}`
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (exists) {
|
|
222
|
+
// Update existing item - edit all fields in a single command
|
|
223
|
+
await $`op item edit ${item} --vault=${vault} ${fieldArgs}`.quiet();
|
|
224
|
+
} else {
|
|
225
|
+
// Create new item with all fields
|
|
226
|
+
await $`op item create --category=login --vault=${vault} --title=${item} ${fieldArgs}`.quiet();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Resolve a secret reference (op://vault/item/field)
|
|
232
|
+
*/
|
|
233
|
+
export async function resolveSecret(reference: string): Promise<string | null> {
|
|
234
|
+
const result = await $`op read ${reference}`.nothrow().quiet();
|
|
235
|
+
if (result.exitCode !== 0) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
return result.text().trim();
|
|
239
|
+
}
|
package/src/resolver.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { TypedEnvResolver, ResolverResult } from '@pokit/core';
|
|
3
|
+
import { type OpVault, type InferOpVaultKeys, parseOpRef } from './vault';
|
|
4
|
+
import * as op from './op';
|
|
5
|
+
|
|
6
|
+
type KeyMapping = {
|
|
7
|
+
key: string;
|
|
8
|
+
item: string;
|
|
9
|
+
field: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type OpResolverConfig<TVault extends OpVault<any>, TEnvs extends string> = {
|
|
13
|
+
vault: TVault;
|
|
14
|
+
vaults: Record<TEnvs, string>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Define a 1Password resolver that fetches and writes secrets to 1Password vaults.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const vault = defineOpVault({
|
|
23
|
+
* POSTGRES_URL: 'supabase:SUPABASE_SESSION_DSN',
|
|
24
|
+
* SUPABASE_URL: 'supabase:SUPABASE_URL',
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* const opResolver = defineOpResolver({
|
|
28
|
+
* vault,
|
|
29
|
+
* vaults: {
|
|
30
|
+
* dev: 'my-app-secrets-dev',
|
|
31
|
+
* staging: 'my-app-secrets-staging',
|
|
32
|
+
* prod: 'my-app-secrets-prod',
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function defineOpResolver<TVault extends OpVault<any>, const TEnvs extends string>(
|
|
38
|
+
config: OpResolverConfig<TVault, TEnvs>
|
|
39
|
+
): TypedEnvResolver<InferOpVaultKeys<TVault> & string> {
|
|
40
|
+
type VaultKey = InferOpVaultKeys<TVault> & string;
|
|
41
|
+
const envValues = Object.keys(config.vaults) as TEnvs[];
|
|
42
|
+
const availableVars = Object.keys(config.vault.secrets) as VaultKey[];
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
requiredContext: z.object({
|
|
46
|
+
env: z.enum(envValues as [TEnvs, ...TEnvs[]]),
|
|
47
|
+
}),
|
|
48
|
+
availableVars,
|
|
49
|
+
|
|
50
|
+
resolve: async (keys, context) => {
|
|
51
|
+
const ctx = z.object({ env: z.enum(envValues as [TEnvs, ...TEnvs[]]) }).parse(context);
|
|
52
|
+
const vaultName = config.vaults[ctx.env as TEnvs];
|
|
53
|
+
if (!vaultName) {
|
|
54
|
+
throw new Error(`No vault configured for environment: ${ctx.env}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build mapping of keys to their 1Password item/field locations
|
|
58
|
+
const keyMappings: KeyMapping[] = [];
|
|
59
|
+
const unknownKeys: string[] = [];
|
|
60
|
+
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
const ref = config.vault.secrets[key];
|
|
63
|
+
if (!ref) {
|
|
64
|
+
unknownKeys.push(key);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const { item, field } = parseOpRef(ref);
|
|
68
|
+
keyMappings.push({ key, item, field });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (unknownKeys.length > 0) {
|
|
72
|
+
throw new Error(`No secret config for keys: ${unknownKeys.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Group by item name to minimize 1Password calls
|
|
76
|
+
const itemNames = [...new Set(keyMappings.map((m) => m.item))];
|
|
77
|
+
|
|
78
|
+
// Fetch all items in parallel
|
|
79
|
+
const items = await op.getItemsBatch(vaultName, itemNames);
|
|
80
|
+
|
|
81
|
+
// Extract requested fields from fetched items
|
|
82
|
+
const result: Record<string, string> = {};
|
|
83
|
+
const missingSecrets: string[] = [];
|
|
84
|
+
|
|
85
|
+
for (const { key, item, field } of keyMappings) {
|
|
86
|
+
const opItem = items.get(item);
|
|
87
|
+
if (!opItem) {
|
|
88
|
+
missingSecrets.push(`op://${vaultName}/${item}/${field} (item not found)`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const value = opItem.fields[field];
|
|
93
|
+
if (value === undefined) {
|
|
94
|
+
missingSecrets.push(`op://${vaultName}/${item}/${field} (field not found)`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
result[key] = value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (missingSecrets.length > 0) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Failed to fetch secrets from 1Password:\n - ${missingSecrets.join('\n - ')}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result as ResolverResult<VaultKey>;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
write: async (values, context) => {
|
|
111
|
+
const ctx = z.object({ env: z.enum(envValues as [TEnvs, ...TEnvs[]]) }).parse(context);
|
|
112
|
+
const vaultName = config.vaults[ctx.env as TEnvs];
|
|
113
|
+
if (!vaultName) {
|
|
114
|
+
throw new Error(`No vault configured for environment: ${ctx.env}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Group values by 1Password item for batch writes
|
|
118
|
+
const byItem = new Map<string, Record<string, string>>();
|
|
119
|
+
|
|
120
|
+
for (const [key, value] of Object.entries(values) as [string, string | undefined][]) {
|
|
121
|
+
if (value === undefined) continue;
|
|
122
|
+
|
|
123
|
+
const ref = config.vault.secrets[key];
|
|
124
|
+
if (!ref) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Unknown variable "${key}". ` + `Ensure it is declared in the vault configuration.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { item, field } = parseOpRef(ref);
|
|
131
|
+
if (!byItem.has(item)) {
|
|
132
|
+
byItem.set(item, {});
|
|
133
|
+
}
|
|
134
|
+
byItem.get(item)![field] = value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Batch write each item (fail fast on error)
|
|
138
|
+
for (const [item, fields] of byItem) {
|
|
139
|
+
await op.setFieldsBatch(vaultName, item, fields);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
package/src/vault.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault definition for 1Password secrets.
|
|
3
|
+
*
|
|
4
|
+
* Each entry maps a variable name to an "item:field" reference.
|
|
5
|
+
* Format: `"itemName:fieldName"` where both are required.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const vault = defineOpVault({
|
|
10
|
+
* POSTGRES_URL: 'supabase:SUPABASE_SESSION_DSN',
|
|
11
|
+
* SUPABASE_URL: 'supabase:SUPABASE_URL',
|
|
12
|
+
* STRIPE_SECRET_KEY: 'stripe:STRIPE_SECRET_KEY',
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type OpVaultRef = `${string}:${string}`;
|
|
18
|
+
|
|
19
|
+
export type OpVault<TSecrets extends Record<string, OpVaultRef>> = {
|
|
20
|
+
secrets: TSecrets;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type InferOpVaultKeys<T> = T extends OpVault<infer S> ? keyof S : never;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse an "item:field" reference into its components.
|
|
27
|
+
*/
|
|
28
|
+
export function parseOpRef(ref: OpVaultRef): { item: string; field: string } {
|
|
29
|
+
const colonIndex = ref.indexOf(':');
|
|
30
|
+
if (colonIndex === -1) {
|
|
31
|
+
throw new Error(`Invalid vault reference: ${ref}. Expected "item:field".`);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
item: ref.slice(0, colonIndex),
|
|
35
|
+
field: ref.slice(colonIndex + 1),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Define a vault with typed secret declarations.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const vault = defineOpVault({
|
|
45
|
+
* POSTGRES_URL: 'supabase:SUPABASE_SESSION_DSN',
|
|
46
|
+
* SUPABASE_URL: 'supabase:SUPABASE_URL',
|
|
47
|
+
* VITE_SUPABASE_URL: 'supabase:SUPABASE_URL',
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function defineOpVault<const TSecrets extends Record<string, OpVaultRef>>(
|
|
52
|
+
secrets: TSecrets
|
|
53
|
+
): OpVault<TSecrets> {
|
|
54
|
+
return { secrets };
|
|
55
|
+
}
|