@sealant/api-contracts-next 0.40.0-next.767 → 0.40.0-next.769

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.
@@ -7,6 +7,7 @@ import { PackagesGroup } from "./packages.js";
7
7
  import { ProfilesGroup } from "./profiles.js";
8
8
  import { RegistriesGroup } from "./registries.js";
9
9
  import { RequestRefusal } from "./request-refusal.js";
10
+ import { ResourceDomainsGroup } from "./resource-domains.js";
10
11
  import { RunsGroup } from "./runs.js";
11
12
  import { SessionsGroup } from "./sessions.js";
12
13
  import { SshKeysGroup } from "./ssh-keys.js";
@@ -27,6 +28,7 @@ export const ControlPlaneAPI = HttpApi.make("sealantControlPlaneApi")
27
28
  .add(RegistriesGroup.prefix("/v1/registries"))
28
29
  .add(GitHubGroup.prefix("/v1/github"))
29
30
  .add(UsersGroup.prefix("/v1/users"))
31
+ .add(ResourceDomainsGroup.prefix("/v1/resource-domains"))
30
32
  // Last, so it reaches every endpoint above (`HttpApi.middleware` skips endpoints added after it).
31
33
  .middleware(RequestRefusal)
32
34
  .annotate(OpenApi.Title, "Sealant Control Plane API")
@@ -1,5 +1,6 @@
1
1
  import { Schema } from "effect";
2
2
  import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api";
3
+ import { PersonId } from "./person-id.js";
3
4
  // These literal sets mirror the control-plane DB enums (packages/db `control-plane.ts`). They are
4
5
  // INLINED here so the public contract package carries NO @sealant/db dependency — which keeps
5
6
  // consumers like @sealant/sdk free of drizzle/pg. Keep them in sync with the db schema.
@@ -17,17 +18,17 @@ export const messageResponseSchema = Schema.Struct({
17
18
  message: Schema.String,
18
19
  });
19
20
  export const githubInstallationsQuerySchema = Schema.Struct({
20
- userId: NonEmptyString,
21
+ userId: PersonId,
21
22
  });
22
23
  export const githubInstallationRepositoriesQuerySchema = Schema.Struct({
23
- userId: NonEmptyString,
24
+ userId: PersonId,
24
25
  search: Schema.optional(NonEmptyString),
25
26
  });
26
27
  export const syncGitHubInstallationQuerySchema = Schema.Struct({
27
- userId: NonEmptyString,
28
+ userId: PersonId,
28
29
  });
29
30
  export const importGitHubInstallationRequestSchema = Schema.Struct({
30
- userId: NonEmptyString,
31
+ userId: PersonId,
31
32
  externalInstallationId: NonEmptyString,
32
33
  });
33
34
  export const githubWebhookHeadersSchema = Schema.Struct({
@@ -22,6 +22,7 @@
22
22
  import { Schema } from "effect";
23
23
  import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api";
24
24
  import { BudgetExceededError } from "./budgets.js";
25
+ import { PersonId } from "./person-id.js";
25
26
  const NonEmptyString = Schema.String.check(Schema.isNonEmpty(), Schema.isTrimmed());
26
27
  /**
27
28
  * Connected-account selection — the same reference shape as workspace creation: values are account
@@ -55,7 +56,7 @@ export const inferenceToolResultSchema = Schema.Struct({
55
56
  * authenticated when it started; credential fields must be absent.
56
57
  */
57
58
  export const inferenceRespondRequestSchema = Schema.Struct({
58
- ownerUserId: NonEmptyString,
59
+ ownerUserId: PersonId,
59
60
  credentials: Schema.optional(inferenceCredentialsSchema),
60
61
  prompt: Schema.optional(Schema.String.check(Schema.isNonEmpty())),
61
62
  system: Schema.optional(Schema.String),
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The ids that name a person, and the one rule that keeps a resource domain out of them.
3
+ *
4
+ * A resource domain (`resource-domains.ts`) is an owner that is not a person: it never holds a
5
+ * login or a provider credential and never runs a harness. Core mints every resource domain id
6
+ * with {@link RESOURCE_DOMAIN_ID_PREFIX}, and no `user` row may carry it (a database check), so
7
+ * the spelling alone tells the two apart. Every field and header that names a person (`ownerUserId`,
8
+ * `onBehalfOfUserId`, `userId`, the SSH gateway's `principalId`) is a {@link PersonId}: a domain id there is refused with a `400`
9
+ * `RequestRefusedError` while the request is decoded, before any handler runs. A test walks every
10
+ * endpoint of the control plane API and fails on a person field that is not one.
11
+ */
12
+ import { Schema } from "effect";
13
+ export declare const RESOURCE_DOMAIN_ID_PREFIX = "rdm:";
14
+ export declare const isResourceDomainId: (id: string) => boolean;
15
+ /** Why an id cannot name a person, after the field's name. Never quotes the id. */
16
+ export declare const NOT_A_PERSON = "names a resource domain, which never holds a login or a provider credential and never runs a harness";
17
+ export declare const PersonId: Schema.String;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The ids that name a person, and the one rule that keeps a resource domain out of them.
3
+ *
4
+ * A resource domain (`resource-domains.ts`) is an owner that is not a person: it never holds a
5
+ * login or a provider credential and never runs a harness. Core mints every resource domain id
6
+ * with {@link RESOURCE_DOMAIN_ID_PREFIX}, and no `user` row may carry it (a database check), so
7
+ * the spelling alone tells the two apart. Every field and header that names a person (`ownerUserId`,
8
+ * `onBehalfOfUserId`, `userId`, the SSH gateway's `principalId`) is a {@link PersonId}: a domain id there is refused with a `400`
9
+ * `RequestRefusedError` while the request is decoded, before any handler runs. A test walks every
10
+ * endpoint of the control plane API and fails on a person field that is not one.
11
+ */
12
+ import { Schema } from "effect";
13
+ export const RESOURCE_DOMAIN_ID_PREFIX = "rdm:";
14
+ export const isResourceDomainId = (id) => id.startsWith(RESOURCE_DOMAIN_ID_PREFIX);
15
+ /** Why an id cannot name a person, after the field's name. Never quotes the id. */
16
+ export const NOT_A_PERSON = "names a resource domain, which never holds a login or a provider credential and never runs a harness";
17
+ export const PersonId = Schema.String.check(Schema.isNonEmpty(), Schema.isTrimmed(), Schema.makeFilter((id) => (isResourceDomainId(id) ? NOT_A_PERSON : undefined)));
@@ -1,6 +1,7 @@
1
1
  import { Schema } from "effect";
2
2
  import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api";
3
3
  import { connectedAccountProviderSchema, connectedAccountSummarySchema, } from "./connected-accounts.js";
4
+ import { PersonId } from "./person-id.js";
4
5
  const NonEmptyString = Schema.String.check(Schema.isNonEmpty(), Schema.isTrimmed());
5
6
  /*
6
7
  Minimal profiles surface: list a user's profiles and manage the per-provider connected-account
@@ -11,7 +12,7 @@ Bindings embed account summaries only — never secret material.
11
12
  export const profileStatusSchema = Schema.Literals(["active", "archived"]);
12
13
  export const profileSummarySchema = Schema.Struct({
13
14
  profileId: NonEmptyString,
14
- ownerUserId: NonEmptyString,
15
+ ownerUserId: PersonId,
15
16
  slug: NonEmptyString,
16
17
  name: NonEmptyString,
17
18
  description: Schema.NullOr(Schema.String),
@@ -21,7 +22,7 @@ export const profileSummarySchema = Schema.Struct({
21
22
  updatedAt: Schema.String,
22
23
  });
23
24
  export const listProfilesQuerySchema = Schema.Struct({
24
- ownerUserId: NonEmptyString,
25
+ ownerUserId: PersonId,
25
26
  });
26
27
  export const listProfilesResponseSchema = Schema.Struct({
27
28
  items: Schema.Array(profileSummarySchema),
@@ -36,7 +37,7 @@ export const listProfileCredentialBindingsResponseSchema = Schema.Struct({
36
37
  items: Schema.Array(profileCredentialBindingWithAccountSchema),
37
38
  });
38
39
  export const setProfileCredentialBindingRequestSchema = Schema.Struct({
39
- ownerUserId: NonEmptyString,
40
+ ownerUserId: PersonId,
40
41
  provider: connectedAccountProviderSchema,
41
42
  /**
42
43
  * The account to bind: its id, or `default` for the owner's chosen default for `provider` (an
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Resource domains: an owner that is a project, not a person.
3
+ *
4
+ * An application (Mend) keeps one domain per project for the things the project owns rather than
5
+ * any one person: its services, volumes and network. A domain outlives every person, and it never
6
+ * holds a login or a provider credential and never runs a harness. Its id (`rdm:<uuid>`) can never
7
+ * name a person: every person field of this API is a `PersonId`, which refuses it.
8
+ *
9
+ * `ensureResourceDomain` is idempotent on `key`, `<application>:<name>` (Mend:
10
+ * `mend:<installation>:project:<project>`): the first call creates the domain and every later or
11
+ * concurrent call answers the same one. Keys are global, and every service key may name any of
12
+ * them; the application prefix keeps applications apart by convention, not by authority.
13
+ *
14
+ * Only a service key reaches these routes. Any other bearer is refused by the transport with `401`
15
+ * before a handler runs. A deployment served without authentication (the development exception)
16
+ * serves them as it serves every other route. A request body is decoded strictly: a field the contract does not declare (an
17
+ * owner, a credential) is refused with `400` instead of being dropped.
18
+ */
19
+ import { Schema } from "effect";
20
+ import { HttpApiEndpoint, HttpApiGroup } from "effect/http-api";
21
+ export declare const resourceDomainSchema: Schema.Struct<{
22
+ readonly resourceDomainId: Schema.String;
23
+ readonly key: Schema.String;
24
+ readonly createdAt: Schema.String;
25
+ }>;
26
+ export type ResourceDomainWire = typeof resourceDomainSchema.Type;
27
+ export declare const ensureResourceDomainRequestSchema: Schema.Struct<{
28
+ readonly key: Schema.String;
29
+ }>;
30
+ export type EnsureResourceDomainRequest = typeof ensureResourceDomainRequestSchema.Type;
31
+ export declare const ensureResourceDomainResponseSchema: Schema.Struct<{
32
+ readonly resourceDomainId: Schema.String;
33
+ readonly key: Schema.String;
34
+ readonly createdAt: Schema.String;
35
+ /** True when this call created the domain. */
36
+ readonly created: Schema.Boolean;
37
+ }>;
38
+ export type EnsureResourceDomainResponse = typeof ensureResourceDomainResponseSchema.Type;
39
+ declare const ResourceDomainForbiddenError_base: Schema.Class<ResourceDomainForbiddenError, Schema.TaggedStruct<"ResourceDomainForbiddenError", {
40
+ readonly message: Schema.String;
41
+ }>, import("effect/Cause").YieldableError>;
42
+ export declare class ResourceDomainForbiddenError extends ResourceDomainForbiddenError_base {
43
+ }
44
+ declare const ResourceDomainNotFoundError_base: Schema.Class<ResourceDomainNotFoundError, Schema.TaggedStruct<"ResourceDomainNotFoundError", {
45
+ readonly message: Schema.String;
46
+ }>, import("effect/Cause").YieldableError>;
47
+ export declare class ResourceDomainNotFoundError extends ResourceDomainNotFoundError_base {
48
+ }
49
+ declare const ResourceDomainInternalServerError_base: Schema.Class<ResourceDomainInternalServerError, Schema.TaggedStruct<"ResourceDomainInternalServerError", {
50
+ readonly message: Schema.String;
51
+ }>, import("effect/Cause").YieldableError>;
52
+ export declare class ResourceDomainInternalServerError extends ResourceDomainInternalServerError_base {
53
+ }
54
+ export declare const ResourceDomainsGroup: HttpApiGroup.HttpApiGroup<"resourceDomains", HttpApiEndpoint.HttpApiEndpoint<"ensureResourceDomain", "POST", "/", never, never, Schema.toCodecJson<Schema.Struct<{
55
+ readonly key: Schema.String;
56
+ }>>, never, Schema.toCodecJson<Schema.Struct<{
57
+ readonly resourceDomainId: Schema.String;
58
+ readonly key: Schema.String;
59
+ readonly createdAt: Schema.String;
60
+ /** True when this call created the domain. */
61
+ readonly created: Schema.Boolean;
62
+ }>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getResourceDomain", "GET", "/:resourceDomainId", Schema.toCodecStringTree<Schema.Struct<{
63
+ readonly resourceDomainId: Schema.String;
64
+ }>>, never, never, never, Schema.toCodecJson<Schema.Struct<{
65
+ readonly resourceDomainId: Schema.String;
66
+ readonly key: Schema.String;
67
+ readonly createdAt: Schema.String;
68
+ }>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainInternalServerError>, never, never>, false>;
69
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Resource domains: an owner that is a project, not a person.
3
+ *
4
+ * An application (Mend) keeps one domain per project for the things the project owns rather than
5
+ * any one person: its services, volumes and network. A domain outlives every person, and it never
6
+ * holds a login or a provider credential and never runs a harness. Its id (`rdm:<uuid>`) can never
7
+ * name a person: every person field of this API is a `PersonId`, which refuses it.
8
+ *
9
+ * `ensureResourceDomain` is idempotent on `key`, `<application>:<name>` (Mend:
10
+ * `mend:<installation>:project:<project>`): the first call creates the domain and every later or
11
+ * concurrent call answers the same one. Keys are global, and every service key may name any of
12
+ * them; the application prefix keeps applications apart by convention, not by authority.
13
+ *
14
+ * Only a service key reaches these routes. Any other bearer is refused by the transport with `401`
15
+ * before a handler runs. A deployment served without authentication (the development exception)
16
+ * serves them as it serves every other route. A request body is decoded strictly: a field the contract does not declare (an
17
+ * owner, a credential) is refused with `400` instead of being dropped.
18
+ */
19
+ import { Schema } from "effect";
20
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api";
21
+ import { RESOURCE_DOMAIN_ID_PREFIX } from "./person-id.js";
22
+ const RESOURCE_DOMAIN_ID_PATTERN = new RegExp(`^${RESOURCE_DOMAIN_ID_PREFIX}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`);
23
+ const RESOURCE_DOMAIN_KEY_PATTERN = /^[a-z][a-z0-9-]{0,31}:[A-Za-z0-9._:/@+=-]{1,223}$/;
24
+ const ResourceDomainId = Schema.String.check(Schema.makeFilter((id) => RESOURCE_DOMAIN_ID_PATTERN.test(id) ? undefined : "must be a resource domain id (rdm:<uuid>)"));
25
+ const ResourceDomainKey = Schema.String.check(Schema.makeFilter((key) => RESOURCE_DOMAIN_KEY_PATTERN.test(key)
26
+ ? undefined
27
+ : "must be <application>:<name>: a lower-case application name (letters, digits, '-'), a colon, then at most 223 letters, digits, '.', '_', ':', '/', '@', '+', '=' or '-'"));
28
+ export const resourceDomainSchema = Schema.Struct({
29
+ resourceDomainId: ResourceDomainId,
30
+ key: ResourceDomainKey,
31
+ createdAt: Schema.String,
32
+ });
33
+ export const ensureResourceDomainRequestSchema = Schema.Struct({ key: ResourceDomainKey });
34
+ export const ensureResourceDomainResponseSchema = Schema.Struct({
35
+ ...resourceDomainSchema.fields,
36
+ /** True when this call created the domain. */
37
+ created: Schema.Boolean,
38
+ });
39
+ export class ResourceDomainForbiddenError extends Schema.TaggedError()("ResourceDomainForbiddenError", { message: Schema.String }, { httpApiStatus: 403 }) {
40
+ }
41
+ export class ResourceDomainNotFoundError extends Schema.TaggedError()("ResourceDomainNotFoundError", { message: Schema.String }, { httpApiStatus: 404 }) {
42
+ }
43
+ export class ResourceDomainInternalServerError extends Schema.TaggedError()("ResourceDomainInternalServerError", { message: Schema.String }, { httpApiStatus: 500 }) {
44
+ }
45
+ export const ResourceDomainsGroup = HttpApiGroup.make("resourceDomains")
46
+ .add(HttpApiEndpoint.post("ensureResourceDomain", "/", {
47
+ payload: ensureResourceDomainRequestSchema,
48
+ success: ensureResourceDomainResponseSchema,
49
+ error: [ResourceDomainForbiddenError, ResourceDomainInternalServerError],
50
+ }))
51
+ .add(HttpApiEndpoint.get("getResourceDomain", "/:resourceDomainId", {
52
+ params: Schema.Struct({ resourceDomainId: ResourceDomainId }),
53
+ success: resourceDomainSchema,
54
+ error: [
55
+ ResourceDomainForbiddenError,
56
+ ResourceDomainNotFoundError,
57
+ ResourceDomainInternalServerError,
58
+ ],
59
+ }))
60
+ .annotate(HttpApi.PayloadParseOptions, { onExcessProperty: "error" })
61
+ .annotate(OpenApi.Description, "Owners that are projects, not people: they never hold a login or a provider credential.");
@@ -12,6 +12,7 @@
12
12
  import { Schema } from "effect";
13
13
  import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api";
14
14
  import { BudgetExceededError } from "./budgets.js";
15
+ import { PersonId } from "./person-id.js";
15
16
  const NonEmptyString = Schema.String.check(Schema.isNonEmpty(), Schema.isTrimmed());
16
17
  export const runStatusSchema = Schema.Literals([
17
18
  "queued",
@@ -46,7 +47,7 @@ export const runSchema = Schema.Struct({
46
47
  runId: NonEmptyString,
47
48
  workspaceId: NonEmptyString,
48
49
  attemptId: Schema.optional(NonEmptyString),
49
- ownerUserId: NonEmptyString,
50
+ ownerUserId: PersonId,
50
51
  harnessId: NonEmptyString,
51
52
  mode: runModeSchema,
52
53
  status: runStatusSchema,
@@ -80,7 +81,7 @@ export const runFileChangeSchema = Schema.Struct({
80
81
  export const createRunRequestSchema = Schema.Struct({
81
82
  workspaceId: NonEmptyString,
82
83
  harnessId: NonEmptyString,
83
- ownerUserId: NonEmptyString,
84
+ ownerUserId: PersonId,
84
85
  mode: Schema.optional(runModeSchema),
85
86
  prompt: Schema.optional(Schema.String),
86
87
  attemptId: Schema.optional(NonEmptyString),
@@ -96,7 +97,7 @@ export const createRunRequestSchema = Schema.Struct({
96
97
  });
97
98
  export const updateRunRequestSchema = Schema.Struct({
98
99
  /** The run's owner. Required by the control plane: an update that names no owner finds no run. */
99
- ownerUserId: Schema.optional(NonEmptyString),
100
+ ownerUserId: Schema.optional(PersonId),
100
101
  status: Schema.optional(runStatusSchema),
101
102
  exitCode: Schema.optional(Schema.Number),
102
103
  errorMessage: Schema.optional(Schema.String),
@@ -109,7 +110,7 @@ export const updateRunRequestSchema = Schema.Struct({
109
110
  });
110
111
  export const listRunsQuerySchema = Schema.Struct({
111
112
  workspaceId: Schema.optional(NonEmptyString),
112
- ownerUserId: Schema.optional(NonEmptyString),
113
+ ownerUserId: Schema.optional(PersonId),
113
114
  status: Schema.optional(runStatusSchema),
114
115
  limit: Schema.optional(NonEmptyString),
115
116
  });
@@ -136,10 +137,10 @@ export const timelineEntrySchema = Schema.Struct({
136
137
  * names no owner finds no run.
137
138
  */
138
139
  export const runOwnerQuerySchema = Schema.Struct({
139
- ownerUserId: Schema.optional(NonEmptyString),
140
+ ownerUserId: Schema.optional(PersonId),
140
141
  });
141
142
  export const getRunTimelineQuerySchema = Schema.Struct({
142
- ownerUserId: Schema.optional(NonEmptyString),
143
+ ownerUserId: Schema.optional(PersonId),
143
144
  fromSequence: Schema.optional(NonEmptyString),
144
145
  toSequence: Schema.optional(NonEmptyString),
145
146
  limit: Schema.optional(NonEmptyString),
@@ -183,7 +184,7 @@ export const runEventSchema = Schema.Struct({
183
184
  // Execution record — scrollback (byte-exact terminal output)
184
185
  // ---------------------------------------------------------------------------------------------
185
186
  export const getRunScrollbackQuerySchema = Schema.Struct({
186
- ownerUserId: Schema.optional(NonEmptyString),
187
+ ownerUserId: Schema.optional(PersonId),
187
188
  processId: NonEmptyString,
188
189
  stream: ioStreamSchema,
189
190
  atSequence: Schema.optional(NonEmptyString),
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { Schema } from "effect";
17
17
  import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api";
18
+ import { PersonId } from "./person-id.js";
18
19
  const NonEmptyString = Schema.String.check(Schema.isNonEmpty(), Schema.isTrimmed());
19
20
  export const sessionStatusSchema = Schema.Literals(["starting", "running", "exited", "failed"]);
20
21
  export const sessionAuthorizationHeadersSchema = Schema.Struct({
@@ -113,7 +114,7 @@ export const sessionArgvIssue = (argv) => {
113
114
  export const sessionArgvSchema = Schema.Array(Schema.String).check(Schema.makeFilter(sessionArgvIssue));
114
115
  export const createSessionRequestSchema = Schema.Struct({
115
116
  workspaceId: NonEmptyString,
116
- ownerUserId: NonEmptyString,
117
+ ownerUserId: PersonId,
117
118
  /**
118
119
  * argv[0] is the program the session runs; the rest its arguments, passed to it as an argv array,
119
120
  * never through a shell. See `sessionArgvIssue` for what is refused.
@@ -151,7 +152,7 @@ export const sessionSchema = Schema.Struct({
151
152
  workspaceId: NonEmptyString,
152
153
  /** The interactive run recording this session; its record is the durable evidence. */
153
154
  runId: NonEmptyString,
154
- ownerUserId: NonEmptyString,
155
+ ownerUserId: PersonId,
155
156
  status: sessionStatusSchema,
156
157
  /**
157
158
  * Only `argv[0]`, the program: Sealant never stores a session's arguments, because they can carry
@@ -181,7 +182,7 @@ export const sessionSchema = Schema.Struct({
181
182
  endedAt: Schema.optional(Schema.String),
182
183
  });
183
184
  export const listSessionsQuerySchema = Schema.Struct({
184
- ownerUserId: NonEmptyString,
185
+ ownerUserId: PersonId,
185
186
  workspaceId: Schema.optional(NonEmptyString),
186
187
  status: Schema.optional(sessionStatusSchema),
187
188
  limit: Schema.optional(NonEmptyString),
@@ -190,22 +191,22 @@ export const listSessionsResponseSchema = Schema.Struct({
190
191
  items: Schema.Array(sessionSchema),
191
192
  });
192
193
  export const sessionInputRequestSchema = Schema.Struct({
193
- ownerUserId: Schema.optional(NonEmptyString),
194
+ ownerUserId: Schema.optional(PersonId),
194
195
  /** Base64-encoded keystrokes (bytes, not text — binary-safe). */
195
196
  dataBase64: NonEmptyString,
196
197
  });
197
198
  export const sessionResizeRequestSchema = Schema.Struct({
198
- ownerUserId: Schema.optional(NonEmptyString),
199
+ ownerUserId: Schema.optional(PersonId),
199
200
  cols: Schema.Int.check(Schema.isGreaterThan(0)),
200
201
  rows: Schema.Int.check(Schema.isGreaterThan(0)),
201
202
  });
202
203
  export const sessionSignalRequestSchema = Schema.Struct({
203
- ownerUserId: Schema.optional(NonEmptyString),
204
+ ownerUserId: Schema.optional(PersonId),
204
205
  /** POSIX signal number (e.g. 2 = SIGINT, 15 = SIGTERM). */
205
206
  signal: Schema.Int.check(Schema.isGreaterThan(0)),
206
207
  });
207
208
  export const closeSessionRequestSchema = Schema.Struct({
208
- ownerUserId: Schema.optional(NonEmptyString),
209
+ ownerUserId: Schema.optional(PersonId),
209
210
  });
210
211
  /**
211
212
  * Byte-exact session output by sequence range. `from` is INCLUSIVE (omit = from the beginning);
@@ -213,7 +214,7 @@ export const closeSessionRequestSchema = Schema.Struct({
213
214
  * as-recorded: redacted upstream by the daemon, PTY output stream only.
214
215
  */
215
216
  export const getSessionOutputQuerySchema = Schema.Struct({
216
- ownerUserId: Schema.optional(NonEmptyString),
217
+ ownerUserId: Schema.optional(PersonId),
217
218
  from: Schema.optional(NonEmptyString),
218
219
  limit: Schema.optional(NonEmptyString),
219
220
  });
@@ -299,7 +300,7 @@ HttpApiEndpoint.post("createSessionAsUser", "/as-user", {
299
300
  HttpApiEndpoint.get("getSession", "/:sessionId", {
300
301
  params: sessionIdParams,
301
302
  headers: sessionAuthorizationHeadersSchema,
302
- query: Schema.Struct({ ownerUserId: Schema.optional(NonEmptyString) }),
303
+ query: Schema.Struct({ ownerUserId: Schema.optional(PersonId) }),
303
304
  success: sessionSchema,
304
305
  error: [
305
306
  SessionBadRequestError,
@@ -1,5 +1,6 @@
1
1
  import { Schema } from "effect";
2
2
  import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api";
3
+ import { PersonId } from "./person-id.js";
3
4
  const NonEmptyString = Schema.String.check(Schema.isNonEmpty(), Schema.isTrimmed());
4
5
  /*
5
6
  User SSH public keys. Two audiences:
@@ -15,21 +16,21 @@ export const sshKeyGatewayHeadersSchema = Schema.Struct({
15
16
  export const createSshKeyRequestSchema = Schema.Struct({
16
17
  // Same trust model as createWorkspace: the internal API trusts the caller-supplied owner; the web
17
18
  // server injects the session user id.
18
- ownerUserId: NonEmptyString,
19
+ ownerUserId: PersonId,
19
20
  name: Schema.optional(NonEmptyString),
20
21
  // Raw `<algorithm> <base64> [comment]` line; the API normalizes and fingerprints it.
21
22
  publicKey: NonEmptyString,
22
23
  });
23
24
  export const sshKeySummarySchema = Schema.Struct({
24
25
  sshKeyId: NonEmptyString,
25
- ownerUserId: NonEmptyString,
26
+ ownerUserId: PersonId,
26
27
  name: NonEmptyString,
27
28
  algorithm: NonEmptyString,
28
29
  fingerprint: NonEmptyString,
29
30
  createdAt: Schema.String,
30
31
  });
31
32
  export const listSshKeysQuerySchema = Schema.Struct({
32
- ownerUserId: NonEmptyString,
33
+ ownerUserId: PersonId,
33
34
  });
34
35
  export const listSshKeysResponseSchema = Schema.Struct({
35
36
  items: Schema.Array(sshKeySummarySchema),
@@ -41,7 +42,7 @@ export const resolveSshPrincipalRequestSchema = Schema.Struct({
41
42
  publicKeyBase64: NonEmptyString,
42
43
  });
43
44
  export const resolveSshPrincipalResponseSchema = Schema.Struct({
44
- principalId: NonEmptyString,
45
+ principalId: PersonId,
45
46
  sshKeyId: NonEmptyString,
46
47
  fingerprint: NonEmptyString,
47
48
  });
@@ -8,9 +8,10 @@
8
8
  */
9
9
  import { Schema } from "effect";
10
10
  import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api";
11
+ import { PersonId } from "./person-id.js";
11
12
  const NonEmptyString = Schema.String.check(Schema.isNonEmpty(), Schema.isTrimmed());
12
13
  export const userSchema = Schema.Struct({
13
- userId: NonEmptyString,
14
+ userId: PersonId,
14
15
  email: NonEmptyString,
15
16
  name: NonEmptyString,
16
17
  createdAt: Schema.String,
@@ -20,7 +21,7 @@ export const ensureUserRequestSchema = Schema.Struct({
20
21
  email: NonEmptyString,
21
22
  name: NonEmptyString,
22
23
  /** Caller-chosen id for a NEW user (`usr_…`); ignored when the email already exists. */
23
- userId: Schema.optional(NonEmptyString),
24
+ userId: Schema.optional(PersonId),
24
25
  });
25
26
  export const ensureUserResponseSchema = Schema.Struct({
26
27
  ...userSchema.fields,
@@ -38,7 +39,7 @@ export const personBindingSchema = Schema.Struct({
38
39
  home: NonEmptyString,
39
40
  });
40
41
  export const bindUserPersonResponseSchema = Schema.Struct({
41
- userId: NonEmptyString,
42
+ userId: PersonId,
42
43
  person: personBindingSchema,
43
44
  /** True when this call made the binding; false when it was already exactly this. */
44
45
  created: Schema.Boolean,
@@ -59,7 +60,7 @@ export class UserNotFoundError extends Schema.TaggedError()("UserNotFoundError",
59
60
  }
60
61
  export class UserInternalServerError extends Schema.TaggedError()("UserInternalServerError", { message: Schema.String }, { httpApiStatus: 500 }) {
61
62
  }
62
- const userIdParams = Schema.Struct({ userId: NonEmptyString });
63
+ const userIdParams = Schema.Struct({ userId: PersonId });
63
64
  export const UsersGroup = HttpApiGroup.make("users")
64
65
  .add(HttpApiEndpoint.post("ensureUser", "/", {
65
66
  payload: ensureUserRequestSchema,
@@ -19,7 +19,7 @@ export declare const FORWARD_V2_SUBPROTOCOL = "sealant.forward.v2";
19
19
  export declare const FORWARD_V2_MAX_FRAME_BYTES: number;
20
20
  /** Ends the socket after a `refused` frame. It says nothing about why: the frame's reason does. */
21
21
  export declare const FORWARD_REFUSED_CLOSE_CODE = 4400;
22
- export declare const forwardRefusalReasonSchema: Schema.Literals<readonly ["workspace-not-ready", "runtime-unreachable", "runtime-untargetable", "connection-refused", "name-unresolved", "network-unreachable", "dial-timeout", "dial-failed", "not-found", "unauthorized", "forbidden", "budget-exceeded", "invalid-target", "owner-required", "internal", "owner-exited", "peer-mismatch", "binding-unverifiable", "binding-unsupported"]>;
22
+ export declare const forwardRefusalReasonSchema: Schema.Literals<readonly ["workspace-not-ready", "runtime-unreachable", "runtime-untargetable", "connection-refused", "name-unresolved", "network-unreachable", "dial-timeout", "dial-failed", "not-found", "unauthorized", "forbidden", "budget-exceeded", "invalid-target", "owner-required", "not-a-person", "internal", "owner-exited", "peer-mismatch", "binding-unverifiable", "binding-unsupported"]>;
23
23
  export type ForwardRefusalReason = typeof forwardRefusalReasonSchema.Type;
24
24
  /** The HTTP status each refusal would have had before the upgrade. Callers branch on the reason. */
25
25
  export declare const FORWARD_REFUSAL_STATUS: {
@@ -37,6 +37,8 @@ export declare const FORWARD_REFUSAL_STATUS: {
37
37
  readonly "budget-exceeded": 429;
38
38
  readonly "invalid-target": 400;
39
39
  readonly "owner-required": 400;
40
+ /** The asserted owner is a resource domain, which never opens a forward. */
41
+ readonly "not-a-person": 400;
40
42
  readonly internal: 500;
41
43
  readonly "owner-exited": 409;
42
44
  readonly "peer-mismatch": 409;
@@ -76,7 +78,7 @@ export declare const forwardRefusedFrameSchema: Schema.Struct<{
76
78
  readonly status: Schema.Int;
77
79
  }>;
78
80
  export type ForwardRefusedFrame = typeof forwardRefusedFrameSchema.Type;
79
- export declare const forwardRefusedFrame: (reason: "binding-unsupported" | "binding-unverifiable" | "budget-exceeded" | "connection-refused" | "dial-failed" | "dial-timeout" | "forbidden" | "internal" | "invalid-target" | "name-unresolved" | "network-unreachable" | "not-found" | "owner-exited" | "owner-required" | "peer-mismatch" | "runtime-unreachable" | "runtime-untargetable" | "unauthorized" | "workspace-not-ready", message: string) => {
81
+ export declare const forwardRefusedFrame: (reason: "binding-unsupported" | "binding-unverifiable" | "budget-exceeded" | "connection-refused" | "dial-failed" | "dial-timeout" | "forbidden" | "internal" | "invalid-target" | "name-unresolved" | "network-unreachable" | "not-a-person" | "not-found" | "owner-exited" | "owner-required" | "peer-mismatch" | "runtime-unreachable" | "runtime-untargetable" | "unauthorized" | "workspace-not-ready", message: string) => {
80
82
  readonly t: "refused";
81
83
  readonly reason: string;
82
84
  readonly message: string;
@@ -34,6 +34,7 @@ export const forwardRefusalReasonSchema = Schema.Literals([
34
34
  "budget-exceeded",
35
35
  "invalid-target",
36
36
  "owner-required",
37
+ "not-a-person",
37
38
  "internal",
38
39
  "owner-exited",
39
40
  "peer-mismatch",
@@ -56,6 +57,8 @@ export const FORWARD_REFUSAL_STATUS = {
56
57
  "budget-exceeded": 429,
57
58
  "invalid-target": 400,
58
59
  "owner-required": 400,
60
+ /** The asserted owner is a resource domain, which never opens a forward. */
61
+ "not-a-person": 400,
59
62
  internal: 500,
60
63
  "owner-exited": 409,
61
64
  "peer-mismatch": 409,