@rebasepro/studio 0.17.2 → 0.17.3-canary.gdd23447

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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The vocabulary of an API key's permission entries.
3
+ *
4
+ * A permission entry is `{ collection, operations }`, but `collection` is not
5
+ * only a collection: the same field addresses three namespaces, and the guard
6
+ * that enforces them lives in `@rebasepro/server`
7
+ * (`auth/api-keys/api-key-permission-guard.ts`):
8
+ *
9
+ * - `"*"` — every collection, every custom function, and storage
10
+ * - `"storage"` — the storage routes
11
+ * - `"functions"` — every custom function, including the function index
12
+ * - `"functions/<name>"`— one named function
13
+ * - anything else — the collection with that slug
14
+ *
15
+ * The UI used to label the field "Collection slug or *", which made two of
16
+ * those namespaces undiscoverable and made `"*"` read as "all collections"
17
+ * when it also hands over storage and every function. This module is the one
18
+ * place that knows the mapping, so the picker, the row description, the grant
19
+ * summary and the detail panel cannot drift from each other.
20
+ *
21
+ * @module
22
+ */
23
+
24
+ import type { ApiKeyPermission } from "@rebasepro/types";
25
+
26
+ /** Grants every collection, every function, and storage. */
27
+ export const RESOURCE_EVERYTHING = "*";
28
+ /** Grants the storage routes. */
29
+ export const RESOURCE_STORAGE = "storage";
30
+ /** Grants every custom function. */
31
+ export const RESOURCE_ALL_FUNCTIONS = "functions";
32
+ /** Prefix addressing a single custom function. */
33
+ export const FUNCTION_PREFIX = "functions/";
34
+
35
+ export type ResourceKind =
36
+ | "everything"
37
+ | "storage"
38
+ | "all-functions"
39
+ | "function"
40
+ | "collection";
41
+
42
+ export interface ParsedResource {
43
+ kind: ResourceKind;
44
+ /** The function name for `"function"`, the slug for `"collection"`. */
45
+ name: string;
46
+ }
47
+
48
+ /** Classify a raw `collection` field into the namespace it addresses. */
49
+ export function parseResource(collection: string): ParsedResource {
50
+ const value = collection.trim();
51
+ if (value === RESOURCE_EVERYTHING) return { kind: "everything", name: "" };
52
+ if (value === RESOURCE_STORAGE) return { kind: "storage", name: "" };
53
+ if (value === RESOURCE_ALL_FUNCTIONS) return { kind: "all-functions", name: "" };
54
+ if (value.startsWith(FUNCTION_PREFIX)) {
55
+ return { kind: "function", name: value.slice(FUNCTION_PREFIX.length) };
56
+ }
57
+ return { kind: "collection", name: value };
58
+ }
59
+
60
+ /**
61
+ * Short label for a resource — what a picker or a chip shows.
62
+ *
63
+ * Deliberately not the raw value: `"*"` alone is the thing nobody could read.
64
+ */
65
+ export function resourceLabel(collection: string): string {
66
+ const { kind, name } = parseResource(collection);
67
+ switch (kind) {
68
+ case "everything": return "Everything";
69
+ case "storage": return "Storage";
70
+ case "all-functions": return "All functions";
71
+ case "function": return name ? `${name}()` : "Function";
72
+ case "collection": return name || "—";
73
+ }
74
+ }
75
+
76
+ /**
77
+ * The resource as a sentence fragment, for "this key can read <fragment>".
78
+ *
79
+ * `"everything"` spells out all three namespaces, because that is exactly the
80
+ * fact the old `*` input hid.
81
+ */
82
+ export function resourcePhrase(collection: string): string {
83
+ const { kind, name } = parseResource(collection);
84
+ switch (kind) {
85
+ case "everything": return "every collection, every custom function and storage";
86
+ case "storage": return "storage";
87
+ case "all-functions": return "every custom function";
88
+ case "function": return name ? `the ${name} function` : "one function";
89
+ case "collection": return name ? `the ${name} collection` : "an unnamed collection";
90
+ }
91
+ }
92
+
93
+ /** How each operation reads for a given namespace, so the sentence stays true. */
94
+ function operationPhrase(kind: ResourceKind, operation: string): string {
95
+ if (kind === "all-functions" || kind === "function") {
96
+ // Functions are invoked, not written to; the HTTP method still picks
97
+ // the operation, so a POST-only function needs `write`.
98
+ return operation === "read" ? "call (GET)"
99
+ : operation === "write" ? "call (POST, PUT, PATCH)"
100
+ : "call (DELETE)";
101
+ }
102
+ if (kind === "storage") {
103
+ return operation === "read" ? "download from"
104
+ : operation === "write" ? "upload to"
105
+ : "delete from";
106
+ }
107
+ return operation;
108
+ }
109
+
110
+ /** Join with an Oxford-less "and", the way the rest of the panel reads. */
111
+ function joinPhrases(parts: string[]): string {
112
+ if (parts.length <= 1) return parts[0] ?? "";
113
+ return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
114
+ }
115
+
116
+ /**
117
+ * One plain sentence per permission entry: what the key will actually be able
118
+ * to do. An entry with no operations selected grants nothing and says so,
119
+ * rather than being silently dropped at submit time.
120
+ */
121
+ export function grantSentence(perm: ApiKeyPermission): string {
122
+ const { kind } = parseResource(perm.collection);
123
+ const target = resourcePhrase(perm.collection);
124
+ if (perm.operations.length === 0) return `No access to ${target}`;
125
+ const verbs = joinPhrases(perm.operations.map(op => operationPhrase(kind, op)));
126
+ const verb = verbs.charAt(0).toUpperCase() + verbs.slice(1);
127
+ return `${verb} ${target}`;
128
+ }
129
+
130
+ /**
131
+ * Dense one-line summary for list rows and the created-key confirmation.
132
+ *
133
+ * The wildcard wins over everything else in the array, because the guard
134
+ * returns on the first match — a key holding `*` is a full-access key no
135
+ * matter what else is listed beside it.
136
+ */
137
+ export function permissionSummary(perms: ApiKeyPermission[]): string {
138
+ if (perms.length === 0) return "No permissions";
139
+ const wildcard = perms.find(p => p.collection === RESOURCE_EVERYTHING);
140
+ if (wildcard) return `Everything (${wildcard.operations.join(", ")})`;
141
+ if (perms.length === 1) {
142
+ return `${resourceLabel(perms[0].collection)} (${perms[0].operations.join(", ")})`;
143
+ }
144
+ return `${perms.length} resources`;
145
+ }