@antasphere/chassis-contract 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +82 -0
  2. package/dist/define.d.ts +177 -0
  3. package/dist/define.d.ts.map +1 -0
  4. package/dist/define.js +35 -0
  5. package/dist/define.js.map +1 -0
  6. package/dist/entitlements.d.ts +486 -0
  7. package/dist/entitlements.d.ts.map +1 -0
  8. package/dist/entitlements.js +344 -0
  9. package/dist/entitlements.js.map +1 -0
  10. package/dist/identity.d.ts +56 -0
  11. package/dist/identity.d.ts.map +1 -0
  12. package/dist/identity.js +2 -0
  13. package/dist/identity.js.map +1 -0
  14. package/dist/index.d.ts +23 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +29 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/routes/index.d.ts +5403 -0
  19. package/dist/routes/index.d.ts.map +1 -0
  20. package/dist/routes/index.js +940 -0
  21. package/dist/routes/index.js.map +1 -0
  22. package/dist/schemas/api-keys.d.ts +54 -0
  23. package/dist/schemas/api-keys.d.ts.map +1 -0
  24. package/dist/schemas/api-keys.js +48 -0
  25. package/dist/schemas/api-keys.js.map +1 -0
  26. package/dist/schemas/audit.d.ts +74 -0
  27. package/dist/schemas/audit.d.ts.map +1 -0
  28. package/dist/schemas/audit.js +65 -0
  29. package/dist/schemas/audit.js.map +1 -0
  30. package/dist/schemas/break-glass.d.ts +42 -0
  31. package/dist/schemas/break-glass.d.ts.map +1 -0
  32. package/dist/schemas/break-glass.js +38 -0
  33. package/dist/schemas/break-glass.js.map +1 -0
  34. package/dist/schemas/cli-auth.d.ts +59 -0
  35. package/dist/schemas/cli-auth.d.ts.map +1 -0
  36. package/dist/schemas/cli-auth.js +68 -0
  37. package/dist/schemas/cli-auth.js.map +1 -0
  38. package/dist/schemas/common.d.ts +96 -0
  39. package/dist/schemas/common.d.ts.map +1 -0
  40. package/dist/schemas/common.js +205 -0
  41. package/dist/schemas/common.js.map +1 -0
  42. package/dist/schemas/demo-passes.d.ts +90 -0
  43. package/dist/schemas/demo-passes.d.ts.map +1 -0
  44. package/dist/schemas/demo-passes.js +60 -0
  45. package/dist/schemas/demo-passes.js.map +1 -0
  46. package/dist/schemas/files.d.ts +39 -0
  47. package/dist/schemas/files.d.ts.map +1 -0
  48. package/dist/schemas/files.js +26 -0
  49. package/dist/schemas/files.js.map +1 -0
  50. package/dist/schemas/instance.d.ts +58 -0
  51. package/dist/schemas/instance.d.ts.map +1 -0
  52. package/dist/schemas/instance.js +70 -0
  53. package/dist/schemas/instance.js.map +1 -0
  54. package/dist/schemas/invitations.d.ts +100 -0
  55. package/dist/schemas/invitations.d.ts.map +1 -0
  56. package/dist/schemas/invitations.js +55 -0
  57. package/dist/schemas/invitations.js.map +1 -0
  58. package/dist/schemas/me.d.ts +81 -0
  59. package/dist/schemas/me.d.ts.map +1 -0
  60. package/dist/schemas/me.js +178 -0
  61. package/dist/schemas/me.js.map +1 -0
  62. package/dist/schemas/members.d.ts +60 -0
  63. package/dist/schemas/members.d.ts.map +1 -0
  64. package/dist/schemas/members.js +43 -0
  65. package/dist/schemas/members.js.map +1 -0
  66. package/dist/schemas/projects.d.ts +232 -0
  67. package/dist/schemas/projects.d.ts.map +1 -0
  68. package/dist/schemas/projects.js +140 -0
  69. package/dist/schemas/projects.js.map +1 -0
  70. package/dist/schemas/scope.d.ts +14 -0
  71. package/dist/schemas/scope.d.ts.map +1 -0
  72. package/dist/schemas/scope.js +5 -0
  73. package/dist/schemas/scope.js.map +1 -0
  74. package/dist/schemas/setup.d.ts +28 -0
  75. package/dist/schemas/setup.d.ts.map +1 -0
  76. package/dist/schemas/setup.js +28 -0
  77. package/dist/schemas/setup.js.map +1 -0
  78. package/dist/schemas/sso-connect.d.ts +30 -0
  79. package/dist/schemas/sso-connect.d.ts.map +1 -0
  80. package/dist/schemas/sso-connect.js +38 -0
  81. package/dist/schemas/sso-connect.js.map +1 -0
  82. package/dist/schemas/sso-logout.d.ts +17 -0
  83. package/dist/schemas/sso-logout.d.ts.map +1 -0
  84. package/dist/schemas/sso-logout.js +16 -0
  85. package/dist/schemas/sso-logout.js.map +1 -0
  86. package/dist/schemas/teams.d.ts +100 -0
  87. package/dist/schemas/teams.d.ts.map +1 -0
  88. package/dist/schemas/teams.js +84 -0
  89. package/dist/schemas/teams.js.map +1 -0
  90. package/dist/schemas/workspaces.d.ts +99 -0
  91. package/dist/schemas/workspaces.d.ts.map +1 -0
  92. package/dist/schemas/workspaces.js +96 -0
  93. package/dist/schemas/workspaces.js.map +1 -0
  94. package/dist/seams.d.ts +185 -0
  95. package/dist/seams.d.ts.map +1 -0
  96. package/dist/seams.js +37 -0
  97. package/dist/seams.js.map +1 -0
  98. package/dist/wire.d.ts +101 -0
  99. package/dist/wire.d.ts.map +1 -0
  100. package/dist/wire.js +161 -0
  101. package/dist/wire.js.map +1 -0
  102. package/package.json +41 -3
  103. package/src/define.ts +64 -0
  104. package/src/entitlements.ts +527 -0
  105. package/src/identity.ts +49 -0
  106. package/src/index.ts +38 -0
  107. package/src/routes/index.ts +1146 -0
  108. package/src/schemas/api-keys.ts +53 -0
  109. package/src/schemas/audit.ts +74 -0
  110. package/src/schemas/break-glass.ts +46 -0
  111. package/src/schemas/cli-auth.ts +78 -0
  112. package/src/schemas/common.ts +206 -0
  113. package/src/schemas/demo-passes.ts +73 -0
  114. package/src/schemas/files.ts +30 -0
  115. package/src/schemas/instance.ts +71 -0
  116. package/src/schemas/invitations.ts +66 -0
  117. package/src/schemas/me.ts +184 -0
  118. package/src/schemas/members.ts +53 -0
  119. package/src/schemas/projects.ts +174 -0
  120. package/src/schemas/scope.ts +18 -0
  121. package/src/schemas/setup.ts +31 -0
  122. package/src/schemas/sso-connect.ts +40 -0
  123. package/src/schemas/sso-logout.ts +17 -0
  124. package/src/schemas/teams.ts +101 -0
  125. package/src/schemas/workspaces.ts +109 -0
  126. package/src/seams.ts +202 -0
  127. package/src/wire.ts +246 -0
  128. package/test/define.test.ts +59 -0
  129. package/test/hub-wire.test.ts +114 -0
  130. package/README.md +0 -3
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ import { plainText, workspaceRoleSchema } from './common.js';
3
+
4
+ export const invitationSchema = z.object({
5
+ id: z.string(),
6
+ email: z.string(),
7
+ role: workspaceRoleSchema,
8
+ invitedBy: z.string(),
9
+ expiresAt: z.string(),
10
+ acceptedAt: z.string().nullable(),
11
+ revokedAt: z.string().nullable(),
12
+ createdAt: z.string()
13
+ });
14
+ export type InvitationInfo = z.infer<typeof invitationSchema>;
15
+
16
+ export const invitationsListSchema = z.object({
17
+ invitations: z.array(invitationSchema),
18
+ nextCursor: z.string().nullable()
19
+ });
20
+
21
+ export const invitationCreateSchema = z.object({
22
+ email: z.email(),
23
+ role: workspaceRoleSchema.default('member')
24
+ });
25
+ export type InvitationCreate = z.infer<typeof invitationCreateSchema>;
26
+
27
+ /**
28
+ * The copyable acceptance link is ALWAYS returned — SMTP is never required
29
+ * to get a teammate in. `emailSent` says whether a mail also went out.
30
+ */
31
+ export const invitationCreatedSchema = z.object({
32
+ invitation: invitationSchema,
33
+ acceptUrl: z.string(),
34
+ emailSent: z.boolean()
35
+ });
36
+ export type InvitationCreated = z.infer<typeof invitationCreatedSchema>;
37
+
38
+ /**
39
+ * Deliberately carries NO account-existence signal (the collaborator-lookup
40
+ * rule, PRDCT-1437): an admin who creates an invitation naming any address
41
+ * holds the copyable accept URL, so the lookup was an instance-global
42
+ * account oracle. The accept page offers sign-in and create-account
43
+ * neutrally; the accept endpoint answers `account_exists` when a create
44
+ * collides.
45
+ */
46
+ export const invitationLookupSchema = z.object({
47
+ email: z.string(),
48
+ role: workspaceRoleSchema,
49
+ workspaceName: z.string(),
50
+ expiresAt: z.string()
51
+ });
52
+ export type InvitationLookup = z.infer<typeof invitationLookupSchema>;
53
+
54
+ export const invitationAcceptSchema = z.object({
55
+ token: z.string().min(16),
56
+ /** Required when the invited email has no account yet. */
57
+ name: plainText(1, 120).optional(),
58
+ password: z.string().min(12).max(256).optional()
59
+ });
60
+ export type InvitationAccept = z.infer<typeof invitationAcceptSchema>;
61
+
62
+ export const invitationAcceptedSchema = z.object({
63
+ workspaceId: z.string(),
64
+ userId: z.string(),
65
+ role: workspaceRoleSchema
66
+ });
@@ -0,0 +1,184 @@
1
+ import { z } from 'zod';
2
+ import { workspaceRoleSchema } from './common.js';
3
+ import type { ScopeSchema } from './scope.js';
4
+ import { workspaceLookSchema } from './workspaces.js';
5
+
6
+ /**
7
+ * POST /api/v1/me/onboarding/dismiss (cloud edition ONLY; sessions only —
8
+ * machines 403 fail-closed via the scope allowlist, oss answers the JSON
9
+ * 404 terminator): records the explicit dismissal that flips
10
+ * `firstRunPending` to false, permanently (upsert; an earlier dismissal
11
+ * timestamp is preserved). Idempotent — dismissing twice is a no-op.
12
+ */
13
+ export const onboardingDismissedSchema = z.object({
14
+ dismissed: z.literal(true)
15
+ });
16
+ export type OnboardingDismissed = z.infer<typeof onboardingDismissedSchema>;
17
+
18
+ /** `/me` reports the credential's scopes, so it is built per tool (../define.ts). */
19
+ export function defineMeSchemas<TScope extends string>(scopeSchema: ScopeSchema<TScope>) {
20
+ /**
21
+ * GET /api/v1/me — the resolved AuthContext of the calling principal,
22
+ * whichever credential path it arrived on. The whoami for humans, keys, and
23
+ * (later) OAuth tokens alike; also the MCP get_me tool's backing endpoint.
24
+ *
25
+ * ZERO-MEMBERSHIP sessions answer 200 too (user-scoped federation): a LIVE
26
+ * session whose user holds no active membership — the cloud operator before
27
+ * break-glass, a hub user whose last org was removed — gets
28
+ * `{ workspaces: [], workspace: null, activeWorkspaceId: null, role: null,
29
+ * origin: null, via: 'session' }` instead of a 401, so the dashboard can
30
+ * render the no-organization zero state instead of a login bounce. Machine
31
+ * credentials never see this shape: a key/token whose user has no active
32
+ * membership fails resolution (401) before the route runs.
33
+ */
34
+ const meResponseSchema = z.object({
35
+ user: z.object({
36
+ id: z.string(),
37
+ email: z.string(),
38
+ name: z.string()
39
+ }),
40
+ /**
41
+ * The ACTIVE workspace this request resolved to (= activeWorkspaceId).
42
+ * Null ONLY for the zero-membership session state described above.
43
+ */
44
+ workspace: z
45
+ .object({
46
+ id: z.string(),
47
+ name: z.string(),
48
+ /**
49
+ * True when this workspace is the lazy projection of a hub org (cloud
50
+ * edition, internal/federation.md) — its MEMBERSHIP is managed at the hub,
51
+ * so clients hide local member/invitation management and link out
52
+ * (P7). Derived from `centralAccountId IS NOT NULL`; the raw hub org
53
+ * id is deliberately never exposed. Always false on oss.
54
+ */
55
+ hubOrigin: z.boolean(),
56
+ /** The workspace's look, a fact of the workspace (schemas/workspaces.ts). */
57
+ look: workspaceLookSchema
58
+ })
59
+ .nullable(),
60
+ /** Null only in the zero-membership session state. */
61
+ role: workspaceRoleSchema.nullable(),
62
+ /**
63
+ * How the caller's membership of the ACTIVE workspace came to exist
64
+ * (mirrors Principal.origin): 'local' = setup/invitation, 'hub' = SSO
65
+ * projection (cloud), 'guest' = a per-resource collaborator (D2) — clients
66
+ * hide the guest-forbidden workspace surfaces (resource creation, files,
67
+ * member roster, export) for 'guest'. Null only in the zero-membership
68
+ * session state.
69
+ */
70
+ origin: z.enum(['local', 'hub', 'guest']).nullable(),
71
+ via: z.enum(['session', 'api_key', 'oauth']),
72
+ scopes: z.array(scopeSchema).nullable(),
73
+ /** Expiry of the presented API key; null for sessions/OAuth or non-expiring keys. */
74
+ apiKeyExpiresAt: z.string().nullable(),
75
+ /**
76
+ * The workspaces this credential can name (user-scoped credential model):
77
+ * EVERY credential kind — session, API key, OAuth bearer — lists ALL of
78
+ * the user's active memberships; the target workspace is a per-request
79
+ * parameter (X-Workspace-Id), never baked into the credential. Clients
80
+ * MUST read the `default` flag to find the selector-less default — never
81
+ * assume index 0.
82
+ */
83
+ workspaces: z.array(
84
+ z.object({
85
+ id: z.string(),
86
+ name: z.string(),
87
+ role: workspaceRoleSchema,
88
+ /** Same semantics as `workspace.hubOrigin`, per listed workspace. */
89
+ hubOrigin: z.boolean(),
90
+ /**
91
+ * The id of the hub organization this workspace projects (cloud
92
+ * edition), null on a local workspace and always on self-hosted. It
93
+ * is the person's own organization id, the one the account site
94
+ * shows them, and it is what `--org <hub org id or name>` resolves
95
+ * against: a request may name a workspace by this id in
96
+ * X-Workspace-Id as well as by `id` (PRDCT-2947).
97
+ */
98
+ centralAccountId: z.string().nullable(),
99
+ /** Each workspace's look, so the switcher's tiles carry them (schemas/workspaces.ts). */
100
+ look: workspaceLookSchema,
101
+ /**
102
+ * The hub asserted this org suspended (cloud edition): it stays
103
+ * VISIBLE here but requests into it are refused. Always false on oss
104
+ * and on local workspaces.
105
+ */
106
+ suspended: z.boolean(),
107
+ /**
108
+ * The user's default workspace — what a request naming no workspace
109
+ * resolves to. At most one entry carries it; when none does, the
110
+ * deterministic fallback (oldest active membership) applies.
111
+ */
112
+ default: z.boolean()
113
+ })
114
+ ),
115
+ /**
116
+ * The workspace THIS request resolved to — what X-Workspace-Id selects.
117
+ * Null only in the zero-membership session state.
118
+ */
119
+ activeWorkspaceId: z.string().nullable(),
120
+ /**
121
+ * Where membership of the ACTIVE workspace is managed when it is
122
+ * hub-origin (the hub account console, e.g. https://account.antasphere.com)
123
+ * — the dashboard's link-out target (P7). Also set for the CLOUD
124
+ * zero-membership session state (the "create an organization at
125
+ * Antasphere" CTA target). null on oss and on local (non-projected)
126
+ * workspaces.
127
+ */
128
+ hubManageUrl: z.string().nullable(),
129
+ /**
130
+ * Whether `POST /workspaces` would be accepted for this caller RIGHT NOW
131
+ * (PRDCT-2444 / PRDCT-2443) — the one flag the dashboard keys its "new
132
+ * workspace" entry off, on both editions, never the edition itself. True
133
+ * only for a SESSION whose user is not guest-only, while the operator's
134
+ * `MAX_WORKSPACES_PER_USER` dial is not 0; on self-hosted the user must
135
+ * also own fewer workspaces than that cap, on cloud they must also hold a
136
+ * live Antasphere link (the organization is created there, as them).
137
+ * Always false for API keys and OAuth bearers. STAYS TRUE on cloud for a
138
+ * person whose stored sign-in predates workspace creation (the route then
139
+ * answers 401 `hub_reauth_required`): the creation is one sign-in away,
140
+ * and false would hide the entry from them for good. Advisory: the route
141
+ * re-judges everything, and on cloud the hub's own cap is only known once
142
+ * asked.
143
+ */
144
+ canCreateWorkspace: z.boolean(),
145
+ /**
146
+ * First-run welcome still owed (SL-6). CLOUD + SESSION callers ONLY —
147
+ * absent on oss and for every machine credential. TOOL-LOCAL and
148
+ * retry-safe semantics: true iff NO user_onboarding row with a dismissal
149
+ * exists (`NOT EXISTS (row WHERE dismissed_at IS NOT NULL)`), so a lost
150
+ * first-login write still shows the welcome and only an explicit
151
+ * POST /me/onboarding/dismiss (or the deploy backfill) ever hides it.
152
+ * The hub's advisory `tool_first_login` claim plays no part.
153
+ */
154
+ firstRunPending: z.boolean().optional(),
155
+ /**
156
+ * The hint-watch discriminator (SL-4/SL-6). CLOUD + SESSION callers ONLY
157
+ * — absent on oss and for machine credentials. True iff the user's ONLY
158
+ * way in is hub SSO: they hold an `antasphere` account row AND no local
159
+ * credential (password) account row. A break-glass-capable operator has
160
+ * a credential account and is therefore NEVER ssoOnly — the dashboard's
161
+ * hint-watch may auto-sign-out ssoOnly users when the hub hint cookie
162
+ * disappears, and must never sign out an operator.
163
+ */
164
+ ssoOnly: z.boolean().optional(),
165
+ /**
166
+ * The organizations the person belongs to at Antasphere that do NOT open
167
+ * this tool to them (the organization lets only some of its teams use
168
+ * it). CLOUD + SESSION callers only; absent on the self-hosted edition
169
+ * and for machine credentials, and a reader treats an absent field as
170
+ * none (an older server never sends it). A hint for the refusal page,
171
+ * read from the hub's last answer as the person: the hub is the truth,
172
+ * never this list.
173
+ */
174
+ hubDenied: z.array(z.object({ id: z.string(), name: z.string() })).optional(),
175
+ /**
176
+ * The Antasphere page that lists whom to ask for this tool (the owners
177
+ * and admins of the refusing organization), ready-made with the tool's
178
+ * client id. CLOUD + SESSION callers only; absent otherwise.
179
+ */
180
+ hubNoAccessUrl: z.string().optional()
181
+ });
182
+
183
+ return { meResponseSchema };
184
+ }
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ import { workspaceRoleSchema } from './common.js';
3
+
4
+ export const memberSchema = z.object({
5
+ id: z.string(),
6
+ userId: z.string(),
7
+ email: z.string(),
8
+ name: z.string(),
9
+ role: workspaceRoleSchema,
10
+ isActive: z.boolean(),
11
+ createdAt: z.string(),
12
+ lastSeenAt: z.string().nullable()
13
+ });
14
+ export type Member = z.infer<typeof memberSchema>;
15
+
16
+ export const membersListSchema = z.object({
17
+ members: z.array(memberSchema),
18
+ nextCursor: z.string().nullable()
19
+ });
20
+
21
+ // Both fields optional, so an empty body must be refused HERE: `{}` used to
22
+ // pass validation and build an empty drizzle `.set({})`, which Postgres
23
+ // refuses — an anonymous-shaped 500 for a plain client mistake (FUZZ-5,
24
+ // PRDCT-1358).
25
+ export const memberUpdateSchema = z
26
+ .object({
27
+ role: workspaceRoleSchema.optional(),
28
+ isActive: z.boolean().optional()
29
+ })
30
+ .refine((v) => v.role !== undefined || v.isActive !== undefined, {
31
+ error: 'at least one of role or isActive is required'
32
+ });
33
+ export type MemberUpdate = z.infer<typeof memberUpdateSchema>;
34
+
35
+ /** Admin-generated password reset link for a member (the SMTP-free recovery path). */
36
+ export const memberResetLinkSchema = z.object({
37
+ resetUrl: z.string(),
38
+ expiresAt: z.string()
39
+ });
40
+ export type MemberResetLink = z.infer<typeof memberResetLinkSchema>;
41
+
42
+ /** Admin-generated email change link for a member (the SMTP-free email-change path). */
43
+ export const memberChangeEmailLinkRequestSchema = z.object({
44
+ newEmail: z.email()
45
+ });
46
+ export type MemberChangeEmailLinkRequest = z.infer<typeof memberChangeEmailLinkRequestSchema>;
47
+
48
+ export const memberChangeEmailLinkSchema = z.object({
49
+ verifyUrl: z.string(),
50
+ newEmail: z.string(),
51
+ expiresAt: z.string()
52
+ });
53
+ export type MemberChangeEmailLink = z.infer<typeof memberChangeEmailLinkSchema>;
@@ -0,0 +1,174 @@
1
+ import { z } from 'zod';
2
+ import { cursorPageQuerySchema, noControlChars, opaqueJsonChecks, plainText } from './common.js';
3
+
4
+ /**
5
+ * A PROJECT is a subgroup of a workspace: a name, a description, members with
6
+ * a role, and the tool's resources linked to it. The chassis owns the
7
+ * concept; what a project HOLDS is the tool's (its own link table, its own
8
+ * routes). A project is archived, never deleted.
9
+ */
10
+
11
+ /**
12
+ * The three project roles, each one containing the one before it:
13
+ * - `viewer`: reads the project and what is linked to it;
14
+ * - `editor`: viewer + writes what is linked to it, links resources of their own;
15
+ * - `manager`: editor + edits the project, archives it, manages its members.
16
+ * A workspace owner or admin acts as a manager on every project.
17
+ */
18
+ export const projectRoleSchema = z.enum(['manager', 'editor', 'viewer']);
19
+ export type ProjectRole = z.infer<typeof projectRoleSchema>;
20
+
21
+ const PROJECT_ROLE_RANK: Record<ProjectRole, number> = { viewer: 0, editor: 1, manager: 2 };
22
+
23
+ /** True when `role` is `min` or contains it (`manager` ≥ `editor` ≥ `viewer`). */
24
+ export function projectRoleAtLeast(role: ProjectRole, min: ProjectRole): boolean {
25
+ return PROJECT_ROLE_RANK[role] >= PROJECT_ROLE_RANK[min];
26
+ }
27
+
28
+ /**
29
+ * Serialized size cap for a project's owner-defined metadata object, in
30
+ * JSON.stringify characters (an environment-free proxy for bytes: the
31
+ * contract runs in browsers and Node alike).
32
+ */
33
+ export const PROJECT_METADATA_MAX_LENGTH = 16 * 1024;
34
+
35
+ /**
36
+ * Owner-defined structured metadata: a plain JSON object, opaque to the
37
+ * server beyond shape and size. This is the tool's and the integrator's seam,
38
+ * the reason no tool ever needs a column on the project. PATCH replaces the
39
+ * whole object; merge is a client concern.
40
+ */
41
+ export const projectMetadataSchema = opaqueJsonChecks(z.record(z.string(), z.unknown())).refine(
42
+ (v) => JSON.stringify(v).length <= PROJECT_METADATA_MAX_LENGTH,
43
+ `metadata must serialize to at most ${PROJECT_METADATA_MAX_LENGTH} characters`
44
+ );
45
+
46
+ export const projectSchema = z.object({
47
+ id: z.string(),
48
+ name: z.string(),
49
+ description: z.string().nullable(),
50
+ metadata: z.record(z.string(), z.unknown()),
51
+ /** Set while the project is archived: out of the default list, read-only. */
52
+ archivedAt: z.string().nullable(),
53
+ createdBy: z.string().nullable(),
54
+ createdAt: z.string(),
55
+ updatedAt: z.string(),
56
+ /** The CALLER's effective role (`manager` for a workspace owner or admin). */
57
+ myRole: projectRoleSchema,
58
+ memberCount: z.number().int()
59
+ });
60
+ export type Project = z.infer<typeof projectSchema>;
61
+
62
+ export const projectsListSchema = z.object({
63
+ projects: z.array(projectSchema),
64
+ nextCursor: z.string().nullable()
65
+ });
66
+ export type ProjectsList = z.infer<typeof projectsListSchema>;
67
+
68
+ /** `false` (the default) = the live projects, `true` = the archived ones, `all` = both. */
69
+ export const projectsArchivedFilterSchema = z.enum(['false', 'true', 'all']);
70
+ export type ProjectsArchivedFilter = z.infer<typeof projectsArchivedFilterSchema>;
71
+
72
+ export const projectsListQuerySchema = cursorPageQuerySchema.extend({
73
+ archived: projectsArchivedFilterSchema.default('false')
74
+ });
75
+
76
+ export const projectCreateSchema = z.object({
77
+ name: plainText(1, 200),
78
+ description: plainText(0, 2000).optional(),
79
+ metadata: projectMetadataSchema.optional()
80
+ });
81
+ export type ProjectCreate = z.infer<typeof projectCreateSchema>;
82
+
83
+ // Every field optional, so an empty body is refused HERE (an empty drizzle
84
+ // `.set({})` is a Postgres error, not a client mistake answered 400).
85
+ export const projectUpdateSchema = z
86
+ .object({
87
+ name: plainText(1, 200).optional(),
88
+ /** `null` clears the description. */
89
+ description: plainText(0, 2000).nullable().optional(),
90
+ metadata: projectMetadataSchema.optional()
91
+ })
92
+ .refine((v) => v.name !== undefined || v.description !== undefined || v.metadata !== undefined, {
93
+ error: 'at least one of name, description or metadata is required'
94
+ });
95
+ export type ProjectUpdate = z.infer<typeof projectUpdateSchema>;
96
+
97
+ /**
98
+ * A project's members are people AND teams, each with a role (PRDCT-2794):
99
+ * one list, told apart by `kind`. A team entry's role is what every active
100
+ * non-guest member of the team holds through it; a person's effective role
101
+ * is the highest of their own entry and their teams' entries. `kind` is
102
+ * `person` on every entry that predates teams, so a reader that never asked
103
+ * for teams still reads what it did.
104
+ */
105
+ export const projectPersonMemberSchema = z.object({
106
+ kind: z.literal('person'),
107
+ userId: z.string(),
108
+ email: z.string(),
109
+ name: z.string(),
110
+ role: projectRoleSchema,
111
+ addedBy: z.string().nullable(),
112
+ createdAt: z.string()
113
+ });
114
+ export type ProjectPersonMember = z.infer<typeof projectPersonMemberSchema>;
115
+
116
+ export const projectTeamMemberSchema = z.object({
117
+ kind: z.literal('team'),
118
+ teamId: z.string(),
119
+ slug: z.string(),
120
+ name: z.string(),
121
+ /**
122
+ * How many seats the team holds here, the account site's count. A
123
+ * deactivated member keeps their seat (a pause) and holds nothing through
124
+ * it until reactivated; the team's page shows each seat's state.
125
+ */
126
+ membersCount: z.number().int(),
127
+ /** The hub's team id when the team is the account site's projection; null for the tool's own. */
128
+ hubTeamId: z.string().nullable(),
129
+ role: projectRoleSchema,
130
+ addedBy: z.string().nullable(),
131
+ createdAt: z.string()
132
+ });
133
+ export type ProjectTeamMember = z.infer<typeof projectTeamMemberSchema>;
134
+
135
+ export const projectMemberSchema = z.discriminatedUnion('kind', [
136
+ projectPersonMemberSchema,
137
+ projectTeamMemberSchema
138
+ ]);
139
+ export type ProjectMember = z.infer<typeof projectMemberSchema>;
140
+
141
+ export const projectMembersListSchema = z.object({
142
+ members: z.array(projectMemberSchema),
143
+ nextCursor: z.string().nullable()
144
+ });
145
+ export type ProjectMembersList = z.infer<typeof projectMembersListSchema>;
146
+
147
+ /**
148
+ * Add one of the workspace's OWN active members, named by user id or by
149
+ * email, or one of the workspace's teams by its id: exactly one of the three.
150
+ * No invitation, no claim token, no account is ever minted here.
151
+ */
152
+ export const projectMemberAddSchema = z
153
+ .object({
154
+ userId: plainText(1, 200).optional(),
155
+ email: z.email().optional(),
156
+ teamId: z.uuid().optional(),
157
+ role: projectRoleSchema
158
+ })
159
+ .refine((v) => [v.userId, v.email, v.teamId].filter((k) => k !== undefined).length === 1, {
160
+ error: 'exactly one of userId, email or teamId is required'
161
+ });
162
+ export type ProjectMemberAdd = z.infer<typeof projectMemberAddSchema>;
163
+
164
+ export const projectMemberRoleSchema = z.object({ role: projectRoleSchema });
165
+ export type ProjectMemberRoleUpdate = z.infer<typeof projectMemberRoleSchema>;
166
+
167
+ /** A user id is Better Auth's text id, never a uuid: bounded free text, no control characters. */
168
+ export const projectMemberParamsSchema = z.object({
169
+ id: z.uuid(),
170
+ userId: noControlChars(z.string().min(1).max(200))
171
+ });
172
+
173
+ /** `{id}` the project and `{teamId}` the team, both uuids. */
174
+ export const projectTeamParamsSchema = z.object({ id: z.uuid(), teamId: z.uuid() });
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * The scope vocabulary is the TOOL's identity (`products:read`, …), so the
5
+ * chassis never spells one: a tool hands its scopes to
6
+ * `defineChassisContract({ scopes })` (../define.ts) and gets back the enum
7
+ * and every schema that carries it.
8
+ */
9
+ export type ScopeTuple = readonly [string, ...string[]];
10
+
11
+ /** The zod enum over a tool's scopes — exactly what `z.enum([...scopes])` builds. */
12
+ export type ScopeSchema<TScope extends string> = z.ZodEnum<{ [K in TScope]: K }>;
13
+
14
+ export function defineScopeSchema<const TScopes extends ScopeTuple>(
15
+ scopes: TScopes
16
+ ): ScopeSchema<TScopes[number]> {
17
+ return z.enum(scopes) as unknown as ScopeSchema<TScopes[number]>;
18
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import { plainText } from './common.js';
3
+
4
+ /**
5
+ * POST /api/v1/setup — one-shot first-boot wizard: creates the owner user
6
+ * and, on the OSS edition, the instance's FIRST workspace (later ones come
7
+ * from `POST /workspaces`). Guarded by the
8
+ * singleton instance_settings row (second attempt → 410) and, optionally, a
9
+ * SETUP_TOKEN. On the CLOUD edition setup creates NO workspace (the
10
+ * response's `workspaceId` is null): every cloud workspace is a hub-org
11
+ * projection, so the operator bootstrap mints a verified, break-glass
12
+ * capable USER only (internal/federation.md, user-scoped federation).
13
+ */
14
+ export const setupRequestSchema = z.object({
15
+ instanceName: plainText(1, 120),
16
+ owner: z.object({
17
+ email: z.email(),
18
+ name: plainText(1, 120),
19
+ password: z.string().min(12).max(256)
20
+ }),
21
+ setupToken: z.string().optional()
22
+ });
23
+ export type SetupRequest = z.infer<typeof setupRequestSchema>;
24
+
25
+ export const setupResponseSchema = z.object({
26
+ instanceId: z.string(),
27
+ /** The first workspace (oss); null on cloud — no workspace is created there. */
28
+ workspaceId: z.string().nullable(),
29
+ ownerUserId: z.string()
30
+ });
31
+ export type SetupResponse = z.infer<typeof setupResponseSchema>;
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * CLI cross-tool connect (cloud edition only — internal/federation.md P5):
5
+ *
6
+ * POST /sso/cli-connect — exchange a hub-minted 120 s JWT for an API key
7
+ *
8
+ * The counterpart of the hub's H3 `POST /sso/tool-token`: `antasphere login`
9
+ * mints a hub API key once; the hub exchanges it for a short-lived RS256 JWT
10
+ * (`aud` = THIS instance's resource URL, `purpose: 'sso-connect'`, unique
11
+ * `jti`) PLUS a one-time raw offline-grant refresh token (`hubRefreshToken`,
12
+ * scopes `offline_access account:read` — the HUB mints it, so it carries no
13
+ * `orgs:create`: a CLI connect REPLACES the stored grant, and the person's
14
+ * next workspace creation answers `hub_reauth_required` until they sign in
15
+ * with the browser again), and this endpoint turns the pair
16
+ * into an ordinary USER-scoped local API key — no tool-specific login, and
17
+ * the stored grant drives the same live as-the-user hub org reads as a
18
+ * browser SSO login's. PUBLIC path (the hub JWT IS the credential),
19
+ * rate-limited, jti one-time-use. Registered ONLY on EDITION=cloud; an oss
20
+ * instance answers 404 (the route does not exist there).
21
+ *
22
+ * The response reuses the CLI-auth completion shape (`CliAuthCompleted`) —
23
+ * the tool's read + write scopes, never its export scope, full key
24
+ * shown exactly once; `workspaceId` is null (user-scoped, unpinned).
25
+ */
26
+
27
+ export const ssoCliConnectSchema = z.object({
28
+ /** The hub-minted exchange JWT (compact JWS — three base64url segments). */
29
+ token: z.string().min(20).max(4096),
30
+ /**
31
+ * The raw one-time offline-grant refresh token from the SAME H3 response
32
+ * (`hubRefreshToken`). Stored encrypted on the account row — it is what
33
+ * keeps a headless CLI user's org reads LIVE between browser logins.
34
+ * Optional on the wire for older clients, but a connect that carries none
35
+ * is refused unless a prior grant already exists (a key must never be
36
+ * minted born-dead).
37
+ */
38
+ hubRefreshToken: z.string().min(20).max(512).optional()
39
+ });
40
+ export type SsoCliConnect = z.infer<typeof ssoCliConnectSchema>;
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * POST /api/v1/sso/logout (cloud edition ONLY — oss answers the JSON 404
5
+ * terminator): the single-logout server leg. The server has ALREADY revoked
6
+ * the local session and cleared the shared hint cookie by the time this
7
+ * body arrives; `url` is the hub RP-initiated-logout URL the browser must
8
+ * now VISIT to end the hub anchor session (it lands back on
9
+ * `/login?signed_out=1`). `null` = no hub leg was possible (no stored
10
+ * id_token, a pre-flip sid-less one, hub unreachable) — the client goes to
11
+ * `/login?signed_out=1` directly; the LOCAL signout already happened either
12
+ * way.
13
+ */
14
+ export const ssoLogoutResponseSchema = z.object({
15
+ url: z.string().nullable()
16
+ });
17
+ export type SsoLogoutResponse = z.infer<typeof ssoLogoutResponseSchema>;