@lifeaitools/clauth 0.3.6 → 0.3.8
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 +184 -184
- package/.clauth-skill/references/keys-guide.md +270 -270
- package/.clauth-skill/references/operator-guide.md +148 -148
- package/README.md +125 -125
- package/cli/api.js +112 -112
- package/cli/commands/install.js +264 -264
- package/cli/commands/serve.js +1207 -1031
- package/cli/commands/uninstall.js +164 -164
- package/cli/fingerprint.js +91 -91
- package/cli/index.js +1 -1
- package/install.ps1 +44 -44
- package/install.sh +38 -38
- package/package.json +1 -1
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bootstrap.cjs +43 -43
- package/scripts/build.sh +45 -45
- package/supabase/functions/auth-vault/index.ts +235 -235
- package/supabase/migrations/001_clauth_schema.sql +103 -103
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
|
@@ -1,103 +1,103 @@
|
|
|
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
|
-
do $$ begin
|
|
81
|
-
if not exists (select 1 from pg_policies where policyname = 'no_anon_services' and tablename = 'clauth_services') then
|
|
82
|
-
create policy "no_anon_services" on public.clauth_services for all using (false);
|
|
83
|
-
end if;
|
|
84
|
-
if not exists (select 1 from pg_policies where policyname = 'no_anon_machines' and tablename = 'clauth_machines') then
|
|
85
|
-
create policy "no_anon_machines" on public.clauth_machines for all using (false);
|
|
86
|
-
end if;
|
|
87
|
-
if not exists (select 1 from pg_policies where policyname = 'no_anon_audit' and tablename = 'clauth_audit') then
|
|
88
|
-
create policy "no_anon_audit" on public.clauth_audit for all using (false);
|
|
89
|
-
end if;
|
|
90
|
-
end $$;
|
|
91
|
-
|
|
92
|
-
-- ============================================================
|
|
93
|
-
-- Updated_at trigger
|
|
94
|
-
-- ============================================================
|
|
95
|
-
create or replace function public.clauth_touch_updated()
|
|
96
|
-
returns trigger language plpgsql as $$
|
|
97
|
-
begin new.updated_at = now(); return new; end;
|
|
98
|
-
$$;
|
|
99
|
-
|
|
100
|
-
drop trigger if exists clauth_services_updated on public.clauth_services;
|
|
101
|
-
create trigger clauth_services_updated
|
|
102
|
-
before update on public.clauth_services
|
|
103
|
-
for each row execute procedure public.clauth_touch_updated();
|
|
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
|
+
do $$ begin
|
|
81
|
+
if not exists (select 1 from pg_policies where policyname = 'no_anon_services' and tablename = 'clauth_services') then
|
|
82
|
+
create policy "no_anon_services" on public.clauth_services for all using (false);
|
|
83
|
+
end if;
|
|
84
|
+
if not exists (select 1 from pg_policies where policyname = 'no_anon_machines' and tablename = 'clauth_machines') then
|
|
85
|
+
create policy "no_anon_machines" on public.clauth_machines for all using (false);
|
|
86
|
+
end if;
|
|
87
|
+
if not exists (select 1 from pg_policies where policyname = 'no_anon_audit' and tablename = 'clauth_audit') then
|
|
88
|
+
create policy "no_anon_audit" on public.clauth_audit for all using (false);
|
|
89
|
+
end if;
|
|
90
|
+
end $$;
|
|
91
|
+
|
|
92
|
+
-- ============================================================
|
|
93
|
+
-- Updated_at trigger
|
|
94
|
+
-- ============================================================
|
|
95
|
+
create or replace function public.clauth_touch_updated()
|
|
96
|
+
returns trigger language plpgsql as $$
|
|
97
|
+
begin new.updated_at = now(); return new; end;
|
|
98
|
+
$$;
|
|
99
|
+
|
|
100
|
+
drop trigger if exists clauth_services_updated on public.clauth_services;
|
|
101
|
+
create trigger clauth_services_updated
|
|
102
|
+
before update on public.clauth_services
|
|
103
|
+
for each row execute procedure public.clauth_touch_updated();
|
|
@@ -1,90 +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;
|
|
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;
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
-- Migration: add fail_count and locked to clauth_machines
|
|
2
|
-
-- Run via: clauth install (picks this up automatically) or apply manually
|
|
3
|
-
|
|
4
|
-
ALTER TABLE clauth_machines
|
|
5
|
-
ADD COLUMN IF NOT EXISTS fail_count INTEGER NOT NULL DEFAULT 0,
|
|
6
|
-
ADD COLUMN IF NOT EXISTS locked BOOLEAN NOT NULL DEFAULT false;
|
|
7
|
-
|
|
8
|
-
-- Index for fast lockout checks
|
|
9
|
-
CREATE INDEX IF NOT EXISTS idx_clauth_machines_locked
|
|
10
|
-
ON clauth_machines (machine_hash, locked);
|
|
11
|
-
|
|
12
|
-
-- Admin helper: unlock a machine
|
|
13
|
-
-- Usage: SELECT clauth_unlock_machine('your_machine_hash');
|
|
14
|
-
CREATE OR REPLACE FUNCTION clauth_unlock_machine(p_machine_hash TEXT)
|
|
15
|
-
RETURNS void
|
|
16
|
-
LANGUAGE plpgsql
|
|
17
|
-
SECURITY DEFINER
|
|
18
|
-
AS $$
|
|
19
|
-
BEGIN
|
|
20
|
-
UPDATE clauth_machines
|
|
21
|
-
SET locked = false, fail_count = 0
|
|
22
|
-
WHERE machine_hash = p_machine_hash;
|
|
23
|
-
END;
|
|
24
|
-
$$;
|
|
25
|
-
|
|
26
|
-
REVOKE EXECUTE ON FUNCTION clauth_unlock_machine(TEXT) FROM PUBLIC;
|
|
1
|
+
-- Migration: add fail_count and locked to clauth_machines
|
|
2
|
+
-- Run via: clauth install (picks this up automatically) or apply manually
|
|
3
|
+
|
|
4
|
+
ALTER TABLE clauth_machines
|
|
5
|
+
ADD COLUMN IF NOT EXISTS fail_count INTEGER NOT NULL DEFAULT 0,
|
|
6
|
+
ADD COLUMN IF NOT EXISTS locked BOOLEAN NOT NULL DEFAULT false;
|
|
7
|
+
|
|
8
|
+
-- Index for fast lockout checks
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_clauth_machines_locked
|
|
10
|
+
ON clauth_machines (machine_hash, locked);
|
|
11
|
+
|
|
12
|
+
-- Admin helper: unlock a machine
|
|
13
|
+
-- Usage: SELECT clauth_unlock_machine('your_machine_hash');
|
|
14
|
+
CREATE OR REPLACE FUNCTION clauth_unlock_machine(p_machine_hash TEXT)
|
|
15
|
+
RETURNS void
|
|
16
|
+
LANGUAGE plpgsql
|
|
17
|
+
SECURITY DEFINER
|
|
18
|
+
AS $$
|
|
19
|
+
BEGIN
|
|
20
|
+
UPDATE clauth_machines
|
|
21
|
+
SET locked = false, fail_count = 0
|
|
22
|
+
WHERE machine_hash = p_machine_hash;
|
|
23
|
+
END;
|
|
24
|
+
$$;
|
|
25
|
+
|
|
26
|
+
REVOKE EXECUTE ON FUNCTION clauth_unlock_machine(TEXT) FROM PUBLIC;
|