@astrale-domains/shell 0.1.0-beta.8 → 0.1.1
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/README.md +264 -64
- package/dist/domain.d.ts +225 -0
- package/dist/domain.js +96 -0
- package/dist/functions/application/index.js +1 -0
- package/dist/functions/application/list-entrypoints.js +6 -0
- package/dist/functions/group/assign-member-to-group.js +4 -0
- package/dist/functions/group/change-group-membership.js +34 -0
- package/dist/functions/group/group-membership-errors.js +23 -0
- package/dist/functions/group/index.js +2 -0
- package/dist/functions/group/unassign-member-from-group.js +4 -0
- package/dist/functions/namespace/index.js +3 -0
- package/dist/functions/namespace/link.js +30 -0
- package/dist/functions/namespace/namespace-errors.js +28 -0
- package/dist/functions/namespace/namespace-name.js +13 -0
- package/dist/functions/namespace/rename.js +31 -0
- package/dist/functions/namespace/unlink.js +21 -0
- package/dist/functions/user/index.js +2 -0
- package/dist/functions/user/invite-user.js +39 -0
- package/dist/functions/user/record-profile.js +18 -0
- package/dist/functions/view-access/assign-views.js +86 -0
- package/dist/functions/view-access/index.js +1 -0
- package/dist/functions/view-access/view-access-errors.js +28 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -3
- package/dist/integrations/index.d.ts +1 -0
- package/dist/integrations/index.js +1 -0
- package/dist/integrations/integrations.d.ts +9 -0
- package/dist/integrations/integrations.js +2 -0
- package/dist/integrations/user-invitation/index.d.ts +4 -0
- package/dist/integrations/user-invitation/index.js +3 -0
- package/dist/integrations/user-invitation/invitation-request-failure.d.ts +5 -0
- package/dist/integrations/user-invitation/invitation-request-failure.js +9 -0
- package/dist/integrations/user-invitation/invite-user-request.d.ts +6 -0
- package/dist/integrations/user-invitation/invite-user-request.js +1 -0
- package/dist/integrations/user-invitation/request-user-invitation.d.ts +8 -0
- package/dist/integrations/user-invitation/request-user-invitation.js +9 -0
- package/dist/mutations/group/index.js +1 -1
- package/dist/mutations/group/set-group-membership.js +29 -0
- package/dist/mutations/namespace/alias-write.js +22 -0
- package/dist/mutations/namespace/index.js +3 -0
- package/dist/mutations/namespace/link-alias.js +15 -0
- package/dist/mutations/namespace/rename-alias.js +13 -0
- package/dist/mutations/namespace/unlink-alias.js +13 -0
- package/dist/mutations/user/index.js +1 -1
- package/dist/mutations/user/record-user-profile.js +13 -0
- package/dist/mutations/view-access/index.js +1 -0
- package/dist/mutations/view-access/set-view-access.js +29 -0
- package/dist/providers/admin-enrollment/create-admin-enrollment-provider.js +78 -0
- package/dist/providers/admin-enrollment/decode-admin-invitation-response.js +20 -0
- package/dist/providers/admin-enrollment/index.js +1 -0
- package/dist/providers/admin-enrollment/read-enrollment-response.js +34 -0
- package/dist/queries/application/index.js +1 -0
- package/dist/queries/application/read-installed-entrypoints.js +28 -0
- package/dist/queries/group/index.js +2 -0
- package/dist/queries/group/read-group-membership-endpoints.js +18 -0
- package/dist/queries/group/read-group-membership.js +35 -0
- package/dist/queries/namespace/index.js +2 -0
- package/dist/queries/namespace/read-designated-node.js +12 -0
- package/dist/queries/namespace/read-view-aliases.js +46 -0
- package/dist/queries/user/index.js +1 -5
- package/dist/queries/user/read-user-profile.js +9 -0
- package/dist/queries/view-access/index.js +1 -0
- package/dist/queries/view-access/read-view-access.js +35 -0
- package/dist/runtime.d.ts +2175 -3
- package/dist/runtime.js +24 -14
- package/dist/schema/index.d.ts +7 -6
- package/dist/schema/index.js +6 -6
- package/dist/schema/modules/application/functions/index.d.ts +1 -0
- package/dist/schema/modules/application/functions/index.js +1 -0
- package/dist/schema/modules/application/functions/list-entrypoints.d.ts +15 -0
- package/dist/schema/modules/application/functions/list-entrypoints.js +16 -0
- package/dist/schema/modules/application/index.d.ts +2 -0
- package/dist/schema/modules/application/index.js +2 -0
- package/dist/schema/modules/application/types/entrypoint-entry.d.ts +8 -0
- package/dist/schema/modules/application/types/entrypoint-entry.js +7 -0
- package/dist/schema/modules/application/types/index.d.ts +1 -0
- package/dist/schema/modules/application/types/index.js +1 -0
- package/dist/schema/modules/group/classes/group.d.ts +24 -0
- package/dist/schema/modules/group/classes/group.js +11 -0
- package/dist/schema/modules/group/classes/index.d.ts +2 -0
- package/dist/schema/modules/group/classes/index.js +2 -0
- package/dist/schema/modules/group/classes/member-of-group.d.ts +30 -0
- package/dist/schema/modules/group/classes/member-of-group.js +12 -0
- package/dist/schema/modules/group/core/administrators-group.d.ts +1 -0
- package/dist/schema/modules/group/core/administrators-group.js +6 -0
- package/dist/schema/modules/group/core/applications-group.d.ts +1 -0
- package/dist/schema/modules/group/core/applications-group.js +6 -0
- package/dist/schema/modules/group/core/index.d.ts +3 -0
- package/dist/schema/modules/group/core/index.js +3 -0
- package/dist/schema/modules/group/core/members-group.d.ts +1 -0
- package/dist/schema/modules/group/core/members-group.js +6 -0
- package/dist/schema/modules/group/functions/assign-member-to-group.js +11 -0
- package/dist/schema/modules/group/functions/index.js +2 -0
- package/dist/schema/modules/group/functions/unassign-member-from-group.js +11 -0
- package/dist/schema/modules/group/index.d.ts +3 -0
- package/dist/schema/modules/group/index.js +3 -0
- package/dist/schema/modules/group/policies/belongs-to-group.d.ts +1 -0
- package/dist/schema/modules/group/policies/belongs-to-group.js +5 -0
- package/dist/schema/modules/group/policies/index.d.ts +1 -0
- package/dist/schema/modules/group/policies/index.js +1 -0
- package/dist/schema/modules/group/types/group-membership-input.js +8 -0
- package/dist/schema/modules/group/types/index.js +1 -0
- package/dist/schema/modules/namespace/classes/alias.d.ts +31 -0
- package/dist/schema/modules/namespace/classes/alias.js +13 -0
- package/dist/schema/modules/namespace/classes/index.d.ts +2 -0
- package/dist/schema/modules/namespace/classes/index.js +2 -0
- package/dist/schema/modules/namespace/classes/namespace.d.ts +31 -0
- package/dist/schema/modules/namespace/classes/namespace.js +10 -0
- package/dist/schema/modules/namespace/core/index.d.ts +1 -0
- package/dist/schema/modules/namespace/core/index.js +1 -0
- package/dist/schema/modules/namespace/core/views-namespace.d.ts +2 -0
- package/dist/schema/modules/namespace/core/views-namespace.js +7 -0
- package/dist/schema/modules/namespace/functions/index.js +3 -0
- package/dist/schema/modules/namespace/functions/link.js +11 -0
- package/dist/schema/modules/namespace/functions/rename.js +11 -0
- package/dist/schema/modules/namespace/functions/unlink.js +11 -0
- package/dist/schema/modules/namespace/index.d.ts +2 -0
- package/dist/schema/modules/namespace/index.js +2 -0
- package/dist/schema/modules/namespace/types/index.js +1 -0
- package/dist/schema/modules/namespace/types/namespace-name.js +7 -0
- package/dist/schema/modules/user/classes/index.d.ts +1 -0
- package/dist/schema/modules/user/classes/index.js +1 -0
- package/dist/schema/modules/user/classes/user.d.ts +34 -0
- package/dist/schema/modules/user/classes/user.js +21 -0
- package/dist/schema/modules/user/functions/index.js +2 -0
- package/dist/schema/modules/user/functions/invite-user.js +12 -0
- package/dist/schema/modules/user/functions/record-profile.js +14 -0
- package/dist/schema/modules/user/index.d.ts +2 -0
- package/dist/schema/modules/user/index.js +2 -0
- package/dist/schema/modules/user/types/index.d.ts +2 -0
- package/dist/schema/modules/user/types/index.js +2 -0
- package/dist/schema/modules/user/types/invitation-summary.d.ts +16 -0
- package/dist/schema/modules/user/types/invitation-summary.js +15 -0
- package/dist/schema/modules/user/types/user-profile.d.ts +7 -0
- package/dist/schema/modules/user/types/user-profile.js +7 -0
- package/dist/schema/modules/view-access/classes/index.d.ts +1 -0
- package/dist/schema/modules/view-access/classes/index.js +1 -0
- package/dist/schema/modules/view-access/classes/view-access.d.ts +32 -0
- package/dist/schema/modules/view-access/classes/view-access.js +18 -0
- package/dist/schema/modules/view-access/core/index.d.ts +1 -0
- package/dist/schema/modules/view-access/core/index.js +1 -0
- package/dist/schema/modules/view-access/core/view-only-group.d.ts +5 -0
- package/dist/schema/modules/view-access/core/view-only-group.js +10 -0
- package/dist/schema/modules/view-access/functions/assign-views.d.ts +16 -0
- package/dist/schema/modules/view-access/functions/assign-views.js +17 -0
- package/dist/schema/modules/view-access/functions/index.d.ts +1 -0
- package/dist/schema/modules/view-access/functions/index.js +1 -0
- package/dist/schema/modules/view-access/index.d.ts +3 -0
- package/dist/schema/modules/view-access/index.js +3 -0
- package/dist/schema/modules/view-access/types/index.js +1 -0
- package/dist/schema/modules/view-access/types/view-assignment.js +16 -0
- package/dist/schema/policies/administers-shell-class.js +11 -0
- package/dist/schema/policies/index.js +4 -0
- package/dist/schema/policies/is-self.js +5 -0
- package/dist/schema/policies/owns-class.js +7 -0
- package/dist/schema/policies/uses-function.js +5 -0
- package/dist/schema/schema.d.ts +833 -0
- package/dist/schema/schema.js +35 -0
- package/dist/schema/views/application-view.js +4 -0
- package/dist/schema/views/index.js +1 -0
- package/dist/utils/canonical-node-path/index.js +1 -0
- package/dist/utils/canonical-node-path/parse-canonical-node-path.js +8 -0
- package/package.json +50 -10
- package/dist/actions/index.js +0 -1
- package/dist/actions/user/index.js +0 -1
- package/dist/actions/user/resolve-home.js +0 -19
- package/dist/application.d.ts +0 -5
- package/dist/application.js +0 -29
- package/dist/mutations/group/membership.js +0 -21
- package/dist/mutations/space/access.js +0 -25
- package/dist/mutations/space/default.js +0 -25
- package/dist/mutations/space/index.js +0 -2
- package/dist/mutations/user/repair.js +0 -85
- package/dist/queries/space/access.js +0 -14
- package/dist/queries/space/admin.js +0 -27
- package/dist/queries/space/index.js +0 -3
- package/dist/queries/space/member.js +0 -24
- package/dist/queries/user/groups.js +0 -24
- package/dist/queries/user/home.js +0 -28
- package/dist/queries/user/owned-spaces.js +0 -24
- package/dist/queries/user/profile.js +0 -17
- package/dist/queries/user/setup.js +0 -18
- package/dist/schema/domain/index.d.ts +0 -26911
- package/dist/schema/domain/index.js +0 -40
- package/dist/schema/group/class.d.ts +0 -3288
- package/dist/schema/group/class.js +0 -7
- package/dist/schema/group/core.d.ts +0 -14
- package/dist/schema/group/core.js +0 -14
- package/dist/schema/group/index.d.ts +0 -6
- package/dist/schema/group/index.js +0 -6
- package/dist/schema/group/methods.d.ts +0 -2059
- package/dist/schema/group/methods.js +0 -21
- package/dist/schema/group/policies.d.ts +0 -1
- package/dist/schema/group/policies.js +0 -6
- package/dist/schema/group/relationships.d.ts +0 -4313
- package/dist/schema/group/relationships.js +0 -8
- package/dist/schema/group/view.d.ts +0 -3291
- package/dist/schema/group/view.js +0 -6
- package/dist/schema/home/index.d.ts +0 -1
- package/dist/schema/home/index.js +0 -1
- package/dist/schema/home/view.d.ts +0 -3
- package/dist/schema/home/view.js +0 -4
- package/dist/schema/setup/functions.d.ts +0 -47
- package/dist/schema/setup/functions.js +0 -27
- package/dist/schema/setup/index.d.ts +0 -1
- package/dist/schema/setup/index.js +0 -1
- package/dist/schema/space/class.d.ts +0 -2313
- package/dist/schema/space/class.js +0 -11
- package/dist/schema/space/core.d.ts +0 -22520
- package/dist/schema/space/core.js +0 -15
- package/dist/schema/space/index.d.ts +0 -6
- package/dist/schema/space/index.js +0 -6
- package/dist/schema/space/methods.d.ts +0 -2089
- package/dist/schema/space/methods.js +0 -30
- package/dist/schema/space/policies.d.ts +0 -4
- package/dist/schema/space/policies.js +0 -21
- package/dist/schema/space/relationships.d.ts +0 -23213
- package/dist/schema/space/relationships.js +0 -24
- package/dist/schema/space/view.d.ts +0 -2316
- package/dist/schema/space/view.js +0 -6
- package/dist/schema/user/class.d.ts +0 -1015
- package/dist/schema/user/class.js +0 -11
- package/dist/schema/user/index.d.ts +0 -3
- package/dist/schema/user/index.js +0 -3
- package/dist/schema/user/methods.d.ts +0 -14
- package/dist/schema/user/methods.js +0 -11
- package/dist/schema/user/view.d.ts +0 -1017
- package/dist/schema/user/view.js +0 -6
- package/dist/workflows/group/assign.js +0 -3
- package/dist/workflows/group/change.js +0 -31
- package/dist/workflows/group/index.js +0 -3
- package/dist/workflows/group/unassign.js +0 -3
- package/dist/workflows/index.js +0 -3
- package/dist/workflows/setup/admin.js +0 -3
- package/dist/workflows/setup/index.js +0 -5
- package/dist/workflows/setup/member.js +0 -3
- package/dist/workflows/setup/plan.js +0 -88
- package/dist/workflows/setup/provision.js +0 -57
- package/dist/workflows/setup/run.js +0 -74
- package/dist/workflows/space/access.js +0 -41
- package/dist/workflows/space/add.js +0 -3
- package/dist/workflows/space/default.js +0 -37
- package/dist/workflows/space/index.js +0 -4
- package/dist/workflows/space/remove.js +0 -3
package/dist/domain.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineDomain, defineFrontend, vite, requirements, } from '@astrale-os/sdk/domain';
|
|
2
|
+
import { K, KernelRootFunctions } from '@astrale-os/sdk/schema';
|
|
3
|
+
import { ShellSchema, S } from './schema/index.js';
|
|
4
|
+
import runtime, {} from './runtime.js';
|
|
5
|
+
const frontend = defineFrontend({
|
|
6
|
+
schema: ShellSchema,
|
|
7
|
+
source: vite({ config: './frontend/vite.config.ts' }),
|
|
8
|
+
routes: { application: { path: '/', handshake: 'shell' } },
|
|
9
|
+
});
|
|
10
|
+
const domainRequirements = requirements({
|
|
11
|
+
// Sujet implicite : le principal installé Shell, utilisé par kernel.self.
|
|
12
|
+
functions: KernelRootFunctions,
|
|
13
|
+
classes: [
|
|
14
|
+
{ class: K.classes.extends_with, operations: ['create', 'read', 'traverse', 'delete'] },
|
|
15
|
+
// listEntrypoints reads every entrypoint View locator and its Domain as Shell.
|
|
16
|
+
{ class: K.classes.Domain, operations: ['read'] },
|
|
17
|
+
{ class: K.classes.of_domain, operations: ['read'] },
|
|
18
|
+
{ class: K.classes.entrypoint, operations: ['traverse'] },
|
|
19
|
+
{ class: K.classes.View, operations: ['traverse'] },
|
|
20
|
+
],
|
|
21
|
+
authority: {
|
|
22
|
+
grant: ['create', 'read', 'update', 'delete', 'traverse', 'use', 'revoke'],
|
|
23
|
+
revoke: ['create', 'read', 'update', 'delete', 'traverse', 'use', 'grant'],
|
|
24
|
+
},
|
|
25
|
+
// Ces profils sont matérialisés par l'installeur, pas par un handler Shell.
|
|
26
|
+
core: [
|
|
27
|
+
{
|
|
28
|
+
subject: S.core.nodes.admin,
|
|
29
|
+
functions: [
|
|
30
|
+
...KernelRootFunctions,
|
|
31
|
+
S.classes.User.static.methods.invite,
|
|
32
|
+
S.classes.User.methods.recordProfile,
|
|
33
|
+
S.classes.Group.static.methods.assignMember,
|
|
34
|
+
S.classes.Group.static.methods.unassignMember,
|
|
35
|
+
S.classes.Namespace.methods.link,
|
|
36
|
+
S.classes.Namespace.methods.unlink,
|
|
37
|
+
S.classes.Namespace.methods.rename,
|
|
38
|
+
// Core Edges cannot author Kernel extends_with, so administrators hold it directly.
|
|
39
|
+
S.functions.listEntrypoints,
|
|
40
|
+
S.functions.assignViews,
|
|
41
|
+
],
|
|
42
|
+
classes: [
|
|
43
|
+
{ class: S.classes.User, operations: ['create', 'read', 'traverse', 'update', 'delete'] },
|
|
44
|
+
{
|
|
45
|
+
class: S.classes.Group,
|
|
46
|
+
operations: ['create', 'read', 'traverse', 'update', 'delete'],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
class: S.classes.member_of_group,
|
|
50
|
+
operations: ['create', 'read', 'traverse', 'update', 'delete'],
|
|
51
|
+
},
|
|
52
|
+
{ class: S.classes.Namespace, operations: ['read', 'traverse'] },
|
|
53
|
+
{ class: S.classes.alias, operations: ['create', 'read', 'traverse', 'update', 'delete'] },
|
|
54
|
+
// assignViews gives and withdraws Views; a Mutation may too. view_access carries no property.
|
|
55
|
+
{ class: S.classes.view_access, operations: ['create', 'read', 'traverse', 'delete'] },
|
|
56
|
+
{ class: K.classes.extends_with, operations: ['create', 'read', 'traverse', 'delete'] },
|
|
57
|
+
],
|
|
58
|
+
authority: {
|
|
59
|
+
grant: ['create', 'read', 'update', 'delete', 'traverse', 'use', 'revoke'],
|
|
60
|
+
revoke: ['create', 'read', 'update', 'delete', 'traverse', 'use', 'grant'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
subject: S.core.nodes.member,
|
|
65
|
+
functions: [
|
|
66
|
+
// Download re-reads the holder Node with the caller's authority: no new data exposure.
|
|
67
|
+
K.functions.download,
|
|
68
|
+
K.functions.introspect,
|
|
69
|
+
K.functions.query,
|
|
70
|
+
K.functions.mutate,
|
|
71
|
+
K.classes.View.static.methods.resolve,
|
|
72
|
+
// The workspace opens only on this catalogue: an invited member must be able to read it.
|
|
73
|
+
S.functions.listEntrypoints,
|
|
74
|
+
// The GUI records the email and name each account signs in with; isSelf bounds it.
|
|
75
|
+
S.classes.User.methods.recordProfile,
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
// For Identities outside Members: Group.assignMember(identity, applications) composes it.
|
|
80
|
+
// `query` opens `/v/<name>`: the full page reads that Path with a Query under the visitor's
|
|
81
|
+
// own rights, so read rules still decide what it sees.
|
|
82
|
+
subject: S.core.nodes.applications,
|
|
83
|
+
functions: [
|
|
84
|
+
S.functions.listEntrypoints,
|
|
85
|
+
K.classes.View.static.methods.resolve,
|
|
86
|
+
K.functions.query,
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
export const domain = defineDomain({
|
|
92
|
+
schema: ShellSchema,
|
|
93
|
+
runtime,
|
|
94
|
+
frontend,
|
|
95
|
+
requirements: domainRequirements,
|
|
96
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { listEntrypoints } from './list-entrypoints.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { defineAction } from '@astrale-os/sdk/action';
|
|
2
|
+
import { readInstalledEntrypoints } from '../../queries/application/index.js';
|
|
3
|
+
/** One graph read: every Domain with an `entrypoint` Edge, its name and its entrypoint View. */
|
|
4
|
+
export const listEntrypoints = defineAction()('listEntrypoints', async ({ query }) => ({
|
|
5
|
+
entrypoints: [...(await query(readInstalledEntrypoints, undefined, { as: 'executor' }))],
|
|
6
|
+
}));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { defineWorkflow } from '@astrale-os/sdk/workflow';
|
|
2
|
+
import { changeGroupMembership } from './change-group-membership.js';
|
|
3
|
+
import { groupMembershipErrors } from './group-membership-errors.js';
|
|
4
|
+
export const assignMember = defineWorkflow()('Group.assignMember', { errors: groupMembershipErrors }, (context) => changeGroupMembership(context, true));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { setGroupMembership } from '../../mutations/group/index.js';
|
|
2
|
+
import { readGroupMembership, readGroupMembershipEndpoints, } from '../../queries/group/index.js';
|
|
3
|
+
import { canonicalNodeId } from '../../utils/canonical-node-path/index.js';
|
|
4
|
+
const matches = (state, present) => state.membership === present && state.authority === present;
|
|
5
|
+
export async function changeGroupMembership(context, present) {
|
|
6
|
+
const invalid = () => context.error('INVALID_GROUP_MEMBERSHIP_REFERENCE', {});
|
|
7
|
+
const membership = {
|
|
8
|
+
member: canonicalNodeId(context.input.member, invalid),
|
|
9
|
+
group: canonicalNodeId(context.input.group, invalid),
|
|
10
|
+
};
|
|
11
|
+
const observe = async () => {
|
|
12
|
+
const endpoints = await context.query(readGroupMembershipEndpoints, membership);
|
|
13
|
+
if (!endpoints.visible)
|
|
14
|
+
throw context.error('GROUP_MEMBERSHIP_ENDPOINTS_UNAVAILABLE', {});
|
|
15
|
+
const current = await context.query(readGroupMembership, membership);
|
|
16
|
+
return { ...current, endpoints: endpoints.guard };
|
|
17
|
+
};
|
|
18
|
+
const current = await context.step.run('observe-membership', observe);
|
|
19
|
+
if (matches(current, present))
|
|
20
|
+
return {};
|
|
21
|
+
const result = await context.step.run('mutate-membership', () => context.mutate(setGroupMembership, { ...membership, current, present }));
|
|
22
|
+
if (result.ok)
|
|
23
|
+
return {};
|
|
24
|
+
if (result.outcomeKnown) {
|
|
25
|
+
throw context.error('GROUP_MEMBERSHIP_CONFLICT', { cause: result.code });
|
|
26
|
+
}
|
|
27
|
+
const recovered = await context.step.run('recover-membership', observe);
|
|
28
|
+
if (matches(recovered, present))
|
|
29
|
+
return {};
|
|
30
|
+
throw context.error('GROUP_MEMBERSHIP_UNCONFIRMED', {
|
|
31
|
+
cause: result.code,
|
|
32
|
+
outcomeKnown: false,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const groupMembershipErrors = {
|
|
3
|
+
GROUP_MEMBERSHIP_ENDPOINTS_UNAVAILABLE: {
|
|
4
|
+
family: 'CONFLICT',
|
|
5
|
+
message: 'Shell must be able to traverse both membership endpoints before confirming membership.',
|
|
6
|
+
details: z.strictObject({}),
|
|
7
|
+
},
|
|
8
|
+
INVALID_GROUP_MEMBERSHIP_REFERENCE: {
|
|
9
|
+
family: 'VALIDATION_ERROR',
|
|
10
|
+
message: 'Member and group must be exact canonical @NodeId paths.',
|
|
11
|
+
details: z.strictObject({}),
|
|
12
|
+
},
|
|
13
|
+
GROUP_MEMBERSHIP_CONFLICT: {
|
|
14
|
+
family: 'CONFLICT',
|
|
15
|
+
message: 'Membership changed concurrently; retry the operation.',
|
|
16
|
+
details: z.strictObject({ cause: z.string() }),
|
|
17
|
+
},
|
|
18
|
+
GROUP_MEMBERSHIP_UNCONFIRMED: {
|
|
19
|
+
family: 'CONFLICT',
|
|
20
|
+
message: 'The requested membership could not be confirmed; retry the operation.',
|
|
21
|
+
details: z.strictObject({ cause: z.string(), outcomeKnown: z.literal(false) }),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { defineWorkflow } from '@astrale-os/sdk/workflow';
|
|
2
|
+
import { changeGroupMembership } from './change-group-membership.js';
|
|
3
|
+
import { groupMembershipErrors } from './group-membership-errors.js';
|
|
4
|
+
export const unassignMember = defineWorkflow()('Group.unassignMember', { errors: groupMembershipErrors }, (context) => changeGroupMembership(context, false));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineAction } from '@astrale-os/sdk/action';
|
|
2
|
+
import { Path } from '@astrale-os/sdk/graph/path';
|
|
3
|
+
import { linkAlias } from '../../mutations/namespace/index.js';
|
|
4
|
+
import { namespaceErrors } from './namespace-errors.js';
|
|
5
|
+
import { namespaceSlug } from './namespace-name.js';
|
|
6
|
+
/**
|
|
7
|
+
* One Mutation with Shell's own authority. An alias grants nothing and each reader sees only the
|
|
8
|
+
* aliases toward Nodes it may read, so `link` names any Node its target Path reaches; the `alias`
|
|
9
|
+
* key keeps a name to one Node for every author.
|
|
10
|
+
*/
|
|
11
|
+
export const link = defineAction()('Namespace.link', { errors: namespaceErrors }, async ({ error, mutate, input, self }) => {
|
|
12
|
+
const name = namespaceSlug(input.name);
|
|
13
|
+
if (name === undefined)
|
|
14
|
+
throw error('INVALID_NAME', { name: input.name });
|
|
15
|
+
const result = await mutate(linkAlias, {
|
|
16
|
+
namespace: self,
|
|
17
|
+
name,
|
|
18
|
+
target: Path.parse(input.target),
|
|
19
|
+
});
|
|
20
|
+
if (result.ok)
|
|
21
|
+
return {};
|
|
22
|
+
switch (result.refusal) {
|
|
23
|
+
case 'NAME_TAKEN':
|
|
24
|
+
throw error('NAME_TAKEN', { name });
|
|
25
|
+
case 'TARGET_ALREADY_ALIASED':
|
|
26
|
+
throw error('TARGET_ALREADY_ALIASED', {});
|
|
27
|
+
case 'TARGET_NOT_FOUND':
|
|
28
|
+
throw error('TARGET_NOT_FOUND', {});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const namespaceErrors = {
|
|
3
|
+
INVALID_NAME: {
|
|
4
|
+
family: 'VALIDATION_ERROR',
|
|
5
|
+
message: 'A name uses lower-case letters, digits, ".", "_" and "-", and starts and ends with a letter or digit.',
|
|
6
|
+
details: z.strictObject({ name: z.string() }),
|
|
7
|
+
},
|
|
8
|
+
TARGET_NOT_FOUND: {
|
|
9
|
+
family: 'NOT_FOUND',
|
|
10
|
+
message: 'The target Path reaches no Node.',
|
|
11
|
+
details: z.strictObject({}),
|
|
12
|
+
},
|
|
13
|
+
NAME_TAKEN: {
|
|
14
|
+
family: 'CONFLICT',
|
|
15
|
+
message: 'This name already designates a Node in this Namespace.',
|
|
16
|
+
details: z.strictObject({ name: z.string() }),
|
|
17
|
+
},
|
|
18
|
+
TARGET_ALREADY_ALIASED: {
|
|
19
|
+
family: 'CONFLICT',
|
|
20
|
+
message: 'This Node already has an alias in this Namespace; rename it instead.',
|
|
21
|
+
details: z.strictObject({}),
|
|
22
|
+
},
|
|
23
|
+
NAME_NOT_FOUND: {
|
|
24
|
+
family: 'NOT_FOUND',
|
|
25
|
+
message: 'No alias of this Namespace has this name.',
|
|
26
|
+
details: z.strictObject({ name: z.string() }),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EdgeSlug, isEdgeSlug } from '@astrale-os/sdk/graph/edge';
|
|
2
|
+
import { Path } from '@astrale-os/sdk/graph/path';
|
|
3
|
+
/** The Edge slug a name is: an exact Edge slug, in lower case. Anything else is no name. */
|
|
4
|
+
export function namespaceSlug(name) {
|
|
5
|
+
return isEdgeSlug(name) && name === name.toLowerCase() ? EdgeSlug(name) : undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The Path of one name of a Namespace: `@<namespace>~"<alias>"(<name>)`. The `alias` key comes from
|
|
9
|
+
* the admitted Domain, so the Worker never loads the Schema's builder.
|
|
10
|
+
*/
|
|
11
|
+
export function aliasPath(domain, namespace, name) {
|
|
12
|
+
return Path.id(namespace).out(domain.classes.alias.key, { slug: name });
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineAction } from '@astrale-os/sdk/action';
|
|
2
|
+
import { renameAlias } from '../../mutations/namespace/index.js';
|
|
3
|
+
import { readDesignatedNode } from '../../queries/namespace/index.js';
|
|
4
|
+
import { namespaceErrors } from './namespace-errors.js';
|
|
5
|
+
import { aliasPath, namespaceSlug } from './namespace-name.js';
|
|
6
|
+
/**
|
|
7
|
+
* One read of the Node the name designates, then one Mutation renaming its alias while it still
|
|
8
|
+
* carries that name. Renaming a name to itself finds the Node and writes nothing.
|
|
9
|
+
*/
|
|
10
|
+
export const rename = defineAction()('Namespace.rename', { errors: namespaceErrors }, async ({ domain, error, query, mutate, input, self }) => {
|
|
11
|
+
const name = namespaceSlug(input.name);
|
|
12
|
+
if (name === undefined)
|
|
13
|
+
throw error('INVALID_NAME', { name: input.name });
|
|
14
|
+
const to = namespaceSlug(input.to);
|
|
15
|
+
if (to === undefined)
|
|
16
|
+
throw error('INVALID_NAME', { name: input.to });
|
|
17
|
+
const target = await query(readDesignatedNode, { path: aliasPath(domain, self, name) });
|
|
18
|
+
if (target === undefined)
|
|
19
|
+
throw error('NAME_NOT_FOUND', { name });
|
|
20
|
+
if (to === name)
|
|
21
|
+
return {};
|
|
22
|
+
const result = await mutate(renameAlias, { namespace: self, target, name, to });
|
|
23
|
+
if (result.ok)
|
|
24
|
+
return {};
|
|
25
|
+
switch (result.refusal) {
|
|
26
|
+
case 'NAME_TAKEN':
|
|
27
|
+
throw error('NAME_TAKEN', { name: to });
|
|
28
|
+
case 'NAME_NOT_FOUND':
|
|
29
|
+
throw error('NAME_NOT_FOUND', { name });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineAction } from '@astrale-os/sdk/action';
|
|
2
|
+
import { unlinkAlias } from '../../mutations/namespace/index.js';
|
|
3
|
+
import { readDesignatedNode } from '../../queries/namespace/index.js';
|
|
4
|
+
import { namespaceErrors } from './namespace-errors.js';
|
|
5
|
+
import { aliasPath, namespaceSlug } from './namespace-name.js';
|
|
6
|
+
/**
|
|
7
|
+
* One read of the Node the name designates, then one Mutation deleting its alias while it still
|
|
8
|
+
* carries that name: a concurrent rename answers NAME_NOT_FOUND instead of removing another name.
|
|
9
|
+
*/
|
|
10
|
+
export const unlink = defineAction()('Namespace.unlink', { errors: namespaceErrors }, async ({ domain, error, query, mutate, input, self }) => {
|
|
11
|
+
const name = namespaceSlug(input.name);
|
|
12
|
+
if (name === undefined)
|
|
13
|
+
throw error('INVALID_NAME', { name: input.name });
|
|
14
|
+
const target = await query(readDesignatedNode, { path: aliasPath(domain, self, name) });
|
|
15
|
+
if (target === undefined)
|
|
16
|
+
throw error('NAME_NOT_FOUND', { name });
|
|
17
|
+
const result = await mutate(unlinkAlias, { namespace: self, target, name });
|
|
18
|
+
if (result.ok)
|
|
19
|
+
return {};
|
|
20
|
+
throw error('NAME_NOT_FOUND', { name });
|
|
21
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineWorkflow } from '@astrale-os/sdk/workflow';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { InvitationRequestFailure } from '../../integrations/user-invitation/index.js';
|
|
4
|
+
import { canonicalNodeId } from '../../utils/canonical-node-path/index.js';
|
|
5
|
+
export const invite = defineWorkflow()('User.invite', {
|
|
6
|
+
errors: {
|
|
7
|
+
INVALID_INVITATION_REFERENCE: {
|
|
8
|
+
family: 'VALIDATION_ERROR',
|
|
9
|
+
message: 'User must be an exact canonical @NodeId path.',
|
|
10
|
+
details: z.strictObject({}),
|
|
11
|
+
},
|
|
12
|
+
INVITATION_CONFLICT: {
|
|
13
|
+
family: 'CONFLICT',
|
|
14
|
+
message: 'The invitation conflicts with an existing request.',
|
|
15
|
+
details: z.strictObject({ requestId: z.string() }),
|
|
16
|
+
},
|
|
17
|
+
INVITATION_REJECTED: {
|
|
18
|
+
family: 'VALIDATION_ERROR',
|
|
19
|
+
message: 'Admin rejected the invitation request.',
|
|
20
|
+
details: z.strictObject({ requestId: z.string() }),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}, async (context) => {
|
|
24
|
+
const user = canonicalNodeId(context.input.user, () => context.error('INVALID_INVITATION_REFERENCE', {}));
|
|
25
|
+
const requestId = context.execution.idempotencyKey ?? crypto.randomUUID();
|
|
26
|
+
try {
|
|
27
|
+
return await context.step.run('request-invitation', () => context.integrations.enrollment.invite({
|
|
28
|
+
user,
|
|
29
|
+
email: context.input.email,
|
|
30
|
+
requestId,
|
|
31
|
+
requestedBy: context.caller.identity,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
catch (cause) {
|
|
35
|
+
if (cause instanceof InvitationRequestFailure)
|
|
36
|
+
throw context.error(cause.code, { requestId });
|
|
37
|
+
throw cause;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineAction } from '@astrale-os/sdk/action';
|
|
2
|
+
import { recordUserProfile } from '../../mutations/user/index.js';
|
|
3
|
+
import { readUserProfile } from '../../queries/user/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* One read and, only when something changed, one write, with Shell's own authority: the policy
|
|
6
|
+
* admits the account itself alone. An email is kept in lower case.
|
|
7
|
+
*/
|
|
8
|
+
export const recordProfile = defineAction()('User.recordProfile', async ({ input, self, query, mutate }) => {
|
|
9
|
+
const asked = {
|
|
10
|
+
...(input.email === undefined ? {} : { email: input.email.trim().toLowerCase() }),
|
|
11
|
+
...(input.displayName === undefined ? {} : { displayName: input.displayName }),
|
|
12
|
+
};
|
|
13
|
+
const current = await query(readUserProfile, { user: self }, { as: 'executor' });
|
|
14
|
+
const set = Object.fromEntries(Object.entries(asked).filter(([key, value]) => current[key] !== value));
|
|
15
|
+
if (Object.keys(set).length > 0)
|
|
16
|
+
await mutate(recordUserProfile, { user: self, set });
|
|
17
|
+
return {};
|
|
18
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { defineAction } from '@astrale-os/sdk/action';
|
|
2
|
+
import { Path } from '@astrale-os/sdk/graph/path';
|
|
3
|
+
import { setViewAccess } from '../../mutations/view-access/index.js';
|
|
4
|
+
import { readViewAccess } from '../../queries/view-access/index.js';
|
|
5
|
+
import { canonicalNodeId } from '../../utils/canonical-node-path/index.js';
|
|
6
|
+
import { viewAccessErrors } from './view-access-errors.js';
|
|
7
|
+
/**
|
|
8
|
+
* One read and at most one Mutation, both with Shell's own authority: holding `can_use` on this
|
|
9
|
+
* Function is what admits the caller, Core admin by default. The write changes only what differs
|
|
10
|
+
* from the Edges read, under their guard, so a View already given or already withdrawn costs
|
|
11
|
+
* nothing and a concurrent change refuses the whole write. An identity listed twice gets both
|
|
12
|
+
* lists; one listed with no View is not read.
|
|
13
|
+
*/
|
|
14
|
+
export const assignViews = defineAction()('assignViews', { errors: viewAccessErrors }, async ({ error, input, query, mutate }) => {
|
|
15
|
+
const nodeId = (reference) => {
|
|
16
|
+
const invalid = () => error('INVALID_VIEW_ASSIGNMENT_REFERENCE', {});
|
|
17
|
+
try {
|
|
18
|
+
return canonicalNodeId(reference, invalid);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw invalid();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const wanted = new Map();
|
|
25
|
+
for (const assignment of input.assignments) {
|
|
26
|
+
const identity = nodeId(assignment.identity);
|
|
27
|
+
const lists = [
|
|
28
|
+
[assignment.set ?? [], true],
|
|
29
|
+
[assignment.unset ?? [], false],
|
|
30
|
+
];
|
|
31
|
+
for (const [list, given] of lists) {
|
|
32
|
+
for (const reference of list) {
|
|
33
|
+
const view = nodeId(reference);
|
|
34
|
+
const views = wanted.get(identity) ?? new Map();
|
|
35
|
+
wanted.set(identity, views);
|
|
36
|
+
if (views.get(view) === !given) {
|
|
37
|
+
throw error('CONTRADICTORY_VIEW_ASSIGNMENT', {
|
|
38
|
+
identity: Path.id(identity).raw,
|
|
39
|
+
view: Path.id(view).raw,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
views.set(view, given);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const [first, ...others] = wanted.keys();
|
|
47
|
+
if (first === undefined)
|
|
48
|
+
return {};
|
|
49
|
+
const read = () => query(readViewAccess, { identities: [first, ...others] });
|
|
50
|
+
const current = await read();
|
|
51
|
+
const { give, withdraw } = changes(wanted, current);
|
|
52
|
+
if (give.length === 0 && withdraw.length === 0)
|
|
53
|
+
return {};
|
|
54
|
+
const result = await mutate(setViewAccess, { give, withdraw, guard: current.guard });
|
|
55
|
+
if (result.ok)
|
|
56
|
+
return {};
|
|
57
|
+
switch (result.refusal) {
|
|
58
|
+
case 'ENDPOINT_NOT_FOUND':
|
|
59
|
+
// Every endpoint of a withdrawal was read: one that vanished was deleted meanwhile.
|
|
60
|
+
if (give.length > 0)
|
|
61
|
+
throw error('VIEW_ASSIGNMENT_ENDPOINT_NOT_FOUND', {});
|
|
62
|
+
throw error('VIEW_ACCESS_CONFLICT', {});
|
|
63
|
+
case 'CHANGED':
|
|
64
|
+
throw error('VIEW_ACCESS_CONFLICT', {});
|
|
65
|
+
case 'UNKNOWN': {
|
|
66
|
+
const after = changes(wanted, await read());
|
|
67
|
+
if (after.give.length === 0 && after.withdraw.length === 0)
|
|
68
|
+
return {};
|
|
69
|
+
throw error('VIEW_ACCESS_UNCONFIRMED', {});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
/** What differs between the Views wanted and the Views held. */
|
|
74
|
+
function changes(wanted, current) {
|
|
75
|
+
const give = [];
|
|
76
|
+
const withdraw = [];
|
|
77
|
+
for (const [identity, views] of wanted) {
|
|
78
|
+
const held = current.given.get(identity);
|
|
79
|
+
for (const [view, given] of views) {
|
|
80
|
+
if (given === (held?.has(view) ?? false))
|
|
81
|
+
continue;
|
|
82
|
+
(given ? give : withdraw).push({ identity, view });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { give, withdraw };
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { assignViews } from './assign-views.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const viewAccessErrors = {
|
|
3
|
+
INVALID_VIEW_ASSIGNMENT_REFERENCE: {
|
|
4
|
+
family: 'VALIDATION_ERROR',
|
|
5
|
+
message: 'Identities and Views must be exact canonical @NodeId paths.',
|
|
6
|
+
details: z.strictObject({}),
|
|
7
|
+
},
|
|
8
|
+
CONTRADICTORY_VIEW_ASSIGNMENT: {
|
|
9
|
+
family: 'VALIDATION_ERROR',
|
|
10
|
+
message: 'One View is both given to and withdrawn from the same identity.',
|
|
11
|
+
details: z.strictObject({ identity: z.string(), view: z.string() }),
|
|
12
|
+
},
|
|
13
|
+
VIEW_ASSIGNMENT_ENDPOINT_NOT_FOUND: {
|
|
14
|
+
family: 'NOT_FOUND',
|
|
15
|
+
message: 'An identity is not a User or a Group, or a View to give is not a View.',
|
|
16
|
+
details: z.strictObject({}),
|
|
17
|
+
},
|
|
18
|
+
VIEW_ACCESS_CONFLICT: {
|
|
19
|
+
family: 'CONFLICT',
|
|
20
|
+
message: 'The Views of these identities changed concurrently; retry the operation.',
|
|
21
|
+
details: z.strictObject({}),
|
|
22
|
+
},
|
|
23
|
+
VIEW_ACCESS_UNCONFIRMED: {
|
|
24
|
+
family: 'CONFLICT',
|
|
25
|
+
message: 'The Views asked for could not be confirmed; retry the operation.',
|
|
26
|
+
details: z.strictObject({}),
|
|
27
|
+
},
|
|
28
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { application } from './application.js';
|
|
3
|
-
export { default as runtime } from './runtime.js';
|
|
1
|
+
export { ShellSchema, S, type Shell, type ShellClasses, type ShellSchemaInput, type InvitationSummary, User, Group, member_of_group, adminGroup, memberGroup, Namespace, alias, viewsNamespace, } from './schema/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { application } from './application.js';
|
|
3
|
-
export { default as runtime } from './runtime.js';
|
|
1
|
+
export { ShellSchema, S, User, Group, member_of_group, adminGroup, memberGroup, Namespace, alias, viewsNamespace, } from './schema/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { integrations } from './integrations.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { integrations } from './integrations.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const integrations: {
|
|
2
|
+
enrollment: import("@astrale-os/sdk/integration").IntegrationDefinition<"shell.enrollment", {
|
|
3
|
+
invite: import("@astrale-os/sdk/integration").IntegrationOperation<import("./user-invitation/index.js").InviteRequest, {
|
|
4
|
+
invitationId: string;
|
|
5
|
+
user: string;
|
|
6
|
+
state: "accepted" | "cancelled" | "completed" | "expired" | "failed" | "pending" | "registered" | "registering";
|
|
7
|
+
}, "keyed" | "safe">;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { enrollment } from './request-user-invitation.js';
|
|
2
|
+
export { InvitationRequestFailure } from './invitation-request-failure.js';
|
|
3
|
+
export { type InviteRequest } from './invite-user-request.js';
|
|
4
|
+
export type { InvitationSummary } from '../../schema/modules/user/index.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Expected business refusal from the invitation service, without its response body. */
|
|
2
|
+
export declare class InvitationRequestFailure extends Error {
|
|
3
|
+
readonly code: 'INVITATION_CONFLICT' | 'INVITATION_REJECTED';
|
|
4
|
+
constructor(code: 'INVITATION_CONFLICT' | 'INVITATION_REJECTED');
|
|
5
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { InviteRequest } from './invite-user-request.js';
|
|
2
|
+
export declare const enrollment: import("@astrale-os/sdk/integration").IntegrationDefinition<"shell.enrollment", {
|
|
3
|
+
invite: import("@astrale-os/sdk/integration").IntegrationOperation<InviteRequest, {
|
|
4
|
+
invitationId: string;
|
|
5
|
+
user: string;
|
|
6
|
+
state: "accepted" | "cancelled" | "completed" | "expired" | "failed" | "pending" | "registered" | "registering";
|
|
7
|
+
}, "keyed" | "safe">;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineIntegration } from '@astrale-os/sdk/integration';
|
|
2
|
+
export const enrollment = defineIntegration({
|
|
3
|
+
id: 'shell.enrollment',
|
|
4
|
+
operations: {
|
|
5
|
+
invite: defineIntegration.operation({
|
|
6
|
+
replay: ({ requestId }) => ({ kind: 'keyed', source: 'input', key: requestId }),
|
|
7
|
+
}),
|
|
8
|
+
},
|
|
9
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { setGroupMembership } from './set-group-membership.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineMutation } from '@astrale-os/sdk/mutation';
|
|
2
|
+
export const setGroupMembership = defineMutation()((domain) => ({
|
|
3
|
+
id: 'shell.group.membership.set',
|
|
4
|
+
build(input, mutation) {
|
|
5
|
+
mutation.expect.query(input.current.endpoints);
|
|
6
|
+
mutation.expect.query(input.current.guard);
|
|
7
|
+
const endpoints = { source: input.member, target: input.group };
|
|
8
|
+
const changes = [
|
|
9
|
+
{ class: domain.classes.member_of_group, current: input.current.membership },
|
|
10
|
+
{ class: domain.dependencies.kernel.classes.extends_with, current: input.current.authority },
|
|
11
|
+
];
|
|
12
|
+
for (const edge of changes) {
|
|
13
|
+
if (edge.current === input.present)
|
|
14
|
+
continue;
|
|
15
|
+
if (input.present)
|
|
16
|
+
mutation.createEdge({ class: edge.class, ...endpoints });
|
|
17
|
+
else
|
|
18
|
+
mutation.deleteEdge({ class: edge.class, ...endpoints });
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
project: () => ({ ok: true }),
|
|
22
|
+
reject: (failure) => {
|
|
23
|
+
if (failure.outcomeKnown &&
|
|
24
|
+
failure.code !== 'MUTATION_PRECONDITION_FAILED' &&
|
|
25
|
+
failure.code !== 'MUTATION_CONFLICT')
|
|
26
|
+
throw failure;
|
|
27
|
+
return { ok: false, code: failure.code, outcomeKnown: failure.outcomeKnown };
|
|
28
|
+
},
|
|
29
|
+
}));
|