alinea 2.0.0-preview.10 → 2.0.0-preview.11

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.
Files changed (132) hide show
  1. package/dist/.init/content/pages/welcome.json +1 -1
  2. package/dist/.mcp/later/dev.json +1 -1
  3. package/dist/.mcp/online/dev.json +1 -1
  4. package/dist/adapter/next/handler.js +6 -4
  5. package/dist/backend/Handler.d.ts +3 -0
  6. package/dist/backend/Handler.js +5 -5
  7. package/dist/backend/api/OAuth2.js +21 -26
  8. package/dist/backend/util/JWKS.d.ts +4 -0
  9. package/dist/backend/util/JWKS.js +40 -0
  10. package/dist/bundled.d.ts +76 -21
  11. package/dist/chunks/{chunk-HXXBAY3B.js → chunk-YCQR5GRT.js} +1 -1
  12. package/dist/cli/Mcp.js +1 -1
  13. package/dist/cli/Serve.d.ts +1 -0
  14. package/dist/cli/Serve.js +5 -2
  15. package/dist/cli/bin.js +3 -2
  16. package/dist/cli/serve/DevServer.d.ts +2 -0
  17. package/dist/cli/serve/DevServer.js +14 -2
  18. package/dist/cli/serve/mcp/DevMcp.js +1 -1
  19. package/dist/cloud/CloudHandshake.js +9 -46
  20. package/dist/cloud/CloudRemote.js +1 -1
  21. package/dist/components/Alert.js +4 -4
  22. package/dist/components/Badge.js +4 -4
  23. package/dist/components/Breadcrumbs.js +7 -7
  24. package/dist/components/Calendar.js +19 -19
  25. package/dist/components/Checkbox.js +7 -7
  26. package/dist/components/Code.js +2 -2
  27. package/dist/components/Collapsible.js +4 -4
  28. package/dist/components/ComboBox.js +6 -6
  29. package/dist/components/Command.js +9 -9
  30. package/dist/components/ContentCard.js +21 -21
  31. package/dist/components/ContentGrid.js +9 -9
  32. package/dist/components/DataList.js +1 -1
  33. package/dist/components/Dialog.js +6 -6
  34. package/dist/components/DropZone.js +2 -2
  35. package/dist/components/DropdownMenu.js +7 -7
  36. package/dist/components/Empty.js +4 -4
  37. package/dist/components/Field.js +2 -2
  38. package/dist/components/List.js +27 -27
  39. package/dist/components/MediaPreview.js +5 -5
  40. package/dist/components/MultipleSelect.js +20 -20
  41. package/dist/components/NavRail.js +3 -3
  42. package/dist/components/NumberField.js +4 -4
  43. package/dist/components/Page.js +6 -6
  44. package/dist/components/PreviewFrame.js +7 -7
  45. package/dist/components/RadioGroup.js +7 -7
  46. package/dist/components/SearchField.js +3 -3
  47. package/dist/components/Select.js +10 -10
  48. package/dist/components/Sidebar.js +5 -5
  49. package/dist/components/SortableList.js +14 -14
  50. package/dist/components/Spinner.js +4 -4
  51. package/dist/components/Surface.js +3 -3
  52. package/dist/components/Switch.js +3 -3
  53. package/dist/components/Table.js +41 -41
  54. package/dist/components/Tabs.js +2 -2
  55. package/dist/components/TagGroup.js +6 -6
  56. package/dist/components/TextField.js +7 -7
  57. package/dist/components/TimeField.js +5 -5
  58. package/dist/components/Tooltip.js +2 -2
  59. package/dist/components/Tree.js +28 -28
  60. package/dist/components/internal/DateControl.js +7 -7
  61. package/dist/components/internal/ListBoxOption.js +9 -9
  62. package/dist/components/internal/PopoverSurface.js +2 -2
  63. package/dist/components/internal/SelectionCheckbox.js +3 -3
  64. package/dist/components/internal/ToggleButton.js +4 -4
  65. package/dist/core/Role.d.ts +4 -5
  66. package/dist/core/Role.js +55 -54
  67. package/dist/core/Scope.d.ts +2 -0
  68. package/dist/core/Scope.js +36 -2
  69. package/dist/core/Type.d.ts +7 -4
  70. package/dist/core/Type.js +5 -1
  71. package/dist/core/Validation.js +3 -3
  72. package/dist/core/media/CompressPdf.browser.js +1 -1
  73. package/dist/core/media/CompressPdf.js +1 -1
  74. package/dist/core/media/CreatePreview.browser.js +1 -1
  75. package/dist/core/util/JWT.d.ts +1 -1
  76. package/dist/dashboard/app/ActivityStatus.js +11 -11
  77. package/dist/dashboard/app/AuthView.js +5 -5
  78. package/dist/dashboard/app/CompactField.js +20 -20
  79. package/dist/dashboard/app/CroppedImage.js +3 -3
  80. package/dist/dashboard/app/DashboardErrorBoundary.js +3 -3
  81. package/dist/dashboard/app/EntryFields.js +2 -2
  82. package/dist/dashboard/app/EntryHeader.js +7 -7
  83. package/dist/dashboard/app/EntryReferences.js +4 -4
  84. package/dist/dashboard/app/EntrySidebar.js +17 -17
  85. package/dist/dashboard/app/EntrySidebarPreview.js +1 -1
  86. package/dist/dashboard/app/EntryValidationModal.js +3 -3
  87. package/dist/dashboard/app/Explorer.js +36 -43
  88. package/dist/dashboard/app/ExplorerBatchActions.js +4 -4
  89. package/dist/dashboard/app/ExplorerControls.js +3 -3
  90. package/dist/dashboard/app/ExplorerList.js +5 -5
  91. package/dist/dashboard/app/ExplorerModal.js +3 -3
  92. package/dist/dashboard/app/ExplorerPickerContent.d.ts +1 -1
  93. package/dist/dashboard/app/ExplorerPickerContent.js +17 -3
  94. package/dist/dashboard/app/ExplorerTable.js +8 -8
  95. package/dist/dashboard/app/ImageEditor.js +27 -27
  96. package/dist/dashboard/app/ImagePicker.js +3 -2
  97. package/dist/dashboard/app/LinkPicker.js +7 -6
  98. package/dist/dashboard/app/LocaleMenu.js +7 -7
  99. package/dist/dashboard/app/MediaPreviewCell.js +2 -2
  100. package/dist/dashboard/app/OverviewCell.js +8 -8
  101. package/dist/dashboard/app/PendingUploadsView.js +2 -2
  102. package/dist/dashboard/app/SidebarTree.js +26 -28
  103. package/dist/dashboard/app/WorkspaceMenu.js +8 -8
  104. package/dist/dashboard/app/WorkspaceRoots.js +10 -10
  105. package/dist/dashboard/app/editor/FileEditor.js +18 -18
  106. package/dist/dashboard/app/modals/CreateEntry.js +3 -3
  107. package/dist/dashboard/app/pages/EntryPage.js +10 -10
  108. package/dist/dashboard/app/pages/SplashPage.js +37 -37
  109. package/dist/dashboard/app/pages/UsersPage.js +10 -10
  110. package/dist/dashboard/app/ui/DashboardModal.js +17 -17
  111. package/dist/dashboard/atoms/config.d.ts +1 -1
  112. package/dist/dashboard/atoms/entry.d.ts +11 -4
  113. package/dist/dashboard/atoms/entry.js +103 -86
  114. package/dist/dashboard/atoms/entryTable.js +2 -1
  115. package/dist/dashboard/atoms/explorer.d.ts +17 -2
  116. package/dist/dashboard/atoms/explorer.js +38 -16
  117. package/dist/dashboard/atoms/link.d.ts +2 -0
  118. package/dist/dashboard/atoms/link.js +6 -3
  119. package/dist/dashboard/atoms/root.d.ts +15 -3
  120. package/dist/dashboard/atoms/root.js +111 -52
  121. package/dist/database/EntryTransaction.js +17 -6
  122. package/dist/field/code/CodeField.view.js +15 -15
  123. package/dist/field/link/LinkField.view.js +36 -35
  124. package/dist/field/list/ListField.view.js +4 -4
  125. package/dist/field/metadata/MetadataField.view.js +17 -17
  126. package/dist/field/richtext/RichTextExtensions.js +33 -33
  127. package/dist/field/richtext/RichTextField.view.js +5 -5
  128. package/docs/deploy.md +1 -1
  129. package/docs/roles-permissions.md +7 -7
  130. package/llms-full.txt +8 -8
  131. package/package.json +1 -1
  132. /package/dist/chunks/{dist-UDQZOKRG.js → dist-OU2RRDGP.js} +0 -0
@@ -1,5 +1,5 @@
1
1
  {
2
- "_id": "3K5v72iiSXMjBJwIX9mSIj8QqhI",
2
+ "_id": "3KHB2PydgBPbUsKveWcaGSXkqjV",
3
3
  "_type": "Page",
4
4
  "_index": "a0",
5
5
  "_seeded": "welcome.json",
@@ -1 +1 @@
1
- {"url":"http://localhost:1","pid":3102}
1
+ {"url":"http://localhost:1","pid":3065}
@@ -1 +1 @@
1
- {"url":"http://localhost:40115","pid":3102}
1
+ {"url":"http://localhost:38409","pid":3065}
@@ -67,8 +67,6 @@ function createHandlerWithDatabase(input, openGeneratedDatabase) {
67
67
  return failure(400, "Expected JSON");
68
68
  }
69
69
  if (!Array.isArray(mutations)) return failure(400, "Expected mutations");
70
- const adjusted = await options.beforeCommit?.({ mutations });
71
- if (adjusted) mutations = [...adjusted];
72
70
  const devServer = new Client({
73
71
  config,
74
72
  url: context.handlerUrl.href,
@@ -80,15 +78,19 @@ function createHandlerWithDatabase(input, openGeneratedDatabase) {
80
78
  }
81
79
  });
82
80
  let sha;
81
+ let user;
83
82
  try {
84
- ;
83
+ user = await devServer.user();
84
+ if (!user) return failure(401, "Unauthorized");
85
+ const adjusted = await options.beforeCommit?.({ mutations, user });
86
+ if (adjusted) mutations = [...adjusted];
85
87
  ({ sha } = await devServer.mutate(mutations));
86
88
  } catch (error) {
87
89
  if (error instanceof HttpError) return failure(error.code, error.message);
88
90
  throw error;
89
91
  }
90
92
  try {
91
- await afterCommit({ mutations, sha });
93
+ await afterCommit({ mutations, sha, user });
92
94
  } catch (error) {
93
95
  console.error("Alinea afterCommit hook failed", error);
94
96
  }
@@ -2,12 +2,15 @@ import type { CMS } from 'alinea/core/CMS';
2
2
  import type { AuthedContext, RemoteConnection, RequestContext } from 'alinea/core/Connection';
3
3
  import type { LocalStore } from 'alinea/core/db/LocalStore';
4
4
  import type { Mutation } from 'alinea/core/db/Mutation';
5
+ import type { User } from 'alinea/core/User';
5
6
  export interface Handler {
6
7
  (request: Request, context: RequestContext): Promise<Response>;
7
8
  }
8
9
  export type HookResponse<T = void> = void | T | Promise<void | T>;
9
10
  export interface BeforeCommitContext {
10
11
  mutations: ReadonlyArray<Mutation>;
12
+ /** The user committing the mutations */
13
+ user: User;
11
14
  }
12
15
  export interface AfterCommitContext extends BeforeCommitContext {
13
16
  sha: string;
@@ -273,7 +273,10 @@ function createHandler({
273
273
  const policy = await user.policy;
274
274
  let mutations = await body;
275
275
  await local.syncWith(cnx);
276
- const adjusted = await hooks.beforeCommit?.({ mutations });
276
+ const adjusted = await hooks.beforeCommit?.({
277
+ mutations,
278
+ user: user.claims
279
+ });
277
280
  if (adjusted) mutations = adjusted;
278
281
  const attempt = async (retry = 0) => {
279
282
  if (retry > 0) await local.syncWith(cnx);
@@ -297,10 +300,7 @@ function createHandler({
297
300
  };
298
301
  const sha = await attempt();
299
302
  try {
300
- await hooks.afterCommit?.({
301
- mutations,
302
- sha
303
- });
303
+ await hooks.afterCommit?.({ mutations, sha, user: user.claims });
304
304
  } catch (error) {
305
305
  console.error("Alinea afterCommit hook failed", error);
306
306
  }
@@ -1,15 +1,12 @@
1
1
  import {
2
2
  parse
3
3
  } from "../../chunks/chunk-BYJRQTFR.js";
4
- import {
5
- PLazy
6
- } from "../../chunks/chunk-IKINPSS5.js";
7
4
  import "../../chunks/chunk-CQ52GEHY.js";
8
5
 
9
6
  // src/backend/api/OAuth2.ts
10
7
  import { AuthResultType } from "alinea/cloud/AuthResult";
11
8
  import { Config } from "alinea/core/Config";
12
- import { HttpError } from "alinea/core/HttpError";
9
+ import { ErrorCode, HttpError } from "alinea/core/HttpError";
13
10
  import { createId } from "alinea/core/Id";
14
11
  import { outcome } from "alinea/core/Outcome";
15
12
  import { assert } from "alinea/core/util/Assert";
@@ -459,6 +456,7 @@ import {
459
456
  MissingCredentialsError
460
457
  } from "../Auth.js";
461
458
  import { router } from "../router/Router.js";
459
+ import { jwks } from "../util/JWKS.js";
462
460
  var COOKIE_VERIFIER = "alinea.cv";
463
461
  var COOKIE_ACCESS_TOKEN = "alinea.at";
464
462
  var COOKIE_REFRESH_TOKEN = "alinea.rt";
@@ -466,7 +464,7 @@ var OAuth2 = class {
466
464
  #context;
467
465
  #config;
468
466
  #client;
469
- #jwks;
467
+ #jwksUri;
470
468
  #validateClaims;
471
469
  constructor(context, config, options) {
472
470
  assert(
@@ -489,19 +487,16 @@ var OAuth2 = class {
489
487
  return response;
490
488
  })
491
489
  });
492
- const loadJwks = async () => {
493
- try {
494
- const res = await fetch(options.jwksUri);
495
- if (res.status !== 200)
496
- throw new HttpError(res.status, await res.text());
497
- const jwks = await res.json();
498
- return jwks.keys;
499
- } catch (cause) {
500
- this.#jwks = PLazy.from(loadJwks);
501
- throw new Error("Remote unavailable", { cause });
502
- }
503
- };
504
- this.#jwks = PLazy.from(loadJwks);
490
+ this.#jwksUri = options.jwksUri;
491
+ }
492
+ async #key(token) {
493
+ const { header } = decode(token);
494
+ const keys = await jwks(this.#jwksUri, header.kid).catch((cause) => {
495
+ throw new HttpError(ErrorCode.ServiceUnavailable, "Remote unavailable", {
496
+ cause
497
+ });
498
+ });
499
+ return selectKey(keys, header);
505
500
  }
506
501
  get #redirectUri() {
507
502
  const url = new URL(this.#context.handlerUrl);
@@ -612,11 +607,10 @@ var OAuth2 = class {
612
607
  [COOKIE_ACCESS_TOKEN]: accessToken,
613
608
  [COOKIE_REFRESH_TOKEN]: refreshToken
614
609
  } = parse(cookieHeader);
615
- const jwks = await this.#jwks;
616
610
  try {
617
611
  if (!accessToken)
618
612
  throw new MissingCredentialsError("Missing access token cookie");
619
- const key = selectKey(jwks, accessToken);
613
+ const key = await this.#key(accessToken);
620
614
  const user = await verify(accessToken, key);
621
615
  await this.#validateClaims(user);
622
616
  assert(user.exp, "Missing exp claim in access token");
@@ -626,13 +620,15 @@ var OAuth2 = class {
626
620
  return { ...ctx, user, token: accessToken };
627
621
  } catch (error) {
628
622
  if (!refreshToken) throw error;
623
+ if (error instanceof HttpError && error.code === ErrorCode.ServiceUnavailable)
624
+ throw error;
629
625
  const token = { accessToken, refreshToken, expiresAt: null };
630
626
  const newToken = await this.#client.refreshToken(token).catch((cause) => {
631
627
  throw new InvalidCredentialsError("Failed to refresh token", {
632
628
  cause: [cause, error]
633
629
  });
634
630
  });
635
- const key = selectKey(jwks, newToken.accessToken);
631
+ const key = await this.#key(newToken.accessToken);
636
632
  const user = await verify(newToken.accessToken, key, {
637
633
  clockTolerance: 30
638
634
  }).catch((cause) => {
@@ -653,13 +649,12 @@ var OAuth2 = class {
653
649
  }
654
650
  }
655
651
  };
656
- function selectKey(jwks, token) {
657
- const { header } = decode(token);
652
+ function selectKey(jwks2, header) {
658
653
  const kid = header.kid;
659
654
  const alg = header.alg;
660
655
  if (!kid) {
661
- if (jwks.length === 1) {
662
- const key2 = jwks[0];
656
+ if (jwks2.length === 1) {
657
+ const key2 = jwks2[0];
663
658
  if (key2.alg && alg && key2.alg !== alg) {
664
659
  throw new Error(
665
660
  `Algorithm mismatch: token uses ${alg}, but key specifies ${key2.alg}`
@@ -671,7 +666,7 @@ function selectKey(jwks, token) {
671
666
  'Token is missing "kid" header, and JWKS contains multiple keys'
672
667
  );
673
668
  }
674
- const key = jwks.find((k) => k.kid === kid);
669
+ const key = jwks2.find((k) => k.kid === kid);
675
670
  if (!key) throw new Error(`No key found for kid: ${kid}`);
676
671
  if (key.alg && alg && key.alg !== alg)
677
672
  throw new Error(
@@ -0,0 +1,4 @@
1
+ export interface WebKey extends JsonWebKey {
2
+ kid: string;
3
+ }
4
+ export declare function jwks(uri: string, kid?: string): Promise<Array<WebKey>>;
@@ -0,0 +1,40 @@
1
+ import "../../chunks/chunk-CQ52GEHY.js";
2
+
3
+ // src/backend/util/JWKS.ts
4
+ import { HttpError } from "alinea/core/HttpError";
5
+ import { isRecord } from "alinea/core/util/Objects";
6
+ var lifetime = 10 * 60 * 1e3;
7
+ var refreshInterval = 60 * 1e3;
8
+ var cache = /* @__PURE__ */ new Map();
9
+ async function jwks(uri, kid) {
10
+ const cached = cache.get(uri);
11
+ if (!cached || Date.now() >= cached.expires) return load(uri, cached);
12
+ const keys = await cached.keys;
13
+ if (!kid || keys.some((key) => key.kid === kid)) return keys;
14
+ const current = cache.get(uri);
15
+ if (current && current !== cached) return current.keys;
16
+ if (Date.now() - cached.loaded < refreshInterval) return keys;
17
+ return load(uri, cached);
18
+ }
19
+ function load(uri, previous) {
20
+ const fetched = fetch(uri).then(async (response) => {
21
+ if (!response.ok) throw new HttpError(response.status);
22
+ const body = await response.json();
23
+ if (!isRecord(body) || !Array.isArray(body.keys))
24
+ throw new Error("Invalid signing keys");
25
+ return body.keys.filter(isKey);
26
+ });
27
+ const keys = previous ? fetched.catch(() => previous.keys) : fetched;
28
+ const entry = { keys, expires: Date.now() + lifetime, loaded: Date.now() };
29
+ cache.set(uri, entry);
30
+ keys.catch(() => {
31
+ if (cache.get(uri) === entry) cache.delete(uri);
32
+ });
33
+ return fetched;
34
+ }
35
+ function isKey(value) {
36
+ return isRecord(value) && typeof value.kid === "string";
37
+ }
38
+ export {
39
+ jwks
40
+ };
package/dist/bundled.d.ts CHANGED
@@ -601,12 +601,15 @@ declare module 'alinea/backend/Handler' {
601
601
  import type { AuthedContext, RemoteConnection, RequestContext } from 'alinea/core/Connection';
602
602
  import type { LocalStore } from 'alinea/core/db/LocalStore';
603
603
  import type { Mutation } from 'alinea/core/db/Mutation';
604
+ import type { User } from 'alinea/core/User';
604
605
  export interface Handler {
605
606
  (request: Request, context: RequestContext): Promise<Response>;
606
607
  }
607
608
  export type HookResponse<T = void> = void | T | Promise<void | T>;
608
609
  export interface BeforeCommitContext {
609
610
  mutations: ReadonlyArray<Mutation>;
611
+ /** The user committing the mutations */
612
+ user: User;
610
613
  }
611
614
  export interface AfterCommitContext extends BeforeCommitContext {
612
615
  sha: string;
@@ -881,6 +884,15 @@ declare module 'alinea/backend/util/JsonPatch' {
881
884
 
882
885
  }
883
886
 
887
+ declare module 'alinea/backend/util/JWKS' {
888
+
889
+ export interface WebKey extends JsonWebKey {
890
+ kid: string;
891
+ }
892
+ export function jwks(uri: string, kid?: string): Promise<Array<WebKey>>;
893
+
894
+ }
895
+
884
896
  declare module 'alinea/backend/util/JWTPreviews' {
885
897
 
886
898
  import type { Previews } from 'alinea/backend/Previews';
@@ -1204,6 +1216,7 @@ declare module 'alinea/cli/Serve' {
1204
1216
  buildOptions?: BuildOptions;
1205
1217
  alineaDev?: boolean;
1206
1218
  production?: boolean;
1219
+ roles?: Array<string>;
1207
1220
  onAfterGenerate?: (env?: Record<string, string>) => void;
1208
1221
  }
1209
1222
  export function serve(options: ServeOptions): Promise<void>;
@@ -1254,6 +1267,8 @@ declare module 'alinea/cli/serve/DevServer' {
1254
1267
  buildOptions?: BuildOptions;
1255
1268
  alineaDev?: boolean;
1256
1269
  production?: boolean;
1270
+ /** Roles of the local user, instead of admin */
1271
+ roles?: Array<string>;
1257
1272
  apiKey?: string;
1258
1273
  dashboardUrl: Promise<string>;
1259
1274
  onAfterGenerate?: (message: string, config: Config, databasePath: string) => void;
@@ -7111,9 +7126,8 @@ declare module 'alinea/core/Role' {
7111
7126
  archive: boolean;
7112
7127
  upload: boolean;
7113
7128
  /**
7114
- * @deprecated Never enforced. What a user can browse in the dashboard's
7115
- * entry tree and media library is controlled by `read`. Kept so existing
7116
- * roles keep type checking; it will be removed in a future major version.
7129
+ * Find entries in pickers, for example to link to them, without opening
7130
+ * them in the dashboard. Allowing `read` allows `explore` as well.
7117
7131
  */
7118
7132
  explore: boolean;
7119
7133
  manageMembers: boolean;
@@ -7149,12 +7163,13 @@ declare module 'alinea/core/Role' {
7149
7163
  constructor(acl?: ACL);
7150
7164
  get(resource: string): number;
7151
7165
  equals(that: ACL): boolean;
7166
+ resolve(resource?: Resource): number;
7152
7167
  }
7153
7168
  export class Policy {
7154
7169
  #private;
7155
7170
  static ALLOW_ALL: Policy;
7156
7171
  static ALLOW_NONE: Policy;
7157
- protected acl: ACL;
7172
+ protected acls: Array<ACL>;
7158
7173
  constructor(root?: Permission);
7159
7174
  static from(policy: Policy): Policy;
7160
7175
  equals(that: Policy): boolean;
@@ -7171,7 +7186,6 @@ declare module 'alinea/core/Role' {
7171
7186
  canPublish(resource?: Resource): boolean;
7172
7187
  canArchive(resource?: Resource): boolean;
7173
7188
  canUpload(resource?: Resource): boolean;
7174
- /** @deprecated `explore` is never enforced, use `canRead` instead. */
7175
7189
  canExplore(resource?: Resource): boolean;
7176
7190
  canManageMembers(): boolean;
7177
7191
  canAll(resource?: Resource): boolean;
@@ -7325,6 +7339,8 @@ declare module 'alinea/core/Scope' {
7325
7339
  constructor(config: Config);
7326
7340
  workspaceOf(root: HasRoot): HasWorkspace;
7327
7341
  keyOf(entity: Entity): string;
7342
+ /** The type of a field and its path in there, such as `seo.title` */
7343
+ fieldOf(field: Field): [type: string, path: string] | undefined;
7328
7344
  locationOf(entity: Entity): string[] | undefined;
7329
7345
  nameOf(entity: Entity): string | undefined;
7330
7346
  /** Configured spellings of a locale, matched case-insensitively. */
@@ -8029,6 +8045,7 @@ declare module 'alinea/core/Type' {
8029
8045
  function childrenOrder(type: Type): OrderBy | Array<OrderBy> | undefined;
8030
8046
  function isHidden(type: Type): boolean;
8031
8047
  function isCollapsed(type: Type): boolean;
8048
+ function expands(type: Type): boolean;
8032
8049
  function searchableText(type: Type, value: any): string;
8033
8050
  function anchors(type: Type, value: Record<string, unknown>, path?: Array<string>, labels?: Array<string>): Array<EntryAnchorTarget>;
8034
8051
  function references(type: Type, value: Record<string, unknown>, path?: Array<string>, labels?: Array<string>): Array<EntryReferenceTarget>;
@@ -8066,11 +8083,13 @@ declare module 'alinea/core/Type' {
8066
8083
  /** Entries do not show up in the sidebar content tree */
8067
8084
  hidden?: true;
8068
8085
  /**
8069
- * Entries keep their children collapsed in the sidebar content tree when
8070
- * they are opened, the arrow expands them. Useful for entries with many
8071
- * children that are browsed in their overview instead.
8086
+ * How entries show their children in the sidebar content tree. By default
8087
+ * they expand when the entry is opened. With `true` they stay collapsed
8088
+ * until the arrow is clicked, useful for entries with many children that
8089
+ * are browsed in their overview instead. With `false` they are expanded
8090
+ * from the start.
8072
8091
  */
8073
- collapsed?: true;
8092
+ collapsed?: boolean;
8074
8093
  /** An icon (React component) to represent this type in the dashboard */
8075
8094
  icon?: ComponentType;
8076
8095
  /** A React component used to view an entry of this type in the dashboard */
@@ -8529,7 +8548,7 @@ declare module 'alinea/core/util/JWT' {
8529
8548
  };
8530
8549
  };
8531
8550
  type Algorithm = keyof typeof algorithms;
8532
- type JWTHeader = {
8551
+ export type JWTHeader = {
8533
8552
  alg: Algorithm;
8534
8553
  kid?: string;
8535
8554
  typ?: string;
@@ -9609,7 +9628,7 @@ declare module 'alinea/dashboard/app/ExplorerPickerContent' {
9609
9628
  * The explorer of a picker opened at `location`, it starts over when the
9610
9629
  * location or condition changes
9611
9630
  */
9612
- export function usePickerExplorer(options: ExplorerOptions, location: ExplorerLocation, initialView: ExplorerView): {
9631
+ export function usePickerExplorer(options: ExplorerOptions, requested: ExplorerLocation, initialView: ExplorerView): {
9613
9632
  explorer: import("alinea/dashboard/atoms/explorer").ExplorerAtoms;
9614
9633
  tree: (root: import("alinea/dashboard/atoms/root").RootAtoms, locale: string | null, location?: ExplorerLocation | undefined) => import("alinea/dashboard/atoms/root").TreeAtoms;
9615
9634
  };
@@ -10258,7 +10277,7 @@ declare module 'alinea/dashboard/atoms/config' {
10258
10277
  overview?: import("alinea/types").OverviewOptions;
10259
10278
  orderChildrenBy?: import("alinea/core/OrderBy").OrderBy | Array<import("alinea/core/OrderBy").OrderBy>;
10260
10279
  hidden?: true;
10261
- collapsed?: true;
10280
+ collapsed?: boolean;
10262
10281
  icon?: ComponentType;
10263
10282
  view?: import("alinea/core/View").View<{
10264
10283
  type: import("alinea/types").Type;
@@ -10533,6 +10552,7 @@ declare module 'alinea/dashboard/atoms/entry' {
10533
10552
  import type { EntryReference } from 'alinea/core/db/EntryReference';
10534
10553
  import { Entry, EntryStatus } from 'alinea/core/Entry';
10535
10554
  import type { Order } from 'alinea/core/Graph';
10555
+ import { Permission } from 'alinea/core/Role';
10536
10556
  import { type EntryDefaultView } from 'alinea/core/Type';
10537
10557
  import { type FieldValidationError } from 'alinea/core/Validation';
10538
10558
  import { Atom, Getter } from 'jotai';
@@ -10725,7 +10745,8 @@ declare module 'alinea/dashboard/atoms/entry' {
10725
10745
  view: import("jotai").WritableAtom<EntryDefaultView, [next: EntryDefaultView], void>;
10726
10746
  locales: (locale: string | null) => EntryLocaleAtoms;
10727
10747
  }
10728
- export const entryAtoms: (entryId: string) => Atom<Promise<EntryAtoms>>;
10748
+ /** An entry with the versions the policy allows with the permission */
10749
+ export function entryAtoms(entryId: string, permission?: Permission): Atom<Promise<EntryAtoms>>;
10729
10750
  /** The versions linking to an entry, that the user can read */
10730
10751
  export const incomingReferencesAtoms: (targetId: string) => Atom<Promise<EntryReferences>>;
10731
10752
  /** The versions linking to the entries, that the user can read */
@@ -10756,11 +10777,12 @@ declare module 'alinea/dashboard/atoms/entry' {
10756
10777
  id: string;
10757
10778
  locale: string | null;
10758
10779
  }>(items: Array<Item>, locale: string | null): Array<Item>;
10759
- export function loadTreeChildren(get: Getter, location: TreeChildrenLocation, locale: string | null, orderBy?: Order | Array<Order>): Promise<Array<TreeEntryAtoms>>;
10780
+ export function loadTreeChildren(get: Getter, location: TreeChildrenLocation, locale: string | null, permission: Permission, orderBy?: Order | Array<Order>): Promise<Array<TreeEntryAtoms>>;
10760
10781
  export class TreeEntryAtoms {
10761
10782
  #private;
10762
10783
  readonly id: string;
10763
- constructor(id: string);
10784
+ readonly permission: Permission;
10785
+ constructor(id: string, permission: Permission);
10764
10786
  ready: Atom<Promise<this>>;
10765
10787
  raw: (locale: string | null) => Atom<Promise<Omit<TreeEntrySummary, "hasChildren">>>;
10766
10788
  summary: (locale: string | null) => Atom<Promise<{
@@ -10779,7 +10801,11 @@ declare module 'alinea/dashboard/atoms/entry' {
10779
10801
  parents: Atom<Promise<TreeEntryAtoms[]>>;
10780
10802
  children: (locale: string | null) => Atom<Promise<TreeEntryAtoms[]>>;
10781
10803
  }
10782
- export const treeEntryAtoms: (id: string) => TreeEntryAtoms;
10804
+ /**
10805
+ * An entry of a tree that lists what the policy allows with the permission.
10806
+ * Pickers list what can be explored, the sidebar what can be read.
10807
+ */
10808
+ export function treeEntryAtoms(id: string, permission?: Permission): TreeEntryAtoms;
10783
10809
  export {};
10784
10810
 
10785
10811
  }
@@ -10802,12 +10828,13 @@ declare module 'alinea/dashboard/atoms/entryTable' {
10802
10828
  declare module 'alinea/dashboard/atoms/explorer' {
10803
10829
 
10804
10830
  import type { DragTypes, DropTarget, Key } from 'alinea/components';
10831
+ import type { Config } from 'alinea/core/Config';
10805
10832
  import { type EntryStatus } from 'alinea/core/Entry';
10806
10833
  import type { EntryFields } from 'alinea/core/EntryFields';
10807
10834
  import type { Filter } from 'alinea/core/Filter';
10808
10835
  import type { GraphQuery } from 'alinea/core/Graph';
10809
10836
  import type { OverviewSort } from 'alinea/core/Overview';
10810
- import { type Policy } from 'alinea/core/Role';
10837
+ import { Permission, type Policy } from 'alinea/core/Role';
10811
10838
  import type { RootData } from 'alinea/core/Root';
10812
10839
  import { Type } from 'alinea/core/Type';
10813
10840
  import { type Atom, type Getter, type PrimitiveAtom, type WritableAtom } from 'jotai';
@@ -10826,6 +10853,7 @@ declare module 'alinea/dashboard/atoms/explorer' {
10826
10853
  workspace: string;
10827
10854
  root: string;
10828
10855
  }
10856
+ export function constrainLocation(location: ExplorerLocation, limits: Array<ExplorerLimitLocation> | undefined): ExplorerLocation;
10829
10857
  export interface ExplorerOptions {
10830
10858
  allowAllWorkspaces?: boolean;
10831
10859
  autoSelectFirstItem?: boolean;
@@ -10841,6 +10869,11 @@ declare module 'alinea/dashboard/atoms/explorer' {
10841
10869
  limitLocations?: Array<ExplorerLimitLocation>;
10842
10870
  mode?: 'browse' | 'search';
10843
10871
  nestedNavigation?: boolean;
10872
+ /**
10873
+ * Entries are listed when the policy allows this permission on them,
10874
+ * defaults to read. Pickers list what can be explored.
10875
+ */
10876
+ permission?: Permission;
10844
10877
  pickChildren?: boolean;
10845
10878
  rootData?: Atom<RootData>;
10846
10879
  /**
@@ -10931,6 +10964,8 @@ declare module 'alinea/dashboard/atoms/explorer' {
10931
10964
  * same entries in the same way and share their scroll offset
10932
10965
  */
10933
10966
  export function explorerScrollKey(page: ExplorerReadyPage): string;
10967
+ /** The locations an explorer can list: those of the picker the policy allows */
10968
+ export function explorerLocations(config: Config, policy: Policy, options: ExplorerOptions): Array<ExplorerLimitLocation>;
10934
10969
  export interface ExplorerItemData {
10935
10970
  active?: boolean;
10936
10971
  createdAt?: number | null;
@@ -10985,7 +11020,7 @@ declare module 'alinea/dashboard/atoms/explorer' {
10985
11020
  * Loads the thumbnails and the entries linked from the overview columns of
10986
11021
  * the given explorer items in a single query
10987
11022
  */
10988
- export function withLinkedEntries<Item extends ExplorerItemData>(get: Getter, overview: OverviewState, items: Array<Item>, thumbnails?: boolean): Promise<Array<Item>>;
11023
+ export function withLinkedEntries<Item extends ExplorerItemData>(get: Getter, overview: OverviewState, items: Array<Item>, permission?: Permission, thumbnails?: boolean): Promise<Array<Item>>;
10989
11024
  /** The selected entry can be moved to another parent */
10990
11025
  export function explorerItemCanMove(policy: Policy, item: ExplorerItemData): boolean;
10991
11026
  /** The selected entry can be deleted, in the language it is listed in */
@@ -11079,6 +11114,7 @@ declare module 'alinea/dashboard/atoms/explorer' {
11079
11114
  readonly supportsInlineExpansion: boolean;
11080
11115
  readonly pickChildren: boolean;
11081
11116
  readonly hasCondition: boolean;
11117
+ readonly permission: Permission;
11082
11118
  readonly canSearchEverything: Atom<boolean>;
11083
11119
  readonly searchesEverything: Atom<boolean>;
11084
11120
  search: PrimitiveAtom<string> & {
@@ -11093,6 +11129,9 @@ declare module 'alinea/dashboard/atoms/explorer' {
11093
11129
  sidebarExpandedKeys: PrimitiveAtom<Set<string>> & {
11094
11130
  init: Set<string>;
11095
11131
  };
11132
+ sidebarClosedKeys: PrimitiveAtom<Set<string>> & {
11133
+ init: Set<string>;
11134
+ };
11096
11135
  /** The column the editor sorted by, undefined for the default order */
11097
11136
  requestedSort: WritableAtom<OverviewSort | undefined, [
11098
11137
  OverviewSort | undefined
@@ -11138,6 +11177,8 @@ declare module 'alinea/dashboard/atoms/explorer' {
11138
11177
  init: null;
11139
11178
  };
11140
11179
  get limitLocations(): ExplorerLimitLocation[] | undefined;
11180
+ /** The locations this explorer lists */
11181
+ locations: Atom<ExplorerLimitLocation[]>;
11141
11182
  uploadsInCurrentFolder: Atom<import("alinea/dashboard/boot/ActivityEvent").Activity[]>;
11142
11183
  upload: WritableAtom<null, [files: ArrayLike<File> | Iterable<File>], Promise<void>> & {
11143
11184
  init: null;
@@ -11213,6 +11254,8 @@ declare module 'alinea/dashboard/atoms/link' {
11213
11254
  title: string;
11214
11255
  }>;
11215
11256
  anchors: Array<EntryAnchorTarget>;
11257
+ /** The editor can open the entry, not only link to it */
11258
+ readable: boolean;
11216
11259
  }
11217
11260
  export type LinkEntryState = {
11218
11261
  state: 'loading';
@@ -11627,6 +11670,7 @@ declare module 'alinea/dashboard/atoms/ReactiveNode' {
11627
11670
  declare module 'alinea/dashboard/atoms/root' {
11628
11671
 
11629
11672
  import type { EntryStatus } from 'alinea/core/Entry';
11673
+ import { Permission } from 'alinea/core/Role';
11630
11674
  import { type RootData, type RootI18n } from 'alinea/core/Root';
11631
11675
  import { Schema } from 'alinea/core/Schema';
11632
11676
  import { ExplorerAtoms } from 'alinea/dashboard/atoms/explorer';
@@ -11671,12 +11715,22 @@ declare module 'alinea/dashboard/atoms/root' {
11671
11715
  interface TreeViewState {
11672
11716
  expandedKeys: PrimitiveAtom<Set<string>>;
11673
11717
  collapsedKeys: PrimitiveAtom<TreeCollapseState>;
11718
+ closedKeys: PrimitiveAtom<Set<string>>;
11674
11719
  }
11675
11720
  export class TreeAtoms {
11676
11721
  #private;
11677
11722
  expandedKeys: PrimitiveAtom<Set<string>>;
11678
11723
  collapsedKeys: PrimitiveAtom<TreeCollapseState>;
11679
- constructor(root: RootAtoms, locale: string | null, selectedKeys: Atom<Set<Key>>, viewState?: TreeViewState);
11724
+ /** Entries closed by hand, which stay closed when their type expands */
11725
+ closedKeys: PrimitiveAtom<Set<string>>;
11726
+ constructor(root: RootAtoms, locale: string | null, selectedKeys: Atom<Set<Key>>, viewState?: TreeViewState, permission?: Permission);
11727
+ /**
11728
+ * Applies what the user expanded and collapsed. Keys closed by hand are
11729
+ * remembered, so entries of types that expand from the start stay closed.
11730
+ */
11731
+ expand: import("jotai").WritableAtom<null, [next: Set<string>], void> & {
11732
+ init: null;
11733
+ };
11680
11734
  view: Atom<TreeView>;
11681
11735
  snapshot: Atom<TreeSnapshot>;
11682
11736
  items: Atom<RootTreeItem[]>;
@@ -11695,8 +11749,9 @@ declare module 'alinea/dashboard/atoms/root' {
11695
11749
  readonly tree: (locale: string | null) => TreeAtoms;
11696
11750
  explorer: ExplorerAtoms;
11697
11751
  constructor(workspace: string, key: string);
11698
- createTree(locale: string | null, selectedKeys: Atom<Set<Key>>, expandedKeys?: PrimitiveAtom<Set<string>>): TreeAtoms;
11699
- treeEntries: (locale: string | null) => Atom<Promise<TreeEntryAtoms[]>>;
11752
+ /** A tree that lists the entries the policy allows with the permission */
11753
+ createTree(locale: string | null, selectedKeys: Atom<Set<Key>>, state?: Partial<Pick<TreeViewState, 'expandedKeys' | 'closedKeys'>>, permission?: Permission): TreeAtoms;
11754
+ treeEntries(locale: string | null, permission?: Permission): Atom<Promise<TreeEntryAtoms[]>>;
11700
11755
  /**
11701
11756
  * The scroll offsets of the explorers of this root. The root explorer and
11702
11757
  * the overview of an entry can list the same location, which then keeps its
@@ -1,7 +1,7 @@
1
1
  // package.json with { type: 'json' }
2
2
  var package_default = {
3
3
  name: "alinea",
4
- version: "2.0.0-preview.10",
4
+ version: "2.0.0-preview.11",
5
5
  description: "Open source, Git-based headless CMS for Next.js",
6
6
  keywords: [
7
7
  "cms",
package/dist/cli/Mcp.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "../chunks/chunk-HXXBAY3B.js";
3
+ } from "../chunks/chunk-YCQR5GRT.js";
4
4
  import "../chunks/chunk-CQ52GEHY.js";
5
5
 
6
6
  // src/cli/Mcp.ts
@@ -8,6 +8,7 @@ export interface ServeOptions {
8
8
  buildOptions?: BuildOptions;
9
9
  alineaDev?: boolean;
10
10
  production?: boolean;
11
+ roles?: Array<string>;
11
12
  onAfterGenerate?: (env?: Record<string, string>) => void;
12
13
  }
13
14
  export declare function serve(options: ServeOptions): Promise<void>;
package/dist/cli/Serve.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "../chunks/chunk-HXXBAY3B.js";
3
+ } from "../chunks/chunk-YCQR5GRT.js";
4
4
  import "../chunks/chunk-CQ52GEHY.js";
5
5
 
6
6
  // src/cli/Serve.ts
@@ -34,6 +34,7 @@ async function serve(options) {
34
34
  configFile: options.configFile,
35
35
  alineaDev: options.alineaDev,
36
36
  production: options.production,
37
+ roles: options.roles,
37
38
  apiKey,
38
39
  buildOptions: {
39
40
  ...buildOptions,
@@ -55,7 +56,9 @@ async function serve(options) {
55
56
  `;
56
57
  const showUrl = cmd === "dev" && !options.onAfterGenerate;
57
58
  const connector = gray(showUrl ? "\u251C" : "\u2570");
58
- const details = `${connector} ${gray(msg)}
59
+ const roles = options.roles ? gray(`\u251C Signed in as ${options.roles.join(", ")}
60
+ `) : "";
61
+ const details = `${roles}${connector} ${gray(msg)}
59
62
  `;
60
63
  const footer = showUrl ? `${gray("\u2570")} Local CMS: ${url}
61
64
 
package/dist/cli/bin.js CHANGED
@@ -220,7 +220,7 @@ var lib_default2 = (e, t2) => new o(e, t2);
220
220
  // package.json
221
221
  var package_default = {
222
222
  name: "alinea",
223
- version: "2.0.0-preview.10",
223
+ version: "2.0.0-preview.11",
224
224
  description: "Open source, Git-based headless CMS for Next.js",
225
225
  keywords: [
226
226
  "cms",
@@ -336,7 +336,7 @@ prog.version(package_default.version).command("init").describe("Initialize a new
336
336
  ensureLibs(libs);
337
337
  const { init } = await import("./Init.js");
338
338
  return init(args);
339
- }).command("dev").alias("serve").describe("Start a development dashboard").option("-c, --config", "Config file location").option("-d, --dir", "Root directory of the project").option("-p, --port", "Port to listen on").option("--production", "Use production backend").option("--dev", "Watch alinea sources").action(async (args) => {
339
+ }).command("dev").alias("serve").describe("Start a development dashboard").option("-c, --config", "Config file location").option("-d, --dir", "Root directory of the project").option("-p, --port", "Port to listen on").option("--production", "Use production backend").option("--dev", "Watch alinea sources").option("--role", "Sign in locally with this role, repeat for more").action(async (args) => {
340
340
  ensureNode();
341
341
  ensureLibs(libs);
342
342
  ensureEnv(args.dir);
@@ -349,6 +349,7 @@ prog.version(package_default.version).command("init").describe("Initialize a new
349
349
  cwd: args.dir,
350
350
  onAfterGenerate,
351
351
  configFile: args.config,
352
+ roles: args.role && [args.role].flat().filter((role) => typeof role === "string"),
352
353
  cmd: "dev"
353
354
  });
354
355
  }).command("mcp").describe("Connect coding agents to the content of the running dev server").option("-c, --config", "Config file location").option("-d, --dir", "Root directory of the project").action(async (args) => {