Skip to content
Switch branches/tags
Go to file
Cannot retrieve contributors at this time
# Sandstorm - Personal Cloud Sandbox
# Copyright (c) 2014 Sandstorm Development Group, Inc. and contributors
# All rights reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
$import "/capnp/c++.capnp".namespace("sandstorm");
using Util = import "util.capnp";
using Powerbox = import "powerbox.capnp";
using Activity = import "activity.capnp";
using Identity = import "identity.capnp";
# ========================================================================================
# Runtime interface
interface SandstormApi(AppObjectId) {
# The Sandstorm platform API, exposed as the default capability over the two-way RPC connection
# formed with the application instance. This object specifically represents the supervisor
# for this application instance -- two different application instances (grains) never share a
# supervisor.
# `AppObjectId` is the format in which the application identifies its persistent objects which
# can be saved from the outside; see `AppPersistent`, below.
# TODO(soon): Read the grain title as set by the user. Also have interface to offer a new
# title and icon?
deprecatedPublish @0 ();
deprecatedRegisterAction @1 ();
# These powerbox-related methods were never implemented. Eventually it was decided that they
# specified the wrong model.
shareCap @2 (cap :Capability, displayInfo :Powerbox.PowerboxDisplayInfo)
-> (sharedCap :Capability, link :SharingLink);
# Share a capability, so that it may safely be sent to another user. `sharedCap` is a wrapper
# (membrane) around `cap` which can have a petname assigned and can be revoked via `link`. The
# share is automatically revoked if `link` is discarded. If `cap` is persistable, then both
# `sharedCap` and `link` also are.
# This method is intended to be used by programs that actually implement a communications link
# over which a capability could be sent from one user to another. For example, a chat app would
# use this to prepare a capability to be embedded into a message. In these cases, capabilities
# may be shared without going through the system sharing UI, and therefore the application must
# set up the sharing link itself.
# In general, you should NOT call this on a capability that you will then pass to
# `SessionContext.offer()`.
shareView @3 (view :UiView) -> (sharedView :UiView, link :ViewSharingLink);
# Like `shareCap` but with extra options for sharing a UiView, such as setting a role and
# permissions.
save @8 (cap :Capability, label :Util.LocalizedText) -> (token :Data);
# Saves a persistent capability and returns a token which can be used to restore it later
# (including in a future run of the app) via `restore()` (below). Not all capabilities can be
# saved -- check the documentation for the capability you are using to see if it is described as
# "persistent".
# The grain owner will be able to inspect saved capabilities via the UI. `label` will be shown
# there and should briefly describe what this capability is used for.
# To see how to make your own app's objects persistent, see the `AppPersistent` interface defined
# later in this file. Note that it's perfectly valid to pass your app's own capabilities to
# `save()`, if they are persistent in this way.
# (Under the hood, `` calls the capability's `` method,
# then stores the result in a table indexed by the new randomly-generated token. The app CANNOT
# call `` on external capabilities itself; such calls will be blocked by the
# supervisor (and the result would be useless to you anyway, because you have no way to restore
# it). You must use `` so that saved capabilities can be inspected by the
# user.)
restore @4 (token :Data) -> (cap :Capability);
# Given a token previously returned by `save()`, get the capability it pointed to. The returned
# capability should implement the same interfaces as the one you saved originally, so you can
# downcast it as appropriate.
drop @5 (token :Data);
# Deletes the token and frees any resources being held with it. Once drop()ed, you can no longer
# restore() the token. This call is idempotent: it is not an error to `drop()` a token that has
# already been dropped.
deleted @6 (ref :AppObjectId);
# Notifies the supervisor that an object hosted by this application has been deleted, and
# therefore all references to it may as well be dropped. This affects *incoming* references,
# whereas `drop()` affects *outgoing*.
stayAwake @7 (displayInfo :Activity.NotificationDisplayInfo,
notification :Activity.OngoingNotification)
-> (handle :Util.Handle);
# Requests that the app be allowed to continue running in the background, even if no user has it
# open in their browser. An ongoing notification is delivered to the user who owns the grain to
# let them know of this. The user may cancel the notification, in which case the app will no
# longer be kept awake. If not canceled, the app remains awake at least until it drops `handle`.
# Unlike other ongoing notifications, `notification` in this case need not be persistent (since
# the whole point is to prevent the app from restarting), and `handle` is not persistent.
# WARNING: A machine failure or similar situation can still cause the app to shut down at any
# time. Currently, the app will NOT be restarted after such a failure.
# TODO(someday): We could make `handle` be persistent. If the app persists it -- and if
# `notification` is persistent -- we would automatically restart the app after an unexpected
# failure.
backgroundActivity @9 (event :Activity.ActivityEvent);
# Post an activity event to the grain's activity log that was *not* initiated by any particular
# user. For activity that should be attributed to a user, use SessionContext.activity().
getIdentityId @10 (identity: Identity.Identity) -> (id :Data);
# Gets the ID of the identity, as it would appear in UserInfo.identityId.
# This method is here on `SandstormApi` rather than on `Identity` in order to ease a possible
# future transition to a model where identity IDs are not globally stable and each grain has a
# separate (possibly incompatible) map from identity ID to account ID.
schedule @11 ScheduledJob;
# Schedule a callback to be executed in the future.
# The job may be scheduled to execute once at a specific time, or periodically. See
# `ScheduledJob` for more information.
# When this method returns, the job will have been scheduled.
struct ScheduledJob {
# A job to be scheduled by `SandstormApi.schedule()`.
name @0 :Util.LocalizedText;
# A name for the job. This will be used to describe the job to the user.
callback @1 :Callback;
# A callback to actually execute when it is time to run the job.
# In addition to implementing the `Callback` interface below, this must also
# implement `AppPersistent`. When a request to schedule the job is made, Sandstorm
# will `save()` the callback, and then later `restore()` it when it is time to
# run the job. See `AppPersistent` for more details on how this works.
# Note that, in some circumstances, the app may need to save some information
# locally to recognize the task. There is a possibility that between scheduling
# the job and actually saving the information, The app may be killed, the server
# may crash, or some other failure may occur that prevents the app from saving
# the information. We recommend dealing with this in one of the following ways:
# - Where possible, save all needed information in the `objectId` returned
# by ``; this avoids the race condition entirely.
# - Otherwise, in your implementation of `MainView.restore()`, if you are asked
# to restore a callback you don't recognize, return a dummy callback that does
# nothing, and returns `cancelFutureRuns = true`. This will cause Sandstorm to
# cancel and delete the unknown job.
# When the callback is called, Sandstorm will avoid killing the grain until the callback returns.
# If the callback throws a "disconnected" exception, or if Sandstorm is forced to kill the
# grain for some reason, the callback will be retried. After a limited number of failed retries,
# Sandstorm may alert the owner to a problem and stop retrying.
interface Callback {
run @0 () -> (cancelFutureRuns :Bool = false);
# Run the job. If the return value has `cancelFutureRuns = true` and this is a periodic job,
# the job will be cancelled and will not be run again.
schedule :union {
# The actual schedule of the job.
oneShot :group {
# Run the job exactly once.
when @2 :Util.DateInNs;
# The time at which the job should be run.
slack @3 :Util.DurationInNs;
# In order to improve resource utilization, Sandstorm prefers imprecise
# scheduling -- this allows it to choose a time of low activity to invoke
# the callback. The `slack` field tells Sandstorm how much freedom it
# has -- it may schedule the task any time between `when` and `when +
# slack`. A value of zero for `slack` is special: it is equivalent to
# 1/8th of the difference between now and `when` -- this sets the window
# adaptively, such that the further in the future the event is scheduled,
# the more slack it has. You must explicitly set a non-zero value if you
# need better precision. Additionally, the value must be greater than
# `minimumSchedulingSlack`, or an exception will be thrown. If you need
# better precision than this, you should schedule a callback for slightly
# before the target time, then countdown the remaining time in-process.
periodic @4 :SchedulingPeriod;
# Run the job on a regular interval.
# In order to improve resource utilization, Sandstorm prefers imprecise
# scheduling -- this allows it to choose a time of low activity to
# invoke the callback. Therefore, Sandstorm guarantees that the callback
# will be called once per scheduling period, but does not make any guarantees
# about exactly when in that period the call will occur. If you need more
# precise scheduling, you will need to use `oneShot`, and schedule the next
# occurance from the callback itself, before returning.
const minimumSchedulingSlack :Util.DurationInNs = 300000000000;
# 5 minutes: The minimum value allowable for the `slack` parameter passed to `scheduleAt()`.
enum SchedulingPeriod {
annually @3;
monthly @2;
weekly @4;
daily @1;
hourly @0;
interface UiView @0xdbb4d798ea67e2e7 {
# Implements a user interface with which a user can interact with the grain. We call this a
# "view" because a single grain may actually have multiple "views" that provide different
# functionality or represent multiple logical objects in the same physical grain.
# When an application starts up, it must export an instance of UiView as its starting
# capability on the Cap'n Proto two-party connection. This represents the grain's main view and
# is what the user will see when they open the grain.
# It is possible for a grain to export additional views via the usual powerbox mechanisms. For
# instance, a spreadsheet app might let the user create a "view" of a few cells of the
# spreadsheet, allowing them to share those cells to another user without sharing the entire
# sheet. To accomplish this, the app would create an alternate UiView object that implements
# an interface just to those cells, and then would use `UiSession.offer()` to offer this object
# to the user. The user could then choose to open it, share it, save it for later, etc.
# TODO(apibump): Parameterize UiView on:
# - A struct type representing permissions, which must have only boolean fields, each annotated
# with a PermissionDef. Replace all istances of PermissionSet with this.
# - An enum type representing roles, each annotated with its permission set and a RoleDef.
# (Requires Cap'n Proto changes to allow parametirizing on enum types, I guess...)
# - An enum type representing activity event types, each annotated with an ActivityTypeDef.
getViewInfo @0 () -> ViewInfo;
# Get metadata about the view, especially relating to sharing.
struct ViewInfo {
permissions @0 :List(PermissionDef);
# List of permission bits which apply to this view. Permissions typically include things like
# "read" and "write". When sharing a view, the sending user may select a set of permissions to
# grant to the receiving user, and may modify this set later on. When a new user interface
# session is initiated, the platform indicates which permissions the user currently has.
# The grain's owner always has all permissions.
# It is important that new versions of the app only add new permissions, never remove existing
# ones, since permission IDs are indexes into the list and persist through upgrades.
# In a true capability system, permissions would normally be implemented by wrapping the main
# view in filters that prohibit disallowed actions. For example, to give a user read-only
# access to a grain, you might wrap its UiView in a wrapper that checks all incoming requests
# and disallows the ones that would modify the content. However, this approach does not work
# terribly well for UiView for a few reasons:
# - For complex UIs, HTTP is often the wrong level of abstraction for this kind of filtering.
# It _may_ work for modern apps that push all UI logic into static client-side Javascript and
# only serve RPCs over dynamic HTTP, but it won't work well for many legacy apps, and we want
# to be able to port some of those apps to Sandstorm.
# - If a UiView is reshared several times, each time adding a new filtering wrapper, then
# requests could get slow as they have to pass through all the separate filters. This would
# be especially bad if some of the filters live in other grains, as those grains would have
# to spin up whenever the resulting view is used.
# - Compared to computers, humans are relatively less likely to be vulnerable to confused
# deputy attacks and relatively more likely to be confused by the concept of having multiple
# capabilities to the same object that provide different access levels. For example, say
# Alice and Bob both share the same document to Carol, but Alice only grants read access
# while Bob gives read/write. Carol should only see one instance of the document in her
# grain list and she should see the read/write interface when she opens it. But this instance
# isn't simply the one she got from Bob -- if Bob revokes his share but Alice continues to
# share read rights, Carol should now see the read-only interface when she opens the same
# grain.
# To solve all three problems, we have permission bits that are processed when creating a new
# session. Instead of filtering individual requests, wrappers of UiView only need to filter
# calls to `newSession()` in order to restrict the permission set as appropriate. Once a
# session is thus created, it represents a direct link to the target grain. Also, the platform
# can implement special handling of sharing and permission bits that allow it to recognize when
# two UiViews are really the same view with different permissions applied, and can then combine
# them in the UI as appropriate.
# Implementation-wise, in addition to the permissions enumerated here, there is an additional
# "is human" pseudopermission provided by the Sandstorm platform, which is invisible to apps,
# but affects how UiView capabilities may be used. In particular, all users are said to possess
# the "is human" pseudopermission, whereas all other ApiTokenOwners do not, and by default, the
# privilege is not passed on. All method calls on UiView require that the caller have the "is
# human" permission. In practical terms, this means that grains can pass around UiView
# capabilities, but only users can actually use them to open sessions.
# It is actually entirely possible to implement a traditional filtering membrane around a
# UiView, perhaps to implement a kind of access that can't be expressed using the permission
# bits defined by the app. But doing so will be awkward, slow, and confusing for all the
# reasons listed above.
roles @1 :List(RoleDef);
# Choosing individual permissions is not very intuitive for most users. Therefore, the sharing
# interface prefers to offer the user a list of "roles" to assign to each recipient. For
# example, a document might have roles like "editor" and "viewer". Each role corresponds to
# some list of permissions. The application may define a set of roles to offer via this list.
# In addition to the roles in this list, the sharing interface will always offer a "full access"
# or "same as me" option. So, it only makes sense to define roles that represent less than
# "full access", and leaving the role list entirely empty is reasonable if there are no such
# restrictions to offer.
# It is important that new versions of the app only add new roles, never remove existing ones,
# since role IDs are indexes into the list and persist through upgrades.
deniedPermissions @2 :Identity.PermissionSet;
# Set of permissions which will be removed from the permission set when creating a new session
# though this object. This set should be empty for the grain's main UiView, but when that view
# is shared with less than full access, recipients will get a proxy UiView which has a non-empty
# `deniedPermissions` set.
# It is not the caller's responsibility to enforce this set. It is provided mainly so that the
# sharing UI can avoid offering options to the user that don't make sense. For instance, if
# Alice has read-only access to a document and wishes to share the document to Bob, the sharing
# UI should not offer Alice the ability to share write access, because she doesn't have it in
# the first place. The sharing UI figures out what Alice has by examining `deniedPermissions`.
matchRequests @3 :List(Powerbox.PowerboxDescriptor);
# Indicates what kinds of powerbox requests this grain may be able to fulfill. If the grain
# is chosen by the user during a powerbox request, then `newRequestSession()` will be called
# to set up the embedded UI session.
matchOffers @4 :List(Powerbox.PowerboxDescriptor);
# Indicates what kinds of powerbox offers this grain is interested in accepting. If the grain
# is chosen by the user during a powerbox offer, then `newOfferSession()` will be called
# to start a session around this.
appTitle @5 :Util.LocalizedText;
# Title for the app of which this grain is an instance.
grainIcon @6 :Util.StaticAsset;
# Icon for the app of which this grain is an instance, suitable for display in a grain list.
eventTypes @7 :List(Activity.ActivityTypeDef);
# Definitions of activity event types generated by this grain. Each activity event is tagged
# with one of these types. Typed events potentially allow for advanced filtering options and
# compact activity summaries.
struct PowerboxTag {
# Tag to be used in a `PowerboxDescriptor` to describe a `UiView`.
title @0 :Text;
# The title of the `UiView` as chosen by the introducer identity.
newSession @1 (userInfo :Identity.UserInfo, context :SessionContext,
sessionType :UInt64, sessionParams :AnyPointer,
tabId :Data)
-> (session :UiSession);
# Start a new user interface session. This happens when a user first opens the view, or when
# the user returns to a tab that has been inactive long enough that the server was killed off in
# the meantime.
# `userInfo` specifies the user's display name and permissions, as authenticated by the system.
# `context` contains callbacks that can be used to invoke system functionality in the context of
# the session, such as displaying the powerbox.
# `sessionType` is the type ID specifying the interface which the returned `session` should
# implement. All views should support the `WebSession` interface to support opening the view
# in a browser. Other session types might be useful for e.g. desktop and mobile apps.
# `sessionParams` is a struct whose type is specified by the session type. By convention, this
# struct should be defined nested in the session interface type with name "Params", e.g.
# `WebSession.Params`. This struct contains some arbitrary startup information.
# `tabId` is a stable identifier for the "tab" (as in, Sandstorm sidebar tab) in which the grain
# is being displayed. A single tab may span multiple UiSessions, for instance if the user
# suspends their laptop and then restores later, or if the grain crashes. More importantly,
# `tabId` allows multiple grains being composed in the same tab to coordinate. For example, when
# a grain is embedded in the powerbox UI to respond to a request, it sees the same `tabId` as
# the requesting grain. Similarly, when a grain embeds other grains within iframes within its
# own UI, they will all see the same `tabId`. One particular case where `tabId` is useful is
# in implementing an `EmailVerifier` that cannot be MITM'd. NOTE: `tabId` should NOT be presumed
# to be a secret, although no two tabs in all of time will have the same `tabId`.
# For API requests, `tabId` uniquely identifies the token, and so can be used to correlate
# multiple requests from the same client.
newRequestSession @2 (userInfo :Identity.UserInfo, context :SessionContext,
sessionType :UInt64, sessionParams :AnyPointer,
requestInfo :List(Powerbox.PowerboxDescriptor), tabId :Data)
-> (session :UiSession);
# Creates a new session based on a powerbox request. `requestInfo` is the subset of the original
# request description which matched descriptors that this grain indicated it provides via
# `ViewInfo.matchRequests`. The list is also sorted with the "best match" first, such that
# it is reasonable for a grain to ignore all descriptors other than the first.
# Keep in mind that, as with any other session, the UiSession capability could become
# disconnected randomly and the front-end will then reconnect by calling `newRequestSession()`
# again with the same parameters. Generally, apps should avoid storing any session-related state
# on the server side; it's easy to use client-side sessionStorage instead.
# The `tabId` passed here identifies the requesting tab; see docs under `newSession()`.
newOfferSession @3 (userInfo :Identity.UserInfo, context :SessionContext,
sessionType :UInt64, sessionParams :AnyPointer,
offer :Capability, descriptor :Powerbox.PowerboxDescriptor,
tabId :Data)
-> (session :UiSession);
# Creates a new session based on a powerbox offer. `offer` is the capability being offered and
# `descriptor` describes it.
# By default, an "offer" session is displayed embedded in the powerbox much like a "request"
# session is. If the the session implements a quick action -- say "share to friend by email" --
# then it may make sense for it to remain embedded, returning the user to the offering app when
# done. The app may call `SessionContext.close()` to indicate that it's time to close. However,
# in some cases it makes a lot of sense for the app to become "full-frame", for example a
# document editor app accepting a document offer may want to then open the editor for long-term
# use. Such apps should call `SessionContext.openView()` to move on to a full-fledged session.
# Finally, some apps will take an offer, wrap it in some filter, and then make a new offer of the
# wrapped capability. To that end, calling `SessionContext.offer()` will end the offer session
# but immediately start a new offer interaction in its place using the new capability.
# Keep in mind that, as with any other session, the UiSession capability could become
# disconnected randomly and the front-end will then reconnect by calling `newOfferSession()`
# again with the same parameters. Generally, apps should avoid storing any session-related state
# on the server side; it's easy to use client-side sessionStorage instead. (Of course, if the
# session calls `SessionContext.openView()`, the new view will be opened as a regular session,
# not an offer session.)
# The `tabId` passed here identifies the offering tab; see docs under `newSession()`.
# ========================================================================================
# User interface sessions
interface UiSession {
# Base interface for UI sessions. The most common subclass is `WebSession`.
interface SessionContext {
# Interface that the application can use to call back to the platform in the context of a
# particular session. This can be used e.g. to ask the platform to present certain system
# dialogs to the user.
getSharedPermissions @0 () -> (var :Util.Assignable(Identity.PermissionSet).Getter);
# Returns an observer on the permissions held by the user of this session.
# This observer can be persisted beyond the end of the session. This is useful for detecting if
# the user later loses their access and auto-revoking things in that case. See also `tieToUser()`
# for an easier way to make a particular capability auto-revoke if the user's permissions change.
tieToUser @1 (cap :Capability, requiredPermissions :Identity.PermissionSet,
displayInfo :Powerbox.PowerboxDisplayInfo)
-> (tiedCap :Capability);
# Create a version of `cap` which will automatically stop working if the user no longer holds the
# permissions indicated by `requiredPermissions` (and starts working again if the user regains
# those permissions). The capability also appears connected to the user in the sharing
# visualization.
# Keep in mind that, security-wise, calling this also implies exposing `tiedCap` to the user, as
# anyone with a UiView capability can always initiate a session and pass in their own
# `SessionContext`. If you need to auto-revoke a capability based on the user's permissions
# _without_ actually passing that capability to the user, use `getSharedPermissions()` to detect
# when the user's permissions change and implement it yourself.
offer @2 (cap :Capability, requiredPermissions :Identity.PermissionSet,
descriptor :Powerbox.PowerboxDescriptor, displayInfo :Powerbox.PowerboxDisplayInfo);
# Offer a capability to the user. A dialog box will ask the user what they want to do with it.
# Depending on the type of capability (as indicated by `descriptor`), different options may be
# provided. All capabilities will offer the user the option to save the capability to their
# capability/grain store. Other type-specific actions may be offered by the platform or by other
# applications.
# For example, offering a UiView will give the user options like "open in new tab", "save to
# grain list", and "share with another user".
# The capability is implicitly tied to the user as if via `tieToUser()`.
request @3 (query :List(Powerbox.PowerboxDescriptor), requiredPermissions :Identity.PermissionSet)
-> (cap :Capability, descriptor :Powerbox.PowerboxDescriptor);
# Although this method exists, it is unimplemented and currently you are meant to use the
# postMessage api to get a temporary token and then restore it with claimRequest().
# The postMessage api is an rpc interface so you will have to listen for a `message` callback
# after sending a postMessage. The postMessage object should have the following form:
# powerboxRequest:
# rpcId: A unique string that should identify this rpc message to the app. You will receive this
# id in the callback to verify which message it is referring to.
# query: A list of PowerboxDescriptor objects, serialized as a Javascript array of
# base64-encoded packed powerbox query descriptors created using the `spk query` tool.
# saveLabel: A petname to give this label. This will be displayed to the user as the name
# for this capability.
# e.g.:
# window.parent.postMessage({
# powerboxRequest: {
# rpcId: myRpcId,
# query: [
# // encoded/packed/base64url of (tags = [(id = 15831515641881813735)])
# ],
# saveLabel: { defaultText: "Linked grain" },
# },
# }, "*");
# The postMessage searches for capabilities in the user's powerbox matching the given query and
# displays a selection UI to the user.
# This will then initiate a powerbox interaction with the user, and when it is done, a postMessage
# callback to the grain will occur. You can listen for such a message like so:
# window.addEventListener("message", function (event) {
# if ( === myRpcId && ! {
# // Pass `` to your app's server and call SessionContext.claimRequest() with
# // it. The token is guaranteed to be a URL-safe string. That is, passing it through
# // encodeURIComponent() should be a no-op.
# }
# }, false)
claimRequest @7 (requestToken :Text, requiredPermissions :Identity.PermissionSet)
-> (cap :Capability);
# When a powerbox request is initiated client-side via the postMessage API and the user completes
# the request flow, the Sandstorm shell responds to the requesting app with a token. This token
# itself is not a SturdyRef, but can be exchanged server-side for a capability which in turn
# can be save()d to produce a SturdyRef. `claimRequest()` is the method to call to exchange the
# token for a capability. It must be called within a short period after the powerbox request
# completes; we recommend that the app immediately send the token up to its server to claim
# the capability.
# If you are familiar with OAuth, the `requestToken` can be compared to an OAuth "authorization
# code", whereas a SturdyRef is like an "access token". The authorization code must be exchanged
# for an access token in a server-to-server interaction.
# You might consider an alternative approach in which the client-side response includes a
# newly-minted SturdyRef directly, avoiding the need for a server-side exchange. The problem with
# that approach is that it makes SturdyRef leaks more dangerous. If an application leaks one of
# its SturdyRefs to an attacker, the attacker may then initiate a powerbox request on the client
# side in which the attacker spoofs the response from the Sandstorm shell to inject the leaked
# SturdyRef. The app likely will not realize that this SturdyRef was not newly-minted and may
# then use it in a context where it was not intended. Adding the claimRequest() requirement makes
# a leaked SturdyRef less likely to be useful to an attacker since the attacker cannot usefully
# inject the SturdyRef into a subsequent spoofed powerbox response, since a SturdyRef is not
# usable as a `requestToken`.
# `requiredPermissions` specifies permissions which must be held on *this* grain by the user
# who completed the powerbox interaction. (The implicit "can access the grain at all" permission
# is always treated as a required permission, even if `requiredPermissions` is null or empty.)
# This way, if a user of a grain connects the grain to other resources, but later has their access
# to the grain revoked, these connections are revoked as well.
# Consider this example: Alice owns a grain which implements a discussion forum. At some point,
# Alice invites Dave to participate in the forum, and she gives him moderator permissions. As
# part of being a moderator, Dave arranges to have a notification emailed to him whenever a post
# is flagged for moderation. To set this up, the forum app makes a powerbox request for an email
# send capability directed to his email address. Later on, Alice decides to demote Dave from
# "moderator" status to "participant". At this point, Dave should stop receiving email
# notifications; the capability he introduced in the powerbox request should be revoked. Alice
# actually has no idea that Dave set up to receive these notifications, so she does not know
# to revoke it manually; we want it to happen automatically, or at least we want to be able to
# call Alice's attention to it.
# To this end, after the Powerbox request is made through Dave and he chooses a capability, the
# app will call `claimRequest()` and indicate in `requiredPermissions` that Dave must have
# "moderator" permission. The app then `save()`s the capability. In the future, if Dave has lost
# this permission, then attempts to `restore()` the SturdyRef will fail.
fulfillRequest @4 (cap :Capability, requiredPermissions :Identity.PermissionSet,
descriptor :Powerbox.PowerboxDescriptor, displayInfo :Powerbox.PowerboxDisplayInfo);
# For sessions started with `newRequestSession()`, fulfills the original request. If only one
# capability was requested, the powerbox will close upon `fulfillRequest()` being called. If
# multiple capabilities were requested, then the powerbox remains open and `fulfillRequest()` may
# be called repeatedly.
# If the session was not started with `newRequestSession()`, this method is equivalent to
# `offer()`. This can be helpful when building a UI that can be used both embedded in the
# powerbox and stand-alone.
close @5 ();
# Closes the session.
# - For regular sessions, the user will be returned to the home screen.
# - For powerbox "request" sessions, the user will be returned to the main grain selection list.
# - For powerbox "offer" sessions, the powerbox will be closed and the user will return to the
# offering app.
# Note that in some cases it is possible for the user to return by clicking "back", so the app
# should not assume that no further requests will happen.
openView @6 (view :UiView, path :Text = "", newTab :Bool = false);
# Navigates the user to some other UiView (from the same grain or another), closing the current
# session. If `view` is null, navigates back to the the current view, in a new session.
# `path` is an optional path to jump directly to within the new session. For WebSessions, this
# is appended to the URL, and may include query (search) and fragment (hash) components, but
# should never start with '/'. Example: "foo/bar?baz=qux#corge"
# If `newTab` is true, the new session is opened in a new tab.
# If the current session is a powerbox session, `openView()` affects the top-level tab, thereby
# closing the powerbox and the app that initiated the powerbox (unless `newTab` is true).
activity @8 (event :Activity.ActivityEvent);
# Call each time the user performs some activity that should be logged.
# ========================================================================================
# Sharing and Access Control
struct PermissionDef {
# Metadata describing a permission bit.
name @3 :Text;
# Name of the permission, used as an identifier for the permission in cases where string names
# are preferred. These names will never be used in Cap'n Proto interfaces, but could show up in
# HTTP or JSON translations, such as in sandstorm-http-bridge's X-Sandstorm-Permissions header.
# The name must be a valid identifier (alphanumerics only, starting with a letter) and must be
# unique among all permissions defined for a particular UiView.
title @0 :Util.LocalizedText;
# Display name of the permission, e.g. to display in a checklist of permissions that may be
# assigned when sharing.
description @1 :Util.LocalizedText;
# Prose describing what this permission means, suitable for a tool tip or similar help text.
obsolete @2 :Bool = false;
# If true, this permission was relevant in a previous version of the application but should no
# longer be offered to the user in future sharing actions.
struct RoleDef {
# Metadata describing a shareable role.
title @0 :Util.LocalizedText;
# Name of the role, e.g. "editor" or "viewer".
verbPhrase @1 :Util.LocalizedText;
# Verb phrase describing what users in this role can do with the grain. Should be something
# like "can edit" or "can view". When the user shares the view with others, these verb phrases
# will be used to populate a drop-list of roles for the user to select.
description @2 :Util.LocalizedText;
# Prose describing what this role means, suitable for a tool tip or similar help text.
permissions @3 :Identity.PermissionSet;
# Permissions which make up this role. For example, the "editor" role on a document would
# typically include "read" and "write" permissions.
obsolete @4 :Bool = false;
# If true, this role was relevant in a previous version of the application but should no longer
# be offered to the user in future sharing actions. The role may still be displayed if it was
# used to share the view while still running the old version.
default @5 :Bool = false;
# If true, this role should be used for any sharing actions that took place using a previous
# version of the app that did not define any roles. This allows you to seamlessly add roles to
# an already-deployed app without breaking existing shares. If you do not mark any roles as
# "default", then such sharing actions will be treated as having an empty permissions set (the
# user can open the grain, but the grain is told that the user has no permissions).
# See also `ViewSharingLink.RoleAssignment.none`, below.
interface SharingLink {
# Represents one link in the sharing graph.
getPetname @0 () -> (name :Util.Assignable(Util.LocalizedText));
# Name assigned by the sharer to the recipient.
interface ViewSharingLink extends(SharingLink) {
# A SharingLink for a UiView. These links can be attenuated with permissions.
getRoleAssignment @0 () -> (var :Util.Assignable(RoleAssignment));
# Returns an Assignable containing a RoleAssignment.
struct RoleAssignment {
union {
none @0: Void;
# No role was explicitly chosen. The main case where this happens is when an app defining
# no roles is shared. Note that "none" means "no role", but does NOT necessarily mean
# "no permissions". If a default role is defined (see `RoleDef.default`), that will be used.
allAccess @1 :Void; # Grant all permissions.
roleId @2 :UInt16; # Grant permissions for the given role.
addPermissions @3 :Identity.PermissionSet;
# Permissions to add on top of those granted above.
removePermissions @4 :Identity.PermissionSet;
# Permissions to remove from those granted above.
# ========================================================================================
# Backup and Restore
struct GrainInfo {
# Format of metadata file stored in grain backups.
appId @0 :Text;
appVersion @1 :UInt32;
title @2 :Text;
ownerIdentityId @3 :Text;
users @4 :List(User);
# Record of users with who had accessed this grain. This can be used to ensure that if a restored
# grain is shared with the same users, they receive the same identities, rather than appearing
# to be new people.
struct User {
identityId @0 :Text;
# Identity ID of this user within the app.
credentialIds @1 :List(Text);
# The user's login credential IDs. If a user with a matching credential visits the restored
# grain, they will regain the same identity. Credential IDs are consistent across differnt
# Sandstorm servers (as long as the same authentication mechanisms are used), so this can allow
# identities to be restored even on a new server.
# Non-login credentials are not included because those credentials could be shared by multiple
# users. However, non-login credentials could be used for matching when restoring identities
# later.
profile @2 :Identity.Profile;
# User's profile. Could be useful to allow the human owner of the restored grain to manually
# remap identities.
originalGrainId @5 :Text;
# The grain's original ID. With admin approval, grains could be allowed to receive the same IDs
# when restored on a new server.
# TODO(someday): Record the whole sharing / capability graph? This gets complicated since grains
# may have been shared via other grains, etc.
# ========================================================================================
# Persistent objects
interface AppPersistent @0xaffa789add8747b8 (AppObjectId) {
# To make an object implemented by your own app persistent, implement this interface.
# `AppObjectId` is a structure like a URL which identifies a specific object within your app.
# You may define this structure any way you want. For example, it could literally be a string
# URL, or it could be a database ID, or it could actually be a serialized representation of an
# object that isn't actually stored anywhere (like a "data URL").
# Other apps and external clients will never actually see your `AppObjectId`; it is stored by
# Sandstorm itself, and clients only see an opaque token. Therefore, you need not encrypt, sign,
# authenticate, or obfuscate this structure. Moreover, Sandstorm will ensure that only clients
# who previously saved the object are able to restore it.
# Note: This interface is called `AppPersistent` rather than just `Persistent` to distinguish it
# from Cap'n Proto's `Persistent` interface, which is a more general (and more confusing)
# version of this concept. Many things that the general Cap'n Proto `Persistent` must deal
# with are handled by Sandstorm, so Sandstorm apps need not think about them. Cap'n Proto
# also uses the term `SturdyRef` rather than `ObjectId` -- the major difference is that
# `SturdyRef` is cryptographically secure whereas `ObjectId` need not be because it is
# protected by the platform.
# TODO(cleanup): Consider eliminating Cap'n Proto's `Persistent` interface in favor of having
# every realm define their own interface. Might actually be less confusing.
save @0 () -> (objectId :AppObjectId, label :Util.LocalizedText);
# Saves the capability to disk (if it isn't there already) and then returns the object ID which
# can be passed to `MainView.restore()` to restore it later.
# The grain owner will be able to inspect externally-held capabilities via the UI. `label` will
# be shown there and should briefly describe what this capability represents.
# Note that Sandstorm compares all object IDs your app produces for equality (using Cap'n Proto
# canonicalization rules) so that it can recognize when the same object is saved multiple times.
# `MainView.drop()` will be called when all such references have been dropped by their respective
# clients.
# TODO(cleanup): How does `label` here relate to `PowerboxDisplayInfo` on `offer()` and
# `fulfillRequest()`? Maybe `label` here should actually be `PowerboxDisplayInfo` and those
# other methods shouldn't take that parameter?
interface MainView(AppObjectId) extends(UiView) {
# The default (bootstrap) interface exported by a grain to the supervisor when it comes up is
# actually `MainView`. Only the Supervisor sees this interface. It proxies the `UiView` subset
# of the interface to the rest of the world, and automatically makes that capability persistent,
# so that a simple app can completely avoid implementing persistence.
# `AppObjectId` is a structure type defined by the app which identifies persistent objects
# within the app, like a URL. See `AppPersistent`, above.
restore @0 (objectId :AppObjectId) -> (cap :Capability);
# Restore a live object corresponding to an `AppObjectId`. See `AppPersistent`, above.
# Apps only need to implement this if they publish persistent capabilities (not including the
# main UiView).
drop @1 (objectId :AppObjectId);
# Indicates that all external persistent references to the given persistent object have been
# dropped. Depending on the nature of the underlying object, the app may wish to delete it at
# this point.
# Note that this method is unreliable. Drop notifications rely on cooperation from the client,
# who has to explicitly call `drop()` on their end when they discard the reference. Buggy clients
# may forget to do this. Clients that are destroyed in a fire may have no opportunity to do this.
# (This differs from live capabilities, which are tied to an ephemeral connection and implicitly
# dropped when that connection is closed.)
# That said, Sandstorm gives the grain owner the ability to inspect incoming refs and revoke them
# explicitly. If all refs to this object are revoked, then Sandstorm will call `drop()`.
# In some rare cases, `drop()` may be called more than once on the same object. The app should
# make sure `drop()` is idempotent.