@astrale-domains/shell 0.1.8 → 0.1.9

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 (47) hide show
  1. package/dist/domain.d.ts +30 -23
  2. package/dist/domain.js +8 -0
  3. package/dist/functions/authority/roles.js +14 -6
  4. package/dist/functions/group/change-group-membership.js +7 -1
  5. package/dist/functions/group/membership-laws.js +20 -22
  6. package/dist/functions/view-access/index.js +2 -2
  7. package/dist/functions/view-access/legacy/index.js +1 -0
  8. package/dist/functions/view-access/legacy/migrate-view-access.js +29 -0
  9. package/dist/mutations/view-access/legacy/index.js +1 -0
  10. package/dist/mutations/view-access/legacy/move-view-access.js +26 -0
  11. package/dist/mutations/view-access/set-view-access.js +1 -1
  12. package/dist/queries/view-access/legacy/index.js +1 -0
  13. package/dist/queries/view-access/legacy/read-view-access-to-move.js +40 -0
  14. package/dist/queries/view-access/read-view-access.js +1 -1
  15. package/dist/runtime.d.ts +1605 -707
  16. package/dist/runtime.js +3 -2
  17. package/dist/schema/index.d.ts +1 -1
  18. package/dist/schema/index.js +2 -1
  19. package/dist/schema/legacy/schema.js +5 -3
  20. package/dist/schema/modules/view-access/index.d.ts +1 -2
  21. package/dist/schema/modules/view-access/index.js +2 -2
  22. package/dist/schema/modules/view-access/legacy/classes/index.d.ts +1 -0
  23. package/dist/schema/modules/view-access/legacy/classes/index.js +1 -0
  24. package/dist/schema/modules/view-access/{classes → legacy/classes}/view-access.d.ts +7 -5
  25. package/dist/schema/modules/view-access/legacy/classes/view-access.js +25 -0
  26. package/dist/schema/modules/view-access/legacy/functions/assign-views.d.ts +18 -0
  27. package/dist/schema/modules/view-access/legacy/functions/index.d.ts +2 -0
  28. package/dist/schema/modules/view-access/legacy/functions/index.js +1 -0
  29. package/dist/schema/modules/view-access/legacy/functions/migrate-view-access.d.ts +13 -0
  30. package/dist/schema/modules/view-access/legacy/functions/migrate-view-access.js +22 -0
  31. package/dist/schema/modules/view-access/legacy/index.d.ts +4 -0
  32. package/dist/schema/modules/view-access/legacy/index.js +3 -1
  33. package/dist/schema/modules/view-access/legacy/policies/holds-view-access.d.ts +5 -0
  34. package/dist/schema/modules/view-access/legacy/policies/holds-view-access.js +8 -0
  35. package/dist/schema/modules/view-access/legacy/policies/index.d.ts +1 -0
  36. package/dist/schema/modules/view-access/legacy/policies/index.js +1 -0
  37. package/dist/schema/modules/view-access/legacy/view-assignment.d.ts +21 -0
  38. package/dist/schema/schema.d.ts +112 -91
  39. package/dist/schema/schema.js +1 -4
  40. package/package.json +1 -1
  41. package/dist/schema/modules/view-access/classes/index.d.ts +0 -1
  42. package/dist/schema/modules/view-access/classes/index.js +0 -1
  43. package/dist/schema/modules/view-access/classes/view-access.js +0 -23
  44. package/dist/schema/modules/view-access/policies/holds-view-access.d.ts +0 -2
  45. package/dist/schema/modules/view-access/policies/holds-view-access.js +0 -5
  46. package/dist/schema/modules/view-access/policies/index.d.ts +0 -1
  47. package/dist/schema/modules/view-access/policies/index.js +0 -1
package/dist/domain.d.ts CHANGED
@@ -35,6 +35,28 @@ declare const frontend: import("@astrale-os/sdk/view").Frontend<import("@astrale
35
35
  readonly constraints: Record<never, never>;
36
36
  readonly effectiveProperties: never;
37
37
  }>;
38
+ readonly view_access: import("@astrale-os/sdk/schema").DirectedEdgeClass<{
39
+ readonly depth: 0;
40
+ readonly abstract: false;
41
+ readonly extends: readonly [];
42
+ readonly properties: never;
43
+ readonly source: {
44
+ readonly as: "audience";
45
+ readonly accepts: readonly (import("@astrale-os/sdk/schema").ClassRef<"Identity"> & {
46
+ readonly origin: "kernel.astrale.ai";
47
+ })[];
48
+ readonly outgoing: "0..*";
49
+ };
50
+ readonly target: {
51
+ readonly as: "view";
52
+ readonly accepts: readonly (import("@astrale-os/sdk/schema").ClassRef<"View"> & {
53
+ readonly origin: "kernel.astrale.ai";
54
+ })[];
55
+ readonly incoming: "0..*";
56
+ };
57
+ readonly constraints: Record<never, never>;
58
+ readonly effectiveProperties: never;
59
+ }>;
38
60
  readonly User: import("@astrale-os/sdk/schema").AuthoredNodeClass<{
39
61
  readonly icon: import("@astrale-os/sdk/schema").ClassIcon;
40
62
  readonly inheritance: "open";
@@ -578,28 +600,6 @@ declare const frontend: import("@astrale-os/sdk/view").Frontend<import("@astrale
578
600
  readonly constraints: Record<never, never>;
579
601
  readonly effectiveProperties: never;
580
602
  }>;
581
- readonly view_access: import("@astrale-os/sdk/schema").DirectedEdgeClass<{
582
- readonly depth: 0;
583
- readonly abstract: false;
584
- readonly extends: readonly [];
585
- readonly properties: never;
586
- readonly source: {
587
- readonly as: "audience";
588
- readonly accepts: readonly (import("@astrale-os/sdk/schema").ClassRef<"Identity"> & {
589
- readonly origin: "kernel.astrale.ai";
590
- })[];
591
- readonly outgoing: "0..*";
592
- };
593
- readonly target: {
594
- readonly as: "view";
595
- readonly accepts: readonly (import("@astrale-os/sdk/schema").ClassRef<"View"> & {
596
- readonly origin: "kernel.astrale.ai";
597
- })[];
598
- readonly incoming: "0..*";
599
- };
600
- readonly constraints: Record<never, never>;
601
- readonly effectiveProperties: never;
602
- }>;
603
603
  };
604
604
  readonly functions: {
605
605
  readonly listEntrypoints: import("@astrale-os/sdk/schema").Function<import("@astrale-os/sdk/schema").CallableProjection<Record<string, never>, {
@@ -633,6 +633,13 @@ declare const frontend: import("@astrale-os/sdk/view").Frontend<import("@astrale
633
633
  profiles: number;
634
634
  };
635
635
  }, "authorized", false, false, undefined, undefined>>;
636
+ readonly legacyMigrateViewAccess: import("@astrale-os/sdk/schema").Function<import("@astrale-os/sdk/schema").CallableProjection<Record<string, never>, {
637
+ readonly mode: 'value';
638
+ readonly value: {
639
+ copied: number;
640
+ removed: number;
641
+ };
642
+ }, "authorized", false, false, undefined, undefined>>;
636
643
  readonly disconnectDirectory: import("@astrale-os/sdk/schema").Function<import("@astrale-os/sdk/schema").CallableProjection<{
637
644
  directory: string;
638
645
  }, {
@@ -650,8 +657,8 @@ declare const frontend: import("@astrale-os/sdk/view").Frontend<import("@astrale
650
657
  readonly usesFunction: import("@astrale-os/sdk/schema").Policy;
651
658
  readonly isSelf: import("@astrale-os/sdk/schema").Policy;
652
659
  readonly legacyOwnsMembershipGroup: import("@astrale-os/sdk/schema").Policy;
653
- readonly hasRole: import("@astrale-os/sdk/schema").Policy;
654
660
  readonly holdsViewAccess: import("@astrale-os/sdk/schema").Policy;
661
+ readonly hasRole: import("@astrale-os/sdk/schema").Policy;
655
662
  readonly describesSubject: import("@astrale-os/sdk/schema").Policy;
656
663
  readonly isProfileSource: import("@astrale-os/sdk/schema").Policy;
657
664
  readonly describesGroupOfSubject: import("@astrale-os/sdk/schema").Policy;
package/dist/domain.js CHANGED
@@ -107,6 +107,14 @@ const domainRequirements = requirements(legacyRelease({
107
107
  members: [S.core.nodes['workspace.member']],
108
108
  granters: [S.core.nodes['workspace.admin']],
109
109
  },
110
+ // Shell writes the Kernel's view_access for assignViews and its migration (L-SHELL-12,
111
+ // L-SHELL-20); an administrator writes it from the console through Kernel admin. Transition
112
+ // only: removed with those two Functions (step 5).
113
+ {
114
+ role: K.core.nodes['view.manager'],
115
+ members: [requirements.domain],
116
+ granters: [S.core.nodes['workspace.admin']],
117
+ },
110
118
  { role: K.core.nodes['domain.reader'], granters: [S.core.nodes['workspace.admin']] },
111
119
  { role: K.core.nodes['execution.reader'], granters: [S.core.nodes['workspace.admin']] },
112
120
  { role: K.core.nodes['journal.reader'], granters: [S.core.nodes['workspace.admin']] },
@@ -26,7 +26,8 @@ const keyOf = (ref) => `${ref.origin}:${ref.name}`;
26
26
  const ROLE_STEP = 256;
27
27
  /**
28
28
  * One handler's view of authority. Each composition, and the Roles it holds, is read once; only the
29
- * Roles a composition reaches are read, never the catalogue of every installed Role.
29
+ * Roles a composition reaches are read, never the catalogue of every installed Role. What the
30
+ * caller holds is asked once per Role too, so a handler may ask early and use the answer later.
30
31
  */
31
32
  export function authority(capabilities) {
32
33
  const S = capabilities.domain;
@@ -75,13 +76,20 @@ export function authority(capabilities) {
75
76
  return ((origin === S.origin && SHELL_ADMINISTRATIVE.has(name)) ||
76
77
  (origin === kernel && !KERNEL_ORDINARY.has(name)));
77
78
  };
79
+ const callerRoles = new Map();
78
80
  return {
79
81
  roleNode,
80
- async callerHolds(role) {
81
- const caller = capabilities.graph.caller;
82
- if (caller === undefined)
83
- return false;
84
- return caller.query(readRoleNode, { role: S.core.nodes[role].ref });
82
+ callerHolds(role) {
83
+ let holds = callerRoles.get(role);
84
+ if (holds === undefined) {
85
+ const caller = capabilities.graph.caller;
86
+ holds =
87
+ caller === undefined
88
+ ? Promise.resolve(false)
89
+ : caller.query(readRoleNode, { role: S.core.nodes[role].ref });
90
+ callerRoles.set(role, holds);
91
+ }
92
+ return holds;
85
93
  },
86
94
  async heldRole(identity) {
87
95
  const held = await heldKeys(identity);
@@ -1,3 +1,4 @@
1
+ import { authority } from '../authority/index.js';
1
2
  import { setGroupMemberships } from '../../mutations/group/index.js';
2
3
  import { readGroupMembers, readGroupMembersEndpoints } from '../../queries/group/index.js';
3
4
  import { readIdentities } from '../../queries/identity/index.js';
@@ -21,12 +22,17 @@ export async function changeGroupMembers(context, input, present, { checkMembers
21
22
  if (first === undefined)
22
23
  throw invalid();
23
24
  const members = [first, ...others];
25
+ const authorities = authority(context);
24
26
  // A fixed number of reads, whatever the number of members: one per kind, never one per member.
27
+ // What the caller holds is asked with them, so G2 and G3 wait on no further round trip for it.
25
28
  const [identities, observed, stored] = await Promise.all([
26
29
  // A list of members is not a typed Path input: each must be an account or a group.
27
30
  checkMembers ? identitiesOnly(context, members) : true,
28
31
  observeMembers(context, { members, group }, present),
29
32
  readGroup(context, group),
33
+ authorities.callerHolds('directory.provisioner'),
34
+ authorities.callerHolds('group.writer'),
35
+ authorities.callerHolds('workspace.admin'),
30
36
  ]);
31
37
  if (!identities)
32
38
  throw invalid();
@@ -41,7 +47,7 @@ export async function changeGroupMembers(context, input, present, { checkMembers
41
47
  }
42
48
  if (stored === undefined)
43
49
  throw context.error('GROUP_NOT_FOUND', {});
44
- const refusal = await membershipRefusal(context, stored);
50
+ const refusal = await membershipRefusal(context, stored, authorities);
45
51
  if (refusal !== undefined) {
46
52
  if (refusal.code === 'GROUP_ROLE_PROTECTED') {
47
53
  throw context.error(refusal.code, { role: refusal.role });
@@ -4,34 +4,32 @@ import { isProvidedGroup } from './group.js';
4
4
  * G2 and G3 for one change of a Shell group's members. A provided group changes its members only
5
5
  * through its directory, a local one through `group.writer`; a group that holds a Role, directly or
6
6
  * through what it extends, only by an admin or its directory. A member that would join a group it
7
- * already holds is refused by the Kernel, which keeps `extends_with` acyclic.
7
+ * already holds is refused by the Kernel, which keeps `extends_with` acyclic. What the caller holds
8
+ * is asked at once; the group's composition is walked only for a caller who is no admin.
8
9
  */
9
- export async function membershipRefusal(capabilities, group) {
10
- const authorities = authority(capabilities);
11
- const provided = isProvidedGroup(group);
12
- if (provided) {
13
- if (!(await authorities.callerHolds('directory.provisioner'))) {
14
- return { code: 'GROUP_DIRECTORY_MANAGED' };
15
- }
10
+ export async function membershipRefusal(capabilities, group, authorities = authority(capabilities)) {
11
+ if (isProvidedGroup(group)) {
12
+ return (await authorities.callerHolds('directory.provisioner'))
13
+ ? undefined
14
+ : { code: 'GROUP_DIRECTORY_MANAGED' };
16
15
  }
17
- else if (!(await authorities.callerHolds('group.writer'))) {
16
+ const [writer, admin] = await Promise.all([
17
+ authorities.callerHolds('group.writer'),
18
+ authorities.callerHolds('workspace.admin'),
19
+ ]);
20
+ if (!writer)
18
21
  return { code: 'GROUP_NOT_DIRECTORY_MANAGED' };
19
- }
20
- if (!provided) {
21
- const role = await roleProtected(capabilities, group);
22
- if (role !== undefined)
23
- return { code: 'GROUP_ROLE_PROTECTED', role };
24
- }
25
- return undefined;
22
+ if (admin)
23
+ return undefined;
24
+ const role = await authorities.heldRole(group.id);
25
+ return role === undefined ? undefined : { code: 'GROUP_ROLE_PROTECTED', role };
26
26
  }
27
27
  /**
28
28
  * G3: the Role a group holds, when the caller is no admin and so may not change who holds it,
29
- * through its members or by deleting it.
29
+ * through its members or by deleting it. An admin is never refused, so its composition is not read.
30
30
  */
31
- export async function roleProtected(capabilities, group) {
32
- const authorities = authority(capabilities);
33
- const role = await authorities.heldRole(group.id);
34
- if (role === undefined || (await authorities.callerHolds('workspace.admin')))
31
+ export async function roleProtected(capabilities, group, authorities = authority(capabilities)) {
32
+ if (await authorities.callerHolds('workspace.admin'))
35
33
  return undefined;
36
- return role;
34
+ return authorities.heldRole(group.id);
37
35
  }
@@ -1,2 +1,2 @@
1
- // Seam L-SHELL-12: the legacy handler the Runtime registers.
2
- export { legacyAssignViews } from './legacy/index.js';
1
+ // Seams L-SHELL-12, L-SHELL-20: the legacy handlers the Runtime registers.
2
+ export { legacyAssignViews, legacyMigrateViewAccess } from './legacy/index.js';
@@ -1 +1,2 @@
1
1
  export { legacyAssignViews } from './assign-views.js';
2
+ export { legacyMigrateViewAccess } from './migrate-view-access.js';
@@ -0,0 +1,29 @@
1
+ import { defineAction } from '@astrale-os/sdk/action';
2
+ import { legacyMoveViewAccess } from '../../../mutations/view-access/legacy/index.js';
3
+ import { legacyGivenViewKey, legacyReadViewAccessToMove } from '../../../queries/view-access/legacy/index.js';
4
+ /** Edges moved by one write. */
5
+ const BATCH = 200;
6
+ /**
7
+ * @deprecated Copy every Shell `view_access` Edge to the Kernel's, the one the Kernel enforces
8
+ * from kernel-host 1.0.0-beta.45, then delete the Shell's (L-SHELL-20). Idempotent: a View the
9
+ * Kernel's Edge already gives is not copied again, and a second run finds no Shell Edge. Each batch
10
+ * copies and deletes in one write, so a View is never given by neither Edge. Replaced by nothing:
11
+ * Views are given through the Kernel's Edge. Removal: Shell major (delivery step 5), once every
12
+ * instance has run it.
13
+ */
14
+ export const legacyMigrateViewAccess = defineAction()('legacyMigrateViewAccess', async (context) => {
15
+ const { shell, kernel } = await context.query(legacyReadViewAccessToMove, {});
16
+ let copied = 0;
17
+ let removed = 0;
18
+ for (let start = 0; start < shell.length; start += BATCH) {
19
+ const remove = shell.slice(start, start + BATCH);
20
+ const copy = remove.filter((given) => !kernel.has(legacyGivenViewKey(given)));
21
+ const result = await context.mutate(legacyMoveViewAccess, { copy, remove });
22
+ if (!result.ok) {
23
+ throw new Error(`view_access migration stopped after ${removed} Edges: ${result.code}; run it again.`);
24
+ }
25
+ copied += copy.length;
26
+ removed += remove.length;
27
+ }
28
+ return { copied, removed };
29
+ });
@@ -0,0 +1 @@
1
+ export { legacyMoveViewAccess } from './move-view-access.js';
@@ -0,0 +1,26 @@
1
+ import { defineMutation } from '@astrale-os/sdk/mutation';
2
+ /**
3
+ * @deprecated Create Kernel `view_access` Edges and delete Shell ones in one write (L-SHELL-20).
4
+ * Replaced by nothing. Removal: with `legacyMigrateViewAccess`.
5
+ */
6
+ export const legacyMoveViewAccess = defineMutation()((domain) => ({
7
+ id: 'shell.view-access.legacy-move',
8
+ build(input, mutation) {
9
+ for (const { identity, view } of input.copy) {
10
+ mutation.createEdge({
11
+ class: domain.dependencies.kernel.classes.view_access,
12
+ source: identity,
13
+ target: view,
14
+ });
15
+ }
16
+ for (const { identity, view } of input.remove) {
17
+ mutation.deleteEdge({ class: domain.classes.view_access, source: identity, target: view });
18
+ }
19
+ },
20
+ project: () => ({ ok: true }),
21
+ reject: (failure) => ({
22
+ ok: false,
23
+ code: failure.code,
24
+ outcomeKnown: failure.outcomeKnown,
25
+ }),
26
+ }));
@@ -5,7 +5,7 @@ export const setViewAccess = defineMutation()((domain) => ({
5
5
  build(input, mutation) {
6
6
  mutation.expect.query(input.guard);
7
7
  const edge = ({ identity, view }) => ({
8
- class: domain.classes.view_access,
8
+ class: domain.dependencies.kernel.classes.view_access,
9
9
  source: identity,
10
10
  target: view,
11
11
  });
@@ -0,0 +1 @@
1
+ export { legacyGivenViewKey, legacyReadViewAccessToMove, } from './read-view-access-to-move.js';
@@ -0,0 +1,40 @@
1
+ import { defineQuery, Query, queryResult, } from '@astrale-os/sdk/query';
2
+ /**
3
+ * @deprecated The key `LegacyViewAccessToMove.kernel` holds for an Edge (L-SHELL-20). Replaced by
4
+ * nothing. Removal: with `legacyMigrateViewAccess`.
5
+ */
6
+ export const legacyGivenViewKey = ({ identity, view }) => `${identity} ${view}`;
7
+ /**
8
+ * @deprecated Every `view_access` Edge, the Shell's and the Kernel's, in one read (L-SHELL-20).
9
+ * Replaced by nothing: the Kernel's alone is written. Removal: with `legacyMigrateViewAccess`.
10
+ */
11
+ export const legacyReadViewAccessToMove = defineQuery()((domain) => {
12
+ const shellClass = domain.classes.view_access;
13
+ const kernelClass = domain.dependencies.kernel.classes.view_access;
14
+ return {
15
+ id: 'shell.view-access.legacy-to-move',
16
+ page: { size: 1000 },
17
+ pagination: { maximumPages: 103 },
18
+ build: () => Query.from({ edges: [shellClass, kernelClass] }).select({
19
+ kind: 'edges',
20
+ projection: { edge: 'value', source: 'reference', target: 'reference' },
21
+ }),
22
+ project(pages) {
23
+ const shell = [];
24
+ const kernel = new Set();
25
+ for (const page of pages) {
26
+ for (const item of queryResult.edgeItems(page, 'Shell view access to move')) {
27
+ if (item.edge.kind !== 'value')
28
+ continue;
29
+ const { class: key, source, target } = item.edge.value;
30
+ const given = { identity: source, view: target };
31
+ if (key === shellClass.key)
32
+ shell.push(given);
33
+ else if (key === kernelClass.key)
34
+ kernel.add(legacyGivenViewKey(given));
35
+ }
36
+ }
37
+ return { shell, kernel };
38
+ },
39
+ };
40
+ });
@@ -7,7 +7,7 @@ export const readViewAccess = defineQuery()((domain) => ({
7
7
  const held = Query.from({
8
8
  nodes: [Path.id(identities[0]), ...identities.slice(1).map((identity) => Path.id(identity))],
9
9
  }).expand({
10
- via: [domain.classes.view_access],
10
+ via: [domain.dependencies.kernel.classes.view_access],
11
11
  direction: 'outgoing',
12
12
  });
13
13
  return held.select({