@pgpm/reserved-names 0.0.0-stage → 0.47.5
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/LICENSE +22 -0
- package/Makefile +6 -0
- package/README.md +81 -2
- package/deploy/schemas/reserved_names/procedures/is_reserved.sql +16 -0
- package/deploy/schemas/reserved_names/procedures/reserved_by.sql +23 -0
- package/deploy/schemas/reserved_names/schema.sql +9 -0
- package/deploy/schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture.sql +14 -0
- package/deploy/schemas/reserved_names/tables/kinds/table.sql +16 -0
- package/deploy/schemas/reserved_names/tables/names/fixtures/1792800000002_fixture.sql +72 -0
- package/deploy/schemas/reserved_names/tables/names/indexes/names_reason_idx.sql +10 -0
- package/deploy/schemas/reserved_names/tables/names/table.sql +23 -0
- package/deploy/schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture.sql +14 -0
- package/deploy/schemas/reserved_names/tables/reasons/table.sql +16 -0
- package/package.json +37 -4
- package/pgpm-reserved-names.control +7 -0
- package/pgpm.plan +14 -0
- package/revert/schemas/reserved_names/procedures/is_reserved.sql +7 -0
- package/revert/schemas/reserved_names/procedures/reserved_by.sql +7 -0
- package/revert/schemas/reserved_names/schema.sql +7 -0
- package/revert/schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture.sql +7 -0
- package/revert/schemas/reserved_names/tables/kinds/table.sql +7 -0
- package/revert/schemas/reserved_names/tables/names/fixtures/1792800000002_fixture.sql +7 -0
- package/revert/schemas/reserved_names/tables/names/indexes/names_reason_idx.sql +7 -0
- package/revert/schemas/reserved_names/tables/names/table.sql +7 -0
- package/revert/schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture.sql +7 -0
- package/revert/schemas/reserved_names/tables/reasons/table.sql +7 -0
- package/sql/pgpm-reserved-names--0.47.5.bundle.tar.gz +0 -0
- package/sql/pgpm-reserved-names--0.47.5.sql +157 -0
- package/verify/schemas/reserved_names/procedures/is_reserved.sql +7 -0
- package/verify/schemas/reserved_names/procedures/reserved_by.sql +7 -0
- package/verify/schemas/reserved_names/schema.sql +7 -0
- package/verify/schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture.sql +7 -0
- package/verify/schemas/reserved_names/tables/kinds/table.sql +7 -0
- package/verify/schemas/reserved_names/tables/names/fixtures/1792800000002_fixture.sql +7 -0
- package/verify/schemas/reserved_names/tables/names/indexes/names_reason_idx.sql +7 -0
- package/verify/schemas/reserved_names/tables/names/table.sql +7 -0
- package/verify/schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture.sql +7 -0
- package/verify/schemas/reserved_names/tables/reasons/table.sql +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
|
|
4
|
+
Copyright (c) 2025 Constructive
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/Makefile
ADDED
package/README.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pgpm/reserved-names
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center" width="100%">
|
|
4
|
+
<img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center" width="100%">
|
|
8
|
+
<a href="https://github.com/constructive-io/pgpm-modules/actions/workflows/ci.yml">
|
|
9
|
+
<img height="20" src="https://github.com/constructive-io/pgpm-modules/actions/workflows/ci.yml/badge.svg" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://github.com/constructive-io/pgpm-modules/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/@pgpm/reserved-names"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/pgpm-modules?filename=packages%2Freserved-names%2Fpackage.json"/></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
Reserved names that nobody may claim — subdomains, usernames, slugs, or any other kind — kept as rows and checked live by one function.
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
`@pgpm/reserved-names` keeps one shared list, as tables, and callers check it live at the moment someone claims a name. A new entry takes effect at once for every caller, with no regeneration or redeploy of the callers.
|
|
20
|
+
|
|
21
|
+
| table | what it holds |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `reserved_names.kinds` | categories of name: `subdomain`, `username`, `domain`, `slug` (add more with a migration) |
|
|
24
|
+
| `reserved_names.reasons` | why a name is reserved: `platform`, `brand`, `lookalike`, `abuse` |
|
|
25
|
+
| `reserved_names.names` | the entries: `kind`, `name` (lowercase), `match` (`exact` / `prefix` / `suffix`), `reason` |
|
|
26
|
+
|
|
27
|
+
Every entry must point at a real kind and reason (foreign keys), so a typo cannot quietly create an empty category. The primary key is `(kind, name, match)`.
|
|
28
|
+
|
|
29
|
+
The starter data, in fixtures, is the four kinds and four reasons plus a `subdomain` list: hostnames a visitor would read as the platform's own under a shared domain (`login`, `admin`, `www`, `mail`, `ns1`, …), and a `xn--` prefix entry that blocks punycode lookalikes (`аpple` with a Cyrillic `а`). The other kinds start empty.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pgpm install @pgpm/reserved-names
|
|
35
|
+
pgpm deploy
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```sql
|
|
41
|
+
SELECT reserved_names.is_reserved('subdomain', 'login'); -- true
|
|
42
|
+
SELECT reserved_names.is_reserved('subdomain', 'LOGIN'); -- true (case-insensitive)
|
|
43
|
+
SELECT reserved_names.is_reserved('subdomain', 'xn--pple-43d'); -- true (prefix entry)
|
|
44
|
+
SELECT reserved_names.is_reserved('subdomain', 'mystore'); -- false
|
|
45
|
+
SELECT reserved_names.is_reserved('username', 'login'); -- false (kinds are separate)
|
|
46
|
+
|
|
47
|
+
SELECT name, match, reason FROM reserved_names.reserved_by('subdomain', 'xn--pple-43d');
|
|
48
|
+
-- xn-- | prefix | lookalike
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `is_reserved(kind, name) RETURNS boolean`: true when any entry matches.
|
|
52
|
+
- `reserved_by(kind, name) RETURNS SETOF reserved_names.names`: the matching entries, so an error can say why.
|
|
53
|
+
|
|
54
|
+
Both are `STABLE` SQL, lowercase `kind` and `name` before comparing, and treat `NULL` as not reserved. Matching:
|
|
55
|
+
|
|
56
|
+
- `exact`: the name itself.
|
|
57
|
+
- `prefix`: every name starting with it (`xn--`, `staff-`).
|
|
58
|
+
- `suffix`: every name ending with it. `.example.com` blocks `shop.example.com`, but not `example.com` itself (add an `exact` entry for that) and not `notexample.com`.
|
|
59
|
+
|
|
60
|
+
### Adding names
|
|
61
|
+
|
|
62
|
+
The list changes only through migrations. Add a pgpm fixture change that inserts rows:
|
|
63
|
+
|
|
64
|
+
```sql
|
|
65
|
+
INSERT INTO reserved_names.names (kind, name, match, reason) VALUES
|
|
66
|
+
('subdomain', 'www2', 'exact', 'platform'),
|
|
67
|
+
('subdomain', 'staff-', 'prefix', 'platform'),
|
|
68
|
+
('domain', '.example.com', 'suffix', 'brand'),
|
|
69
|
+
('username', 'support', 'exact', 'platform');
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
A new category or reason is a row in `kinds` or `reasons` first.
|
|
73
|
+
|
|
74
|
+
## Access
|
|
75
|
+
|
|
76
|
+
Everyone can read the three tables and call both functions, so a UI can say "that name is reserved" before submitting. The list is public, so there is no row-level security. No role is granted `INSERT`, `UPDATE` or `DELETE`: only the owner (migrations, or a `SECURITY DEFINER` function it owns) can change it.
|
|
77
|
+
|
|
78
|
+
## Testing
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pnpm test
|
|
82
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Deploy schemas/reserved_names/procedures/is_reserved to pg
|
|
2
|
+
|
|
3
|
+
-- requires: schemas/reserved_names/schema
|
|
4
|
+
-- requires: schemas/reserved_names/procedures/reserved_by
|
|
5
|
+
|
|
6
|
+
BEGIN;
|
|
7
|
+
|
|
8
|
+
CREATE FUNCTION reserved_names.is_reserved(kind text, name text) RETURNS boolean AS $$
|
|
9
|
+
SELECT EXISTS (SELECT 1 FROM reserved_names.reserved_by(is_reserved.kind, is_reserved.name));
|
|
10
|
+
$$ LANGUAGE sql STABLE;
|
|
11
|
+
|
|
12
|
+
COMMENT ON FUNCTION reserved_names.is_reserved(text, text) IS 'True when name (any case) is reserved for kind';
|
|
13
|
+
|
|
14
|
+
GRANT EXECUTE ON FUNCTION reserved_names.is_reserved(text, text) TO public;
|
|
15
|
+
|
|
16
|
+
COMMIT;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Deploy schemas/reserved_names/procedures/reserved_by to pg
|
|
2
|
+
|
|
3
|
+
-- requires: schemas/reserved_names/schema
|
|
4
|
+
-- requires: schemas/reserved_names/tables/names/table
|
|
5
|
+
|
|
6
|
+
BEGIN;
|
|
7
|
+
|
|
8
|
+
CREATE FUNCTION reserved_names.reserved_by(kind text, name text) RETURNS SETOF reserved_names.names AS $$
|
|
9
|
+
SELECT n.*
|
|
10
|
+
FROM reserved_names.names n
|
|
11
|
+
WHERE n.kind = pg_catalog.lower(reserved_by.kind)
|
|
12
|
+
AND CASE n.match
|
|
13
|
+
WHEN 'exact' THEN n.name = pg_catalog.lower(reserved_by.name)
|
|
14
|
+
WHEN 'prefix' THEN pg_catalog.starts_with(pg_catalog.lower(reserved_by.name), n.name)
|
|
15
|
+
WHEN 'suffix' THEN pg_catalog.right(pg_catalog.lower(reserved_by.name), pg_catalog.length(n.name)) = n.name
|
|
16
|
+
END;
|
|
17
|
+
$$ LANGUAGE sql STABLE;
|
|
18
|
+
|
|
19
|
+
COMMENT ON FUNCTION reserved_names.reserved_by(text, text) IS 'The entries (with their reason) that reserve name for kind; empty when it is free';
|
|
20
|
+
|
|
21
|
+
GRANT EXECUTE ON FUNCTION reserved_names.reserved_by(text, text) TO public;
|
|
22
|
+
|
|
23
|
+
COMMIT;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Deploy schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture to pg
|
|
2
|
+
|
|
3
|
+
-- requires: schemas/reserved_names/schema
|
|
4
|
+
-- requires: schemas/reserved_names/tables/kinds/table
|
|
5
|
+
|
|
6
|
+
BEGIN;
|
|
7
|
+
|
|
8
|
+
INSERT INTO reserved_names.kinds (kind, description) VALUES
|
|
9
|
+
('subdomain', 'A label under a shared domain, like login in login.example.com'),
|
|
10
|
+
('username', 'A person''s handle'),
|
|
11
|
+
('domain', 'A whole domain name, like example.com'),
|
|
12
|
+
('slug', 'A name in a URL path, like an organization or app slug');
|
|
13
|
+
|
|
14
|
+
COMMIT;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Deploy schemas/reserved_names/tables/kinds/table to pg
|
|
2
|
+
|
|
3
|
+
-- requires: schemas/reserved_names/schema
|
|
4
|
+
|
|
5
|
+
BEGIN;
|
|
6
|
+
|
|
7
|
+
CREATE TABLE reserved_names.kinds (
|
|
8
|
+
kind text PRIMARY KEY CHECK (kind ~ '^[a-z][a-z0-9_]*$'),
|
|
9
|
+
description text NOT NULL
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
COMMENT ON TABLE reserved_names.kinds IS 'Categories of names that can be reserved: subdomain, username, domain, slug, ...';
|
|
13
|
+
|
|
14
|
+
GRANT SELECT ON TABLE reserved_names.kinds TO public;
|
|
15
|
+
|
|
16
|
+
COMMIT;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
-- Deploy schemas/reserved_names/tables/names/fixtures/1792800000002_fixture to pg
|
|
2
|
+
|
|
3
|
+
-- requires: schemas/reserved_names/schema
|
|
4
|
+
-- requires: schemas/reserved_names/tables/names/table
|
|
5
|
+
-- requires: schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture
|
|
6
|
+
-- requires: schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture
|
|
7
|
+
|
|
8
|
+
BEGIN;
|
|
9
|
+
|
|
10
|
+
INSERT INTO reserved_names.names (kind, name, match, reason) VALUES
|
|
11
|
+
('subdomain', 'www', 'exact', 'platform'),
|
|
12
|
+
('subdomain', 'api', 'exact', 'platform'),
|
|
13
|
+
('subdomain', 'auth', 'exact', 'platform'),
|
|
14
|
+
('subdomain', 'login', 'exact', 'platform'),
|
|
15
|
+
('subdomain', 'logout', 'exact', 'platform'),
|
|
16
|
+
('subdomain', 'signin', 'exact', 'platform'),
|
|
17
|
+
('subdomain', 'signup', 'exact', 'platform'),
|
|
18
|
+
('subdomain', 'register', 'exact', 'platform'),
|
|
19
|
+
('subdomain', 'admin', 'exact', 'platform'),
|
|
20
|
+
('subdomain', 'administrator', 'exact', 'platform'),
|
|
21
|
+
('subdomain', 'root', 'exact', 'platform'),
|
|
22
|
+
('subdomain', 'app', 'exact', 'platform'),
|
|
23
|
+
('subdomain', 'apps', 'exact', 'platform'),
|
|
24
|
+
('subdomain', 'dashboard', 'exact', 'platform'),
|
|
25
|
+
('subdomain', 'console', 'exact', 'platform'),
|
|
26
|
+
('subdomain', 'account', 'exact', 'platform'),
|
|
27
|
+
('subdomain', 'accounts', 'exact', 'platform'),
|
|
28
|
+
('subdomain', 'billing', 'exact', 'platform'),
|
|
29
|
+
('subdomain', 'pay', 'exact', 'platform'),
|
|
30
|
+
('subdomain', 'payments', 'exact', 'platform'),
|
|
31
|
+
('subdomain', 'checkout', 'exact', 'platform'),
|
|
32
|
+
('subdomain', 'support', 'exact', 'platform'),
|
|
33
|
+
('subdomain', 'help', 'exact', 'platform'),
|
|
34
|
+
('subdomain', 'status', 'exact', 'platform'),
|
|
35
|
+
('subdomain', 'docs', 'exact', 'platform'),
|
|
36
|
+
('subdomain', 'mail', 'exact', 'platform'),
|
|
37
|
+
('subdomain', 'email', 'exact', 'platform'),
|
|
38
|
+
('subdomain', 'smtp', 'exact', 'platform'),
|
|
39
|
+
('subdomain', 'imap', 'exact', 'platform'),
|
|
40
|
+
('subdomain', 'ftp', 'exact', 'platform'),
|
|
41
|
+
('subdomain', 'ns', 'exact', 'platform'),
|
|
42
|
+
('subdomain', 'ns1', 'exact', 'platform'),
|
|
43
|
+
('subdomain', 'ns2', 'exact', 'platform'),
|
|
44
|
+
('subdomain', 'mx', 'exact', 'platform'),
|
|
45
|
+
('subdomain', 'cdn', 'exact', 'platform'),
|
|
46
|
+
('subdomain', 'static', 'exact', 'platform'),
|
|
47
|
+
('subdomain', 'assets', 'exact', 'platform'),
|
|
48
|
+
('subdomain', 'files', 'exact', 'platform'),
|
|
49
|
+
('subdomain', 'storage', 'exact', 'platform'),
|
|
50
|
+
('subdomain', 'media', 'exact', 'platform'),
|
|
51
|
+
('subdomain', 'ws', 'exact', 'platform'),
|
|
52
|
+
('subdomain', 'wss', 'exact', 'platform'),
|
|
53
|
+
('subdomain', 'graphql', 'exact', 'platform'),
|
|
54
|
+
('subdomain', 'modules', 'exact', 'platform'),
|
|
55
|
+
('subdomain', 'agent', 'exact', 'platform'),
|
|
56
|
+
('subdomain', 'compute', 'exact', 'platform'),
|
|
57
|
+
('subdomain', 'machines', 'exact', 'platform'),
|
|
58
|
+
('subdomain', 'cluster', 'exact', 'platform'),
|
|
59
|
+
('subdomain', 'migrate', 'exact', 'platform'),
|
|
60
|
+
('subdomain', 'internal', 'exact', 'platform'),
|
|
61
|
+
('subdomain', 'system', 'exact', 'platform'),
|
|
62
|
+
('subdomain', 'platform', 'exact', 'platform'),
|
|
63
|
+
('subdomain', 'security', 'exact', 'platform'),
|
|
64
|
+
('subdomain', 'sso', 'exact', 'platform'),
|
|
65
|
+
('subdomain', 'oauth', 'exact', 'platform'),
|
|
66
|
+
('subdomain', 'id', 'exact', 'platform'),
|
|
67
|
+
('subdomain', 'identity', 'exact', 'platform'),
|
|
68
|
+
('subdomain', 'verify', 'exact', 'platform'),
|
|
69
|
+
('subdomain', 'secure', 'exact', 'platform'),
|
|
70
|
+
('subdomain', 'xn--', 'prefix', 'lookalike');
|
|
71
|
+
|
|
72
|
+
COMMIT;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- Deploy schemas/reserved_names/tables/names/indexes/names_reason_idx to pg
|
|
2
|
+
|
|
3
|
+
-- requires: schemas/reserved_names/schema
|
|
4
|
+
-- requires: schemas/reserved_names/tables/names/table
|
|
5
|
+
|
|
6
|
+
BEGIN;
|
|
7
|
+
|
|
8
|
+
CREATE INDEX names_reason_idx ON reserved_names.names (reason);
|
|
9
|
+
|
|
10
|
+
COMMIT;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Deploy schemas/reserved_names/tables/names/table to pg
|
|
2
|
+
|
|
3
|
+
-- requires: schemas/reserved_names/schema
|
|
4
|
+
-- requires: schemas/reserved_names/tables/kinds/table
|
|
5
|
+
-- requires: schemas/reserved_names/tables/reasons/table
|
|
6
|
+
|
|
7
|
+
BEGIN;
|
|
8
|
+
|
|
9
|
+
CREATE TABLE reserved_names.names (
|
|
10
|
+
kind text NOT NULL REFERENCES reserved_names.kinds (kind),
|
|
11
|
+
name text NOT NULL CHECK (name <> '' AND name = lower(name)),
|
|
12
|
+
match text NOT NULL DEFAULT 'exact' CHECK (match IN ('exact', 'prefix', 'suffix')),
|
|
13
|
+
reason text NOT NULL REFERENCES reserved_names.reasons (reason),
|
|
14
|
+
PRIMARY KEY (kind, name, match)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
COMMENT ON TABLE reserved_names.names IS 'Names nobody may claim, by kind. Changed only by migrations.';
|
|
18
|
+
COMMENT ON COLUMN reserved_names.names.name IS 'The reserved name, lowercase';
|
|
19
|
+
COMMENT ON COLUMN reserved_names.names.match IS 'exact: the name itself; prefix: every name starting with it; suffix: every name ending with it';
|
|
20
|
+
|
|
21
|
+
GRANT SELECT ON TABLE reserved_names.names TO public;
|
|
22
|
+
|
|
23
|
+
COMMIT;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Deploy schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture to pg
|
|
2
|
+
|
|
3
|
+
-- requires: schemas/reserved_names/schema
|
|
4
|
+
-- requires: schemas/reserved_names/tables/reasons/table
|
|
5
|
+
|
|
6
|
+
BEGIN;
|
|
7
|
+
|
|
8
|
+
INSERT INTO reserved_names.reasons (reason, description) VALUES
|
|
9
|
+
('platform', 'Reads as the platform''s own (login, admin, status, ...)'),
|
|
10
|
+
('brand', 'Our brand or product names'),
|
|
11
|
+
('lookalike', 'Can be made to look like another name, e.g. punycode'),
|
|
12
|
+
('abuse', 'Offensive or used for abuse');
|
|
13
|
+
|
|
14
|
+
COMMIT;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Deploy schemas/reserved_names/tables/reasons/table to pg
|
|
2
|
+
|
|
3
|
+
-- requires: schemas/reserved_names/schema
|
|
4
|
+
|
|
5
|
+
BEGIN;
|
|
6
|
+
|
|
7
|
+
CREATE TABLE reserved_names.reasons (
|
|
8
|
+
reason text PRIMARY KEY CHECK (reason ~ '^[a-z][a-z0-9_]*$'),
|
|
9
|
+
description text NOT NULL
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
COMMENT ON TABLE reserved_names.reasons IS 'Why a name is reserved: platform, brand, lookalike, abuse, ...';
|
|
13
|
+
|
|
14
|
+
GRANT SELECT ON TABLE reserved_names.reasons TO public;
|
|
15
|
+
|
|
16
|
+
COMMIT;
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpm/reserved-names",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.47.5",
|
|
4
|
+
"description": "Reserved names (subdomains, usernames, domains, slugs, ...) that nobody may claim, as tables checked live by reserved_names.is_reserved(kind, name)",
|
|
5
|
+
"author": "Dan Lynch <pyramation@gmail.com>",
|
|
6
|
+
"contributors": [
|
|
7
|
+
"Constructive <developers@constructive.io>"
|
|
8
|
+
],
|
|
9
|
+
"keywords": [
|
|
10
|
+
"postgresql",
|
|
11
|
+
"pgpm",
|
|
12
|
+
"reserved",
|
|
13
|
+
"names",
|
|
14
|
+
"subdomains"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"bundle": "pgpm package",
|
|
21
|
+
"test": "jest",
|
|
22
|
+
"test:watch": "jest --watch"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@pgpm/verify": "0.47.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"pgpm": "^5"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/constructive-io/pgpm-modules"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/constructive-io/pgpm-modules",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/constructive-io/pgpm-modules/issues"
|
|
37
|
+
},
|
|
38
|
+
"gitHead": "6b16b597af00c20dc2ca9b21c15bda69b4ce1186"
|
|
39
|
+
}
|
package/pgpm.plan
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
%syntax-version=1.0.0
|
|
2
|
+
%project=pgpm-reserved-names
|
|
3
|
+
%uri=pgpm-reserved-names
|
|
4
|
+
|
|
5
|
+
schemas/reserved_names/schema [pgpm-verify:procedures/assert_function] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/schema
|
|
6
|
+
schemas/reserved_names/tables/kinds/table [schemas/reserved_names/schema] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/kinds/table
|
|
7
|
+
schemas/reserved_names/tables/reasons/table [schemas/reserved_names/schema] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/reasons/table
|
|
8
|
+
schemas/reserved_names/tables/names/table [schemas/reserved_names/schema schemas/reserved_names/tables/kinds/table schemas/reserved_names/tables/reasons/table] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/names/table
|
|
9
|
+
schemas/reserved_names/tables/names/indexes/names_reason_idx [schemas/reserved_names/schema schemas/reserved_names/tables/names/table] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/names/indexes/names_reason_idx
|
|
10
|
+
schemas/reserved_names/procedures/reserved_by [schemas/reserved_names/schema schemas/reserved_names/tables/names/table] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/procedures/reserved_by
|
|
11
|
+
schemas/reserved_names/procedures/is_reserved [schemas/reserved_names/schema schemas/reserved_names/procedures/reserved_by] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/procedures/is_reserved
|
|
12
|
+
schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture [schemas/reserved_names/schema schemas/reserved_names/tables/kinds/table] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture
|
|
13
|
+
schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture [schemas/reserved_names/schema schemas/reserved_names/tables/reasons/table] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture
|
|
14
|
+
schemas/reserved_names/tables/names/fixtures/1792800000002_fixture [schemas/reserved_names/schema schemas/reserved_names/tables/names/table schemas/reserved_names/tables/kinds/fixtures/1792800000000_fixture schemas/reserved_names/tables/reasons/fixtures/1792800000001_fixture] 2026-10-22T19:40:00Z devin <devin@cognition.ai> # add schemas/reserved_names/tables/names/fixtures/1792800000002_fixture
|
|
Binary file
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
\echo Use "CREATE EXTENSION pgpm-reserved-names" to load this file. \quit
|
|
2
|
+
CREATE SCHEMA reserved_names;
|
|
3
|
+
|
|
4
|
+
GRANT USAGE ON SCHEMA reserved_names TO PUBLIC;
|
|
5
|
+
|
|
6
|
+
CREATE TABLE reserved_names.kinds (
|
|
7
|
+
kind text PRIMARY KEY CHECK (kind ~ '^[a-z][a-z0-9_]*$'),
|
|
8
|
+
description text NOT NULL
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
COMMENT ON TABLE reserved_names.kinds IS 'Categories of names that can be reserved: subdomain, username, domain, slug, ...';
|
|
12
|
+
|
|
13
|
+
GRANT SELECT ON reserved_names.kinds TO PUBLIC;
|
|
14
|
+
|
|
15
|
+
CREATE TABLE reserved_names.reasons (
|
|
16
|
+
reason text PRIMARY KEY CHECK (reason ~ '^[a-z][a-z0-9_]*$'),
|
|
17
|
+
description text NOT NULL
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
COMMENT ON TABLE reserved_names.reasons IS 'Why a name is reserved: platform, brand, lookalike, abuse, ...';
|
|
21
|
+
|
|
22
|
+
GRANT SELECT ON reserved_names.reasons TO PUBLIC;
|
|
23
|
+
|
|
24
|
+
CREATE TABLE reserved_names.names (
|
|
25
|
+
kind text NOT NULL REFERENCES reserved_names.kinds (kind),
|
|
26
|
+
name text NOT NULL CHECK (
|
|
27
|
+
name <> ''
|
|
28
|
+
AND name = lower(name)
|
|
29
|
+
),
|
|
30
|
+
match text NOT NULL DEFAULT 'exact' CHECK (match IN ('exact', 'prefix', 'suffix')),
|
|
31
|
+
reason text NOT NULL REFERENCES reserved_names.reasons (reason),
|
|
32
|
+
PRIMARY KEY (kind, name, match)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
COMMENT ON TABLE reserved_names.names IS 'Names nobody may claim, by kind. Changed only by migrations.';
|
|
36
|
+
|
|
37
|
+
COMMENT ON COLUMN reserved_names.names.name IS 'The reserved name, lowercase';
|
|
38
|
+
|
|
39
|
+
COMMENT ON COLUMN reserved_names.names.match IS 'exact: the name itself; prefix: every name starting with it; suffix: every name ending with it';
|
|
40
|
+
|
|
41
|
+
GRANT SELECT ON reserved_names.names TO PUBLIC;
|
|
42
|
+
|
|
43
|
+
CREATE INDEX names_reason_idx ON reserved_names.names (reason);
|
|
44
|
+
|
|
45
|
+
CREATE FUNCTION reserved_names.reserved_by(
|
|
46
|
+
kind text,
|
|
47
|
+
name text
|
|
48
|
+
) RETURNS SETOF reserved_names.names AS $EOFCODE$
|
|
49
|
+
SELECT n.*
|
|
50
|
+
FROM reserved_names.names n
|
|
51
|
+
WHERE n.kind = pg_catalog.lower(reserved_by.kind)
|
|
52
|
+
AND CASE n.match
|
|
53
|
+
WHEN 'exact' THEN n.name = pg_catalog.lower(reserved_by.name)
|
|
54
|
+
WHEN 'prefix' THEN pg_catalog.starts_with(pg_catalog.lower(reserved_by.name), n.name)
|
|
55
|
+
WHEN 'suffix' THEN pg_catalog.right(pg_catalog.lower(reserved_by.name), pg_catalog.length(n.name)) = n.name
|
|
56
|
+
END;
|
|
57
|
+
$EOFCODE$ LANGUAGE sql STABLE;
|
|
58
|
+
|
|
59
|
+
COMMENT ON FUNCTION reserved_names.reserved_by(text, text) IS 'The entries (with their reason) that reserve name for kind; empty when it is free';
|
|
60
|
+
|
|
61
|
+
GRANT EXECUTE ON FUNCTION reserved_names.reserved_by(text, text) TO PUBLIC;
|
|
62
|
+
|
|
63
|
+
CREATE FUNCTION reserved_names.is_reserved(
|
|
64
|
+
kind text,
|
|
65
|
+
name text
|
|
66
|
+
) RETURNS boolean AS $EOFCODE$
|
|
67
|
+
SELECT EXISTS (SELECT 1 FROM reserved_names.reserved_by(is_reserved.kind, is_reserved.name));
|
|
68
|
+
$EOFCODE$ LANGUAGE sql STABLE;
|
|
69
|
+
|
|
70
|
+
COMMENT ON FUNCTION reserved_names.is_reserved(text, text) IS 'True when name (any case) is reserved for kind';
|
|
71
|
+
|
|
72
|
+
GRANT EXECUTE ON FUNCTION reserved_names.is_reserved(text, text) TO PUBLIC;
|
|
73
|
+
|
|
74
|
+
INSERT INTO reserved_names.kinds (
|
|
75
|
+
kind,
|
|
76
|
+
description
|
|
77
|
+
) VALUES
|
|
78
|
+
('subdomain', 'A label under a shared domain, like login in login.example.com'),
|
|
79
|
+
('username', 'A person''s handle'),
|
|
80
|
+
('domain', 'A whole domain name, like example.com'),
|
|
81
|
+
('slug', 'A name in a URL path, like an organization or app slug');
|
|
82
|
+
|
|
83
|
+
INSERT INTO reserved_names.reasons (
|
|
84
|
+
reason,
|
|
85
|
+
description
|
|
86
|
+
) VALUES
|
|
87
|
+
('platform', 'Reads as the platform''s own (login, admin, status, ...)'),
|
|
88
|
+
('brand', 'Our brand or product names'),
|
|
89
|
+
('lookalike', 'Can be made to look like another name, e.g. punycode'),
|
|
90
|
+
('abuse', 'Offensive or used for abuse');
|
|
91
|
+
|
|
92
|
+
INSERT INTO reserved_names.names (
|
|
93
|
+
kind,
|
|
94
|
+
name,
|
|
95
|
+
match,
|
|
96
|
+
reason
|
|
97
|
+
) VALUES
|
|
98
|
+
('subdomain', 'www', 'exact', 'platform'),
|
|
99
|
+
('subdomain', 'api', 'exact', 'platform'),
|
|
100
|
+
('subdomain', 'auth', 'exact', 'platform'),
|
|
101
|
+
('subdomain', 'login', 'exact', 'platform'),
|
|
102
|
+
('subdomain', 'logout', 'exact', 'platform'),
|
|
103
|
+
('subdomain', 'signin', 'exact', 'platform'),
|
|
104
|
+
('subdomain', 'signup', 'exact', 'platform'),
|
|
105
|
+
('subdomain', 'register', 'exact', 'platform'),
|
|
106
|
+
('subdomain', 'admin', 'exact', 'platform'),
|
|
107
|
+
('subdomain', 'administrator', 'exact', 'platform'),
|
|
108
|
+
('subdomain', 'root', 'exact', 'platform'),
|
|
109
|
+
('subdomain', 'app', 'exact', 'platform'),
|
|
110
|
+
('subdomain', 'apps', 'exact', 'platform'),
|
|
111
|
+
('subdomain', 'dashboard', 'exact', 'platform'),
|
|
112
|
+
('subdomain', 'console', 'exact', 'platform'),
|
|
113
|
+
('subdomain', 'account', 'exact', 'platform'),
|
|
114
|
+
('subdomain', 'accounts', 'exact', 'platform'),
|
|
115
|
+
('subdomain', 'billing', 'exact', 'platform'),
|
|
116
|
+
('subdomain', 'pay', 'exact', 'platform'),
|
|
117
|
+
('subdomain', 'payments', 'exact', 'platform'),
|
|
118
|
+
('subdomain', 'checkout', 'exact', 'platform'),
|
|
119
|
+
('subdomain', 'support', 'exact', 'platform'),
|
|
120
|
+
('subdomain', 'help', 'exact', 'platform'),
|
|
121
|
+
('subdomain', 'status', 'exact', 'platform'),
|
|
122
|
+
('subdomain', 'docs', 'exact', 'platform'),
|
|
123
|
+
('subdomain', 'mail', 'exact', 'platform'),
|
|
124
|
+
('subdomain', 'email', 'exact', 'platform'),
|
|
125
|
+
('subdomain', 'smtp', 'exact', 'platform'),
|
|
126
|
+
('subdomain', 'imap', 'exact', 'platform'),
|
|
127
|
+
('subdomain', 'ftp', 'exact', 'platform'),
|
|
128
|
+
('subdomain', 'ns', 'exact', 'platform'),
|
|
129
|
+
('subdomain', 'ns1', 'exact', 'platform'),
|
|
130
|
+
('subdomain', 'ns2', 'exact', 'platform'),
|
|
131
|
+
('subdomain', 'mx', 'exact', 'platform'),
|
|
132
|
+
('subdomain', 'cdn', 'exact', 'platform'),
|
|
133
|
+
('subdomain', 'static', 'exact', 'platform'),
|
|
134
|
+
('subdomain', 'assets', 'exact', 'platform'),
|
|
135
|
+
('subdomain', 'files', 'exact', 'platform'),
|
|
136
|
+
('subdomain', 'storage', 'exact', 'platform'),
|
|
137
|
+
('subdomain', 'media', 'exact', 'platform'),
|
|
138
|
+
('subdomain', 'ws', 'exact', 'platform'),
|
|
139
|
+
('subdomain', 'wss', 'exact', 'platform'),
|
|
140
|
+
('subdomain', 'graphql', 'exact', 'platform'),
|
|
141
|
+
('subdomain', 'modules', 'exact', 'platform'),
|
|
142
|
+
('subdomain', 'agent', 'exact', 'platform'),
|
|
143
|
+
('subdomain', 'compute', 'exact', 'platform'),
|
|
144
|
+
('subdomain', 'machines', 'exact', 'platform'),
|
|
145
|
+
('subdomain', 'cluster', 'exact', 'platform'),
|
|
146
|
+
('subdomain', 'migrate', 'exact', 'platform'),
|
|
147
|
+
('subdomain', 'internal', 'exact', 'platform'),
|
|
148
|
+
('subdomain', 'system', 'exact', 'platform'),
|
|
149
|
+
('subdomain', 'platform', 'exact', 'platform'),
|
|
150
|
+
('subdomain', 'security', 'exact', 'platform'),
|
|
151
|
+
('subdomain', 'sso', 'exact', 'platform'),
|
|
152
|
+
('subdomain', 'oauth', 'exact', 'platform'),
|
|
153
|
+
('subdomain', 'id', 'exact', 'platform'),
|
|
154
|
+
('subdomain', 'identity', 'exact', 'platform'),
|
|
155
|
+
('subdomain', 'verify', 'exact', 'platform'),
|
|
156
|
+
('subdomain', 'secure', 'exact', 'platform'),
|
|
157
|
+
('subdomain', 'xn--', 'prefix', 'lookalike');
|