All files / src/entities/access_tokens schema.ts

100% Statements 12/12
100% Branches 5/5
100% Functions 1/1
100% Lines 12/12

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 10817x 17x 17x                                 17x               17x         17x             17x                       17x                       17x                         17x                       17x                     13x                
import { z } from 'zod';
import { requiredId } from '../utils';
import { projectIdField, groupIdField, tokenIdField } from './schema-readonly';
 
// ============================================================================
// manage_access_token - CQRS Command Tool (discriminated union schema)
// Actions: create_project, create_group, rotate, revoke
//
// Creates, rotates, and revokes project / group / personal access tokens via the
// GitLab REST endpoints (no GraphQL surface exists). create_* and rotate return a
// token value exactly once; the handler flags those responses as sensitive.
// Personal-token creation is intentionally absent: GitLab only allows it for
// admins against a specific user, which is out of scope for this tool. Gated
// behind USE_ACCESS_TOKENS. Free tier; project/group create needs owner/admin.
// ============================================================================
 
// Access-token scopes are a free-form, version-dependent set (api, read_api,
// read_repository, write_repository, read_registry, write_registry, ...). Kept
// as strings rather than an enum so new GitLab scopes work without a code change.
const scopesField = z
  .array(z.string().trim().min(1, 'scope entries must be non-empty'))
  .min(1)
  .describe(
    "Token scopes, e.g. ['api'], ['read_repository','write_repository']. At least one required.",
  );
 
// GitLab member access levels: 10 Guest, 20 Reporter, 30 Developer, 40 Maintainer, 50 Owner.
const accessLevelField = z
  .union([z.literal(10), z.literal(20), z.literal(30), z.literal(40), z.literal(50)])
  .optional()
  .describe('Access level: 10 Guest, 20 Reporter, 30 Developer, 40 Maintainer, 50 Owner.');
 
const expiresAtField = z
  .string()
  .regex(/^\d{4}-\d{2}-\d{2}$/, 'expires_at must be a YYYY-MM-DD date')
  .optional()
  .describe('Expiry date in YYYY-MM-DD format (e.g. "2026-12-31").');
 
// --- Action: create_project ---
const CreateProjectTokenSchema = z.object({
  action: z
    .literal('create_project')
    .describe('Create a project access token (returns the value once)'),
  project_id: projectIdField,
  name: z.string().describe('Human-readable token name.'),
  scopes: scopesField,
  access_level: accessLevelField,
  expires_at: expiresAtField,
});
 
// --- Action: create_group ---
const CreateGroupTokenSchema = z.object({
  action: z
    .literal('create_group')
    .describe('Create a group access token (returns the value once)'),
  group_id: groupIdField,
  name: z.string().describe('Human-readable token name.'),
  scopes: scopesField,
  access_level: accessLevelField,
  expires_at: expiresAtField,
});
 
// --- Action: rotate (scope inferred from project_id/group_id) ---
const RotateTokenSchema = z.object({
  action: z
    .literal('rotate')
    .describe(
      'Rotate a token: revoke the old one and return a new value. Pass project_id for a project token, group_id for a group token, or neither for a personal token.',
    ),
  token_id: tokenIdField,
  project_id: requiredId.optional().describe('Set for a project access token.'),
  group_id: requiredId.optional().describe('Set for a group access token.'),
  expires_at: expiresAtField,
});
 
// --- Action: revoke (scope inferred from project_id/group_id) ---
const RevokeTokenSchema = z.object({
  action: z
    .literal('revoke')
    .describe(
      'Revoke a token permanently. Pass project_id for a project token, group_id for a group token, or neither for a personal token.',
    ),
  token_id: tokenIdField,
  project_id: requiredId.optional().describe('Set for a project access token.'),
  group_id: requiredId.optional().describe('Set for a group access token.'),
});
 
// --- Discriminated union combining all actions ---
export const ManageAccessTokenSchema = z
  .discriminatedUnion('action', [
    CreateProjectTokenSchema,
    CreateGroupTokenSchema,
    RotateTokenSchema,
    RevokeTokenSchema,
  ])
  // A token belongs to a single scope; project_id and group_id are mutually exclusive
  // on the scope-inferring actions.
  .refine(
    (data) =>
      (data.action !== 'rotate' && data.action !== 'revoke') || !(data.project_id && data.group_id),
    {
      message: 'Pass at most one of project_id or group_id (a token belongs to a single scope)',
      path: ['project_id'],
    },
  );
 
export type ManageAccessTokenInput = z.infer<typeof ManageAccessTokenSchema>;