@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.
- package/dist/ApiKeysView-DiCurTEU.js +1224 -0
- package/dist/ApiKeysView-DiCurTEU.js.map +1 -0
- package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +6 -0
- package/dist/components/ApiKeys/permissions.d.ts +66 -0
- package/dist/index.es.js +1 -1
- package/package.json +9 -9
- package/src/components/ApiKeys/ApiKeysView.tsx +69 -211
- package/src/components/ApiKeys/CreateApiKeyDialog.tsx +711 -0
- package/src/components/ApiKeys/permissions.ts +145 -0
- package/dist/ApiKeysView-C_UoUPuR.js +0 -728
- package/dist/ApiKeysView-C_UoUPuR.js.map +0 -1
|
@@ -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
|
+
}
|