@antasphere/chassis-contract 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +82 -0
- package/dist/define.d.ts +177 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +35 -0
- package/dist/define.js.map +1 -0
- package/dist/entitlements.d.ts +265 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +155 -0
- package/dist/entitlements.js.map +1 -0
- package/dist/identity.d.ts +56 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +2 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/index.d.ts +5403 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +940 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/schemas/api-keys.d.ts +54 -0
- package/dist/schemas/api-keys.d.ts.map +1 -0
- package/dist/schemas/api-keys.js +48 -0
- package/dist/schemas/api-keys.js.map +1 -0
- package/dist/schemas/audit.d.ts +74 -0
- package/dist/schemas/audit.d.ts.map +1 -0
- package/dist/schemas/audit.js +65 -0
- package/dist/schemas/audit.js.map +1 -0
- package/dist/schemas/break-glass.d.ts +42 -0
- package/dist/schemas/break-glass.d.ts.map +1 -0
- package/dist/schemas/break-glass.js +38 -0
- package/dist/schemas/break-glass.js.map +1 -0
- package/dist/schemas/cli-auth.d.ts +59 -0
- package/dist/schemas/cli-auth.d.ts.map +1 -0
- package/dist/schemas/cli-auth.js +68 -0
- package/dist/schemas/cli-auth.js.map +1 -0
- package/dist/schemas/common.d.ts +96 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +205 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/demo-passes.d.ts +90 -0
- package/dist/schemas/demo-passes.d.ts.map +1 -0
- package/dist/schemas/demo-passes.js +60 -0
- package/dist/schemas/demo-passes.js.map +1 -0
- package/dist/schemas/files.d.ts +39 -0
- package/dist/schemas/files.d.ts.map +1 -0
- package/dist/schemas/files.js +26 -0
- package/dist/schemas/files.js.map +1 -0
- package/dist/schemas/instance.d.ts +58 -0
- package/dist/schemas/instance.d.ts.map +1 -0
- package/dist/schemas/instance.js +70 -0
- package/dist/schemas/instance.js.map +1 -0
- package/dist/schemas/invitations.d.ts +100 -0
- package/dist/schemas/invitations.d.ts.map +1 -0
- package/dist/schemas/invitations.js +55 -0
- package/dist/schemas/invitations.js.map +1 -0
- package/dist/schemas/me.d.ts +81 -0
- package/dist/schemas/me.d.ts.map +1 -0
- package/dist/schemas/me.js +178 -0
- package/dist/schemas/me.js.map +1 -0
- package/dist/schemas/members.d.ts +60 -0
- package/dist/schemas/members.d.ts.map +1 -0
- package/dist/schemas/members.js +43 -0
- package/dist/schemas/members.js.map +1 -0
- package/dist/schemas/projects.d.ts +232 -0
- package/dist/schemas/projects.d.ts.map +1 -0
- package/dist/schemas/projects.js +140 -0
- package/dist/schemas/projects.js.map +1 -0
- package/dist/schemas/scope.d.ts +14 -0
- package/dist/schemas/scope.d.ts.map +1 -0
- package/dist/schemas/scope.js +5 -0
- package/dist/schemas/scope.js.map +1 -0
- package/dist/schemas/setup.d.ts +28 -0
- package/dist/schemas/setup.d.ts.map +1 -0
- package/dist/schemas/setup.js +28 -0
- package/dist/schemas/setup.js.map +1 -0
- package/dist/schemas/sso-connect.d.ts +30 -0
- package/dist/schemas/sso-connect.d.ts.map +1 -0
- package/dist/schemas/sso-connect.js +38 -0
- package/dist/schemas/sso-connect.js.map +1 -0
- package/dist/schemas/sso-logout.d.ts +17 -0
- package/dist/schemas/sso-logout.d.ts.map +1 -0
- package/dist/schemas/sso-logout.js +16 -0
- package/dist/schemas/sso-logout.js.map +1 -0
- package/dist/schemas/teams.d.ts +100 -0
- package/dist/schemas/teams.d.ts.map +1 -0
- package/dist/schemas/teams.js +84 -0
- package/dist/schemas/teams.js.map +1 -0
- package/dist/schemas/workspaces.d.ts +99 -0
- package/dist/schemas/workspaces.d.ts.map +1 -0
- package/dist/schemas/workspaces.js +96 -0
- package/dist/schemas/workspaces.js.map +1 -0
- package/dist/seams.d.ts +185 -0
- package/dist/seams.d.ts.map +1 -0
- package/dist/seams.js +37 -0
- package/dist/seams.js.map +1 -0
- package/package.json +41 -3
- package/src/define.ts +64 -0
- package/src/entitlements.ts +340 -0
- package/src/identity.ts +49 -0
- package/src/index.ts +37 -0
- package/src/routes/index.ts +1146 -0
- package/src/schemas/api-keys.ts +53 -0
- package/src/schemas/audit.ts +74 -0
- package/src/schemas/break-glass.ts +46 -0
- package/src/schemas/cli-auth.ts +78 -0
- package/src/schemas/common.ts +206 -0
- package/src/schemas/demo-passes.ts +73 -0
- package/src/schemas/files.ts +30 -0
- package/src/schemas/instance.ts +71 -0
- package/src/schemas/invitations.ts +66 -0
- package/src/schemas/me.ts +184 -0
- package/src/schemas/members.ts +53 -0
- package/src/schemas/projects.ts +174 -0
- package/src/schemas/scope.ts +18 -0
- package/src/schemas/setup.ts +31 -0
- package/src/schemas/sso-connect.ts +40 -0
- package/src/schemas/sso-logout.ts +17 -0
- package/src/schemas/teams.ts +101 -0
- package/src/schemas/workspaces.ts +109 -0
- package/src/seams.ts +202 -0
- package/test/define.test.ts +59 -0
- 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>;
|