Guard / foldkit-guard/server

GuardServer

namespace · packages/guard/src/server.ts:42

const GuardServer = { me, filter, memory, writer, mutation, validate }

In the guides

Members

GuardServer.me

function · packages/guard/src/server.ts:52

me<User, R>(
  entity: MeEntity,
  policy: Policy<User, R>,
): EntitySource<Option.Option<User>, R>

Guards.me's Source: the one row, me, as the asker is, with may answered by policy. signedIn is whether the server answered as someone; key is their key. Any other id is no row.

ts
RemoteServer.make({ entities: [GuardServer.me(Me, Policy), …] })

GuardServer.filter

function · packages/guard/src/server.ts:100

filter<User, R>(
  entity: AnyEntity,
  policy: Policy<User, R>,
): (
  principal: Option.Option<User>,
  fields: readonly string[],
) => Effect.Effect<ReadPlan, never, R>

An EntitySource.filter for entity: each requested member with a Read rule is decided per record, omitted or redacted where the rule refuses. The fields a rule reads are read too, so it can be decided.

ts
RemoteServer.entity(Todo, { read, filter: GuardServer.filter(Todo, Policy) })

GuardServer.memory

function · packages/guard/src/server.ts:173

memory<User>(
  policy: Policy<User>,
  entities: ReadonlyArray<AnyEntity>,
): {
  readable: (
    principal: Option.Option<User>,
    row: (
      entity: string,
      id: string,
    ) => Option.Option<Readonly<Record<string, unknown>>>,
  ) => Effect.Effect<MemoryReadable>
  filter: {
    [k: string]: (
      principal: Option.Option<User>,
      fields: readonly string[],
    ) => Effect.Effect<ReadPlan, never, never>
  }
}

RemoteServer.memory's readable and filter for entities, the Entities as Guards.attach returned them. A row's Read is decided per stored row, an inherited Read by looking the related row up; an Entity not listed is read by everyone.

GuardServer.writer

function · packages/guard/src/server.ts:245

writer<E extends AnyEntity, User, R, RW>(
  entity: E,
  policy: Policy<User, R>,
  writer: EntityWriter<Option.Option<User>, RW>,
  options?: {
    readonly presets?: {
      readonly [K in keyof E['fields']]?: (
        user: User,
      ) => E['fields'][K]['schema']['Type']
    }
  },
): EntityWriter<Option.Option<User>, R | RW>

writer with entity's write rules checked before each write lands, for RemoteServer.write. An update or a delete reads the row first under the writer's own read (its Source's read rule): a row the writer may not see is refused as one that is not there, and a delete of it changes nothing. Then the row's rule for the command must hold over the row (an update's over the row as it would be after it too, so a write cannot move a row out of its writer's reach), each member it sets must allow setting it, and each relation it points elsewhere must allow the unlink and the link. A create's rules hold over the values it proposes, where a field the create leaves unset matches no guard, not even one for an empty value. A refusal is Forbidden, with the sentence an explain gave; the write never runs.

presets are fields a create takes from who is asking, never from the client (ownerId: user => user.id): set before the rules are checked, so a rule over them holds of the creator. A visitor has nothing to preset from, and a create whose input sets a preset field is a mistake of the server's, refused.

ts
RemoteServer.write(EditTodo, GuardServer.writer(Todo, Policy, writer(Db.Todo)))

GuardServer.mutation

function · packages/guard/src/server.ts:492

mutation<User, R, RS, RM, Name extends string, Input, Output, Refused>(
  mutation: MutationDescriptor<Name, Input, Output, Refused>,
  guard: {
    readonly entity: AnyEntity
    readonly policy: Policy<User, R>
    readonly interaction: 'Create' | 'Update' | 'Delete' | InteractionKey
    readonly id?:
      ((input: Input) => string | Option.Option<string>) | undefined
    readonly source: EntitySource<Option.Option<User>, RS>
    readonly fields?: ReadonlyArray<string> | undefined
  },
  run: (context: {
    readonly input: Input
    readonly principal: Option.Option<User>
    readonly requestId: string
    readonly row: Option.Option<Readonly<Record<string, unknown>>>
  }) => Effect.Effect<
    MutationOutcome<Output>,
    RemoteServerError | MutationRefused | RemoteForbidden,
    RM
  >,
): MutationSource<Option.Option<User>, R | RS | RM>

A handler's Source (RemoteServer.mutation's) with entity's rule for interaction checked before it runs. The row the input names (id) is read through source, which must apply the Entity's Read rules, and the rule must hold over it; a row its writer may not see is refused. With no id (a create), the rule must hold of who is asking alone, a field it reads matching no guard. run is given the row as read: its fields the rule reads, its id, and fields. A refusal is Forbidden; run never runs. What run writes is its own: check members it sets there. Each row of entity its answer carries is read back through source, so the answer shows its writer only what they may see, a row it hid from them as deleted.

ts
GuardServer.mutation(ArchiveTodo, {
  entity: Todo, policy: Policy, interaction: Archive.key,
  id: input => input.id, source: source(Db.Todo),
}, ({ input, row }) => …)

GuardServer.validate

function · packages/guard/src/server.ts:612

validate<User, R>(
  policy: Policy<User, R>,
  config: {
    readonly entities: ReadonlyArray<AnyEntity>
    readonly domain: {
      readonly registry: {
        readonly queries: ReadonlyMap<
          string,
          {
            readonly Result: unknown
            readonly body?: AnyQuery | undefined
          }
        >
      }
    }
    readonly server: ServerDefinition<any, any>
  },
): void

Checks, at startup, that server applies the guards of entities, the Entities as Guards.attach returned them: each one's Source and each query listing it apply its row's Read rules (GuardDrizzle.visible, or GuardServer.memory), its Source applies its members' and relations' (GuardServer.filter), and each declared write of it lands through GuardServer.writer, all built from policy; and that no query's body filters or orders by a member with a Read rule, which would answer for the value it hides. A hand-written Source over a guarded Entity would serve every row, and a bare writer would take every write; this throws instead, naming each. A handler (RemoteServer.mutation) writes what its code writes, which no check at startup can read.

ts
GuardServer.validate(Policy, { entities: [Todo, Project], domain: Data, server: Server })