@rebasepro/utils 0.8.0 → 0.9.1-canary.09aaf62
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/dist/index.d.ts +2 -0
- package/dist/index.es.js +156 -3
- package/dist/index.es.js.map +1 -1
- package/dist/objects.d.ts +1 -1
- package/dist/policy-names.d.ts +27 -0
- package/dist/sha1.d.ts +20 -0
- package/dist/strings.d.ts +10 -0
- package/package.json +8 -8
- package/src/index.ts +2 -0
- package/src/objects.ts +1 -1
- package/src/policy-names.ts +65 -0
- package/src/sha1.ts +98 -0
- package/src/strings.ts +16 -1
- package/dist/index.umd.js +0 -633
- package/dist/index.umd.js.map +0 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SecurityOperation, SecurityRule } from "@rebasepro/types";
|
|
2
|
+
import { sha1Hex } from "./sha1";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Naming of the Postgres policies generated from a collection's security rules.
|
|
6
|
+
*
|
|
7
|
+
* A rule without an explicit `name` is compiled to `<table>_<op>_<hash>`, where
|
|
8
|
+
* the hash covers the rule's semantics. The Studio needs the same names to tell
|
|
9
|
+
* "this policy came from your code" apart from "someone wrote this in SQL" —
|
|
10
|
+
* without them it treats generated policies as foreign and offers to import
|
|
11
|
+
* them back into the codebase they came from.
|
|
12
|
+
*
|
|
13
|
+
* This is the single definition of that naming. The DDL and Drizzle generators
|
|
14
|
+
* both derive names from here, so a change cannot silently rename every policy
|
|
15
|
+
* in every deployed database while the UI keeps matching the old ones.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Stable digest of the parts of a rule that determine what the policy does. */
|
|
19
|
+
export function getPolicyNameHash(rule: SecurityRule): string {
|
|
20
|
+
const data = JSON.stringify({
|
|
21
|
+
a: rule.access,
|
|
22
|
+
m: rule.mode,
|
|
23
|
+
op: rule.operation,
|
|
24
|
+
ops: rule.operations?.slice().sort(),
|
|
25
|
+
own: rule.ownerField,
|
|
26
|
+
rol: rule.roles?.slice().sort(),
|
|
27
|
+
pg: rule.pgRoles?.slice().sort(),
|
|
28
|
+
u: rule.using,
|
|
29
|
+
w: rule.withCheck,
|
|
30
|
+
c: rule.condition,
|
|
31
|
+
ch: rule.check
|
|
32
|
+
});
|
|
33
|
+
return sha1Hex(data).substring(0, 7);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The operations a rule expands to — `operations` wins over `operation`. */
|
|
37
|
+
export function getPolicyOperations(rule: SecurityRule): readonly SecurityOperation[] {
|
|
38
|
+
return rule.operations && rule.operations.length > 0
|
|
39
|
+
? rule.operations
|
|
40
|
+
: [rule.operation ?? "all"];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every Postgres policy name a single rule compiles to — one per operation.
|
|
45
|
+
*
|
|
46
|
+
* @param rule The security rule as written in the collection config.
|
|
47
|
+
* @param tableName The rule's table (see `getTableName` in `@rebasepro/common`).
|
|
48
|
+
*/
|
|
49
|
+
export function getPolicyNamesForRule(rule: SecurityRule, tableName: string): string[] {
|
|
50
|
+
const ops = getPolicyOperations(rule);
|
|
51
|
+
const ruleHash = getPolicyNameHash(rule);
|
|
52
|
+
|
|
53
|
+
return ops.map((op, opIdx) => rule.name
|
|
54
|
+
? (ops.length > 1 ? `${rule.name}_${op}` : rule.name)
|
|
55
|
+
: `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Every policy name a set of rules compiles to, for membership checks. */
|
|
59
|
+
export function getPolicyNamesForRules(rules: SecurityRule[], tableName: string): Set<string> {
|
|
60
|
+
const names = new Set<string>();
|
|
61
|
+
for (const rule of rules) {
|
|
62
|
+
for (const name of getPolicyNamesForRule(rule, tableName)) names.add(name);
|
|
63
|
+
}
|
|
64
|
+
return names;
|
|
65
|
+
}
|
package/src/sha1.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SHA-1 implementation that runs in both Node and the browser.
|
|
3
|
+
*
|
|
4
|
+
* This exists because generated Postgres policy names embed a SHA-1 digest of
|
|
5
|
+
* the security rule. The DDL generator runs on the server (where `node:crypto`
|
|
6
|
+
* is available) but the Studio has to derive the same names in the browser to
|
|
7
|
+
* tell a policy it generated apart from one it did not. `node:crypto` cannot be
|
|
8
|
+
* bundled for the browser, so the shared derivation needs a portable digest.
|
|
9
|
+
*
|
|
10
|
+
* SHA-1 is used purely to name things deterministically — never for security.
|
|
11
|
+
* The output is byte-identical to `createHash("sha1").update(str).digest("hex")`,
|
|
12
|
+
* which `sha1.test.ts` pins against `node:crypto` directly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Rotate a 32-bit word left by `n` bits. */
|
|
16
|
+
function rotl(value: number, n: number): number {
|
|
17
|
+
return (value << n) | (value >>> (32 - n));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* SHA-1 digest of a string, hex-encoded.
|
|
22
|
+
*
|
|
23
|
+
* The input is encoded as UTF-8, matching Node's default handling of strings
|
|
24
|
+
* passed to `hash.update(str)`.
|
|
25
|
+
*/
|
|
26
|
+
export function sha1Hex(input: string): string {
|
|
27
|
+
const bytes: number[] = Array.from(new TextEncoder().encode(input));
|
|
28
|
+
const bitLength = bytes.length * 8;
|
|
29
|
+
|
|
30
|
+
// Padding: 0x80, then zeroes up to 56 bytes mod 64, then the length as a
|
|
31
|
+
// 64-bit big-endian integer.
|
|
32
|
+
bytes.push(0x80);
|
|
33
|
+
while (bytes.length % 64 !== 56) bytes.push(0);
|
|
34
|
+
|
|
35
|
+
const hi = Math.floor(bitLength / 0x100000000);
|
|
36
|
+
const lo = bitLength >>> 0;
|
|
37
|
+
bytes.push((hi >>> 24) & 0xff, (hi >>> 16) & 0xff, (hi >>> 8) & 0xff, hi & 0xff);
|
|
38
|
+
bytes.push((lo >>> 24) & 0xff, (lo >>> 16) & 0xff, (lo >>> 8) & 0xff, lo & 0xff);
|
|
39
|
+
|
|
40
|
+
let h0 = 0x67452301;
|
|
41
|
+
let h1 = 0xefcdab89;
|
|
42
|
+
let h2 = 0x98badcfe;
|
|
43
|
+
let h3 = 0x10325476;
|
|
44
|
+
let h4 = 0xc3d2e1f0;
|
|
45
|
+
|
|
46
|
+
const w = new Array<number>(80);
|
|
47
|
+
|
|
48
|
+
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
49
|
+
for (let i = 0; i < 16; i++) {
|
|
50
|
+
const j = offset + i * 4;
|
|
51
|
+
w[i] = ((bytes[j] << 24) | (bytes[j + 1] << 16) | (bytes[j + 2] << 8) | bytes[j + 3]) | 0;
|
|
52
|
+
}
|
|
53
|
+
for (let i = 16; i < 80; i++) {
|
|
54
|
+
w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let a = h0;
|
|
58
|
+
let b = h1;
|
|
59
|
+
let c = h2;
|
|
60
|
+
let d = h3;
|
|
61
|
+
let e = h4;
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < 80; i++) {
|
|
64
|
+
let f: number;
|
|
65
|
+
let k: number;
|
|
66
|
+
if (i < 20) {
|
|
67
|
+
f = (b & c) | (~b & d);
|
|
68
|
+
k = 0x5a827999;
|
|
69
|
+
} else if (i < 40) {
|
|
70
|
+
f = b ^ c ^ d;
|
|
71
|
+
k = 0x6ed9eba1;
|
|
72
|
+
} else if (i < 60) {
|
|
73
|
+
f = (b & c) | (b & d) | (c & d);
|
|
74
|
+
k = 0x8f1bbcdc;
|
|
75
|
+
} else {
|
|
76
|
+
f = b ^ c ^ d;
|
|
77
|
+
k = 0xca62c1d6;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const temp = (rotl(a, 5) + f + e + k + w[i]) | 0;
|
|
81
|
+
e = d;
|
|
82
|
+
d = c;
|
|
83
|
+
c = rotl(b, 30);
|
|
84
|
+
b = a;
|
|
85
|
+
a = temp;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
h0 = (h0 + a) | 0;
|
|
89
|
+
h1 = (h1 + b) | 0;
|
|
90
|
+
h2 = (h2 + c) | 0;
|
|
91
|
+
h3 = (h3 + d) | 0;
|
|
92
|
+
h4 = (h4 + e) | 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [h0, h1, h2, h3, h4]
|
|
96
|
+
.map(word => (word >>> 0).toString(16).padStart(8, "0"))
|
|
97
|
+
.join("");
|
|
98
|
+
}
|
package/src/strings.ts
CHANGED
|
@@ -37,8 +37,23 @@ export function camelCase(str: string): string {
|
|
|
37
37
|
.join("");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* A random base-36 string of exactly `strLength` characters.
|
|
42
|
+
*
|
|
43
|
+
* Not `Math.random().toString(36).slice(2, 2 + strLength)`: that has no
|
|
44
|
+
* guaranteed length. Base-36 of a double drops trailing zeros, so the source
|
|
45
|
+
* string is short about once in 36 calls and the slice quietly returns fewer
|
|
46
|
+
* characters than asked for — `randomString(10)` returning 9. These values
|
|
47
|
+
* prefix uploaded filenames to keep them apart, so a short one is a likelier
|
|
48
|
+
* collision, and it fails at the rate that makes a test look flaky.
|
|
49
|
+
*/
|
|
40
50
|
export function randomString(strLength = 5) {
|
|
41
|
-
|
|
51
|
+
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
52
|
+
let result = "";
|
|
53
|
+
for (let i = 0; i < strLength; i++) {
|
|
54
|
+
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
export function randomColor() {
|