@lifeaitools/clauth 0.4.1 → 0.5.1

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,235 +1,235 @@
1
- // clauth — auth-vault Edge Function v2
2
- // Added: IP whitelist, rate limiting, machine lockout (fail_count + locked)
3
-
4
- import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
5
-
6
- const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
7
- const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
8
- const CLAUTH_HMAC_SALT = Deno.env.get("CLAUTH_HMAC_SALT")!;
9
- const ADMIN_BOOTSTRAP_TOKEN = Deno.env.get("CLAUTH_ADMIN_BOOTSTRAP_TOKEN")!;
10
-
11
- const ALLOWED_IPS: string[] = (Deno.env.get("CLAUTH_ALLOWED_IPS") || "")
12
- .split(",").map(s => s.trim()).filter(Boolean);
13
-
14
- const RATE_LIMIT_MAX = 30;
15
- const RATE_LIMIT_WINDOW = 60;
16
- const REPLAY_WINDOW_MS = 5 * 60 * 1000;
17
- const MAX_FAIL_COUNT = 5;
18
-
19
- async function hmacSha256(key: string, message: string): Promise<string> {
20
- const cryptoKey = await crypto.subtle.importKey(
21
- "raw", new TextEncoder().encode(key),
22
- { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
23
- );
24
- const sig = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
25
- return Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, "0")).join("");
26
- }
27
-
28
- function getClientIP(req: Request): string {
29
- return req.headers.get("cf-connecting-ip") ||
30
- req.headers.get("x-real-ip") ||
31
- req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
32
- }
33
-
34
- function checkIP(ip: string): { allowed: boolean; reason?: string } {
35
- if (ALLOWED_IPS.length === 0) return { allowed: true };
36
- if (ALLOWED_IPS.includes(ip)) return { allowed: true };
37
- return { allowed: false, reason: `IP not whitelisted: ${ip}` };
38
- }
39
-
40
- async function checkRateLimit(sb: any, machine_hash: string): Promise<{ allowed: boolean; reason?: string }> {
41
- const windowStart = new Date(Date.now() - RATE_LIMIT_WINDOW * 1000).toISOString();
42
- const { count } = await sb.from("clauth_audit")
43
- .select("id", { count: "exact", head: true })
44
- .eq("machine_hash", machine_hash)
45
- .gte("created_at", windowStart);
46
- if ((count || 0) >= RATE_LIMIT_MAX) {
47
- return { allowed: false, reason: `Rate limit: ${count}/${RATE_LIMIT_MAX} per ${RATE_LIMIT_WINDOW}s` };
48
- }
49
- return { allowed: true };
50
- }
51
-
52
- async function validateHMAC(sb: any, body: any): Promise<{ valid: boolean; reason?: string }> {
53
- const now = Date.now();
54
- if (Math.abs(now - body.timestamp) > REPLAY_WINDOW_MS) return { valid: false, reason: "timestamp_expired" };
55
-
56
- const { data: machine, error } = await sb.from("clauth_machines")
57
- .select("hmac_seed_hash, enabled, fail_count, locked")
58
- .eq("machine_hash", body.machine_hash).single();
59
-
60
- if (error || !machine) return { valid: false, reason: "machine_not_found" };
61
- if (!machine.enabled) return { valid: false, reason: "machine_disabled" };
62
- if (machine.locked) return { valid: false, reason: "machine_locked" };
63
-
64
- const window = Math.floor(body.timestamp / REPLAY_WINDOW_MS);
65
- const message = `${body.machine_hash}:${window}`;
66
- const expected = await hmacSha256(body.password, message);
67
-
68
- if (expected !== body.token) {
69
- const newCount = (machine.fail_count || 0) + 1;
70
- const shouldLock = newCount >= MAX_FAIL_COUNT;
71
- await sb.from("clauth_machines")
72
- .update({ fail_count: newCount, locked: shouldLock })
73
- .eq("machine_hash", body.machine_hash);
74
- return { valid: false, reason: shouldLock ? `machine_locked after ${newCount} failures` : `invalid_token (${newCount}/${MAX_FAIL_COUNT})` };
75
- }
76
-
77
- await sb.from("clauth_machines")
78
- .update({ last_seen: new Date().toISOString(), fail_count: 0 })
79
- .eq("machine_hash", body.machine_hash);
80
- return { valid: true };
81
- }
82
-
83
- async function auditLog(sb: any, machine_hash: string, service_name: string, action: string, result: string, detail?: string) {
84
- await sb.from("clauth_audit").insert({ machine_hash, service_name, action, result, detail });
85
- }
86
-
87
- async function handleRetrieve(sb: any, body: any, mh: string) {
88
- const { service } = body;
89
- if (!service) return { error: "service required" };
90
- const { data: svc } = await sb.from("clauth_services").select("*").eq("name", service).single();
91
- if (!svc) { await auditLog(sb, mh, service, "retrieve", "fail", "service_not_found"); return { error: "service_not_found" }; }
92
- if (!svc.enabled) { await auditLog(sb, mh, service, "retrieve", "denied", "service_disabled"); return { error: "service_disabled" }; }
93
- if (!svc.vault_key) { await auditLog(sb, mh, service, "retrieve", "fail", "no_key_stored"); return { error: "no_key_stored" }; }
94
- const { data: secret } = await sb.rpc("vault_decrypt_secret", { secret_name: svc.vault_key });
95
- await sb.from("clauth_services").update({ last_retrieved: new Date().toISOString() }).eq("name", service);
96
- await auditLog(sb, mh, service, "retrieve", "success");
97
- return { service, key_type: svc.key_type, value: secret };
98
- }
99
-
100
- async function handleWrite(sb: any, body: any, mh: string) {
101
- const { service, value } = body;
102
- if (!service || !value) return { error: "service and value required" };
103
- const vaultKey = `clauth.${service}`;
104
- const { error } = await sb.rpc("vault_upsert_secret", { secret_name: vaultKey, secret_value: typeof value === "string" ? value : JSON.stringify(value) });
105
- if (error) { await auditLog(sb, mh, service, "write", "fail", error.message); return { error: error.message }; }
106
- await sb.from("clauth_services").update({ vault_key: vaultKey, last_rotated: new Date().toISOString() }).eq("name", service);
107
- await auditLog(sb, mh, service, "write", "success");
108
- return { success: true, service, vault_key: vaultKey };
109
- }
110
-
111
- async function handleEnable(sb: any, body: any, mh: string) {
112
- const { service, enabled } = body;
113
- let q = sb.from("clauth_services").update({ enabled });
114
- q = service !== "all" ? q.eq("name", service) : q.not("vault_key", "is", null);
115
- const { error } = await q;
116
- if (error) return { error: error.message };
117
- await auditLog(sb, mh, service, enabled ? "enable" : "disable", "success");
118
- return { success: true, service, enabled };
119
- }
120
-
121
- async function handleAdd(sb: any, body: any, mh: string) {
122
- const { name, label, key_type, description } = body;
123
- if (!name || !label || !key_type) return { error: "name, label, key_type required" };
124
- const { error } = await sb.from("clauth_services").insert({ name, label, key_type, description: description || null });
125
- if (error) return { error: error.message };
126
- await auditLog(sb, mh, name, "add", "success");
127
- return { success: true, name, label, key_type };
128
- }
129
-
130
- async function handleRemove(sb: any, body: any, mh: string) {
131
- const { service, confirm } = body;
132
- if (confirm !== `CONFIRM REMOVE ${service.toUpperCase()}`) return { error: "confirm phrase mismatch" };
133
- await sb.rpc("vault_delete_secret", { secret_name: `clauth.${service}` });
134
- await sb.from("clauth_services").delete().eq("name", service);
135
- await auditLog(sb, mh, service, "remove", "success");
136
- return { success: true, service };
137
- }
138
-
139
- async function handleRevoke(sb: any, body: any, mh: string) {
140
- const { service, confirm } = body;
141
- const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
142
- if (confirm !== phrase) return { error: "confirm phrase mismatch", required: phrase };
143
- if (service === "all") {
144
- const { data: svcs } = await sb.from("clauth_services").select("name").not("vault_key", "is", null);
145
- for (const s of svcs || []) await sb.rpc("vault_delete_secret", { secret_name: `clauth.${s.name}` });
146
- await sb.from("clauth_services").update({ vault_key: null, enabled: false }).neq("id", "00000000-0000-0000-0000-000000000000");
147
- } else {
148
- await sb.rpc("vault_delete_secret", { secret_name: `clauth.${service}` });
149
- await sb.from("clauth_services").update({ vault_key: null, enabled: false }).eq("name", service);
150
- }
151
- await auditLog(sb, mh, service, "revoke", "success");
152
- return { success: true, service };
153
- }
154
-
155
- async function handleStatus(sb: any, mh: string) {
156
- const { data: services } = await sb.from("clauth_services")
157
- .select("name, label, key_type, enabled, vault_key, last_retrieved, last_rotated, created_at").order("name");
158
- await auditLog(sb, mh, "all", "status", "success");
159
- return { services: services || [] };
160
- }
161
-
162
- async function handleChangePassword(sb: any, body: any, mh: string) {
163
- const { new_hmac_seed_hash } = body;
164
- if (!new_hmac_seed_hash) return { error: "new_hmac_seed_hash required" };
165
- const { error } = await sb.from("clauth_machines")
166
- .update({ hmac_seed_hash: new_hmac_seed_hash, fail_count: 0, locked: false })
167
- .eq("machine_hash", mh);
168
- if (error) return { error: error.message };
169
- await auditLog(sb, mh, "system", "change-password", "success");
170
- return { success: true };
171
- }
172
-
173
- async function handleRegisterMachine(sb: any, body: any) {
174
- const { machine_hash, hmac_seed_hash, label, admin_token } = body;
175
- if (admin_token !== ADMIN_BOOTSTRAP_TOKEN) return { error: "invalid_admin_token" };
176
- const { error } = await sb.from("clauth_machines").upsert(
177
- { machine_hash, hmac_seed_hash, label, enabled: true, fail_count: 0, locked: false },
178
- { onConflict: "machine_hash" }
179
- );
180
- if (error) return { error: error.message };
181
- return { success: true, machine_hash };
182
- }
183
-
184
- Deno.serve(async (req: Request) => {
185
- if (req.method === "OPTIONS") {
186
- return new Response(null, { headers: {
187
- "Access-Control-Allow-Origin": "*",
188
- "Access-Control-Allow-Methods": "POST, OPTIONS",
189
- "Access-Control-Allow-Headers": "Content-Type, Authorization"
190
- }});
191
- }
192
- if (req.method !== "POST") return Response.json({ error: "POST only" }, { status: 405 });
193
-
194
- const url = new URL(req.url);
195
- const route = url.pathname.replace(/^\/auth-vault\/?/, "").replace(/^\//, "");
196
- const body = await req.json().catch(() => ({}));
197
- const sb = createClient(SUPABASE_URL, SERVICE_ROLE_KEY);
198
- const ip = getClientIP(req);
199
-
200
- if (route === "register-machine") return Response.json(await handleRegisterMachine(sb, body));
201
-
202
- const ipCheck = checkIP(ip);
203
- if (!ipCheck.allowed) {
204
- await auditLog(sb, body.machine_hash || "unknown", "system", route, "blocked", ipCheck.reason);
205
- return Response.json({ error: "ip_blocked", reason: ipCheck.reason }, { status: 403 });
206
- }
207
-
208
- if (body.machine_hash) {
209
- const rateCheck = await checkRateLimit(sb, body.machine_hash);
210
- if (!rateCheck.allowed) {
211
- await auditLog(sb, body.machine_hash, "system", route, "rate_limited", rateCheck.reason);
212
- return Response.json({ error: "rate_limited", reason: rateCheck.reason }, { status: 429 });
213
- }
214
- }
215
-
216
- const authResult = await validateHMAC(sb, { machine_hash: body.machine_hash, token: body.token, timestamp: body.timestamp, password: body.password });
217
- if (!authResult.valid) {
218
- await auditLog(sb, body.machine_hash || "unknown", body.service || "unknown", route, "denied", authResult.reason);
219
- return Response.json({ error: "auth_failed", reason: authResult.reason }, { status: 401 });
220
- }
221
-
222
- const mh = body.machine_hash;
223
- switch (route) {
224
- case "retrieve": return Response.json(await handleRetrieve(sb, body, mh));
225
- case "write": return Response.json(await handleWrite(sb, body, mh));
226
- case "enable": return Response.json(await handleEnable(sb, body, mh));
227
- case "add": return Response.json(await handleAdd(sb, body, mh));
228
- case "remove": return Response.json(await handleRemove(sb, body, mh));
229
- case "revoke": return Response.json(await handleRevoke(sb, body, mh));
230
- case "status": return Response.json(await handleStatus(sb, mh));
231
- case "change-password": return Response.json(await handleChangePassword(sb, body, mh));
232
- case "test": return Response.json({ valid: true, machine_hash: mh, timestamp: body.timestamp, ip });
233
- default: return Response.json({ error: "unknown_route", route }, { status: 404 });
234
- }
235
- });
1
+ // clauth — auth-vault Edge Function v2
2
+ // Added: IP whitelist, rate limiting, machine lockout (fail_count + locked)
3
+
4
+ import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
5
+
6
+ const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
7
+ const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
8
+ const CLAUTH_HMAC_SALT = Deno.env.get("CLAUTH_HMAC_SALT")!;
9
+ const ADMIN_BOOTSTRAP_TOKEN = Deno.env.get("CLAUTH_ADMIN_BOOTSTRAP_TOKEN")!;
10
+
11
+ const ALLOWED_IPS: string[] = (Deno.env.get("CLAUTH_ALLOWED_IPS") || "")
12
+ .split(",").map(s => s.trim()).filter(Boolean);
13
+
14
+ const RATE_LIMIT_MAX = 30;
15
+ const RATE_LIMIT_WINDOW = 60;
16
+ const REPLAY_WINDOW_MS = 5 * 60 * 1000;
17
+ const MAX_FAIL_COUNT = 5;
18
+
19
+ async function hmacSha256(key: string, message: string): Promise<string> {
20
+ const cryptoKey = await crypto.subtle.importKey(
21
+ "raw", new TextEncoder().encode(key),
22
+ { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
23
+ );
24
+ const sig = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
25
+ return Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, "0")).join("");
26
+ }
27
+
28
+ function getClientIP(req: Request): string {
29
+ return req.headers.get("cf-connecting-ip") ||
30
+ req.headers.get("x-real-ip") ||
31
+ req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
32
+ }
33
+
34
+ function checkIP(ip: string): { allowed: boolean; reason?: string } {
35
+ if (ALLOWED_IPS.length === 0) return { allowed: true };
36
+ if (ALLOWED_IPS.includes(ip)) return { allowed: true };
37
+ return { allowed: false, reason: `IP not whitelisted: ${ip}` };
38
+ }
39
+
40
+ async function checkRateLimit(sb: any, machine_hash: string): Promise<{ allowed: boolean; reason?: string }> {
41
+ const windowStart = new Date(Date.now() - RATE_LIMIT_WINDOW * 1000).toISOString();
42
+ const { count } = await sb.from("clauth_audit")
43
+ .select("id", { count: "exact", head: true })
44
+ .eq("machine_hash", machine_hash)
45
+ .gte("created_at", windowStart);
46
+ if ((count || 0) >= RATE_LIMIT_MAX) {
47
+ return { allowed: false, reason: `Rate limit: ${count}/${RATE_LIMIT_MAX} per ${RATE_LIMIT_WINDOW}s` };
48
+ }
49
+ return { allowed: true };
50
+ }
51
+
52
+ async function validateHMAC(sb: any, body: any): Promise<{ valid: boolean; reason?: string }> {
53
+ const now = Date.now();
54
+ if (Math.abs(now - body.timestamp) > REPLAY_WINDOW_MS) return { valid: false, reason: "timestamp_expired" };
55
+
56
+ const { data: machine, error } = await sb.from("clauth_machines")
57
+ .select("hmac_seed_hash, enabled, fail_count, locked")
58
+ .eq("machine_hash", body.machine_hash).single();
59
+
60
+ if (error || !machine) return { valid: false, reason: "machine_not_found" };
61
+ if (!machine.enabled) return { valid: false, reason: "machine_disabled" };
62
+ if (machine.locked) return { valid: false, reason: "machine_locked" };
63
+
64
+ const window = Math.floor(body.timestamp / REPLAY_WINDOW_MS);
65
+ const message = `${body.machine_hash}:${window}`;
66
+ const expected = await hmacSha256(body.password, message);
67
+
68
+ if (expected !== body.token) {
69
+ const newCount = (machine.fail_count || 0) + 1;
70
+ const shouldLock = newCount >= MAX_FAIL_COUNT;
71
+ await sb.from("clauth_machines")
72
+ .update({ fail_count: newCount, locked: shouldLock })
73
+ .eq("machine_hash", body.machine_hash);
74
+ return { valid: false, reason: shouldLock ? `machine_locked after ${newCount} failures` : `invalid_token (${newCount}/${MAX_FAIL_COUNT})` };
75
+ }
76
+
77
+ await sb.from("clauth_machines")
78
+ .update({ last_seen: new Date().toISOString(), fail_count: 0 })
79
+ .eq("machine_hash", body.machine_hash);
80
+ return { valid: true };
81
+ }
82
+
83
+ async function auditLog(sb: any, machine_hash: string, service_name: string, action: string, result: string, detail?: string) {
84
+ await sb.from("clauth_audit").insert({ machine_hash, service_name, action, result, detail });
85
+ }
86
+
87
+ async function handleRetrieve(sb: any, body: any, mh: string) {
88
+ const { service } = body;
89
+ if (!service) return { error: "service required" };
90
+ const { data: svc } = await sb.from("clauth_services").select("*").eq("name", service).single();
91
+ if (!svc) { await auditLog(sb, mh, service, "retrieve", "fail", "service_not_found"); return { error: "service_not_found" }; }
92
+ if (!svc.enabled) { await auditLog(sb, mh, service, "retrieve", "denied", "service_disabled"); return { error: "service_disabled" }; }
93
+ if (!svc.vault_key) { await auditLog(sb, mh, service, "retrieve", "fail", "no_key_stored"); return { error: "no_key_stored" }; }
94
+ const { data: secret } = await sb.rpc("vault_decrypt_secret", { secret_name: svc.vault_key });
95
+ await sb.from("clauth_services").update({ last_retrieved: new Date().toISOString() }).eq("name", service);
96
+ await auditLog(sb, mh, service, "retrieve", "success");
97
+ return { service, key_type: svc.key_type, value: secret };
98
+ }
99
+
100
+ async function handleWrite(sb: any, body: any, mh: string) {
101
+ const { service, value } = body;
102
+ if (!service || !value) return { error: "service and value required" };
103
+ const vaultKey = `clauth.${service}`;
104
+ const { error } = await sb.rpc("vault_upsert_secret", { secret_name: vaultKey, secret_value: typeof value === "string" ? value : JSON.stringify(value) });
105
+ if (error) { await auditLog(sb, mh, service, "write", "fail", error.message); return { error: error.message }; }
106
+ await sb.from("clauth_services").update({ vault_key: vaultKey, last_rotated: new Date().toISOString() }).eq("name", service);
107
+ await auditLog(sb, mh, service, "write", "success");
108
+ return { success: true, service, vault_key: vaultKey };
109
+ }
110
+
111
+ async function handleEnable(sb: any, body: any, mh: string) {
112
+ const { service, enabled } = body;
113
+ let q = sb.from("clauth_services").update({ enabled });
114
+ q = service !== "all" ? q.eq("name", service) : q.not("vault_key", "is", null);
115
+ const { error } = await q;
116
+ if (error) return { error: error.message };
117
+ await auditLog(sb, mh, service, enabled ? "enable" : "disable", "success");
118
+ return { success: true, service, enabled };
119
+ }
120
+
121
+ async function handleAdd(sb: any, body: any, mh: string) {
122
+ const { name, label, key_type, description } = body;
123
+ if (!name || !label || !key_type) return { error: "name, label, key_type required" };
124
+ const { error } = await sb.from("clauth_services").insert({ name, label, key_type, description: description || null });
125
+ if (error) return { error: error.message };
126
+ await auditLog(sb, mh, name, "add", "success");
127
+ return { success: true, name, label, key_type };
128
+ }
129
+
130
+ async function handleRemove(sb: any, body: any, mh: string) {
131
+ const { service, confirm } = body;
132
+ if (confirm !== `CONFIRM REMOVE ${service.toUpperCase()}`) return { error: "confirm phrase mismatch" };
133
+ await sb.rpc("vault_delete_secret", { secret_name: `clauth.${service}` });
134
+ await sb.from("clauth_services").delete().eq("name", service);
135
+ await auditLog(sb, mh, service, "remove", "success");
136
+ return { success: true, service };
137
+ }
138
+
139
+ async function handleRevoke(sb: any, body: any, mh: string) {
140
+ const { service, confirm } = body;
141
+ const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
142
+ if (confirm !== phrase) return { error: "confirm phrase mismatch", required: phrase };
143
+ if (service === "all") {
144
+ const { data: svcs } = await sb.from("clauth_services").select("name").not("vault_key", "is", null);
145
+ for (const s of svcs || []) await sb.rpc("vault_delete_secret", { secret_name: `clauth.${s.name}` });
146
+ await sb.from("clauth_services").update({ vault_key: null, enabled: false }).neq("id", "00000000-0000-0000-0000-000000000000");
147
+ } else {
148
+ await sb.rpc("vault_delete_secret", { secret_name: `clauth.${service}` });
149
+ await sb.from("clauth_services").update({ vault_key: null, enabled: false }).eq("name", service);
150
+ }
151
+ await auditLog(sb, mh, service, "revoke", "success");
152
+ return { success: true, service };
153
+ }
154
+
155
+ async function handleStatus(sb: any, mh: string) {
156
+ const { data: services } = await sb.from("clauth_services")
157
+ .select("name, label, key_type, enabled, vault_key, last_retrieved, last_rotated, created_at").order("name");
158
+ await auditLog(sb, mh, "all", "status", "success");
159
+ return { services: services || [] };
160
+ }
161
+
162
+ async function handleChangePassword(sb: any, body: any, mh: string) {
163
+ const { new_hmac_seed_hash } = body;
164
+ if (!new_hmac_seed_hash) return { error: "new_hmac_seed_hash required" };
165
+ const { error } = await sb.from("clauth_machines")
166
+ .update({ hmac_seed_hash: new_hmac_seed_hash, fail_count: 0, locked: false })
167
+ .eq("machine_hash", mh);
168
+ if (error) return { error: error.message };
169
+ await auditLog(sb, mh, "system", "change-password", "success");
170
+ return { success: true };
171
+ }
172
+
173
+ async function handleRegisterMachine(sb: any, body: any) {
174
+ const { machine_hash, hmac_seed_hash, label, admin_token } = body;
175
+ if (admin_token !== ADMIN_BOOTSTRAP_TOKEN) return { error: "invalid_admin_token" };
176
+ const { error } = await sb.from("clauth_machines").upsert(
177
+ { machine_hash, hmac_seed_hash, label, enabled: true, fail_count: 0, locked: false },
178
+ { onConflict: "machine_hash" }
179
+ );
180
+ if (error) return { error: error.message };
181
+ return { success: true, machine_hash };
182
+ }
183
+
184
+ Deno.serve(async (req: Request) => {
185
+ if (req.method === "OPTIONS") {
186
+ return new Response(null, { headers: {
187
+ "Access-Control-Allow-Origin": "*",
188
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
189
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
190
+ }});
191
+ }
192
+ if (req.method !== "POST") return Response.json({ error: "POST only" }, { status: 405 });
193
+
194
+ const url = new URL(req.url);
195
+ const route = url.pathname.replace(/^\/auth-vault\/?/, "").replace(/^\//, "");
196
+ const body = await req.json().catch(() => ({}));
197
+ const sb = createClient(SUPABASE_URL, SERVICE_ROLE_KEY);
198
+ const ip = getClientIP(req);
199
+
200
+ if (route === "register-machine") return Response.json(await handleRegisterMachine(sb, body));
201
+
202
+ const ipCheck = checkIP(ip);
203
+ if (!ipCheck.allowed) {
204
+ await auditLog(sb, body.machine_hash || "unknown", "system", route, "blocked", ipCheck.reason);
205
+ return Response.json({ error: "ip_blocked", reason: ipCheck.reason }, { status: 403 });
206
+ }
207
+
208
+ if (body.machine_hash) {
209
+ const rateCheck = await checkRateLimit(sb, body.machine_hash);
210
+ if (!rateCheck.allowed) {
211
+ await auditLog(sb, body.machine_hash, "system", route, "rate_limited", rateCheck.reason);
212
+ return Response.json({ error: "rate_limited", reason: rateCheck.reason }, { status: 429 });
213
+ }
214
+ }
215
+
216
+ const authResult = await validateHMAC(sb, { machine_hash: body.machine_hash, token: body.token, timestamp: body.timestamp, password: body.password });
217
+ if (!authResult.valid) {
218
+ await auditLog(sb, body.machine_hash || "unknown", body.service || "unknown", route, "denied", authResult.reason);
219
+ return Response.json({ error: "auth_failed", reason: authResult.reason }, { status: 401 });
220
+ }
221
+
222
+ const mh = body.machine_hash;
223
+ switch (route) {
224
+ case "retrieve": return Response.json(await handleRetrieve(sb, body, mh));
225
+ case "write": return Response.json(await handleWrite(sb, body, mh));
226
+ case "enable": return Response.json(await handleEnable(sb, body, mh));
227
+ case "add": return Response.json(await handleAdd(sb, body, mh));
228
+ case "remove": return Response.json(await handleRemove(sb, body, mh));
229
+ case "revoke": return Response.json(await handleRevoke(sb, body, mh));
230
+ case "status": return Response.json(await handleStatus(sb, mh));
231
+ case "change-password": return Response.json(await handleChangePassword(sb, body, mh));
232
+ case "test": return Response.json({ valid: true, machine_hash: mh, timestamp: body.timestamp, ip });
233
+ default: return Response.json({ error: "unknown_route", route }, { status: 404 });
234
+ }
235
+ });