@lifeaitools/clauth 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/.clauth-skill/SKILL.md +141 -0
- package/.clauth-skill/references/keys-guide.md +270 -0
- package/.clauth-skill/references/operator-guide.md +148 -0
- package/README.md +101 -0
- package/cli/api.js +108 -0
- package/cli/commands/install.js +258 -0
- package/cli/fingerprint.js +91 -0
- package/cli/index.js +403 -0
- package/install.ps1 +44 -0
- package/install.sh +38 -0
- package/package.json +54 -0
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bin/bootstrap-win.exe +0 -0
- package/scripts/bootstrap.cjs +43 -0
- package/scripts/build.sh +45 -0
- package/supabase/functions/auth-vault/index.ts +326 -0
- package/supabase/migrations/001_clauth_schema.sql +94 -0
- package/supabase/migrations/002_vault_helpers.sql +90 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// clauth — auth-vault Edge Function
|
|
3
|
+
// Supabase Deno runtime
|
|
4
|
+
// Routes:
|
|
5
|
+
// POST /auth-vault/retrieve — validate HMAC, return key
|
|
6
|
+
// POST /auth-vault/write — write/update key in vault
|
|
7
|
+
// POST /auth-vault/enable — enable/disable service
|
|
8
|
+
// POST /auth-vault/add — add new service to registry
|
|
9
|
+
// POST /auth-vault/remove — remove service from registry
|
|
10
|
+
// POST /auth-vault/status — list all services + state
|
|
11
|
+
// POST /auth-vault/test — dry-run HMAC check only
|
|
12
|
+
// POST /auth-vault/rotate — flag a service for rotation
|
|
13
|
+
// POST /auth-vault/revoke — delete key from vault
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
17
|
+
|
|
18
|
+
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
|
|
19
|
+
const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
20
|
+
const CLAUTH_HMAC_SALT = Deno.env.get("CLAUTH_HMAC_SALT")!; // stored in Supabase secrets
|
|
21
|
+
|
|
22
|
+
const REPLAY_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
|
|
23
|
+
|
|
24
|
+
// ============================================================
|
|
25
|
+
// Helpers
|
|
26
|
+
// ============================================================
|
|
27
|
+
|
|
28
|
+
async function sha256hex(input: string): Promise<string> {
|
|
29
|
+
const buf = await crypto.subtle.digest(
|
|
30
|
+
"SHA-256",
|
|
31
|
+
new TextEncoder().encode(input)
|
|
32
|
+
);
|
|
33
|
+
return Array.from(new Uint8Array(buf))
|
|
34
|
+
.map(b => b.toString(16).padStart(2, "0"))
|
|
35
|
+
.join("");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function hmacSha256(key: string, message: string): Promise<string> {
|
|
39
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
40
|
+
"raw",
|
|
41
|
+
new TextEncoder().encode(key),
|
|
42
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
43
|
+
false,
|
|
44
|
+
["sign"]
|
|
45
|
+
);
|
|
46
|
+
const sig = await crypto.subtle.sign(
|
|
47
|
+
"HMAC",
|
|
48
|
+
cryptoKey,
|
|
49
|
+
new TextEncoder().encode(message)
|
|
50
|
+
);
|
|
51
|
+
return Array.from(new Uint8Array(sig))
|
|
52
|
+
.map(b => b.toString(16).padStart(2, "0"))
|
|
53
|
+
.join("");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function validateHMAC(body: {
|
|
57
|
+
machine_hash: string;
|
|
58
|
+
token: string;
|
|
59
|
+
timestamp: number;
|
|
60
|
+
password: string;
|
|
61
|
+
}): Promise<{ valid: boolean; reason?: string }> {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
|
|
64
|
+
// Replay check
|
|
65
|
+
if (Math.abs(now - body.timestamp) > REPLAY_WINDOW_MS) {
|
|
66
|
+
return { valid: false, reason: "timestamp_expired" };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Lookup machine
|
|
70
|
+
const sb = createClient(SUPABASE_URL, SERVICE_ROLE_KEY);
|
|
71
|
+
const { data: machine, error } = await sb
|
|
72
|
+
.from("clauth_machines")
|
|
73
|
+
.select("hmac_seed_hash, enabled")
|
|
74
|
+
.eq("machine_hash", body.machine_hash)
|
|
75
|
+
.single();
|
|
76
|
+
|
|
77
|
+
if (error || !machine) {
|
|
78
|
+
return { valid: false, reason: "machine_not_found" };
|
|
79
|
+
}
|
|
80
|
+
if (!machine.enabled) {
|
|
81
|
+
return { valid: false, reason: "machine_disabled" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Expected token = HMAC(machine_hash + timestamp_window, password + SALT)
|
|
85
|
+
const window = Math.floor(body.timestamp / REPLAY_WINDOW_MS);
|
|
86
|
+
const message = `${body.machine_hash}:${window}`;
|
|
87
|
+
const hmacKey = `${body.password}:${CLAUTH_HMAC_SALT}`;
|
|
88
|
+
const expected = await hmacSha256(hmacKey, message);
|
|
89
|
+
|
|
90
|
+
if (expected !== body.token) {
|
|
91
|
+
return { valid: false, reason: "invalid_token" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Update last_seen
|
|
95
|
+
await sb
|
|
96
|
+
.from("clauth_machines")
|
|
97
|
+
.update({ last_seen: new Date().toISOString() })
|
|
98
|
+
.eq("machine_hash", body.machine_hash);
|
|
99
|
+
|
|
100
|
+
return { valid: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function auditLog(
|
|
104
|
+
sb: ReturnType<typeof createClient>,
|
|
105
|
+
machine_hash: string,
|
|
106
|
+
service_name: string,
|
|
107
|
+
action: string,
|
|
108
|
+
result: string,
|
|
109
|
+
detail?: string
|
|
110
|
+
) {
|
|
111
|
+
await sb.from("clauth_audit").insert({
|
|
112
|
+
machine_hash, service_name, action, result, detail
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================================
|
|
117
|
+
// Route handlers
|
|
118
|
+
// ============================================================
|
|
119
|
+
|
|
120
|
+
async function handleRetrieve(sb: ReturnType<typeof createClient>, body: any, machine_hash: string) {
|
|
121
|
+
const { service } = body;
|
|
122
|
+
if (!service) return { error: "service required" };
|
|
123
|
+
|
|
124
|
+
const { data: svc } = await sb
|
|
125
|
+
.from("clauth_services")
|
|
126
|
+
.select("*")
|
|
127
|
+
.eq("name", service)
|
|
128
|
+
.single();
|
|
129
|
+
|
|
130
|
+
if (!svc) {
|
|
131
|
+
await auditLog(sb, machine_hash, service, "retrieve", "fail", "service_not_found");
|
|
132
|
+
return { error: "service_not_found" };
|
|
133
|
+
}
|
|
134
|
+
if (!svc.enabled) {
|
|
135
|
+
await auditLog(sb, machine_hash, service, "retrieve", "denied", "service_disabled");
|
|
136
|
+
return { error: "service_disabled" };
|
|
137
|
+
}
|
|
138
|
+
if (!svc.vault_key) {
|
|
139
|
+
await auditLog(sb, machine_hash, service, "retrieve", "fail", "no_key_stored");
|
|
140
|
+
return { error: "no_key_stored" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Retrieve from vault
|
|
144
|
+
const { data: secret } = await sb.rpc("vault_decrypt_secret", {
|
|
145
|
+
secret_name: svc.vault_key
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await sb
|
|
149
|
+
.from("clauth_services")
|
|
150
|
+
.update({ last_retrieved: new Date().toISOString() })
|
|
151
|
+
.eq("name", service);
|
|
152
|
+
|
|
153
|
+
await auditLog(sb, machine_hash, service, "retrieve", "success");
|
|
154
|
+
return { service, key_type: svc.key_type, value: secret };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function handleWrite(sb: ReturnType<typeof createClient>, body: any, machine_hash: string) {
|
|
158
|
+
const { service, value } = body;
|
|
159
|
+
if (!service || !value) return { error: "service and value required" };
|
|
160
|
+
|
|
161
|
+
const vaultKey = `clauth.${service}`;
|
|
162
|
+
|
|
163
|
+
// Upsert into vault
|
|
164
|
+
const { error } = await sb.rpc("vault_upsert_secret", {
|
|
165
|
+
secret_name: vaultKey,
|
|
166
|
+
secret_value: typeof value === "string" ? value : JSON.stringify(value)
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (error) {
|
|
170
|
+
await auditLog(sb, machine_hash, service, "write", "fail", error.message);
|
|
171
|
+
return { error: error.message };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Update registry
|
|
175
|
+
await sb
|
|
176
|
+
.from("clauth_services")
|
|
177
|
+
.update({ vault_key: vaultKey, last_rotated: new Date().toISOString() })
|
|
178
|
+
.eq("name", service);
|
|
179
|
+
|
|
180
|
+
await auditLog(sb, machine_hash, service, "write", "success");
|
|
181
|
+
return { success: true, service, vault_key: vaultKey };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function handleEnable(sb: ReturnType<typeof createClient>, body: any, machine_hash: string) {
|
|
185
|
+
const { service, enabled } = body; // enabled: true | false
|
|
186
|
+
const target = service === "all" ? null : service;
|
|
187
|
+
|
|
188
|
+
let query = sb.from("clauth_services").update({ enabled });
|
|
189
|
+
if (target) query = query.eq("name", target);
|
|
190
|
+
else query = query.not("vault_key", "is", null); // only enable services with keys
|
|
191
|
+
|
|
192
|
+
const { error } = await query;
|
|
193
|
+
if (error) return { error: error.message };
|
|
194
|
+
|
|
195
|
+
await auditLog(sb, machine_hash, service, enabled ? "enable" : "disable", "success");
|
|
196
|
+
return { success: true, service, enabled };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function handleAdd(sb: ReturnType<typeof createClient>, body: any, machine_hash: string) {
|
|
200
|
+
const { name, label, key_type, description } = body;
|
|
201
|
+
if (!name || !label || !key_type) return { error: "name, label, key_type required" };
|
|
202
|
+
|
|
203
|
+
const { error } = await sb.from("clauth_services").insert({
|
|
204
|
+
name, label, key_type, description: description || null
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (error) return { error: error.message };
|
|
208
|
+
await auditLog(sb, machine_hash, name, "add", "success");
|
|
209
|
+
return { success: true, name, label, key_type };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function handleRemove(sb: ReturnType<typeof createClient>, body: any, machine_hash: string) {
|
|
213
|
+
const { service, confirm } = body;
|
|
214
|
+
if (confirm !== `CONFIRM REMOVE ${service.toUpperCase()}`) {
|
|
215
|
+
return { error: "confirm phrase mismatch", required: `CONFIRM REMOVE ${service.toUpperCase()}` };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await sb.rpc("vault_delete_secret", { secret_name: `clauth.${service}` });
|
|
219
|
+
await sb.from("clauth_services").delete().eq("name", service);
|
|
220
|
+
await auditLog(sb, machine_hash, service, "remove", "success");
|
|
221
|
+
return { success: true, service };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function handleRevoke(sb: ReturnType<typeof createClient>, body: any, machine_hash: string) {
|
|
225
|
+
const { service, confirm } = body;
|
|
226
|
+
const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
|
|
227
|
+
if (confirm !== phrase) {
|
|
228
|
+
return { error: "confirm phrase mismatch", required: phrase };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (service === "all") {
|
|
232
|
+
const { data: svcs } = await sb.from("clauth_services").select("name").not("vault_key", "is", null);
|
|
233
|
+
for (const s of svcs || []) {
|
|
234
|
+
await sb.rpc("vault_delete_secret", { secret_name: `clauth.${s.name}` });
|
|
235
|
+
}
|
|
236
|
+
await sb.from("clauth_services").update({ vault_key: null, enabled: false }).neq("id", "00000000-0000-0000-0000-000000000000");
|
|
237
|
+
} else {
|
|
238
|
+
await sb.rpc("vault_delete_secret", { secret_name: `clauth.${service}` });
|
|
239
|
+
await sb.from("clauth_services").update({ vault_key: null, enabled: false }).eq("name", service);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await auditLog(sb, machine_hash, service, "revoke", "success");
|
|
243
|
+
return { success: true, service };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function handleStatus(sb: ReturnType<typeof createClient>, machine_hash: string) {
|
|
247
|
+
const { data: services } = await sb
|
|
248
|
+
.from("clauth_services")
|
|
249
|
+
.select("name, label, key_type, enabled, vault_key, last_retrieved, last_rotated, created_at")
|
|
250
|
+
.order("name");
|
|
251
|
+
|
|
252
|
+
await auditLog(sb, machine_hash, "all", "status", "success");
|
|
253
|
+
return { services: services || [] };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function handleRegisterMachine(sb: ReturnType<typeof createClient>, body: any) {
|
|
257
|
+
const { machine_hash, hmac_seed_hash, label, admin_token } = body;
|
|
258
|
+
// Bootstrap: requires a one-time admin token stored as env var
|
|
259
|
+
if (admin_token !== Deno.env.get("CLAUTH_ADMIN_BOOTSTRAP_TOKEN")) {
|
|
260
|
+
return { error: "invalid_admin_token" };
|
|
261
|
+
}
|
|
262
|
+
const { error } = await sb.from("clauth_machines").upsert({
|
|
263
|
+
machine_hash, hmac_seed_hash, label, enabled: true
|
|
264
|
+
}, { onConflict: "machine_hash" });
|
|
265
|
+
|
|
266
|
+
if (error) return { error: error.message };
|
|
267
|
+
return { success: true, machine_hash };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================
|
|
271
|
+
// Main handler
|
|
272
|
+
// ============================================================
|
|
273
|
+
|
|
274
|
+
Deno.serve(async (req: Request) => {
|
|
275
|
+
const url = new URL(req.url);
|
|
276
|
+
const route = url.pathname.replace(/^\/auth-vault\/?/, "").replace(/^\//, "");
|
|
277
|
+
|
|
278
|
+
if (req.method === "OPTIONS") {
|
|
279
|
+
return new Response(null, {
|
|
280
|
+
headers: {
|
|
281
|
+
"Access-Control-Allow-Origin": "*",
|
|
282
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
283
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (req.method !== "POST") {
|
|
289
|
+
return Response.json({ error: "POST only" }, { status: 405 });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const body = await req.json().catch(() => ({}));
|
|
293
|
+
const sb = createClient(SUPABASE_URL, SERVICE_ROLE_KEY);
|
|
294
|
+
|
|
295
|
+
// Bootstrap machine registration — no HMAC required, uses admin token
|
|
296
|
+
if (route === "register-machine") {
|
|
297
|
+
return Response.json(await handleRegisterMachine(sb, body));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// All other routes require HMAC validation
|
|
301
|
+
const authResult = await validateHMAC({
|
|
302
|
+
machine_hash: body.machine_hash,
|
|
303
|
+
token: body.token,
|
|
304
|
+
timestamp: body.timestamp,
|
|
305
|
+
password: body.password
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!authResult.valid) {
|
|
309
|
+
await auditLog(sb, body.machine_hash || "unknown", body.service || "unknown", route, "denied", authResult.reason);
|
|
310
|
+
return Response.json({ error: "auth_failed", reason: authResult.reason }, { status: 401 });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const machine_hash = body.machine_hash;
|
|
314
|
+
|
|
315
|
+
switch (route) {
|
|
316
|
+
case "retrieve": return Response.json(await handleRetrieve(sb, body, machine_hash));
|
|
317
|
+
case "write": return Response.json(await handleWrite(sb, body, machine_hash));
|
|
318
|
+
case "enable": return Response.json(await handleEnable(sb, body, machine_hash));
|
|
319
|
+
case "add": return Response.json(await handleAdd(sb, body, machine_hash));
|
|
320
|
+
case "remove": return Response.json(await handleRemove(sb, body, machine_hash));
|
|
321
|
+
case "revoke": return Response.json(await handleRevoke(sb, body, machine_hash));
|
|
322
|
+
case "status": return Response.json(await handleStatus(sb, machine_hash));
|
|
323
|
+
case "test": return Response.json({ valid: true, machine_hash, timestamp: body.timestamp });
|
|
324
|
+
default: return Response.json({ error: "unknown_route", route }, { status: 404 });
|
|
325
|
+
}
|
|
326
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- clauth schema
|
|
3
|
+
-- Migration: 001_clauth_schema.sql
|
|
4
|
+
-- ============================================================
|
|
5
|
+
|
|
6
|
+
-- Enable vault extension (Supabase enables by default, but guard)
|
|
7
|
+
-- vault.secrets is already available in Supabase projects
|
|
8
|
+
|
|
9
|
+
-- ============================================================
|
|
10
|
+
-- Service Registry
|
|
11
|
+
-- ============================================================
|
|
12
|
+
create table if not exists public.clauth_services (
|
|
13
|
+
id uuid primary key default gen_random_uuid(),
|
|
14
|
+
name text not null unique, -- e.g. 'github', 'r2'
|
|
15
|
+
label text not null, -- human display name
|
|
16
|
+
key_type text not null -- 'token' | 'keypair' | 'connstring' | 'oauth'
|
|
17
|
+
check (key_type in ('token','keypair','connstring','oauth')),
|
|
18
|
+
enabled boolean not null default false,
|
|
19
|
+
vault_key text, -- vault secret name: 'clauth.<name>'
|
|
20
|
+
description text,
|
|
21
|
+
last_retrieved timestamptz,
|
|
22
|
+
last_rotated timestamptz,
|
|
23
|
+
created_at timestamptz not null default now(),
|
|
24
|
+
updated_at timestamptz not null default now()
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- ============================================================
|
|
28
|
+
-- Machine Registry (hardware fingerprints — hashed only)
|
|
29
|
+
-- ============================================================
|
|
30
|
+
create table if not exists public.clauth_machines (
|
|
31
|
+
id uuid primary key default gen_random_uuid(),
|
|
32
|
+
machine_hash text not null unique, -- SHA256(machine_id + os_install_id)
|
|
33
|
+
label text, -- e.g. 'Dave-Desktop-Win11'
|
|
34
|
+
hmac_seed_hash text not null, -- SHA256 of the HMAC seed stored in vault
|
|
35
|
+
enabled boolean not null default true,
|
|
36
|
+
created_at timestamptz not null default now(),
|
|
37
|
+
last_seen timestamptz
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
-- ============================================================
|
|
41
|
+
-- Audit Log
|
|
42
|
+
-- ============================================================
|
|
43
|
+
create table if not exists public.clauth_audit (
|
|
44
|
+
id uuid primary key default gen_random_uuid(),
|
|
45
|
+
machine_hash text,
|
|
46
|
+
service_name text,
|
|
47
|
+
action text not null, -- 'retrieve' | 'enable' | 'disable' | 'rotate' | 'revoke' | 'add' | 'remove'
|
|
48
|
+
result text not null, -- 'success' | 'fail' | 'denied'
|
|
49
|
+
detail text,
|
|
50
|
+
created_at timestamptz not null default now()
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
-- ============================================================
|
|
54
|
+
-- Seed: built-in service definitions (no keys — just registry)
|
|
55
|
+
-- ============================================================
|
|
56
|
+
insert into public.clauth_services (name, label, key_type, description) values
|
|
57
|
+
('github', 'GitHub PAT', 'token', 'GitHub Personal Access Token — repo, workflow, org'),
|
|
58
|
+
('supabase-anon', 'Supabase Anon Key', 'token', 'Supabase public anon key (RLS-gated)'),
|
|
59
|
+
('supabase-service', 'Supabase Service Role Key', 'token', 'Supabase service_role — bypasses RLS'),
|
|
60
|
+
('supabase-db', 'Supabase DB Connection', 'connstring','PostgreSQL connection string (pooled + direct)'),
|
|
61
|
+
('vercel', 'Vercel API Token', 'keypair', 'Vercel API token + Team ID'),
|
|
62
|
+
('namecheap', 'Namecheap API', 'keypair', 'Namecheap API key + username'),
|
|
63
|
+
('neo4j', 'Neo4j Aura', 'connstring','Neo4j Aura URI + credentials'),
|
|
64
|
+
('anthropic', 'Anthropic API Key', 'token', 'Claude API — sk-ant-...'),
|
|
65
|
+
('r2', 'Cloudflare R2 Keypair', 'keypair', 'R2 Access Key ID + Secret Access Key'),
|
|
66
|
+
('r2-bucket', 'Cloudflare R2 Bucket Config', 'connstring','R2 bucket name + endpoint URL'),
|
|
67
|
+
('cloudflare', 'Cloudflare API Token', 'token', 'Cloudflare zone/DNS/admin token'),
|
|
68
|
+
('rocketreach', 'RocketReach API Key', 'token', 'RocketReach contact intelligence API')
|
|
69
|
+
on conflict (name) do nothing;
|
|
70
|
+
|
|
71
|
+
-- ============================================================
|
|
72
|
+
-- RLS — lock down all tables
|
|
73
|
+
-- ============================================================
|
|
74
|
+
alter table public.clauth_services enable row level security;
|
|
75
|
+
alter table public.clauth_machines enable row level security;
|
|
76
|
+
alter table public.clauth_audit enable row level security;
|
|
77
|
+
|
|
78
|
+
-- service_role bypasses RLS — all access from Edge Function only
|
|
79
|
+
-- No direct anon access to any clauth table
|
|
80
|
+
create policy "no_anon_services" on public.clauth_services for all using (false);
|
|
81
|
+
create policy "no_anon_machines" on public.clauth_machines for all using (false);
|
|
82
|
+
create policy "no_anon_audit" on public.clauth_audit for all using (false);
|
|
83
|
+
|
|
84
|
+
-- ============================================================
|
|
85
|
+
-- Updated_at trigger
|
|
86
|
+
-- ============================================================
|
|
87
|
+
create or replace function public.clauth_touch_updated()
|
|
88
|
+
returns trigger language plpgsql as $$
|
|
89
|
+
begin new.updated_at = now(); return new; end;
|
|
90
|
+
$$;
|
|
91
|
+
|
|
92
|
+
create trigger clauth_services_updated
|
|
93
|
+
before update on public.clauth_services
|
|
94
|
+
for each row execute procedure public.clauth_touch_updated();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- Vault helper functions
|
|
3
|
+
-- Migration: 002_vault_helpers.sql
|
|
4
|
+
-- These wrap vault.create_secret / vault.update_secret
|
|
5
|
+
-- so Edge Functions don't need direct vault schema access
|
|
6
|
+
-- ============================================================
|
|
7
|
+
|
|
8
|
+
-- Upsert a secret (create or update)
|
|
9
|
+
create or replace function public.vault_upsert_secret(
|
|
10
|
+
secret_name text,
|
|
11
|
+
secret_value text
|
|
12
|
+
)
|
|
13
|
+
returns void
|
|
14
|
+
language plpgsql
|
|
15
|
+
security definer
|
|
16
|
+
as $$
|
|
17
|
+
declare
|
|
18
|
+
existing_id uuid;
|
|
19
|
+
begin
|
|
20
|
+
select id into existing_id
|
|
21
|
+
from vault.secrets
|
|
22
|
+
where name = secret_name
|
|
23
|
+
limit 1;
|
|
24
|
+
|
|
25
|
+
if existing_id is not null then
|
|
26
|
+
perform vault.update_secret(existing_id, secret_value);
|
|
27
|
+
else
|
|
28
|
+
perform vault.create_secret(secret_value, secret_name);
|
|
29
|
+
end if;
|
|
30
|
+
end;
|
|
31
|
+
$$;
|
|
32
|
+
|
|
33
|
+
-- Decrypt and return a secret value by name
|
|
34
|
+
create or replace function public.vault_decrypt_secret(
|
|
35
|
+
secret_name text
|
|
36
|
+
)
|
|
37
|
+
returns text
|
|
38
|
+
language plpgsql
|
|
39
|
+
security definer
|
|
40
|
+
as $$
|
|
41
|
+
declare
|
|
42
|
+
result text;
|
|
43
|
+
begin
|
|
44
|
+
select decrypted_secret into result
|
|
45
|
+
from vault.decrypted_secrets
|
|
46
|
+
where name = secret_name
|
|
47
|
+
limit 1;
|
|
48
|
+
return result;
|
|
49
|
+
end;
|
|
50
|
+
$$;
|
|
51
|
+
|
|
52
|
+
-- Delete a secret by name
|
|
53
|
+
create or replace function public.vault_delete_secret(
|
|
54
|
+
secret_name text
|
|
55
|
+
)
|
|
56
|
+
returns void
|
|
57
|
+
language plpgsql
|
|
58
|
+
security definer
|
|
59
|
+
as $$
|
|
60
|
+
declare
|
|
61
|
+
target_id uuid;
|
|
62
|
+
begin
|
|
63
|
+
select id into target_id
|
|
64
|
+
from vault.secrets
|
|
65
|
+
where name = secret_name
|
|
66
|
+
limit 1;
|
|
67
|
+
|
|
68
|
+
if target_id is not null then
|
|
69
|
+
delete from vault.secrets where id = target_id;
|
|
70
|
+
end if;
|
|
71
|
+
end;
|
|
72
|
+
$$;
|
|
73
|
+
|
|
74
|
+
-- List all clauth secrets (names only — never values)
|
|
75
|
+
create or replace function public.vault_list_clauth_secrets()
|
|
76
|
+
returns table(name text, created_at timestamptz, updated_at timestamptz)
|
|
77
|
+
language sql
|
|
78
|
+
security definer
|
|
79
|
+
as $$
|
|
80
|
+
select name, created_at, updated_at
|
|
81
|
+
from vault.secrets
|
|
82
|
+
where name like 'clauth.%'
|
|
83
|
+
order by name;
|
|
84
|
+
$$;
|
|
85
|
+
|
|
86
|
+
-- Revoke execute from public, grant to service_role only
|
|
87
|
+
revoke execute on function public.vault_upsert_secret from public;
|
|
88
|
+
revoke execute on function public.vault_decrypt_secret from public;
|
|
89
|
+
revoke execute on function public.vault_delete_secret from public;
|
|
90
|
+
revoke execute on function public.vault_list_clauth_secrets from public;
|