@riducms/admin 0.3.0 → 0.4.0

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/package.json CHANGED
@@ -21,19 +21,21 @@
21
21
  "@iconify/json": "^2.2.509",
22
22
  "@internationalized/date": "^3.12.3",
23
23
  "@lezer/highlight": "^1.2.3",
24
- "@riducms/plugin": "0.3.0",
25
- "@riducms/protocol": "0.3.0",
26
- "@riducms/sdk": "0.3.0",
27
- "@riducms/translations": "0.3.0",
28
- "@riducms/ui": "0.3.0",
24
+ "@riducms/plugin": "0.4.0",
25
+ "@riducms/protocol": "0.4.0",
26
+ "@riducms/sdk": "0.4.0",
27
+ "@riducms/translations": "0.4.0",
28
+ "@riducms/ui": "0.4.0",
29
+ "@twinkleplop/bash": "0.1.5",
30
+ "@twinkleplop/go": "0.1.5",
31
+ "@twinkleplop/typescript": "0.1.5",
29
32
  "bits-ui": "^2.18.1",
30
- "shiki": "4.4.3",
31
33
  "svelte-sonner": "^1.1.1"
32
34
  },
33
35
  "description": "Framework-owned Svelte 5 authoring application embedded by Ridu servers.",
34
36
  "devDependencies": {
35
37
  "@hvniel/lexical-svelte": "^0.1.1",
36
- "@riducms/build": "0.3.0",
38
+ "@riducms/build": "0.4.0",
37
39
  "svelte": "^5.57.0"
38
40
  },
39
41
  "exports": {
@@ -72,5 +74,5 @@
72
74
  "test:unit": "bun test tests"
73
75
  },
74
76
  "type": "module",
75
- "version": "0.3.0"
77
+ "version": "0.4.0"
76
78
  }
@@ -455,7 +455,7 @@ export class AdminRuntime {
455
455
  const accessAuthoritative = await this.refreshAccess(this.contentLocale);
456
456
  if (!accessAuthoritative || this.session === undefined || this.adminAccessAllowed) return;
457
457
  try {
458
- await this.client.logout();
458
+ await this.client.auth.logout();
459
459
  } catch {
460
460
  // The local session is still denied even if server cleanup fails.
461
461
  }
@@ -471,21 +471,18 @@ export class AdminRuntime {
471
471
  if (request !== this.#manifestRequest) return;
472
472
  const admin = this.#installManifest(manifest);
473
473
  if (admin !== undefined) {
474
- const bootstrap = await this.client.authBootstrap(admin.userCollectionSlug);
474
+ const bootstrap = await this.client.auth.bootstrap({
475
+ collection: admin.userCollectionSlug,
476
+ });
475
477
  if (request !== this.#manifestRequest) return;
476
478
  this.authBootstrapAvailable = bootstrap.available;
477
479
  if (bootstrap.available) {
478
480
  this.session = undefined;
479
481
  } else {
480
- try {
481
- const session = await this.client.session();
482
- if (request !== this.#manifestRequest) return;
483
- this.session = session.collection === admin.userCollectionSlug ? session : undefined;
484
- } catch (error) {
485
- if (request !== this.#manifestRequest) return;
486
- if (!(error instanceof RiduError) || error.code !== "access_denied") throw error;
487
- this.session = undefined;
488
- }
482
+ // getSession resolves null without a session and rejects on an outage.
483
+ const session = await this.client.auth.getSession();
484
+ if (request !== this.#manifestRequest) return;
485
+ this.session = session?.collection === admin.userCollectionSlug ? session : undefined;
489
486
  }
490
487
  } else {
491
488
  this.session = undefined;
@@ -513,7 +510,9 @@ export class AdminRuntime {
513
510
  this.session = undefined;
514
511
  }
515
512
  if (admin !== undefined && this.session === undefined) {
516
- const bootstrap = await this.client.authBootstrap(admin.userCollectionSlug);
513
+ const bootstrap = await this.client.auth.bootstrap({
514
+ collection: admin.userCollectionSlug,
515
+ });
517
516
  if (request !== this.#manifestRequest) return;
518
517
  this.authBootstrapAvailable = bootstrap.available;
519
518
  } else {
@@ -336,7 +336,7 @@
336
336
  const request = beginProfileRequest();
337
337
 
338
338
  try {
339
- await runtime.client.logout({ signal: request.signal });
339
+ await runtime.client.auth.logout({ signal: request.signal });
340
340
  if (!ownsProfileRequest(request, owner)) return;
341
341
 
342
342
  await runtime.hardNavigate(adminRoutePatterns.login);
@@ -47,8 +47,10 @@ export class AccountSecurityController {
47
47
  this.error = undefined;
48
48
  try {
49
49
  const [sessions, apiKeys] = await Promise.all([
50
- this.client.sessions({ signal: request.signal }),
51
- this.apiKeysEnabled ? this.client.apiKeys({ signal: request.signal }) : Promise.resolve([]),
50
+ this.client.auth.sessions({ signal: request.signal }),
51
+ this.apiKeysEnabled
52
+ ? this.client.auth.apiKeys({ signal: request.signal })
53
+ : Promise.resolve([]),
52
54
  ]);
53
55
  if (request.signal.aborted || generation !== this.#generation) return;
54
56
  this.sessions = sessions;
@@ -64,7 +66,7 @@ export class AccountSecurityController {
64
66
  revokeSession = async (id: string) => {
65
67
  return this.#run(
66
68
  "session:" + id,
67
- (signal) => this.client.revokeSession(id, { signal }),
69
+ (signal) => this.client.auth.revokeSession(id, { signal }),
68
70
  () => (this.sessions = this.sessions.filter((session) => session.id !== id))
69
71
  );
70
72
  };
@@ -72,7 +74,7 @@ export class AccountSecurityController {
72
74
  logoutAll = async () => {
73
75
  return this.#run(
74
76
  "logout-all",
75
- (signal) => this.client.logoutAll({ signal }),
77
+ (signal) => this.client.auth.logoutAll({ signal }),
76
78
  () => (this.sessions = [])
77
79
  );
78
80
  };
@@ -80,7 +82,7 @@ export class AccountSecurityController {
80
82
  createAPIKey = async (name: string, expiresAt?: string) => {
81
83
  return this.#run(
82
84
  "api-key:create",
83
- (signal) => this.client.createAPIKey({ name, expiresAt }, { signal }),
85
+ (signal) => this.client.auth.createAPIKey({ name, expiresAt }, { signal }),
84
86
  (created) => {
85
87
  this.createdAPIKey = created;
86
88
  this.apiKeys = [
@@ -103,7 +105,7 @@ export class AccountSecurityController {
103
105
  revokeAPIKey = async (id: string) => {
104
106
  return this.#run(
105
107
  "api-key:" + id,
106
- (signal) => this.client.revokeAPIKey(id, { signal }),
108
+ (signal) => this.client.auth.revokeAPIKey(id, { signal }),
107
109
  () => {
108
110
  this.apiKeys = this.apiKeys.filter((key) => key.id !== id);
109
111
  if (this.createdAPIKey?.id === id) this.createdAPIKey = undefined;
@@ -113,7 +115,7 @@ export class AccountSecurityController {
113
115
 
114
116
  changePassword = async (currentPassword: string, password: string) => {
115
117
  return this.#run("password", (signal) =>
116
- this.client.changePassword(currentPassword, password, { signal })
118
+ this.client.auth.changePassword({ currentPassword, password }, { signal })
117
119
  );
118
120
  };
119
121
 
@@ -194,7 +194,7 @@
194
194
  hostRequest = request;
195
195
 
196
196
  try {
197
- await runtime.client.logout({ signal: request.signal });
197
+ await runtime.client.auth.logout({ signal: request.signal });
198
198
  if (request.signal.aborted || runtime.session?.id !== owner) return;
199
199
 
200
200
  await runtime.hardNavigate(adminRoutePatterns.login);
@@ -1,44 +1,43 @@
1
1
  <script lang="ts" module>
2
- import { createHighlighterCoreSync } from "shiki/core";
3
- import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
4
- import typescript from "shiki/langs/typescript.mjs";
5
- import shell from "shiki/langs/shellscript.mjs";
6
- import go from "shiki/langs/go.mjs";
7
- import { riduCodeTheme } from "@riducms/ui/code-theme";
2
+ import { tokenize as tokenizeTypeScript } from "@twinkleplop/typescript";
3
+ import { tokenize as tokenizeBash } from "@twinkleplop/bash";
4
+ import { tokenize as tokenizeGo } from "@twinkleplop/go";
8
5
  import "@fontsource-variable/martian-mono";
6
+ import type { ReferenceLanguage } from "@admin/features/api-reference/api-reference-examples";
7
+
8
+ // Keep the grammars in the drawer's lazy chunk and reuse their tokenizers across examples.
9
+ const tokenizers = {
10
+ typescript: tokenizeTypeScript(),
11
+ curl: tokenizeBash(),
12
+ go: tokenizeGo(),
13
+ };
9
14
 
10
- // This module belongs to the drawer's lazy chunk. Reuse one tokenizer across examples.
11
- const highlighter = createHighlighterCoreSync({
12
- engine: createJavaScriptRegexEngine(),
13
- themes: [riduCodeTheme],
14
- langs: [typescript, shell, go],
15
- });
15
+ function highlightedTokens(code: string, language: ReferenceLanguage) {
16
+ const { tokens, token_types } = tokenizers[language](code);
17
+ const parts: { content: string; type?: string }[] = [];
18
+ let offset = 0;
19
+
20
+ for (let index = 0; index < tokens.length; index += 3) {
21
+ const start = tokens[index + 1];
22
+ const end = tokens[index + 2];
23
+ if (start > offset) parts.push({ content: code.slice(offset, start) });
24
+ parts.push({ content: code.slice(start, end), type: token_types[tokens[index]] });
25
+ offset = end;
26
+ }
27
+
28
+ if (offset < code.length) parts.push({ content: code.slice(offset) });
29
+ return parts;
30
+ }
16
31
  </script>
17
32
 
18
33
  <script lang="ts">
19
- import type { ReferenceLanguage } from "@admin/features/api-reference/api-reference-examples";
20
-
21
34
  let { code, language }: { code: string; language: ReferenceLanguage } = $props();
22
-
23
- const highlighted = $derived(
24
- highlighter.codeToTokens(code, {
25
- lang: language === "curl" ? "shellscript" : language,
26
- theme: riduCodeTheme.name,
27
- })
28
- );
35
+ const highlighted = $derived(highlightedTokens(code, language));
29
36
  </script>
30
37
 
31
38
  <!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable examples need a keyboard scroll target.) -->
32
- <pre
33
- class="ridu-api-reference__code"
34
- dir="ltr"
35
- tabindex="0"
36
- style:--code-block-foreground={highlighted.fg}><code>{#each highlighted.tokens as line, index}{#if index > 0}{"\n"}{/if}{#each line as token}<span
37
- style:color={token.color}
38
- style:font-style={token.fontStyle && token.fontStyle & 1 ? "italic" : undefined}
39
- style:font-weight={token.fontStyle && token.fontStyle & 2
40
- ? "700"
41
- : undefined}>{token.content}</span>{/each}{/each}</code></pre>
39
+ <pre class="ridu-api-reference__code" dir="ltr" tabindex="0"><code>{#each highlighted as token}<span
40
+ class={token.type}>{token.content}</span>{/each}</code></pre>
42
41
 
43
42
  <style lang="scss">
44
43
  @layer ridu.components {
@@ -47,7 +46,8 @@
47
46
  padding: 18px 20px;
48
47
  max-height: 430px;
49
48
  overflow: auto;
50
- color: var(--code-block-foreground);
49
+ background: #0d0a0c;
50
+ color: #e9e6e1;
51
51
  border: 1px solid var(--border);
52
52
  border-radius: var(--radius-sm);
53
53
  font-family: "Martian Mono Variable", "Martian Mono", ui-monospace, monospace;
@@ -59,6 +59,56 @@
59
59
  code {
60
60
  font: inherit;
61
61
  }
62
+
63
+ :global(.comment) {
64
+ color: #7d7a8c;
65
+ font-style: italic;
66
+ }
67
+
68
+ :global(.keyword),
69
+ :global(.decorator) {
70
+ color: #ff7eb6;
71
+ }
72
+
73
+ :global(.string),
74
+ :global(.string_escape),
75
+ :global(.template),
76
+ :global(.regex) {
77
+ color: #b5e48c;
78
+ }
79
+
80
+ :global(.number),
81
+ :global(.boolean),
82
+ :global(.constant),
83
+ :global(.property) {
84
+ color: #c4a5ff;
85
+ }
86
+
87
+ :global(.function) {
88
+ color: #8fb4ff;
89
+ font-weight: 700;
90
+ }
91
+
92
+ :global(.type),
93
+ :global(.class_name) {
94
+ color: #7fdbec;
95
+ font-style: italic;
96
+ }
97
+
98
+ :global(.parameter) {
99
+ color: #ffb977;
100
+ font-style: italic;
101
+ }
102
+
103
+ :global(.builtin),
104
+ :global(.namespace) {
105
+ color: #8fb4ff;
106
+ }
107
+
108
+ :global(.punctuation),
109
+ :global(.operator) {
110
+ color: #80807e;
111
+ }
62
112
  }
63
113
  }
64
114
  </style>
@@ -127,7 +127,7 @@ export function referenceCode(
127
127
  break;
128
128
  case "create":
129
129
  if (request.authCreate)
130
- call = `const doc = await ridu.createAuthUser(${slug}, ${data}, "REPLACE_WITH_A_PASSWORD");`;
130
+ call = `const doc = await ridu.auth.createUser({\n collection: ${slug},\n data: ${data},\n password: "REPLACE_WITH_A_PASSWORD",\n});`;
131
131
  else if (request.uploadCreate)
132
132
  call = `async function createUpload(file: File) {\n const data: RiduConfig["collections"][${slug}]["create"] = ${data};\n return ridu.upload(${slug}, file, { data });\n}`;
133
133
  else call = `const doc = await ridu.create(${slug}, ${data});`;
@@ -77,9 +77,10 @@ export class CreateFirstUserController {
77
77
  let accountCreated = false;
78
78
  try {
79
79
  await this.form.submit(this.fields, true, (values) =>
80
- this.options.runtime.client.createAuthUser(collection.slug, values, this.password, {
81
- signal: request.signal,
82
- })
80
+ this.options.runtime.client.auth.createUser(
81
+ { collection: collection.slug, data: values, password: this.password },
82
+ { signal: request.signal }
83
+ )
83
84
  );
84
85
  accountCreated = true;
85
86
  if (!this.#current(generation, request)) return undefined;
@@ -88,9 +89,8 @@ export class CreateFirstUserController {
88
89
  if (typeof identity !== "string" || identity.trim() === "") {
89
90
  throw new Error(this.options.runtime.i18n.t("auth:firstAccountIdentityUnavailable"));
90
91
  }
91
- await this.options.runtime.client.login(
92
- collection.slug,
93
- { email: identity, password: this.password },
92
+ await this.options.runtime.client.auth.login(
93
+ { collection: collection.slug, email: identity, password: this.password },
94
94
  { signal: request.signal }
95
95
  );
96
96
  if (!this.#current(generation, request)) return undefined;
@@ -101,7 +101,7 @@ export class CreateFirstUserController {
101
101
  this.options.runtime.collectionOperations[collection.slug]?.admin !== true
102
102
  ) {
103
103
  try {
104
- await this.options.runtime.client.logout({ signal: request.signal });
104
+ await this.options.runtime.client.auth.logout({ signal: request.signal });
105
105
  } catch {
106
106
  // The setup route remains closed even if cookie cleanup fails.
107
107
  }
@@ -133,9 +133,10 @@ export class CreateFirstUserController {
133
133
  }
134
134
 
135
135
  try {
136
- const bootstrap = await this.options.runtime.client.authBootstrap(collection.slug, {
137
- signal: request.signal,
138
- });
136
+ const bootstrap = await this.options.runtime.client.auth.bootstrap(
137
+ { collection: collection.slug },
138
+ { signal: request.signal }
139
+ );
139
140
  if (!this.#current(generation, request)) return undefined;
140
141
  if (!bootstrap.available) {
141
142
  await this.#continueToLogin("completed");
@@ -25,7 +25,7 @@
25
25
  error = undefined;
26
26
 
27
27
  try {
28
- await runtime.client.requestPasswordReset(authCollection.slug, email);
28
+ await runtime.client.auth.requestPasswordReset({ collection: authCollection.slug, email });
29
29
  sent = true;
30
30
  } catch (cause) {
31
31
  error =
@@ -50,7 +50,7 @@
50
50
  throw new Error(runtime.i18n.t("auth:configuredCollectionUnavailable"));
51
51
  }
52
52
 
53
- await runtime.client.login(authCollection.slug, credentials);
53
+ await runtime.client.auth.login({ collection: authCollection.slug, ...credentials });
54
54
  const destinationLocale = runtime.contentLocaleForPath(postLoginPath);
55
55
  const destinationAccessAuthoritative = await runtime.refreshAccess(
56
56
  destinationLocale ?? runtime.contentLocale
@@ -60,7 +60,7 @@
60
60
  runtime.collectionOperations[authCollection.slug]?.admin !== true
61
61
  ) {
62
62
  try {
63
- await runtime.client.logout();
63
+ await runtime.client.auth.logout();
64
64
  } catch {
65
65
  // The shell remains denied even if server cleanup fails.
66
66
  }
@@ -18,7 +18,7 @@
18
18
  error = undefined;
19
19
 
20
20
  try {
21
- await runtime.client.logout();
21
+ await runtime.client.auth.logout();
22
22
  await runtime.hardNavigate(adminRoutePatterns.login);
23
23
  } catch (cause) {
24
24
  error = cause instanceof Error ? cause.message : runtime.i18n.t("account:signOutFailed");
@@ -25,7 +25,7 @@
25
25
  error = undefined;
26
26
 
27
27
  try {
28
- await runtime.client.requestVerification(authCollection.slug, email);
28
+ await runtime.client.auth.requestVerification({ collection: authCollection.slug, email });
29
29
  sent = true;
30
30
  } catch (cause) {
31
31
  error =
@@ -67,9 +67,12 @@
67
67
  pending = true;
68
68
 
69
69
  try {
70
- await runtime.client.resetPassword(authCollection.slug, token, password, {
71
- signal: activeRequest.signal,
72
- });
70
+ await runtime.client.auth.resetPassword(
71
+ { collection: authCollection.slug, token, password },
72
+ {
73
+ signal: activeRequest.signal,
74
+ }
75
+ );
73
76
  if (!current()) return;
74
77
  complete = true;
75
78
  password = "";
@@ -43,9 +43,12 @@
43
43
  error = undefined;
44
44
 
45
45
  try {
46
- await runtime.client.verifyEmail(authCollection.slug, token, {
47
- signal: activeRequest.signal,
48
- });
46
+ await runtime.client.auth.verifyEmail(
47
+ { collection: authCollection.slug, token },
48
+ {
49
+ signal: activeRequest.signal,
50
+ }
51
+ );
49
52
  if (!current()) return;
50
53
  complete = true;
51
54
  } catch (cause) {
@@ -395,9 +395,10 @@ export class DocumentController {
395
395
  this.#forceUnlockRequest = request;
396
396
  this.unlockOperation = true;
397
397
  try {
398
- await runtime.client.forceUnlock(this.collection.slug, this.currentDocument.id, {
399
- signal: request.signal,
400
- });
398
+ await runtime.client.auth.forceUnlock(
399
+ { collection: this.collection.slug, id: this.currentDocument.id },
400
+ { signal: request.signal }
401
+ );
401
402
  if (request.signal.aborted) return;
402
403
  notifications.success({ title: runtime.i18n.t("documents:accountUnlocked") });
403
404
  } catch (cause) {
@@ -545,10 +546,10 @@ export class DocumentController {
545
546
  if (this.collection?.capabilities.auth) {
546
547
  if (password === undefined)
547
548
  throw new Error(this.options.runtime.i18n.t("documents:enterNewAccountPassword"));
548
- return this.options.runtime.client.createAuthUser(collectionSlug, values, password, {
549
- signal: request.signal,
550
- locale: this.contentLocale,
551
- });
549
+ return this.options.runtime.client.auth.createUser(
550
+ { collection: collectionSlug, data: values, password },
551
+ { signal: request.signal, locale: this.contentLocale }
552
+ );
552
553
  }
553
554
  if (!this.uploadCollection) {
554
555
  return this.options.runtime.client.create(collectionSlug, values, {
@@ -485,10 +485,8 @@ export class ReferenceBrowserWorkflow {
485
485
  );
486
486
  }
487
487
  if (this.collection.capabilities.auth) {
488
- return this.options.runtime.client.createAuthUser(
489
- this.collection.slug,
490
- values,
491
- this.newUserPassword,
488
+ return this.options.runtime.client.auth.createUser(
489
+ { collection: this.collection.slug, data: values, password: this.newUserPassword },
492
490
  { signal: request.signal, locale: this.options.locale }
493
491
  );
494
492
  }
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { flushSync } from "svelte";
2
3
  import {
3
4
  Combobox,
4
5
  ComboboxInput,
@@ -33,13 +34,16 @@
33
34
  const values = $derived(selectManyValues(form.get(field.path)));
34
35
  const options = $derived(field.select?.options ?? []);
35
36
  const filtered = $derived(
36
- options.filter((option) =>
37
- option.label
38
- .toLocaleLowerCase(runtime.i18n.language)
39
- .includes(query.toLocaleLowerCase(runtime.i18n.language))
37
+ options.filter(
38
+ (option) =>
39
+ (!hasMany || !values.includes(option.value)) &&
40
+ option.label
41
+ .toLocaleLowerCase(runtime.i18n.language)
42
+ .includes(query.toLocaleLowerCase(runtime.i18n.language))
40
43
  )
41
44
  );
42
45
  const selectedLabel = $derived(options.find((option) => option.value === value)?.label ?? "");
46
+ const inputValue = $derived(open || hasMany ? query : selectedLabel);
43
47
  const placeholder = $derived(
44
48
  field.admin.placeholder ?? runtime.i18n.t("fields:select", { label: field.admin.label })
45
49
  );
@@ -58,6 +62,7 @@
58
62
 
59
63
  function setValues(next: string[]) {
60
64
  if (!editingBlocked) form.set(field.path, [...next]);
65
+ query = "";
61
66
  }
62
67
 
63
68
  function setValue(next: string) {
@@ -80,7 +85,7 @@
80
85
  if (!next) query = "";
81
86
  }
82
87
  }
83
- inputValue={open || hasMany ? query : selectedLabel}
88
+ {inputValue}
84
89
  >
85
90
  <div
86
91
  bind:this={anchor}
@@ -114,17 +119,26 @@
114
119
  autocorrect="off"
115
120
  autocapitalize="none"
116
121
  spellcheck={false}
117
- {placeholder}
122
+ placeholder={hasMany && values.length > 0 ? "" : placeholder}
118
123
  {...controlARIA}
119
124
  aria-label={field.admin.label}
120
125
  aria-required={field.required}
121
126
  clearOnDeselect
122
127
  onpointerdown={() => (open = true)}
123
128
  oninput={(event) => {
124
- query = event.currentTarget.value;
125
- open = true;
129
+ const next = event.currentTarget.value;
130
+ // Filter before Bits chooses its keyboard highlight from the rendered options.
131
+ flushSync(() => {
132
+ query = next;
133
+ open = true;
134
+ });
126
135
  }}
127
- />
136
+ >
137
+ {#snippet child({ props })}
138
+ <!-- Bits writes the picked label internally; tags already display it. -->
139
+ <input {...props} value={inputValue} />
140
+ {/snippet}
141
+ </ComboboxInput>
128
142
  </div>
129
143
  {#if !hasMany && value}
130
144
  <button
@@ -158,9 +172,11 @@
158
172
  {option.label}
159
173
  </ComboboxItem>
160
174
  {:else}
161
- <p class="ridu-combobox-message">
162
- {runtime.i18n.t("fields:noMatch", { label: field.admin.label })}
163
- </p>
175
+ {#if query.trim()}
176
+ <p class="ridu-combobox-message">
177
+ {runtime.i18n.t("fields:noMatch", { label: field.admin.label })}
178
+ </p>
179
+ {/if}
164
180
  {/each}
165
181
  </ComboboxViewport>
166
182
  </ComboboxContent>